Skip to content

Commit b2284c5

Browse files
authoredMay 20, 2024··
feat: Added support for redis v4 legacyMode client.v4.<command> (#2200)
1 parent ed81852 commit b2284c5

File tree

3 files changed

+263
-1
lines changed

3 files changed

+263
-1
lines changed
 

‎lib/instrumentation/@node-redis/client.js

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ module.exports = function initialize(_agent, redis, _moduleName, shim) {
2121
const client = original.apply(this, arguments)
2222
client[opts] = getRedisParams(client.options)
2323
CMDS_TO_INSTRUMENT.forEach(instrumentClientCommand.bind(null, shim, client))
24+
if (client.options.legacyMode) {
25+
client.v4[opts] = getRedisParams(client.options)
26+
CMDS_TO_INSTRUMENT.forEach(instrumentClientCommand.bind(null, shim, client.v4))
27+
}
2428
return client
2529
}
2630
})

‎test/versioned/redis/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@
5656
"redis": ">=4.0.0"
5757
},
5858
"files": [
59-
"redis-v4.tap.js"
59+
"redis-v4.tap.js",
60+
"redis-v4-legacy-mode.tap.js"
6061
]
6162
}
6263
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
/*
2+
* Copyright 2024 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
const tap = require('tap')
9+
const test = tap.test
10+
const helper = require('../../lib/agent_helper')
11+
const { removeMatchedModules } = require('../../lib/cache-buster')
12+
const params = require('../../lib/params')
13+
const urltils = require('../../../lib/util/urltils')
14+
15+
// Indicates unique database in Redis. 0-15 supported.
16+
const DB_INDEX = 2
17+
18+
test('Redis instrumentation', function (t) {
19+
t.autoend()
20+
21+
let METRIC_HOST_NAME = null
22+
let HOST_ID = null
23+
24+
let agent
25+
let client
26+
27+
t.beforeEach(async function () {
28+
agent = helper.instrumentMockedAgent()
29+
30+
const redis = require('redis')
31+
client = redis.createClient({
32+
legacyMode: true,
33+
socket: { port: params.redis_port, host: params.redis_host }
34+
})
35+
36+
await client.connect()
37+
await client.v4.flushAll()
38+
39+
await client.v4.select(DB_INDEX)
40+
// eslint-disable-next-line new-cap
41+
42+
METRIC_HOST_NAME = urltils.isLocalhost(params.redis_host)
43+
? agent.config.getHostnameSafe()
44+
: params.redis_host
45+
HOST_ID = METRIC_HOST_NAME + '/' + params.redis_port
46+
47+
// need to capture attributes
48+
agent.config.attributes.enabled = true
49+
})
50+
51+
t.afterEach(async function () {
52+
agent && helper.unloadAgent(agent)
53+
if (client) {
54+
await client.v4.flushAll()
55+
await client.v4.quit()
56+
}
57+
// must purge require cache of redis related instrumentation
58+
// otherwise it will not re-register on subsequent test runs
59+
removeMatchedModules(/redis/)
60+
})
61+
62+
t.test('should find Redis calls in the transaction trace', function (t) {
63+
t.notOk(agent.getTransaction(), 'no transaction should be in play')
64+
helper.runInTransaction(agent, async function transactionInScope() {
65+
const transaction = agent.getTransaction()
66+
t.ok(transaction, 'transaction should be visible')
67+
68+
const ok = await client.v4.set('testkey', 'arglbargle')
69+
t.ok(agent.getTransaction(), 'transaction should still be visible')
70+
t.ok(ok, 'everything should be peachy after setting')
71+
72+
const value = await client.v4.get('testkey')
73+
t.ok(agent.getTransaction(), 'transaction should still still be visible')
74+
t.equal(value, 'arglbargle', 'redis client should still work')
75+
76+
const trace = transaction.trace
77+
t.ok(trace, 'trace should exist')
78+
t.ok(trace.root, 'root element should exist')
79+
t.equal(trace.root.children.length, 2, 'there should be only two children of the root')
80+
81+
const setSegment = trace.root.children[0]
82+
const setAttributes = setSegment.getAttributes()
83+
t.ok(setSegment, 'trace segment for set should exist')
84+
t.equal(setSegment.name, 'Datastore/operation/Redis/set', 'should register the set')
85+
t.equal(setAttributes.key, '"testkey"', 'should have the set key as a attribute')
86+
t.equal(setSegment.children.length, 0, 'set should have no children')
87+
88+
const getSegment = trace.root.children[1]
89+
const getAttributes = getSegment.getAttributes()
90+
t.ok(getSegment, 'trace segment for get should exist')
91+
92+
t.equal(getSegment.name, 'Datastore/operation/Redis/get', 'should register the get')
93+
94+
t.equal(getAttributes.key, '"testkey"', 'should have the get key as a attribute')
95+
96+
t.ok(getSegment.timer.hrDuration, 'trace segment should have ended')
97+
t.end()
98+
})
99+
})
100+
101+
t.test('should create correct metrics', function (t) {
102+
t.notOk(agent.getTransaction(), 'no transaction should be in play')
103+
helper.runInTransaction(agent, async function transactionInScope() {
104+
const transaction = agent.getTransaction()
105+
await client.v4.set('testkey', 'arglbargle')
106+
await client.v4.get('testkey')
107+
transaction.end()
108+
const unscoped = transaction.metrics.unscoped
109+
const expected = {
110+
'Datastore/all': 2,
111+
'Datastore/allWeb': 2,
112+
'Datastore/Redis/all': 2,
113+
'Datastore/Redis/allWeb': 2,
114+
'Datastore/operation/Redis/set': 1,
115+
'Datastore/operation/Redis/get': 1
116+
}
117+
checkMetrics(t, unscoped, expected)
118+
t.end()
119+
})
120+
})
121+
122+
t.test('should add `key` attribute to trace segment', function (t) {
123+
t.notOk(agent.getTransaction(), 'no transaction should be in play')
124+
agent.config.attributes.enabled = true
125+
126+
helper.runInTransaction(agent, async function () {
127+
await client.v4.set('saveme', 'foobar')
128+
129+
const segment = agent.tracer.getSegment().children[0]
130+
t.equal(segment.getAttributes().key, '"saveme"', 'should have `key` attribute')
131+
t.end()
132+
})
133+
})
134+
135+
t.test('should not add `key` attribute to trace segment', function (t) {
136+
t.notOk(agent.getTransaction(), 'no transaction should be in play')
137+
agent.config.attributes.enabled = false
138+
139+
helper.runInTransaction(agent, async function () {
140+
await client.v4.set('saveme', 'foobar')
141+
142+
const segment = agent.tracer.getSegment().children[0]
143+
t.notOk(segment.getAttributes().key, 'should not have `key` attribute')
144+
t.end()
145+
})
146+
})
147+
148+
t.test('should add datastore instance attributes to trace segments', function (t) {
149+
t.notOk(agent.getTransaction(), 'no transaction should be in play')
150+
// Enable.
151+
agent.config.datastore_tracer.instance_reporting.enabled = true
152+
agent.config.datastore_tracer.database_name_reporting.enabled = true
153+
154+
helper.runInTransaction(agent, async function transactionInScope() {
155+
const transaction = agent.getTransaction()
156+
await client.v4.set('testkey', 'arglbargle')
157+
158+
const trace = transaction.trace
159+
const setSegment = trace.root.children[0]
160+
const attributes = setSegment.getAttributes()
161+
t.equal(attributes.host, METRIC_HOST_NAME, 'should have host as attribute')
162+
t.equal(
163+
attributes.port_path_or_id,
164+
String(params.redis_port),
165+
'should have port as attribute'
166+
)
167+
t.equal(attributes.database_name, String(DB_INDEX), 'should have database id as attribute')
168+
t.equal(attributes.product, 'Redis', 'should have product attribute')
169+
t.end()
170+
})
171+
})
172+
173+
t.test('should not add instance attributes/metrics when disabled', function (t) {
174+
t.notOk(agent.getTransaction(), 'no transaction should be in play')
175+
// disable
176+
agent.config.datastore_tracer.instance_reporting.enabled = false
177+
agent.config.datastore_tracer.database_name_reporting.enabled = false
178+
179+
helper.runInTransaction(agent, async function transactionInScope() {
180+
const transaction = agent.getTransaction()
181+
await client.v4.set('testkey', 'arglbargle')
182+
183+
const setSegment = transaction.trace.root.children[0]
184+
const attributes = setSegment.getAttributes()
185+
t.equal(attributes.host, undefined, 'should not have host attribute')
186+
t.equal(attributes.port_path_or_id, undefined, 'should not have port attribute')
187+
t.equal(attributes.database_name, undefined, 'should not have db name attribute')
188+
189+
transaction.end()
190+
const unscoped = transaction.metrics.unscoped
191+
t.equal(
192+
unscoped['Datastore/instance/Redis/' + HOST_ID],
193+
undefined,
194+
'should not have instance metric'
195+
)
196+
t.end()
197+
})
198+
})
199+
200+
t.test('should follow selected database', function (t) {
201+
t.notOk(agent.getTransaction(), 'no transaction should be in play')
202+
let transaction = null
203+
const SELECTED_DB = 3
204+
helper.runInTransaction(agent, async function (tx) {
205+
transaction = tx
206+
await client.v4.set('select:test:key', 'foo')
207+
t.ok(agent.getTransaction(), 'should not lose transaction state')
208+
209+
await client.v4.select(SELECTED_DB)
210+
t.ok(agent.getTransaction(), 'should not lose transaction state')
211+
212+
await client.v4.set('select:test:key:2', 'bar')
213+
t.ok(agent.getTransaction(), 'should not lose transaction state')
214+
transaction.end()
215+
verify()
216+
t.end()
217+
})
218+
219+
function verify() {
220+
const setSegment1 = transaction.trace.root.children[0]
221+
const selectSegment = transaction.trace.root.children[1]
222+
const setSegment2 = transaction.trace.root.children[2]
223+
224+
t.equal(setSegment1.name, 'Datastore/operation/Redis/set', 'should register the first set')
225+
t.equal(
226+
setSegment1.getAttributes().database_name,
227+
String(DB_INDEX),
228+
'should have the starting database id as attribute for the first set'
229+
)
230+
t.equal(selectSegment.name, 'Datastore/operation/Redis/select', 'should register the select')
231+
t.equal(
232+
selectSegment.getAttributes().database_name,
233+
String(DB_INDEX),
234+
'should have the starting database id as attribute for the select'
235+
)
236+
t.equal(setSegment2.name, 'Datastore/operation/Redis/set', 'should register the second set')
237+
t.equal(
238+
setSegment2.getAttributes().database_name,
239+
String(SELECTED_DB),
240+
'should have the selected database id as attribute for the second set'
241+
)
242+
}
243+
})
244+
})
245+
246+
function checkMetrics(t, metrics, expected) {
247+
Object.keys(expected).forEach(function (name) {
248+
t.ok(metrics[name], 'should have metric ' + name)
249+
if (metrics[name]) {
250+
t.equal(
251+
metrics[name].callCount,
252+
expected[name],
253+
'should have ' + expected[name] + ' calls for ' + name
254+
)
255+
}
256+
})
257+
}

0 commit comments

Comments
 (0)
Please sign in to comment.