Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Provided ability to disable instrumentation for core Node.js libraries #2927

Merged
merged 1 commit into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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 { 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 @@
// 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 @@
* @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`
)

Check warning on line 288 in lib/shimmer.js

View check run for this annotation

Codecov / codecov/patch

lib/shimmer.js#L286-L288

Added lines #L286 - L288 were not covered by tests
} 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)
}

Check warning on line 297 in lib/shimmer.js

View check run for this annotation

Codecov / codecov/patch

lib/shimmer.js#L296-L297

Added lines #L296 - L297 were not covered by tests

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 @@

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
}

Check warning on line 796 in lib/shimmer.js

View check run for this annotation

Codecov / codecov/patch

lib/shimmer.js#L794-L796

Added lines #L794 - L796 were not covered by tests

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
}

Check warning on line 812 in lib/shimmer.js

View check run for this annotation

Codecov / codecov/patch

lib/shimmer.js#L810-L812

Added lines #L810 - L812 were not covered by tests
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
Loading