newrelic 13.15.0 → 13.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NEWS.md +21 -0
- package/lib/instrumentations.js +0 -1
- package/lib/subscriber-configs.js +1 -0
- package/lib/subscribers/base.js +20 -5
- package/lib/subscribers/redis/config.js +23 -0
- package/lib/subscribers/redis/internal-send-command.js +60 -0
- package/package.json +2 -2
- package/lib/instrumentation/redis.js +0 -104
package/NEWS.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
### v13.16.0 (2026-03-11)
|
|
2
|
+
|
|
3
|
+
#### Bug fixes
|
|
4
|
+
|
|
5
|
+
* Updated `@apm-js-collab/tracing-hooks` to `0.5.0`, to allow instrumentation on windows environments. ([#3819](https://github.com/newrelic/node-newrelic/pull/3819)) ([d8f6d0f](https://github.com/newrelic/node-newrelic/commit/d8f6d0f6ac7f3ff17a667391269c41e6e57dc2fb))
|
|
6
|
+
* This version of `@apm-js-collab/tracing-hooks` includes the windows path fix for `@apm-js-collab/code-transformer`, [see](https://github.com/apm-js-collab/tracing-hooks/issues/19).
|
|
7
|
+
|
|
8
|
+
#### Code refactoring
|
|
9
|
+
|
|
10
|
+
* Updated `redis` v3 instrumentation to subscribe to events emitted ([#3802](https://github.com/newrelic/node-newrelic/pull/3802)) ([4d18302](https://github.com/newrelic/node-newrelic/commit/4d1830237c91d3e60c6b4e898f2c82ff131ebd57))
|
|
11
|
+
|
|
12
|
+
#### Documentation
|
|
13
|
+
|
|
14
|
+
* Updated compatibility report ([#3813](https://github.com/newrelic/node-newrelic/pull/3813)) ([27cb0a5](https://github.com/newrelic/node-newrelic/commit/27cb0a5e7a4c6047cce54483bd18d1337023efd3))
|
|
15
|
+
|
|
16
|
+
#### Tests
|
|
17
|
+
|
|
18
|
+
* Add `redis-client` and `node-redis-client` segment duration timing tests ([#3810](https://github.com/newrelic/node-newrelic/pull/3810)) ([f07bcfe](https://github.com/newrelic/node-newrelic/commit/f07bcfecf06453c846e12f030fbf7d8c055da139))
|
|
19
|
+
|
|
1
20
|
### v13.15.0 (2026-03-09)
|
|
2
21
|
|
|
3
22
|
#### Features
|
|
@@ -8431,6 +8450,8 @@ Special thanks to Ryan Copley (@RyanCopley) for the contribution.
|
|
|
8431
8450
|
|
|
8432
8451
|
|
|
8433
8452
|
|
|
8453
|
+
|
|
8454
|
+
|
|
8434
8455
|
|
|
8435
8456
|
|
|
8436
8457
|
|
package/lib/instrumentations.js
CHANGED
|
@@ -25,7 +25,6 @@ module.exports = function instrumentations() {
|
|
|
25
25
|
mongodb: { type: InstrumentationDescriptor.TYPE_DATASTORE },
|
|
26
26
|
next: { module: './instrumentation/nextjs' },
|
|
27
27
|
q: { type: null },
|
|
28
|
-
redis: { type: InstrumentationDescriptor.TYPE_DATASTORE },
|
|
29
28
|
restify: { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
|
|
30
29
|
when: { module: './instrumentation/when' },
|
|
31
30
|
winston: { type: InstrumentationDescriptor.TYPE_GENERIC }
|
|
@@ -27,6 +27,7 @@ const subscribers = {
|
|
|
27
27
|
...require('./subscribers/openai/config'),
|
|
28
28
|
...require('./subscribers/pino/config'),
|
|
29
29
|
...require('./subscribers/pg/config'),
|
|
30
|
+
...require('./subscribers/redis/config'),
|
|
30
31
|
...require('./subscribers/redis-client/config'),
|
|
31
32
|
...require('./subscribers/undici/config')
|
|
32
33
|
}
|
package/lib/subscribers/base.js
CHANGED
|
@@ -42,6 +42,9 @@ const ArrayPrototypeSplice = makeCall(Array.prototype.splice)
|
|
|
42
42
|
* @property {AsyncLocalStorage} store The async local storage instance used for context management.
|
|
43
43
|
* @property {number} [callback=null] Position of callback if it needs to be wrapped for instrumentation.
|
|
44
44
|
* -1 means last argument.
|
|
45
|
+
* @property {string} [callbackKey=null] Property name on the argument at `callback` position
|
|
46
|
+
* that holds the actual callback function. Used when the callback is not a direct argument but a
|
|
47
|
+
* property on an object argument (e.g. `args[0].callback` when callback=0 and callbackKey='callback').
|
|
45
48
|
* @property {string[]} [events=[]] Set of tracing channel event names to
|
|
46
49
|
* register handlers for. For any name in the set, a corresponding method
|
|
47
50
|
* must exist on the subscriber instance. The method will be passed the
|
|
@@ -88,6 +91,7 @@ class Subscriber {
|
|
|
88
91
|
this.channel = tracingChannel(this.id)
|
|
89
92
|
this.store = agent.tracer._contextManager._asyncLocalStorage
|
|
90
93
|
this.callback = null
|
|
94
|
+
this.callbackKey = null
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
shouldCreateSegment(parent) {
|
|
@@ -103,11 +107,23 @@ class Subscriber {
|
|
|
103
107
|
* This will wrap a callback at a given position and reassign the callback argument to the wrapped one
|
|
104
108
|
*
|
|
105
109
|
* @param {number} position index of the callback, you can specify -1 to be the last
|
|
110
|
+
* @param {string} [key] property name to look up on the argument at `position` when the
|
|
111
|
+
* callback is not a direct argument but a property on an object argument
|
|
112
|
+
* (e.g. `args[0].callback` when position=0 and key='callback')
|
|
106
113
|
* @param {object} context the event passed to the tracing channel hooks
|
|
107
114
|
*/
|
|
108
|
-
traceCallback(position, context) {
|
|
115
|
+
traceCallback(position, key, context) {
|
|
109
116
|
this.logger.trace('Wrapping the callback at position %s', position)
|
|
110
117
|
const { asyncStart, asyncEnd, error } = this.channel
|
|
118
|
+
|
|
119
|
+
const holder = ArrayPrototypeAt(context.arguments, position)
|
|
120
|
+
const callback = key ? holder?.[key] : holder
|
|
121
|
+
|
|
122
|
+
if (typeof callback !== 'function') {
|
|
123
|
+
this.logger.trace('Callback is not present, not wrapping')
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
111
127
|
function wrappedCallback(err, res) {
|
|
112
128
|
// assigning a boolean to the context so we know that the
|
|
113
129
|
// `error`, `asyncStart`, and `asyncEnd` are coming from the wrapped callback
|
|
@@ -133,9 +149,8 @@ class Subscriber {
|
|
|
133
149
|
})
|
|
134
150
|
}
|
|
135
151
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
this.logger.trace('Callback is not present, not wrapping')
|
|
152
|
+
if (key) {
|
|
153
|
+
context.arguments[position][key] = wrappedCallback
|
|
139
154
|
} else {
|
|
140
155
|
ArrayPrototypeSplice(context.arguments, position, 1, wrappedCallback)
|
|
141
156
|
}
|
|
@@ -254,7 +269,7 @@ class Subscriber {
|
|
|
254
269
|
|
|
255
270
|
// only wrap the callback if a subscriber has a callback property defined
|
|
256
271
|
if (this.callback !== null) {
|
|
257
|
-
this.traceCallback(this.callback, data)
|
|
272
|
+
this.traceCallback(this.callback, this.callbackKey, data)
|
|
258
273
|
}
|
|
259
274
|
const ctx = this.agent.tracer.getContext()
|
|
260
275
|
if (this.requireActiveTx && !ctx?.transaction?.isActive()) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const internalSendCommand = {
|
|
7
|
+
path: './redis/internal-send-command',
|
|
8
|
+
instrumentations: [{
|
|
9
|
+
channelName: 'nr_internalSendCommand',
|
|
10
|
+
module: { name: 'redis', versionRange: '>=2.6.0 <4', filePath: 'index.js' },
|
|
11
|
+
functionQuery: {
|
|
12
|
+
// RedisClient.prototype.internal_send_command
|
|
13
|
+
expressionName: 'internal_send_command',
|
|
14
|
+
kind: 'Sync'
|
|
15
|
+
}
|
|
16
|
+
}]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
redis: [
|
|
21
|
+
internalSendCommand
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 New Relic Corporation. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DbOperationSubscriber = require('../db-operation')
|
|
7
|
+
const stringify = require('json-stringify-safe')
|
|
8
|
+
|
|
9
|
+
module.exports = class RedisInternalSendCommandSubscriber extends DbOperationSubscriber {
|
|
10
|
+
constructor({ agent, logger }) {
|
|
11
|
+
super({ agent, logger, packageName: 'redis', channelName: 'nr_internalSendCommand', system: 'Redis' })
|
|
12
|
+
this.events = ['asyncEnd', 'end']
|
|
13
|
+
this.propagateContext = true
|
|
14
|
+
this.callback = 0
|
|
15
|
+
this.callbackKey = 'callback'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
handler(data, ctx) {
|
|
19
|
+
const { arguments: args, self: client } = data
|
|
20
|
+
const commandObject = args[0]
|
|
21
|
+
const keys = commandObject.args
|
|
22
|
+
this.parameters = this.#getInstanceParameters(client, keys)
|
|
23
|
+
this.operation = commandObject.command || 'other'
|
|
24
|
+
|
|
25
|
+
return super.handler(data, ctx)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Captures the necessary datastore parameters from redis v3 client
|
|
30
|
+
*
|
|
31
|
+
* @param {object} client instance of redis v3 client
|
|
32
|
+
* @param {string[]} keys list of keys
|
|
33
|
+
* @returns {object} datastore parameters: host, port_path_or_id, database_name, product, key
|
|
34
|
+
*/
|
|
35
|
+
#getInstanceParameters(client, keys) {
|
|
36
|
+
const opts = client?.connection_options
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
host: opts.host || 'localhost',
|
|
40
|
+
port_path_or_id: opts.path || opts.port || '6379',
|
|
41
|
+
database_name: client.selected_db || opts.db || 0,
|
|
42
|
+
product: this.system,
|
|
43
|
+
key: this.#stringifyKeys(keys)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
#stringifyKeys(keys) {
|
|
48
|
+
let key = null
|
|
49
|
+
if (keys && keys.length) {
|
|
50
|
+
try {
|
|
51
|
+
key = stringify(keys[0])
|
|
52
|
+
} catch (err) {
|
|
53
|
+
this.logger.debug(err, 'Failed to stringify redis key for send command')
|
|
54
|
+
key = '<unknown>'
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return key
|
|
59
|
+
}
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "newrelic",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.16.0",
|
|
4
4
|
"author": "New Relic Node.js agent team <nodejs@newrelic.com>",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"contributors": [
|
|
@@ -202,7 +202,7 @@
|
|
|
202
202
|
"#test/assert": "./test/lib/custom-assertions/index.js"
|
|
203
203
|
},
|
|
204
204
|
"dependencies": {
|
|
205
|
-
"@apm-js-collab/tracing-hooks": "^0.
|
|
205
|
+
"@apm-js-collab/tracing-hooks": "^0.5.0",
|
|
206
206
|
"@grpc/grpc-js": "^1.13.2",
|
|
207
207
|
"@grpc/proto-loader": "^0.7.5",
|
|
208
208
|
"@newrelic/security-agent": "^3.0.0",
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2020 New Relic Corporation. All rights reserved.
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
'use strict'
|
|
7
|
-
|
|
8
|
-
const stringify = require('json-stringify-safe')
|
|
9
|
-
const {
|
|
10
|
-
OperationSpec,
|
|
11
|
-
params: { DatastoreParameters }
|
|
12
|
-
} = require('../shim/specs')
|
|
13
|
-
|
|
14
|
-
module.exports = function initialize(_agent, redis, _moduleName, shim) {
|
|
15
|
-
const proto = redis?.RedisClient?.prototype
|
|
16
|
-
if (!proto) {
|
|
17
|
-
// This will happen when redis >= 4.0.0
|
|
18
|
-
shim.logger.warn('Skipping redis instrumentation due to unrecognized module shape')
|
|
19
|
-
return false
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
shim.setDatastore(shim.REDIS)
|
|
23
|
-
|
|
24
|
-
if (!proto.internal_send_command) {
|
|
25
|
-
shim.logger.warn(
|
|
26
|
-
'New Relic Node.js agent no longer supports redis < 2.6.0, current version %s. Please downgrade to v11 for support, if needed',
|
|
27
|
-
shim.pkgVersion
|
|
28
|
-
)
|
|
29
|
-
return
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
registerInternalSendCommand(shim, proto)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Instrumentation used in versions of redis >= 2.6.0 < 4 to record all redis commands
|
|
37
|
-
*
|
|
38
|
-
* @param {Shim} shim instance of shim
|
|
39
|
-
* @param {object} proto RedisClient prototype
|
|
40
|
-
*/
|
|
41
|
-
function registerInternalSendCommand(shim, proto) {
|
|
42
|
-
shim.recordOperation(
|
|
43
|
-
proto,
|
|
44
|
-
'internal_send_command',
|
|
45
|
-
function wrapInternalSendCommand(shim, _, __, args) {
|
|
46
|
-
const commandObject = args[0]
|
|
47
|
-
const keys = commandObject.args
|
|
48
|
-
const parameters = getInstanceParameters(this)
|
|
49
|
-
|
|
50
|
-
parameters.key = stringifyKeys(shim, keys)
|
|
51
|
-
|
|
52
|
-
return new OperationSpec({
|
|
53
|
-
name: commandObject.command || 'other',
|
|
54
|
-
parameters,
|
|
55
|
-
callback: function bindCallback(shim, _f, _n, segment) {
|
|
56
|
-
if (shim.isFunction(commandObject.callback)) {
|
|
57
|
-
shim.bindCallbackSegment(null, commandObject, 'callback', segment)
|
|
58
|
-
} else {
|
|
59
|
-
const self = this
|
|
60
|
-
commandObject.callback = shim.bindSegment(
|
|
61
|
-
function NRCallback(err) {
|
|
62
|
-
if (err && self.emit instanceof Function) {
|
|
63
|
-
self.emit('error', err)
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
segment,
|
|
67
|
-
true
|
|
68
|
-
)
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
})
|
|
72
|
-
}
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function stringifyKeys(shim, keys) {
|
|
77
|
-
let key = null
|
|
78
|
-
if (keys && keys.length && !shim.isFunction(keys)) {
|
|
79
|
-
try {
|
|
80
|
-
key = stringify(keys[0])
|
|
81
|
-
} catch (err) {
|
|
82
|
-
shim.logger.debug(err, 'Failed to stringify redis key for send command')
|
|
83
|
-
key = '<unknown>'
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return key
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Captures the necessary datastore parameters from redis client
|
|
92
|
-
*
|
|
93
|
-
* @param {object} client instance of redis client
|
|
94
|
-
* @returns {object} datastore parameters
|
|
95
|
-
*/
|
|
96
|
-
function getInstanceParameters(client = {}) {
|
|
97
|
-
const opts = client?.connection_options
|
|
98
|
-
|
|
99
|
-
return new DatastoreParameters({
|
|
100
|
-
host: opts.host || 'localhost',
|
|
101
|
-
port_path_or_id: opts.path || opts.port || '6379',
|
|
102
|
-
database_name: client.selected_db || opts.db || 0
|
|
103
|
-
})
|
|
104
|
-
}
|