Skip to content

Commit

Permalink
feat: Provided ability to disable instrumentation for core Node.js li…
Browse files Browse the repository at this point in the history
…braries (#2927)
  • Loading branch information
bizob2828 authored Feb 10, 2025
1 parent aa2781f commit 2d232f1
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 88 deletions.
7 changes: 7 additions & 0 deletions lib/config/build-instrumentation-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down
6 changes: 3 additions & 3 deletions lib/config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand Down
51 changes: 51 additions & 0 deletions lib/core-instrumentation.js
Original file line number Diff line number Diff line change
@@ -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'
}
}
161 changes: 77 additions & 84 deletions lib/shimmer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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
Expand Down Expand Up @@ -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)
},

/**
Expand All @@ -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)
}
}
},

Expand Down Expand Up @@ -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)
})
}
7 changes: 7 additions & 0 deletions test/unit/config/build-instrumentation-config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 } })
})
})
3 changes: 3 additions & 0 deletions test/unit/config/config-defaults.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})

Expand Down
8 changes: 7 additions & 1 deletion test/unit/config/config-env.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
Expand Down

0 comments on commit 2d232f1

Please sign in to comment.