diff --git a/lib/config/build-instrumentation-config.js b/lib/config/build-instrumentation-config.js index c4fdebfb55..8582cbeef0 100644 --- a/lib/config/build-instrumentation-config.js +++ b/lib/config/build-instrumentation-config.js @@ -7,6 +7,13 @@ const { boolean } = require('./formatters') const instrumentedLibraries = require('../instrumentations')() const pkgNames = Object.keys(instrumentedLibraries) +const coreLibraries = require('../core-instrumentation') +const corePkgs = Object.keys(coreLibraries) +// Manually adding undici as it is registered separately in shimmer +corePkgs.push('undici') +// Manually adding domain as it is registered separately in shimmer +corePkgs.push('domain') +pkgNames.push(...corePkgs) /** * Builds the stanza for config.instrumentation.* diff --git a/lib/config/default.js b/lib/config/default.js index ac2d1481c3..68d28c2f21 100644 --- a/lib/config/default.js +++ b/lib/config/default.js @@ -1592,9 +1592,9 @@ defaultConfig.definition = () => ({ } }, /** - * Stanza that contains all keys to disable 3rd party package instrumentation(i.e. mongodb, pg, redis, etc) - * Note: Disabling a given 3rd party library may affect the instrumentation of 3rd party libraries used after - * the disabled library. + * Stanza that contains all keys to disable core & 3rd party package instrumentation(i.e. dns, http, mongodb, pg, redis, etc) + * **Note**: Disabling a given library may affect the instrumentation of libraries used after + * the disabled library. Use at your own risk. */ instrumentation: pkgInstrumentation }) diff --git a/lib/core-instrumentation.js b/lib/core-instrumentation.js new file mode 100644 index 0000000000..f0c64ca68d --- /dev/null +++ b/lib/core-instrumentation.js @@ -0,0 +1,51 @@ +/* + * Copyright 2025 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const InstrumentationDescriptor = require('./instrumentation-descriptor') + +module.exports = { + child_process: { + type: InstrumentationDescriptor.TYPE_GENERIC, + file: 'child_process.js' + }, + crypto: { + type: InstrumentationDescriptor.TYPE_GENERIC, + file: 'crypto.js' + }, + dns: { + type: InstrumentationDescriptor.TYPE_GENERIC, + file: 'dns.js' + }, + fs: { + type: InstrumentationDescriptor.TYPE_GENERIC, + file: 'fs.js' + }, + http: { + type: InstrumentationDescriptor.TYPE_TRANSACTION, + file: 'http.js' + }, + https: { + type: InstrumentationDescriptor.TYPE_TRANSACTION, + file: 'http.js' + }, + inspector: { + type: InstrumentationDescriptor.TYPE_GENERIC, + file: 'inspector.js' + }, + net: { + type: InstrumentationDescriptor.TYPE_GENERIC, + file: 'net.js' + }, + timers: { + type: InstrumentationDescriptor.TYPE_GENERIC, + file: 'timers.js' + }, + zlib: { + type: InstrumentationDescriptor.TYPE_GENERIC, + file: 'zlib.js' + } +} diff --git a/lib/shimmer.js b/lib/shimmer.js index ec2ee4cc2f..938d99ec0e 100644 --- a/lib/shimmer.js +++ b/lib/shimmer.js @@ -10,6 +10,7 @@ const semver = require('semver') const fs = require('./util/unwrapped-core').fs const logger = require('./logger').child({ component: 'shimmer' }) const INSTRUMENTATIONS = require('./instrumentations')() +const CORE_INSTRUMENTATION = require('./core-instrumentation') const shims = require('./shim') const { Hook } = require('require-in-the-middle') const IitmHook = require('import-in-the-middle') @@ -24,53 +25,6 @@ const symbols = require('./symbols') const { unsubscribe } = require('./instrumentation/undici') const setupOtel = require('./otel/setup') -const CORE_INSTRUMENTATION = { - child_process: { - type: InstrumentationDescriptor.TYPE_GENERIC, - file: 'child_process.js' - }, - crypto: { - type: InstrumentationDescriptor.TYPE_GENERIC, - file: 'crypto.js' - }, - // domain: { // XXX Do not include domains in this list! The - // type: InstrumentationDescriptor.TYPE_GENERIC, // core instrumentations are run at startup by - // file: 'domain.js' // requiring each of their modules. Loading - // }, // `domain` has side effects that we try to avoid. - dns: { - type: InstrumentationDescriptor.TYPE_GENERIC, - file: 'dns.js' - }, - fs: { - type: InstrumentationDescriptor.TYPE_GENERIC, - file: 'fs.js' - }, - http: { - type: InstrumentationDescriptor.TYPE_TRANSACTION, - file: 'http.js' - }, - https: { - type: InstrumentationDescriptor.TYPE_TRANSACTION, - file: 'http.js' - }, - inspector: { - type: InstrumentationDescriptor.TYPE_GENERIC, - file: 'inspector.js' - }, - net: { - type: InstrumentationDescriptor.TYPE_GENERIC, - file: 'net.js' - }, - timers: { - type: InstrumentationDescriptor.TYPE_GENERIC, - file: 'timers.js' - }, - zlib: { - type: InstrumentationDescriptor.TYPE_GENERIC, - file: 'zlib.js' - } -} - /** * Unwrapping is only likely to be used by test code, and is a fairly drastic * maneuver, but it should be pretty safe if there's a desire to reboot the @@ -314,12 +268,7 @@ const shimmer = (module.exports = { // Even though domain is a core module we add it as a registered // instrumentation to be lazy-loaded because we do not want to cause domain // usage. - const domainPath = path.join(__dirname, 'instrumentation/core/domain.js') - shimmer.registerInstrumentation({ - moduleName: 'domain', - type: null, - onRequire: _firstPartyInstrumentation.bind(null, agent, domainPath) - }) + instrumentDomain(agent) }, /** @@ -328,42 +277,34 @@ const shimmer = (module.exports = { * @param {object} agent NR agent */ registerCoreInstrumentation(agent) { - // Instrument global. - const globalShim = new shims.Shim(agent, 'globals', '.') - applyDebugState(globalShim, global, false) - const globalsFilepath = path.join(__dirname, 'instrumentation', 'core', 'globals.js') - _firstPartyInstrumentation(agent, globalsFilepath, globalShim, global, 'globals') - - // Since this just registers subscriptions to diagnostics_channel events from undici - // We register this as core and it'll work for both fetch and undici - const undiciPath = path.join(__dirname, 'instrumentation', 'undici.js') - const undiciShim = shims.createShimFromType({ - type: InstrumentationDescriptor.TYPE_TRANSACTION, - agent, - moduleName: 'undici', - resolvedName: '.' - }) - _firstPartyInstrumentation(agent, undiciPath, undiciShim) + instrumentProcessMethods(agent) + instrumentUndiciFetch(agent) // Instrument each of the core modules. for (const [mojule, core] of Object.entries(CORE_INSTRUMENTATION)) { - const filePath = path.join(__dirname, 'instrumentation', 'core', core.file) - let uninstrumented = null + if (agent.config.instrumentation?.[mojule].enabled === false) { + logger.warn( + `Instrumentation for ${mojule} has been disabled via 'config.instrumentation.${mojule}.enabled. Not instrumenting package` + ) + } else if (core.file) { + const filePath = path.join(__dirname, 'instrumentation', 'core', core.file) + let uninstrumented = null - try { - uninstrumented = require(mojule) - } catch (err) { - logger.trace('Could not load core module %s got error %s', mojule, err) - } + try { + uninstrumented = require(mojule) + } catch (err) { + logger.trace('Could not load core module %s got error %s', mojule, err) + } - const shim = shims.createShimFromType({ - type: core.type, - agent, - moduleName: mojule, - resolvedName: mojule - }) - applyDebugState(shim, core, false) - _firstPartyInstrumentation(agent, filePath, shim, uninstrumented, mojule) + const shim = shims.createShimFromType({ + type: core.type, + agent, + moduleName: mojule, + resolvedName: mojule + }) + applyDebugState(shim, core, false) + _firstPartyInstrumentation(agent, filePath, shim, uninstrumented, mojule) + } } }, @@ -824,3 +765,55 @@ function tryGetVersion(shim) { return shim.pkgVersion } + +/** + * Loads the instrumentation with `./lib/instrumentation/core/globals.js` + * This instrumentation cannot be disabled by config at the moment as it just + * handles logging errors for `uncaughtException` and `unhandledRejections` + * + * @param {Agent} agent the agent instance + */ +function instrumentProcessMethods(agent) { + const globalShim = new shims.Shim(agent, 'globals', '.') + applyDebugState(globalShim, global, false) + const globalsFilepath = path.join(__dirname, 'instrumentation', 'core', 'globals.js') + _firstPartyInstrumentation(agent, globalsFilepath, globalShim, global, 'globals') +} + +/** + * Checks if undici/fetch instrumentation is enabled. + * If so, it loads the diagnostics_channel hooks to instrument outbound + * undici/fetch calls. + * Since this just registers subscriptions to diagnostics_channel events from undici, + * we register this as core and it'll work for both fetch and undici + * + * @param {Agent} agent the agent instance + */ +function instrumentUndiciFetch(agent) { + if (agent.config.instrumentation?.undici.enabled === false) { + logger.warn('Instrumentation for undici/fetch has been disabled via `config.instrumentation.undici.enabled`. Not instrumenting package') + return + } + + const undiciPath = path.join(__dirname, 'instrumentation', 'undici.js') + const undiciShim = shims.createShimFromType({ + type: InstrumentationDescriptor.TYPE_TRANSACTION, + agent, + moduleName: 'undici', + resolvedName: '.' + }) + _firstPartyInstrumentation(agent, undiciPath, undiciShim) +} + +function instrumentDomain(agent) { + if (agent.config.instrumentation?.domain.enabled === false) { + logger.warn('Instrumentation for domain has been disabled via `config.instrumentation.domain.enabled`. Not instrumenting package') + return + } + const domainPath = path.join(__dirname, 'instrumentation/core/domain.js') + shimmer.registerInstrumentation({ + moduleName: 'domain', + type: null, + onRequire: _firstPartyInstrumentation.bind(null, agent, domainPath) + }) +} diff --git a/test/unit/config/build-instrumentation-config.test.js b/test/unit/config/build-instrumentation-config.test.js index 70f0fbebf5..30a7cf899a 100644 --- a/test/unit/config/build-instrumentation-config.test.js +++ b/test/unit/config/build-instrumentation-config.test.js @@ -17,4 +17,11 @@ test('should default the instrumentation stanza', () => { pkgNames.forEach((pkg) => { assert.deepEqual(pkgs[pkg], { enabled: { formatter: boolean, default: true } }) }) + + assert.deepEqual(pkgs.undici, { enabled: { formatter: boolean, default: true } }) + const coreLibraries = require('../../../lib/core-instrumentation') + const corePkgs = Object.keys(coreLibraries) + corePkgs.forEach((pkg) => { + assert.deepEqual(pkgs[pkg], { enabled: { formatter: boolean, default: true } }) + }) }) diff --git a/test/unit/config/config-defaults.test.js b/test/unit/config/config-defaults.test.js index b67975fb2a..0907b4b986 100644 --- a/test/unit/config/config-defaults.test.js +++ b/test/unit/config/config-defaults.test.js @@ -315,6 +315,9 @@ test('with default properties', async (t) => { assert.equal(configuration.instrumentation.express.enabled, true) assert.equal(configuration.instrumentation['@prisma/client'].enabled, true) assert.equal(configuration.instrumentation.npmlog.enabled, true) + assert.equal(configuration.instrumentation.http.enabled, true) + assert.equal(configuration.instrumentation.undici.enabled, true) + assert.equal(configuration.instrumentation.domain.enabled, true) }) }) diff --git a/test/unit/config/config-env.test.js b/test/unit/config/config-env.test.js index 35501d9365..6fda03ef1c 100644 --- a/test/unit/config/config-env.test.js +++ b/test/unit/config/config-env.test.js @@ -874,12 +874,18 @@ test('when overriding configuration values via environment variables', async (t) const env = { NEW_RELIC_INSTRUMENTATION_IOREDIS_ENABLED: 'false', 'NEW_RELIC_INSTRUMENTATION_@GRPC/GRPC-JS_ENABLED': 'false', - NEW_RELIC_INSTRUMENTATION_KNEX_ENABLED: 'false' + NEW_RELIC_INSTRUMENTATION_KNEX_ENABLED: 'false', + NEW_RELIC_INSTRUMENTATION_HTTP_ENABLED: 'false', + NEW_RELIC_INSTRUMENTATION_UNDICI_ENABLED: 'false', + NEW_RELIC_INSTRUMENTATION_DOMAIN_ENABLED: 'false', } idempotentEnv(env, (config) => { assert.equal(config.instrumentation.ioredis.enabled, false) assert.equal(config.instrumentation['@grpc/grpc-js'].enabled, false) assert.equal(config.instrumentation.knex.enabled, false) + assert.equal(config.instrumentation.http.enabled, false) + assert.equal(config.instrumentation.undici.enabled, false) + assert.equal(config.instrumentation.domain.enabled, false) end() }) })