Skip to content

Commit 7e467b8

Browse files
bizob2828sumitsuthar
authored andcommitted
feat: Provided ability to disable instrumentation for a 3rd party package (newrelic#2551)
1 parent 32d1ad3 commit 7e467b8

12 files changed

+341
-3
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2024 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
const { boolean } = require('./formatters')
8+
const instrumentedLibraries = require('../instrumentations')()
9+
const pkgNames = Object.keys(instrumentedLibraries)
10+
11+
/**
12+
* Builds the stanza for config.instrumentation.*
13+
* It defaults every library to true and assigns a boolean
14+
* formatter for the environment variable conversion of the values
15+
*/
16+
module.exports = pkgNames.reduce((config, pkg) => {
17+
config[pkg] = { enabled: { formatter: boolean, default: true } }
18+
return config
19+
}, {})

lib/config/default.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
const defaultConfig = module.exports
99
const { array, int, float, boolean, object, objectList, allowList, regex } = require('./formatters')
10+
const pkgInstrumentation = require('./build-instrumentation-config')
1011

1112
/**
1213
* A function that returns the definition of the agent configuration
@@ -1382,7 +1383,13 @@ defaultConfig.definition = () => ({
13821383
default: true
13831384
}
13841385
}
1385-
}
1386+
},
1387+
/**
1388+
* Stanza that contains all keys to disable 3rd party package instrumentation(i.e. mongodb, pg, redis, etc)
1389+
* Note: Disabling a given 3rd party library may affect the instrumentation of 3rd party libraries used after
1390+
* the disabled library.
1391+
*/
1392+
instrumentation: pkgInstrumentation
13861393
})
13871394

