Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: monitor io-valkey #5484

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/plugins.yml
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,7 @@ jobs:
ports:
- 6379:6379
env:
PLUGINS: redis|ioredis # TODO: move ioredis to its own job
PLUGINS: redis|ioredis|iovalkey # TODO: move ioredis & iovalkey to its own job
SERVICES: redis
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Expand Down
4 changes: 4 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ tracer.use('pg', {
<h5 id="ioredis"></h5>
<h5 id="ioredis-tags"></h5>
<h5 id="ioredis-config"></h5>
<h5 id="iovalkey"></h5>
<h5 id="iovalkey-tags"></h5>
<h5 id="iovalkey-config"></h5>
<h5 id="jest"></h5>
<h5 id="kafkajs"></h5>
<h5 id="koa"></h5>
Expand Down Expand Up @@ -126,6 +129,7 @@ tracer.use('pg', {
* [http](./interfaces/export_.plugins.http.html)
* [http2](./interfaces/export_.plugins.http2.html)
* [ioredis](./interfaces/export_.plugins.ioredis.html)
* [iovalkey](./interfaces/export_.plugins.iovalkey.html)
* [jest](./interfaces/export_.plugins.jest.html)
* [kafkajs](./interfaces/export_.plugins.kafkajs.html)
* [knex](./interfaces/export_.plugins.knex.html)
Expand Down
1 change: 1 addition & 0 deletions docs/add-redirects.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ declare -a plugins=(
"http"
"http2"
"ioredis"
"iovalkey"
"jest"
"kafkajs"
"knex"
Expand Down
3 changes: 3 additions & 0 deletions docs/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,9 @@ tracer.use('http2', {
tracer.use('ioredis');
tracer.use('ioredis', redisOptions);
tracer.use('ioredis', { splitByInstance: true });
tracer.use('iovalkey');
tracer.use('iovalkey', redisOptions);
tracer.use('iovalkey', { splitByInstance: true });
tracer.use('jest');
tracer.use('jest', { service: 'jest-service' });
tracer.use('kafkajs');
Expand Down
48 changes: 48 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ interface Plugins {
"http": tracer.plugins.http;
"http2": tracer.plugins.http2;
"ioredis": tracer.plugins.ioredis;
"iovalkey": tracer.plugins.iovalkey;
"jest": tracer.plugins.jest;
"kafkajs": tracer.plugins.kafkajs
"knex": tracer.plugins.knex;
Expand Down Expand Up @@ -1593,6 +1594,53 @@ declare namespace tracer {
splitByInstance?: boolean;
}

/**
* This plugin automatically instruments the
* [iovalkey](https://github.com/valkey-io/iovalkey) module.
*/
interface iovalkey extends Instrumentation {
/**
* List of commands that should be instrumented. Commands must be in
* lowercase for example 'xread'.
*
* @default /^.*$/
*/
allowlist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[];

/**
* Deprecated in favor of `allowlist`.
*
* @deprecated
* @hidden
*/
whitelist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[];

/**
* List of commands that should not be instrumented. Takes precedence over
* allowlist if a command matches an entry in both. Commands must be in
* lowercase for example 'xread'.
*
* @default []
*/
blocklist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[];

/**
* Deprecated in favor of `blocklist`.
*
* @deprecated
* @hidden
*/
blacklist?: string | RegExp | ((command: string) => boolean) | (string | RegExp | ((command: string) => boolean))[];

/**
* Whether to use a different service name for each Redis instance based
* on the configured connection name of the client.
*
* @default false
*/
splitByInstance?: boolean;
}

/**
* This plugin automatically instruments the
* [jest](https://github.com/jestjs/jest) module.
Expand Down
1 change: 1 addition & 0 deletions packages/datadog-instrumentations/src/helpers/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ module.exports = {
http2: () => require('../http2'),
https: () => require('../http'),
ioredis: () => require('../ioredis'),
iovalkey: () => require('../valkey'),
'jest-circus': () => require('../jest'),
'jest-config': () => require('../jest'),
'jest-environment-node': () => require('../jest'),
Expand Down
51 changes: 51 additions & 0 deletions packages/datadog-instrumentations/src/iovalkey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use strict'

const {
channel,
addHook,
AsyncResource
} = require('./helpers/instrument')
const shimmer = require('../../datadog-shimmer')

const startCh = channel('apm:iovalkey:command:start')
const finishCh = channel('apm:iovalkey:command:finish')
const errorCh = channel('apm:iovalkey:command:error')

addHook({ name: 'iovalkey', versions: ['>=0'] }, Redis => {
shimmer.wrap(Redis.prototype, 'sendCommand', sendCommand => function (command, stream) {
if (!startCh.hasSubscribers) return sendCommand.apply(this, arguments)

if (!command || !command.promise) return sendCommand.apply(this, arguments)

const options = this.options || {}
const connectionName = options.connectionName
const db = options.db
const connectionOptions = { host: options.host, port: options.port }

const asyncResource = new AsyncResource('bound-anonymous-fn')
return asyncResource.runInAsyncScope(() => {
startCh.publish({ db, command: command.name, args: command.args, connectionOptions, connectionName })

const onResolve = asyncResource.bind(() => finish(finishCh, errorCh))
const onReject = asyncResource.bind(err => finish(finishCh, errorCh, err))

command.promise.then(onResolve, onReject)

try {
return sendCommand.apply(this, arguments)
} catch (err) {
errorCh.publish(err)

throw err
}
})
})
return Redis
})

function finish (finishCh, errorCh, error) {
if (error) {
errorCh.publish(error)
}
finishCh.publish()
}
11 changes: 11 additions & 0 deletions packages/datadog-plugin-iovalkey/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict'

const RedisPlugin = require('../../datadog-plugin-redis/src')

class IOValkeyPlugin extends RedisPlugin {
static get id () {
return 'iovalkey'
}
}

module.exports = IOValkeyPlugin
184 changes: 184 additions & 0 deletions packages/datadog-plugin-iovalkey/test/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
'use strict'

const agent = require('../../dd-trace/test/plugins/agent')
const { breakThen, unbreakThen } = require('../../dd-trace/test/plugins/helpers')
const { ERROR_MESSAGE, ERROR_TYPE, ERROR_STACK } = require('../../dd-trace/src/constants')

const { expectedSchema, rawExpectedSchema } = require('./naming')

describe('Plugin', () => {
let Redis
let redis
let tracer

describe('iovalkey', () => {
withVersions('iovalkey', 'iovalkey', version => {
beforeEach(() => {
tracer = require('../../dd-trace')
Redis = require(`../../../versions/iovalkey@${version}`).get()
redis = new Redis({ connectionName: 'test' })
})

afterEach(() => {
unbreakThen(Promise.prototype)
redis.quit()
})

describe('without configuration', () => {
before(() => agent.load(['iovalkey']))

after(() => agent.close({ ritmReset: false }))

it('should do automatic instrumentation when using callbacks', done => {
agent.use(() => {}) // wait for initial info command
agent
.use(traces => {
expect(traces[0][0]).to.have.property('name', expectedSchema.outbound.opName)
expect(traces[0][0]).to.have.property('service', expectedSchema.outbound.serviceName)
expect(traces[0][0]).to.have.property('resource', 'get')
expect(traces[0][0]).to.have.property('type', 'redis')
expect(traces[0][0].meta).to.have.property('component', 'iovalkey')
expect(traces[0][0].meta).to.have.property('db.name', '0')
expect(traces[0][0].meta).to.have.property('db.type', 'redis')
expect(traces[0][0].meta).to.have.property('span.kind', 'client')
expect(traces[0][0].meta).to.have.property('out.host', 'localhost')
expect(traces[0][0].meta).to.have.property('redis.raw_command', 'GET foo')
expect(traces[0][0].metrics).to.have.property('network.destination.port', 6379)
})
.then(done)
.catch(done)

redis.get('foo').catch(done)
})

it('should run the callback in the parent context', () => {
const span = {}

return tracer.scope().activate(span, () => {
return redis.get('foo')
.then(() => {
expect(tracer.scope().active()).to.equal(span)
})
})
})

it('should handle errors', done => {
let error

agent.use(() => {}) // wait for initial info command
agent
.use(traces => {
expect(traces[0][0]).to.have.property('error', 1)
expect(traces[0][0].meta).to.have.property(ERROR_TYPE, error.name)
expect(traces[0][0].meta).to.have.property(ERROR_MESSAGE, error.message)
expect(traces[0][0].meta).to.have.property(ERROR_STACK, error.stack)
expect(traces[0][0].meta).to.have.property('component', 'iovalkey')
})
.then(done)
.catch(done)

redis.set('foo', 123, 'bar')
.catch(err => {
error = err
})
})

it('should work with userland promises', done => {
agent.use(() => {}) // wait for initial info command
agent
.use(traces => {
expect(traces[0][0]).to.have.property('name', expectedSchema.outbound.opName)
expect(traces[0][0]).to.have.property('service', expectedSchema.outbound.serviceName)
expect(traces[0][0]).to.have.property('resource', 'get')
expect(traces[0][0]).to.have.property('type', 'redis')
expect(traces[0][0].meta).to.have.property('db.name', '0')
expect(traces[0][0].meta).to.have.property('db.type', 'redis')
expect(traces[0][0].meta).to.have.property('span.kind', 'client')
expect(traces[0][0].meta).to.have.property('out.host', 'localhost')
expect(traces[0][0].meta).to.have.property('redis.raw_command', 'GET foo')
expect(traces[0][0].meta).to.have.property('component', 'iovalkey')
expect(traces[0][0].metrics).to.have.property('network.destination.port', 6379)
})
.then(done)
.catch(done)

breakThen(Promise.prototype)

redis.get('foo').catch(done)
})

withNamingSchema(
done => redis.get('foo').catch(done),
rawExpectedSchema.outbound
)
})

describe('with configuration', () => {
before(() => agent.load('iovalkey', {
service: 'custom',
splitByInstance: true,
allowlist: ['get']
}))

after(() => agent.close({ ritmReset: false }))

it('should be configured with the correct values', done => {
agent
.use(traces => {
expect(traces[0][0]).to.have.property('service', 'custom-test')
})
.then(done)
.catch(done)

redis.get('foo').catch(done)
})

it('should be able to filter commands', done => {
agent.use(() => {}) // wait for initial command
agent
.use(traces => {
expect(traces[0][0]).to.have.property('resource', 'get')
})
.then(done)
.catch(done)

redis.get('foo').catch(done)
})

withNamingSchema(
done => redis.get('foo').catch(done),
{
v0: {
opName: 'redis.command',
serviceName: 'custom-test'
},
v1: {
opName: 'redis.command',
serviceName: 'custom'
}
}
)
})

describe('with legacy configuration', () => {
before(() => agent.load('iovalkey', {
whitelist: ['get']
}))

after(() => agent.close({ ritmReset: false }))

it('should be able to filter commands', done => {
agent.use(() => {}) // wait for initial command
agent
.use(traces => {
expect(traces[0][0]).to.have.property('resource', 'get')
})
.then(done)
.catch(done)

redis.get('foo').catch(done)
})
})
})
})
})
Loading
Loading