Skip to content

Commit f0da50f

Browse files
committed
redis v5 passing
1 parent 0ccad21 commit f0da50f

File tree

10 files changed

+307
-4
lines changed

10 files changed

+307
-4
lines changed

lib/instrumentations.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ module.exports = function instrumentations() {
1515
'@hapi/hapi': { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
1616
'@hapi/vision': { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
1717
'@nestjs/core': { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
18-
'@node-redis/client': { type: InstrumentationDescriptor.TYPE_DATASTORE },
1918
'@prisma/client': { type: InstrumentationDescriptor.TYPE_DATASTORE },
20-
'@redis/client': { type: InstrumentationDescriptor.TYPE_DATASTORE },
2119
'aws-sdk': { module: './instrumentation/aws-sdk' },
2220
bluebird: { type: InstrumentationDescriptor.TYPE_PROMISE },
2321
connect: { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
@@ -29,7 +27,6 @@ module.exports = function instrumentations() {
2927
mongodb: { type: InstrumentationDescriptor.TYPE_DATASTORE },
3028
next: { module: './instrumentation/nextjs' },
3129
q: { type: null },
32-
redis: { type: InstrumentationDescriptor.TYPE_DATASTORE },
3330
restify: { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
3431
superagent: { type: InstrumentationDescriptor.TYPE_GENERIC },
3532
when: { module: './instrumentation/when' },

lib/subscriber-configs.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const subscribers = {
2525
...require('./subscribers/openai/config'),
2626
...require('./subscribers/pino/config'),
2727
...require('./subscribers/pg/config'),
28+
...require('./subscribers/redis/config'),
2829
...require('./subscribers/undici/config')
2930
}
3031

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2026 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
const DbOperationSubscriber = require('../db-operation')
8+
const { redisClientOpts } = require('../../symbols')
9+
10+
/**
11+
* Listens to events on `RedisCommandQueue.addCommand` to create
12+
* the segment with necessary datastore parameters for a given Redis
13+
* operation.
14+
*
15+
* Relies on `ctx[redisClientOpts]` being set for the `host`, `port_path_or_id`,
16+
* and `database` parameters.
17+
*/
18+
module.exports = class CmdQueueAddCmdSubscriber extends DbOperationSubscriber {
19+
constructor({ agent, logger, channelName = 'nr_addCommand' }) {
20+
super({ agent, logger, packageName: '@redis/client', channelName, system: 'Redis' })
21+
this.events = ['asyncEnd']
22+
}
23+
24+
handler(data, ctx) {
25+
const { arguments: args } = data
26+
const [cmd, key, value] = args[0]
27+
this.setParameters({ ctx, key, value })
28+
this.operation = cmd?.toLowerCase() || 'other'
29+
return super.handler(data, ctx)
30+
}
31+
32+
setParameters({ ctx, key, value }) {
33+
const clientParams = ctx[redisClientOpts] ?? {}
34+
this.parameters = Object.assign({}, clientParams)
35+
this.parameters.product = this.system
36+
37+
if (this.agent.config.attributes.enabled) {
38+
if (key) {
39+
this.parameters.key = JSON.stringify(key)
40+
}
41+
if (value) {
42+
this.parameters.value = JSON.stringify(value)
43+
}
44+
}
45+
}
46+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2026 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
const PropagationSubscriber = require('../propagation')
8+
const { redisClientOpts } = require('../../symbols')
9+
10+
/**
11+
* Stores `RedisClient.options` on the Redis client via the symbol `RedisClient[redisClientOpts]`,
12+
* and then to the coordinating context via `ctx[redisClientOpts]`.
13+
*
14+
* We have to store the `redisClientOpts` symbol on the client directly because
15+
* the client could set up its options (e.g. selecting a database) outside of a
16+
* transaction, so we cannot rely on `ctx[redisClientOpts]` being sent.
17+
*
18+
* When `ctx` is valid (we're now in a transaction), we copy over
19+
* `RedisClient[redisClientOpts]` to `ctx[redisClientOpts]`.
20+
*
21+
* This is required because the subscriber responsible
22+
* for segment creation (`./add-command`) is listening to events on the
23+
* `RedisCommandQueue` class, and it does NOT have access to `RedisClient`.
24+
* It will read from `ctx[redisClientOpts]` to retrieve the datastore parameters
25+
* it needs.
26+
*
27+
* Any `RedisClient` method that calls `RedisClient.#queue.addCommand()` MUST
28+
* have a subscriber that extends from this class, so the `addCommand` subscriber
29+
* knows what the datastore parameters are.
30+
*/
31+
module.exports = class ClientPropagationSubscriber extends PropagationSubscriber {
32+
constructor({ agent, logger, channelName }) {
33+
super({ agent, logger, packageName: '@redis/client', channelName })
34+
this.events = ['asyncStart']
35+
}
36+
37+
asyncStart(data) {
38+
// asyncStart always fires (with or without context)
39+
// Initialize client options here so they're available for
40+
// both in-transaction and out-of-transaction commands
41+
const { self: client } = data
42+
if (!client[redisClientOpts]) {
43+
client[redisClientOpts] = this.getRedisParams(client.options)
44+
}
45+
return super.asyncStart(data)
46+
}
47+
48+
handler(data, ctx) {
49+
// handler only fires when there's a transaction context
50+
// Transfer a COPY of the client opts to context, so that
51+
// updates to client opts don't affect the current context.
52+
// This ensures SELECT reports the old database, but subsequent
53+
// commands report the new database (feature parity with v3)
54+
const { self: client } = data
55+
ctx[redisClientOpts] = Object.assign({}, client[redisClientOpts])
56+
return super.handler(data, ctx)
57+
}
58+
59+
/**
60+
* Extracts the datastore parameters from the client options
61+
*
62+
* @param {object} clientOpts client.options
63+
* @returns {object} { host, port_path_or_id, database_name }
64+
*/
65+
getRedisParams(clientOpts) {
66+
// need to replicate logic done in RedisClient
67+
// to parse the url to assign to socket.host/port
68+
// see: https://github.com/redis/node-redis/blob/5576a0db492cda2cd88e09881bc330aa956dd0f5/packages/client/lib/client/index.ts#L160
69+
if (clientOpts?.url) {
70+
const parsedURL = new URL(clientOpts.url)
71+
clientOpts.socket = Object.assign({}, clientOpts.socket, { host: parsedURL.hostname })
72+
if (parsedURL.port) {
73+
clientOpts.socket.port = parsedURL.port
74+
}
75+
76+
if (parsedURL.pathname) {
77+
clientOpts.database = parsedURL.pathname.substring(1)
78+
}
79+
}
80+
81+
return {
82+
host: clientOpts?.host || clientOpts?.socket?.host || 'localhost',
83+
port_path_or_id:
84+
clientOpts?.port || clientOpts?.socket?.path || clientOpts?.socket?.port || '6379',
85+
database_name: clientOpts?.database || 0
86+
}
87+
}
88+
}

lib/subscribers/redis/config.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2026 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
const addCommand = {
7+
path: './redis/add-command',
8+
instrumentations: [{
9+
channelName: 'nr_addCommand',
10+
module: { name: '@redis/client',
11+
versionRange: '>=4',
12+
filePath: 'dist/lib/client/commands-queue.js' },
13+
functionQuery: {
14+
className: 'RedisCommandsQueue',
15+
methodName: 'addCommand',
16+
kind: 'Async'
17+
}
18+
}]
19+
}
20+
21+
const sendCommand = {
22+
path: './redis/send-command',
23+
instrumentations: [
24+
{
25+
channelName: 'nr_sendCommand',
26+
module: { name: '@redis/client', versionRange: '>=4', filePath: 'dist/lib/client/index.js' },
27+
functionQuery: {
28+
className: 'RedisClient',
29+
methodName: 'sendCommand',
30+
kind: 'Async'
31+
}
32+
}
33+
]
34+
}
35+
36+
const executeMulti = {
37+
path: './redis/execute-multi',
38+
instrumentations: [
39+
{
40+
channelName: 'nr_executeMulti',
41+
module: { name: '@redis/client', versionRange: '>=4', filePath: 'dist/lib/client/index.js' },
42+
functionQuery: {
43+
className: 'RedisClient',
44+
methodName: '_executeMulti',
45+
kind: 'Async'
46+
}
47+
},
48+
]
49+
}
50+
51+
const executePipeline = {
52+
path: './redis/execute-multi',
53+
instrumentations: [
54+
{
55+
channelName: 'nr_executePipeline',
56+
module: { name: '@redis/client', versionRange: '>=4', filePath: 'dist/lib/client/index.js' },
57+
functionQuery: {
58+
className: 'RedisClient',
59+
methodName: '_executePipeline',
60+
kind: 'Async'
61+
}
62+
},
63+
]
64+
}
65+
66+
const clientSelect = {
67+
path: './redis/select',
68+
instrumentations: [
69+
{
70+
channelName: 'nr_select',
71+
module: { name: '@redis/client', versionRange: '>=4', filePath: 'dist/lib/client/index.js' },
72+
functionQuery: {
73+
className: 'RedisClient',
74+
methodName: 'SELECT',
75+
kind: 'Async'
76+
}
77+
}
78+
]
79+
}
80+
81+
module.exports = {
82+
'@redis/client': [
83+
addCommand,
84+
sendCommand,
85+
clientSelect,
86+
executeMulti,
87+
executePipeline
88+
]
89+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2026 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
const ClientPropagationSubscriber = require('./client-propagation')
8+
9+
/**
10+
* Propagates the context in `RedisClient._executeMulti` and
11+
* `RedisClient[redisClientOpts]` into `RedisCommandQueue.addCommand`.
12+
*/
13+
module.exports = class ClientExecuteMultiSubscriber extends ClientPropagationSubscriber {
14+
constructor({ agent, logger }) {
15+
super({ agent, logger, channelName: 'nr_executeMulti' })
16+
}
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2026 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
const ClientPropagationSubscriber = require('./client-propagation')
8+
9+
/**
10+
* Propagates the context in `RedisClient._executePipeline` and
11+
* `RedisClient[redisClientOpts]` into `RedisCommandQueue.addCommand`.
12+
*/
13+
module.exports = class ClientExecutePipelineSubscriber extends ClientPropagationSubscriber {
14+
constructor({ agent, logger }) {
15+
super({ agent, logger, channelName: 'nr_executePipeline' })
16+
}
17+
}

lib/subscribers/redis/select.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2026 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
const PropagationSubscriber = require('../propagation')
7+
const { redisClientOpts } = require('../../symbols')
8+
9+
/**
10+
* Updates `RedisClient[redisClientOpts]` to reflect the new `database_name`
11+
* as provided by the argument in `RedisClient.SELECT()`.
12+
*
13+
* This is required because `RedisClient.#selectedDB` is truly private, or
14+
* else we'd just read from that.
15+
*/
16+
module.exports = class ClientSelectSubscriber extends PropagationSubscriber {
17+
constructor({ agent, logger }) {
18+
super({ agent, logger, packageName: '@redis/client', channelName: 'nr_select' })
19+
}
20+
21+
asyncStart(data) {
22+
const { self: client, arguments: args } = data
23+
// `client[redisClientOpts]` is defined by client-propagation subscriber
24+
if (client[redisClientOpts]) {
25+
// Subsequent commands will be using said database, so update the
26+
// redisClientOpts on the client (but not the context) (feature parity with v3)
27+
client[redisClientOpts].database_name = args[0]
28+
}
29+
return super.asyncStart(data)
30+
}
31+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright 2026 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
const ClientPropagationSubscriber = require('./client-propagation')
8+
9+
/**
10+
* Propagates the context in `RedisClient.sendCommand` and
11+
* `RedisClient[redisClientOpts]` into `RedisCommandQueue.addCommand`.
12+
*/
13+
module.exports = class ClientSendCommandSubscriber extends ClientPropagationSubscriber {
14+
constructor({ agent, logger }) {
15+
super({ agent, logger, channelName: 'nr_sendCommand' })
16+
}
17+
}

test/versioned/redis/redis-v4.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ test('Redis instrumentation', async function (t) {
5959
await t.test('should log tracking metrics', function(t) {
6060
const { agent } = t.nr
6161
const { version } = require('redis/package.json')
62-
assertPackageMetrics({ agent, pkg: 'redis', version })
62+
assertPackageMetrics({ agent, pkg: '@redis/client', version, subscriberType: true })
6363
})
6464

6565
await t.test('should find Redis calls in the transaction trace', function (t, end) {

0 commit comments

Comments
 (0)