13881395
/**

lib/shimmer.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,11 @@ const shimmer = (module.exports = {
286286
*/
287287
registerThirdPartyInstrumentation(agent) {
288288
for (const [moduleName, instrInfo] of Object.entries(INSTRUMENTATIONS)) {
289-
if (instrInfo.module) {
289+
if (agent.config.instrumentation?.[moduleName]?.enabled === false) {
290+
logger.warn(
291+
`Instrumentation for ${moduleName} has been disabled via 'config.instrumentation.${moduleName}.enabled. Not instrumenting package`
292+
)
293+
} else if (instrInfo.module) {
290294
// Because external instrumentations can change independent of
291295
// the agent core, we don't want breakages in them to entirely
292296
// disable the agent.

test/lib/custom-assertions.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,94 @@ function compareSegments(parent, segments) {
8080
})
8181
}
8282

83+
/**
84+
* @param {TraceSegment} parent Parent segment
85+
* @param {Array} expected Array of strings that represent segment names.
86+
* If an item in the array is another array, it
87+
* represents children of the previous item.
88+
* @param {boolean} options.exact If true, then the expected segments must match
89+
* exactly, including their position and children on all
90+
* levels. When false, then only check that each child
91+
* exists.
92+
* @param {array} options.exclude Array of segment names that should be excluded from
93+
* validation. This is useful, for example, when a
94+
* segment may or may not be created by code that is not
95+
* directly under test. Only used when `exact` is true.
96+
*/
97+
function assertSegments(parent, expected, options) {
98+
let child
99+
let childCount = 0
100+
101+
// rather default to what is more likely to fail than have a false test
102+
let exact = true
103+
if (options && options.exact === false) {
104+
exact = options.exact
105+
} else if (options === false) {
106+
exact = false
107+
}
108+
109+
function getChildren(_parent) {
110+
return _parent.children.filter(function (item) {
111+
if (exact && options && options.exclude) {
112+
return options.exclude.indexOf(item.name) === -1
113+
}
114+
return true
115+
})
116+
}
117+
118+
const children = getChildren(parent)
119+
if (exact) {
120+
for (let i = 0; i < expected.length; ++i) {
121+
const sequenceItem = expected[i]
122+
123+
if (typeof sequenceItem === 'string') {
124+
child = children[childCount++]
125+
assert.equal(
126+
child ? child.name : undefined,
127+
sequenceItem,
128+
'segment "' +
129+
parent.name +
130+
'" should have child "' +
131+
sequenceItem +
132+
'" in position ' +
133+
childCount
134+
)
135+
136+
// If the next expected item is not array, then check that the current
137+
// child has no children
138+
if (!Array.isArray(expected[i + 1])) {
139+
assert.ok(
140+
getChildren(child).length === 0,
141+
'segment "' + child.name + '" should not have any children'
142+
)
143+
}
144+
} else if (typeof sequenceItem === 'object') {
145+
assertSegments(child, sequenceItem, options)
146+
}
147+
}
148+
149+
// check if correct number of children was found
150+
assert.equal(children.length, childCount)
151+
} else {
152+
for (let i = 0; i < expected.length; i++) {
153+
const sequenceItem = expected[i]
154+
155+
if (typeof sequenceItem === 'string') {
156+
// find corresponding child in parent
157+
for (let j = 0; j < parent.children.length; j++) {
158+
if (parent.children[j].name === sequenceItem) {
159+
child = parent.children[j]
160+
}
161+
}
162+
assert.ok(child, 'segment "' + parent.name + '" should have child "' + sequenceItem + '"')
163+
if (typeof expected[i + 1] === 'object') {
164+
assertSegments(child, expected[i + 1], exact)
165+
}
166+
}
167+
}
168+
}
169+
}
170+
83171
/**
84172
* Like `tap.prototype.match`. Verifies that `actual` satisfies the shape
85173
* provided by `expected`.
@@ -147,6 +235,7 @@ function match(actual, expected) {
147235
module.exports = {
148236
assertCLMAttrs,
149237
assertExactClmAttrs,
238+
assertSegments,
150239
compareSegments,
151240
isNonWritable,
152241
match

test/lib/metrics_helper.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,6 @@ function assertSegments(parent, expected, options) {
161161
// If the next expected item is not array, then check that the current
162162
// child has no children
163163
if (!Array.isArray(expected[i + 1])) {
164-
// var children = child.children
165164
this.ok(
166165
getChildren(child).length === 0,
167166
'segment "' + child.name + '" should not have any children'
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2022 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
const test = require('node:test')
9+
const assert = require('node:assert')
10+
11+
test('should default the instrumentation stanza', () => {
12+
const { boolean } = require('../../../lib/config/formatters')
13+
const pkgs = require('../../../lib/config/build-instrumentation-config')
14+
const instrumentation = require('../../../lib/instrumentations')()
15+
const pkgNames = Object.keys(instrumentation)
16+
17+
pkgNames.forEach((pkg) => {
18+
assert.deepEqual(pkgs[pkg], { enabled: { formatter: boolean, default: true } })
19+
})
20+
})

test/unit/config/config-defaults.test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,4 +262,10 @@ test('with default properties', async (t) => {
262262
assert.equal(configuration.ai_monitoring.enabled, false)
263263
assert.equal(configuration.ai_monitoring.streaming.enabled, true)
264264
})
265+
266+
await t.test('instrumentation defaults', () => {
267+
assert.equal(configuration.instrumentation.express.enabled, true)
268+
assert.equal(configuration.instrumentation['@prisma/client'].enabled, true)
269+
assert.equal(configuration.instrumentation.npmlog.enabled, true)
270+
})
265271
})

test/unit/config/config-env.test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,5 +768,19 @@ test('when overriding configuration values via environment variables', async (t)
768768
end()
769769
})
770770
})
771+
772+
await t.test('should convert NEW_RELIC_INSTRUMENTATION* accordingly', (t, end) => {
773+
const env = {
774+
NEW_RELIC_INSTRUMENTATION_IOREDIS_ENABLED: 'false',
775+
['NEW_RELIC_INSTRUMENTATION_@GRPC/GRPC-JS_ENABLED']: 'false',
776+
NEW_RELIC_INSTRUMENTATION_KNEX_ENABLED: 'false'
777+
}
778+
idempotentEnv(env, (config) => {
779+
assert.equal(config.instrumentation.ioredis.enabled, false)
780+
assert.equal(config.instrumentation['@grpc/grpc-js'].enabled, false)
781+
assert.equal(config.instrumentation.knex.enabled, false)
782+
end()
783+
})
784+
})
771785
}
772786
})
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2024 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
const assert = require('node:assert')
8+
const test = require('node:test')
9+
const helper = require('../../lib/agent_helper')
10+
const http = require('http')
11+
const params = require('../../lib/params')
12+
const { assertSegments } = require('../../lib/custom-assertions')
13+
14+
test('should still record child segments if express instrumentation is disabled', async (t) => {
15+
const agent = helper.instrumentMockedAgent({
16+
instrumentation: {
17+
express: {
18+
enabled: false
19+
}
20+
}
21+
})
22+
const express = require('express')
23+
const app = express()
24+
const Redis = require('ioredis')
25+
const client = new Redis(params.redis_port, params.redis_host)
26+
27+
app.get('/test-me', (_req, res) => {
28+
client.get('foo', (err) => {
29+
assert.equal(err, undefined)
30+
res.end()
31+
})
32+
})
33+
34+
const promise = new Promise((resolve) => {
35+
agent.on('transactionFinished', (tx) => {
36+
assert.equal(tx.name, 'WebTransaction/NormalizedUri/*', 'should not name transactions')
37+
const rootSegment = tx.trace.root
38+
const expectedSegments = ['WebTransaction/NormalizedUri/*', ['Datastore/operation/Redis/get']]
39+
assertSegments(rootSegment, expectedSegments)
40+
resolve()
41+
})
42+
})
43+
44+
const server = app.listen(() => {
45+
const { port } = server.address()
46+
http.request({ port, path: '/test-me' }).end()
47+
})
48+
49+
t.after(() => {
50+
server.close()
51+
client.disconnect()
52+
helper.unloadAgent(agent)
53+
})
54+
55+
await promise
56+
})
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2024 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
const assert = require('node:assert')
8+
const test = require('node:test')
9+
const helper = require('../../lib/agent_helper')
10+
const params = require('../../lib/params')
11+
const { assertSegments } = require('../../lib/custom-assertions')
12+
const mongoCommon = require('../mongodb/common')
13+
14+
test('Disabled PG scenarios', async (t) => {
15+
t.beforeEach(async (ctx) => {
16+
ctx.nr = {}
17+
const agent = helper.instrumentMockedAgent({
18+
instrumentation: {
19+
ioredis: {
20+
enabled: false
21+
}
22+
}
23+
})
24+
const Redis = require('ioredis')
25+
const mongodb = require('mongodb')
26+
const mongo = await mongoCommon.connect({ mongodb })
27+
const collection = mongo.db.collection('disabled-inst-test')
28+
const redisClient = new Redis(params.redis_port, params.redis_host)
29+
await redisClient.select(1)
30+
ctx.nr.redisClient = redisClient
31+
ctx.nr.agent = agent
32+
ctx.nr.collection = collection
33+
ctx.nr.db = mongo.db
34+
ctx.nr.mongoClient = mongo.client
35+
})
36+
37+
t.afterEach(async (ctx) => {
38+
const { agent, redisClient, mongoClient, db } = ctx.nr
39+
await mongoCommon.close(mongoClient, db)
40+
redisClient.disconnect()
41+
helper.unloadAgent(agent)
42+
})
43+
44+
await t.test('should record child segments if pg is disabled and using promises', async (t) => {
45+
const { agent, redisClient, collection } = t.nr
46+
await helper.runInTransaction(agent, async (tx) => {
47+
await redisClient.get('foo')
48+
await collection.countDocuments()
49+
await redisClient.get('bar')
50+
tx.end()
51+
assertSegments(tx.trace.root, [
52+
'Datastore/statement/MongoDB/disabled-inst-test/aggregate',
53+
'Datastore/statement/MongoDB/disabled-inst-test/next'
54+
])
55+
})
56+
})
57+
58+
await t.test('should record child segments if pg is disabled and using callbacks', async (t) => {
59+
const { agent, redisClient, collection } = t.nr
60+
await helper.runInTransaction(agent, async (tx) => {
61+
await new Promise((resolve) => {
62+
redisClient.get('foo', async (err) => {
63+
assert.equal(err, null)
64+
await collection.countDocuments()
65+
redisClient.get('bar', (innerErr) => {
66+
tx.end()
67+
assert.equal(innerErr, null)
68+
assertSegments(tx.trace.root, [
69+
'Datastore/statement/MongoDB/disabled-inst-test/aggregate',
70+
'Datastore/statement/MongoDB/disabled-inst-test/next'
71+
])
72+
resolve()
73+
})
74+
})
75+
})
76+
})
77+
})
78+
})

0 commit comments

Comments
 (0)