From 6f6f7e68bf382c6082550306aee30a670652347d Mon Sep 17 00:00:00 2001 From: Bob Evans Date: Mon, 5 Jun 2023 11:00:50 -0400 Subject: [PATCH] feat: added supportability metrics to indicate how agent was loaded and if --enable-source-maps was passed to Node.js runtime (#1657) * `Supportability/Features/CJS/Preload` - recorded if `-r newrelic` was used to load agent * `Supportability/Features/CJS/Require` - recorded if `require('newrelic')` was used to load agent * `Supportability/Features/EnableSourceMaps` - recorded if `node --enable-source-maps` was present to start application --- index.js | 64 ++++++++++++++++++- lib/metrics/names.js | 5 ++ test/unit/index.test.js | 127 +++++++++++++++++++++++++++++++++++++ test/unit/mocks/agent.js | 18 ++++++ test/unit/mocks/metrics.js | 13 ++++ test/unit/mocks/shimmer.js | 14 ++++ 6 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 test/unit/index.test.js create mode 100644 test/unit/mocks/agent.js create mode 100644 test/unit/mocks/metrics.js create mode 100644 test/unit/mocks/shimmer.js diff --git a/index.js b/index.js index 0bcbada6e3..aefa540bb4 100644 --- a/index.js +++ b/index.js @@ -15,6 +15,7 @@ require('./lib/util/unwrapped-core') const featureFlags = require('./lib/feature_flags').prerelease const psemver = require('./lib/util/process-version') let logger = require('./lib/logger') // Gets re-loaded after initialization. +const NAMES = require('./lib/metrics/names') const pkgJSON = require('./package.json') logger.info( @@ -173,10 +174,30 @@ function createAgent(config) { } function addStartupSupportabilities(agent) { - // TODO: As new versions come out, make sure to update Angler metrics. + recordLoaderMetric(agent) + recordNodeVersionMetric(agent) + recordFeatureFlagMetrics(agent) + recordSourceMapMetric(agent) +} + +/** + * Records the major version of the Node.js runtime + * TODO: As new versions come out, make sure to update Angler metrics. + * + * @param {Agent} agent active NR agent + */ +function recordNodeVersionMetric(agent) { const nodeMajor = /^v?(\d+)/.exec(process.version) - agent.recordSupportability('Nodejs/Version/' + ((nodeMajor && nodeMajor[1]) || 'unknown')) + const version = (nodeMajor && nodeMajor[1]) || 'unknown' + agent.recordSupportability(`Nodejs/Version/${version}`) +} +/** + * Records all the feature flags configured and if they are enabled/disabled + * + * @param {Agent} agent active NR agent + */ +function recordFeatureFlagMetrics(agent) { const configFlags = Object.keys(agent.config.feature_flag) for (let i = 0; i < configFlags.length; ++i) { const flag = configFlags[i] @@ -189,3 +210,42 @@ function addStartupSupportabilities(agent) { } } } + +/** + * Used to determine how the agent is getting loaded: + * 1. -r newrelic + * 2. --loader newrelic/esm-loader.mjs + * 3. require('newrelic') + * + * Then a supportability metric is loaded to decide. + * Note: We already take care of scenario #2 in newrelic/esm-loader.mjs + * + * @param {Agent} agent active NR agent + */ +function recordLoaderMetric(agent) { + const isESM = agent.metrics.getMetric(NAMES.FEATURES.ESM.LOADER) + let isDashR = false + + process.execArgv.forEach((arg, index) => { + if (arg === '-r' && process.execArgv[index + 1] === 'newrelic') { + agent.metrics.getOrCreateMetric(NAMES.FEATURES.CJS.PRELOAD).incrementCallCount() + isDashR = true + } + }) + + if (!isESM && !isDashR) { + agent.metrics.getOrCreateMetric(NAMES.FEATURES.CJS.REQUIRE).incrementCallCount() + } +} + +/** + * Checks to see if `--enable-source-maps` is being used and logs a supportability metric. + * + * @param {Agent} agent active NR agent + */ +function recordSourceMapMetric(agent) { + const isSourceMapsEnabled = process.execArgv.includes('--enable-source-maps') + if (isSourceMapsEnabled) { + agent.metrics.getOrCreateMetric(NAMES.FEATURES.SOURCE_MAPS).incrementCallCount() + } +} diff --git a/lib/metrics/names.js b/lib/metrics/names.js index c43ab4e3ec..a4b0f636ae 100644 --- a/lib/metrics/names.js +++ b/lib/metrics/names.js @@ -275,6 +275,11 @@ const FEATURES = { UNSUPPORTED_LOADER: `${SUPPORTABILITY.FEATURES}/ESM/UnsupportedLoader`, CUSTOM_INSTRUMENTATION: `${SUPPORTABILITY.FEATURES}/ESM/CustomInstrumentation` }, + CJS: { + PRELOAD: `${SUPPORTABILITY.FEATURES}/CJS/Preload`, + REQUIRE: `${SUPPORTABILITY.FEATURES}/CJS/Require` + }, + SOURCE_MAPS: `${SUPPORTABILITY.FEATURES}/EnableSourceMaps`, CERTIFICATES: SUPPORTABILITY.FEATURES + '/Certificates', INSTRUMENTATION: { ON_RESOLVED: SUPPORTABILITY.FEATURES + '/Instrumentation/OnResolved', diff --git a/test/unit/index.test.js b/test/unit/index.test.js new file mode 100644 index 0000000000..8a7fefe9d2 --- /dev/null +++ b/test/unit/index.test.js @@ -0,0 +1,127 @@ +/* + * Copyright 2023 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const tap = require('tap') +const proxyquire = require('proxyquire') +const sinon = require('sinon') + +tap.test('loader metrics', (t) => { + t.autoend() + let metricsMock + let MockAgent + let shimmerMock + let ApiMock + let sandbox + + t.beforeEach(() => { + sandbox = sinon.createSandbox() + metricsMock = require('./mocks/metrics')(sandbox) + MockAgent = require('./mocks/agent')(sandbox, metricsMock) + shimmerMock = require('./mocks/shimmer')(sandbox) + + ApiMock = function (agent) { + this.agent = agent + } + }) + + t.afterEach(() => { + process.execArgv = [] + sandbox.restore() + delete require.cache.__NR_cache + }) + + t.test('should load preload metric when agent is loaded via -r', (t) => { + process.execArgv = ['-r', 'newrelic'] + const agent = proxyquire('../../index', { + './lib/agent': MockAgent, + './lib/shimmer': shimmerMock, + './api': ApiMock + }) + + const metricCall = agent.agent.metrics.getOrCreateMetric + + t.equal(metricCall.args.length, 1) + t.equal(metricCall.args[0][0], 'Supportability/Features/CJS/Preload') + t.end() + }) + + t.test('should not load preload metric if -r is present but is not newrelic', (t) => { + process.execArgv = ['-r', 'some-cool-lib'] + const agent = proxyquire('../../index', { + './lib/agent': MockAgent, + './lib/shimmer': shimmerMock, + './api': ApiMock + }) + + const metricCall = agent.agent.metrics.getOrCreateMetric + + t.equal(metricCall.args.length, 1) + t.equal(metricCall.args[0][0], 'Supportability/Features/CJS/Require') + t.end() + }) + + t.test( + 'should detect preload metric if newrelic is one of the -r calls but not the first', + (t) => { + process.execArgv = ['-r', 'some-cool-lib', '--inspect', '-r', 'newrelic'] + const agent = proxyquire('../../index', { + './lib/agent': MockAgent, + './lib/shimmer': shimmerMock, + './api': ApiMock + }) + + const metricCall = agent.agent.metrics.getOrCreateMetric + + t.equal(metricCall.args.length, 1) + t.equal(metricCall.args[0][0], 'Supportability/Features/CJS/Preload') + t.end() + } + ) + + t.test('should not load preload nor require metric is esm loader loads agent', (t) => { + metricsMock.getMetric.withArgs('Supportability/Features/ESM/Loader').returns(true) + const agent = proxyquire('../../index', { + './lib/agent': MockAgent, + './lib/shimmer': shimmerMock, + './api': ApiMock + }) + + const metricCall = agent.agent.metrics.getOrCreateMetric + + t.equal(metricCall.args.length, 0) + t.end() + }) + + t.test('should load require metric when agent is required', (t) => { + const agent = proxyquire('../../index', { + './lib/agent': MockAgent, + './lib/shimmer': shimmerMock, + './api': ApiMock + }) + + const metricCall = agent.agent.metrics.getOrCreateMetric + + t.equal(metricCall.args.length, 1) + t.equal(metricCall.args[0][0], 'Supportability/Features/CJS/Require') + t.end() + }) + + t.test('should load enable source map metric when --enable-source-maps is present', (t) => { + process.execArgv = ['--enable-source-maps'] + const agent = proxyquire('../../index', { + './lib/agent': MockAgent, + './lib/shimmer': shimmerMock, + './api': ApiMock + }) + + const metricCall = agent.agent.metrics.getOrCreateMetric + + t.equal(metricCall.args.length, 2) + t.equal(metricCall.args[1][0], 'Supportability/Features/EnableSourceMaps') + t.end() + }) +}) diff --git a/test/unit/mocks/agent.js b/test/unit/mocks/agent.js new file mode 100644 index 0000000000..833f43ff61 --- /dev/null +++ b/test/unit/mocks/agent.js @@ -0,0 +1,18 @@ +/* + * Copyright 2023 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const sinon = require('sinon') +module.exports = (sandbox = sinon, metricsMock) => { + function MockAgent(config) { + this.config = config + this.config.app_name = 'Unit Test App' + this.metrics = metricsMock + } + MockAgent.prototype.start = sandbox.stub() + MockAgent.prototype.recordSupportability = sandbox.stub() + MockAgent.prototype.once = sandbox.stub() + return MockAgent +} diff --git a/test/unit/mocks/metrics.js b/test/unit/mocks/metrics.js new file mode 100644 index 0000000000..ea248ab0da --- /dev/null +++ b/test/unit/mocks/metrics.js @@ -0,0 +1,13 @@ +/* + * Copyright 2023 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const sinon = require('sinon') +module.exports = (sandbox = sinon) => { + return { + getMetric: sandbox.stub(), + getOrCreateMetric: sandbox.stub().returns({ incrementCallCount: sandbox.stub() }) + } +} diff --git a/test/unit/mocks/shimmer.js b/test/unit/mocks/shimmer.js new file mode 100644 index 0000000000..943a2f308a --- /dev/null +++ b/test/unit/mocks/shimmer.js @@ -0,0 +1,14 @@ +/* + * Copyright 2023 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' +const sinon = require('sinon') +module.exports = (sandbox = sinon) => { + return { + patchModule: sandbox.stub(), + bootstrapInstrumentation: sandbox.stub(), + registeredInstrumentations: {} + } +}