Skip to content

Commit 397bc59

Browse files
author
Nicolas Hansse
committed
feat: monitor io-valkey
1 parent 1662072 commit 397bc59

File tree

15 files changed

+379
-1
lines changed

15 files changed

+379
-1
lines changed

.github/workflows/plugins.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -978,7 +978,7 @@ jobs:
978978
ports:
979979
- 6379:6379
980980
env:
981-
PLUGINS: redis|ioredis # TODO: move ioredis to its own job
981+
PLUGINS: redis|ioredis|iovalkey # TODO: move ioredis & iovalkey to its own job
982982
SERVICES: redis
983983
steps:
984984
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

docs/API.md

+4
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ tracer.use('pg', {
5656
<h5 id="ioredis"></h5>
5757
<h5 id="ioredis-tags"></h5>
5858
<h5 id="ioredis-config"></h5>
59+
<h5 id="iovalkey"></h5>
60+
<h5 id="iovalkey-tags"></h5>
61+
<h5 id="iovalkey-config"></h5>
5962
<h5 id="jest"></h5>
6063
<h5 id="kafkajs"></h5>
6164
<h5 id="koa"></h5>
@@ -126,6 +129,7 @@ tracer.use('pg', {
126129
* [http](./interfaces/export_.plugins.http.html)
127130
* [http2](./interfaces/export_.plugins.http2.html)
128131
* [ioredis](./interfaces/export_.plugins.ioredis.html)
132+
* [iovalkey](./interfaces/export_.plugins.iovalkey.html)
129133
* [jest](./interfaces/export_.plugins.jest.html)
130134
* [kafkajs](./interfaces/export_.plugins.kafkajs.html)
131135
* [knex](./interfaces/export_.plugins.knex.html)

docs/add-redirects.sh

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ declare -a plugins=(
3535
"http"
3636
"http2"
3737
"ioredis"
38+
"iovalkey"
3839
"jest"
3940
"kafkajs"
4041
"knex"

docs/test.ts

+3
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,9 @@ tracer.use('http2', {
343343
tracer.use('ioredis');
344344
tracer.use('ioredis', redisOptions);
345345
tracer.use('ioredis', { splitByInstance: true });
346+
tracer.use('iovalkey');
347+
tracer.use('iovalkey', redisOptions);
348+
tracer.use('iovalkey', { splitByInstance: true });
346349
tracer.use('jest');
347350
tracer.use('jest', { service: 'jest-service' });
348351
tracer.use('kafkajs');

index.d.ts

+48
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ interface Plugins {
172172
"http": tracer.plugins.http;
173173
"http2": tracer.plugins.http2;
174174
"ioredis": tracer.plugins.ioredis;
175+
"iovalkey": tracer.plugins.iovalkey;
175176
"jest": tracer.plugins.jest;
176177
"kafkajs": tracer.plugins.kafkajs
177178
"knex": tracer.plugins.knex;
@@ -1593,6 +1594,53 @@ declare namespace tracer {
15931594
splitByInstance?: boolean;
15941595
}
15951596

1597+
/**
1598+
* This plugin automatically instruments the
1599+
* [iovalkey](https://github.com/valkey-io/iovalkey) module.
1600+
*/
1601+
interface iovalkey extends Instrumentation {
1602+
/**
1603+
* List of commands that should be instrumented. Commands must be in
1604+
* lowercase for example 'xread'.
1605+
*
1606+
* @default /^.*$/
1607+
*/
1608+
allowlist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[];
1609+
1610+
/**
1611+
* Deprecated in favor of `allowlist`.
1612+
*
1613+
* @deprecated
1614+
* @hidden
1615+
*/
1616+
whitelist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[];
1617+
1618+
/**
1619+
* List of commands that should not be instrumented. Takes precedence over
1620+
* allowlist if a command matches an entry in both. Commands must be in
1621+
* lowercase for example 'xread'.
1622+
*
1623+
* @default []
1624+
*/
1625+
blocklist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[];
1626+
1627+
/**
1628+
* Deprecated in favor of `blocklist`.
1629+
*
1630+
* @deprecated
1631+
* @hidden
1632+
*/
1633+
blacklist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[];
1634+
1635+
/**
1636+
* Whether to use a different service name for each Redis instance based
1637+
* on the configured connection name of the client.
1638+
*
1639+
* @default false
1640+
*/
1641+
splitByInstance?: boolean;
1642+
}
1643+
15961644
/**
15971645
* This plugin automatically instruments the
15981646
* [jest](https://github.com/jestjs/jest) module.

packages/datadog-instrumentations/src/helpers/hooks.js

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ module.exports = {
6161
http2: () => require('../http2'),
6262
https: () => require('../http'),
6363
ioredis: () => require('../ioredis'),
64+
iovalkey: () => require('../valkey'),
6465
'jest-circus': () => require('../jest'),
6566
'jest-config': () => require('../jest'),
6667
'jest-environment-node': () => require('../jest'),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use strict'
2+
3+
const {
4+
channel,
5+
addHook,
6+
AsyncResource
7+
} = require('./helpers/instrument')
8+
const shimmer = require('../../datadog-shimmer')
9+
10+
const startCh = channel('apm:iovalkey:command:start')
11+
const finishCh = channel('apm:iovalkey:command:finish')
12+
const errorCh = channel('apm:iovalkey:command:error')
13+
14+
addHook({ name: 'iovalkey', versions: ['>=0'] }, Redis => {
15+
shimmer.wrap(Redis.prototype, 'sendCommand', sendCommand => function (command, stream) {
16+
if (!startCh.hasSubscribers) return sendCommand.apply(this, arguments)
17+
18+
if (!command || !command.promise) return sendCommand.apply(this, arguments)
19+
20+
const options = this.options || {}
21+
const connectionName = options.connectionName
22+
const db = options.db
23+
const connectionOptions = { host: options.host, port: options.port }
24+
25+
const asyncResource = new AsyncResource('bound-anonymous-fn')
26+
return asyncResource.runInAsyncScope(() => {
27+
startCh.publish({ db, command: command.name, args: command.args, connectionOptions, connectionName })
28+
29+
const onResolve = asyncResource.bind(() => finish(finishCh, errorCh))
30+
const onReject = asyncResource.bind(err => finish(finishCh, errorCh, err))
31+
32+
command.promise.then(onResolve, onReject)
33+
34+
try {
35+
return sendCommand.apply(this, arguments)
36+
} catch (err) {
37+
errorCh.publish(err)
38+
39+
throw err
40+
}
41+
})
42+
})
43+
return Redis
44+
})
45+
46+
function finish (finishCh, errorCh, error) {
47+
if (error) {
48+
errorCh.publish(error)
49+
}
50+
finishCh.publish()
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'use strict'
2+
3+
const RedisPlugin = require('../../datadog-plugin-redis/src')
4+
5+
class IOValkeyPlugin extends RedisPlugin {
6+
static get id () {
7+
return 'iovalkey'
8+
}
9+
}
10+
11+
module.exports = IOValkeyPlugin
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
'use strict'
2+
3+
const agent = require('../../dd-trace/test/plugins/agent')
4+
const { breakThen, unbreakThen } = require('../../dd-trace/test/plugins/helpers')
5+
const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants')
6+
7+
const { expectedSchema, rawExpectedSchema } = require('./naming')
8+
9+
describe('Plugin', () => {
10+
let Redis
11+
let redis
12+
let tracer
13+
14+
describe('iovalkey', () => {
15+
withVersions('iovalkey', 'iovalkey', version => {
16+
beforeEach(() => {
17+
tracer = require('../../dd-trace')
18+
Redis = require(`../../../versions/iovalkey@${version}`).get()
19+
redis = new Redis({ connectionName: 'test' })
20+
})
21+
22+
afterEach(() => {
23+
unbreakThen(Promise.prototype)
24+
redis.quit()
25+
})
26+
27+
describe('without configuration', () => {
28+
before(() => agent.load(['iovalkey']))
29+
30+
after(() => agent.close({ ritmReset: false }))
31+
32+
it('should do automatic instrumentation when using callbacks', done => {
33+
agent.use(() => {}) // wait for initial info command
34+
agent
35+
.use(traces => {
36+
expect(traces[0][0]).to.have.property('name', expectedSchema.outbound.opName)
37+
expect(traces[0][0]).to.have.property('service', expectedSchema.outbound.serviceName)
38+
expect(traces[0][0]).to.have.property('resource', 'get')
39+
expect(traces[0][0]).to.have.property('type', 'redis')
40+
expect(traces[0][0].meta).to.have.property('component', 'iovalkey')
41+
expect(traces[0][0].meta).to.have.property('db.name', '0')
42+
expect(traces[0][0].meta).to.have.property('db.type', 'redis')
43+
expect(traces[0][0].meta).to.have.property('span.kind', 'client')
44+
expect(traces[0][0].meta).to.have.property('out.host', 'localhost')
45+
expect(traces[0][0].meta).to.have.property('redis.raw_command', 'GET foo')
46+
expect(traces[0][0].metrics).to.have.property('network.destination.port', 6379)
47+
})
48+
.then(done)
49+
.catch(done)
50+
51+
redis.get('foo').catch(done)
52+
})
53+
54+
it('should run the callback in the parent context', () => {
55+
const span = {}
56+
57+
return tracer.scope().activate(span, () => {
58+
return redis.get('foo')
59+
.then(() => {
60+
expect(tracer.scope().active()).to.equal(span)
61+
})
62+
})
63+
})
64+
65+
it('should handle errors', done => {
66+
let error
67+
68+
agent.use(() => {}) // wait for initial info command
69+
agent
70+
.use(traces => {
71+
expect(traces[0][0]).to.have.property('error', 1)
72+
expect(traces[0][0].meta).to.have.property(ERROR_TYPE, error.name)
73+
expect(traces[0][0].meta).to.have.property(ERROR_MESSAGE, error.message)
74+
expect(traces[0][0].meta).to.have.property(ERROR_STACK, error.stack)
75+
expect(traces[0][0].meta).to.have.property('component', 'iovalkey')
76+
})
77+
.then(done)
78+
.catch(done)
79+
80+
redis.set('foo', 123, 'bar')
81+
.catch(err => {
82+
error = err
83+
})
84+
})
85+
86+
it('should work with userland promises', done => {
87+
agent.use(() => {}) // wait for initial info command
88+
agent
89+
.use(traces => {
90+
expect(traces[0][0]).to.have.property('name', expectedSchema.outbound.opName)
91+
expect(traces[0][0]).to.have.property('service', expectedSchema.outbound.serviceName)
92+
expect(traces[0][0]).to.have.property('resource', 'get')
93+
expect(traces[0][0]).to.have.property('type', 'redis')
94+
expect(traces[0][0].meta).to.have.property('db.name', '0')
95+
expect(traces[0][0].meta).to.have.property('db.type', 'redis')
96+
expect(traces[0][0].meta).to.have.property('span.kind', 'client')
97+
expect(traces[0][0].meta).to.have.property('out.host', 'localhost')
98+
expect(traces[0][0].meta).to.have.property('redis.raw_command', 'GET foo')
99+
expect(traces[0][0].meta).to.have.property('component', 'iovalkey')
100+
expect(traces[0][0].metrics).to.have.property('network.destination.port', 6379)
101+
})
102+
.then(done)
103+
.catch(done)
104+
105+
breakThen(Promise.prototype)
106+
107+
redis.get('foo').catch(done)
108+
})
109+
110+
withNamingSchema(
111+
done => redis.get('foo').catch(done),
112+
rawExpectedSchema.outbound
113+
)
114+
})
115+
116+
describe('with configuration', () => {
117+
before(() => agent.load('iovalkey', {
118+
service: 'custom',
119+
splitByInstance: true,
120+
allowlist: ['get']
121+
}))
122+
123+
after(() => agent.close({ ritmReset: false }))
124+
125+
it('should be configured with the correct values', done => {
126+
agent
127+
.use(traces => {
128+
expect(traces[0][0]).to.have.property('service', 'custom-test')
129+
})
130+
.then(done)
131+
.catch(done)
132+
133+
redis.get('foo').catch(done)
134+
})
135+
136+
it('should be able to filter commands', done => {
137+
agent.use(() => {}) // wait for initial command
138+
agent
139+
.use(traces => {
140+
expect(traces[0][0]).to.have.property('resource', 'get')
141+
})
142+
.then(done)
143+
.catch(done)
144+
145+
redis.get('foo').catch(done)
146+
})
147+
148+
withNamingSchema(
149+
done => redis.get('foo').catch(done),
150+
{
151+
v0: {
152+
opName: 'redis.command',
153+
serviceName: 'custom-test'
154+
},
155+
v1: {
156+
opName: 'redis.command',
157+
serviceName: 'custom'
158+
}
159+
}
160+
)
161+
})
162+
163+
describe('with legacy configuration', () => {
164+
before(() => agent.load('iovalkey', {
165+
whitelist: ['get']
166+
}))
167+
168+
after(() => agent.close({ ritmReset: false }))
169+
170+
it('should be able to filter commands', done => {
171+
agent.use(() => {}) // wait for initial command
172+
agent
173+
.use(traces => {
174+
expect(traces[0][0]).to.have.property('resource', 'get')
175+
})
176+
.then(done)
177+
.catch(done)
178+
179+
redis.get('foo').catch(done)
180+
})
181+
})
182+
})
183+
})
184+
})

0 commit comments

Comments
 (0)