Skip to content
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
3 changes: 2 additions & 1 deletion dev-packages/node-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@
"graphql-tag": "^2.12.6",
"hono": "^4.12.18",
"http-terminator": "^3.2.0",
"ioredis": "^5.4.1",
"ioredis": "5.10.1",
"ioredis-5": "npm:ioredis@^5.11.0",
"kafkajs": "2.2.4",
"knex": "^2.5.1",
"lru-memoizer": "2.3.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
version: '3.9'

services:
db:
image: redis:latest
restart: always
container_name: integration-tests-ioredis-dc
ports:
- '6379:6379'
healthcheck:
test: ['CMD-SHELL', 'redis-cli ping | grep -q PONG']
interval: 2s
timeout: 3s
retries: 30
start_period: 5s
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
integrations: [Sentry.redisIntegration({ cachePrefixes: ['dc-cache:'] })],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as Sentry from '@sentry/node';

async function run() {
// Yield a microtick so the DC subscriber (deferred via Promise.resolve().then)
// is registered before ioredis creates its native TracingChannels on import.
await Promise.resolve();

const { default: Redis } = await import('ioredis-5');
const redisClient = new Redis({ host: '127.0.0.1', port: 6379, lazyConnect: true });

await redisClient.connect();

await Sentry.startSpan(
{
name: 'Test Span IORedis 5.11 DC',
op: 'test-span-ioredis-5-11-dc',
},
async () => {
try {
await redisClient.set('dc-test-key', 'test-value');
await redisClient.set('dc-cache:test-key', 'test-value');

await redisClient.set('dc-cache:test-key-ex', 'test-value', 'EX', 10);

await redisClient.get('dc-test-key');
await redisClient.get('dc-cache:test-key');
await redisClient.get('dc-cache:unavailable-data');

await redisClient.mget('dc-test-key', 'dc-cache:test-key', 'dc-cache:unavailable-data');
} finally {
await redisClient.disconnect();
}
},
);
}

run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { afterAll, describe, expect } from 'vitest';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

describe('ioredis v5.11 diagnostics_channel auto instrumentation', () => {
afterAll(() => {
cleanupChildProcesses();
});

const EXPECTED_TRANSACTION = {
transaction: 'Test Span IORedis 5.11 DC',
spans: expect.arrayContaining([
expect.objectContaining({
op: 'db.redis',
origin: 'auto.db.redis.diagnostic_channel',
data: expect.objectContaining({
'sentry.op': 'db.redis',
'sentry.origin': 'auto.db.redis.diagnostic_channel',
'db.system': 'redis',
'db.statement': 'set dc-test-key [1 other arguments]',
}),
}),
expect.objectContaining({
description: 'dc-cache:test-key',
op: 'cache.put',
origin: 'auto.db.redis.diagnostic_channel',
data: expect.objectContaining({
'sentry.origin': 'auto.db.redis.diagnostic_channel',
'db.statement': 'set dc-cache:test-key [1 other arguments]',
'cache.key': ['dc-cache:test-key'],
'cache.item_size': 2,
}),
}),
expect.objectContaining({
description: 'dc-cache:test-key-ex',
op: 'cache.put',
origin: 'auto.db.redis.diagnostic_channel',
data: expect.objectContaining({
'sentry.origin': 'auto.db.redis.diagnostic_channel',
'db.statement': 'set dc-cache:test-key-ex [3 other arguments]',
'cache.key': ['dc-cache:test-key-ex'],
'cache.item_size': 2,
}),
}),
expect.objectContaining({
op: 'db.redis',
origin: 'auto.db.redis.diagnostic_channel',
data: expect.objectContaining({
'sentry.op': 'db.redis',
'sentry.origin': 'auto.db.redis.diagnostic_channel',
'db.system': 'redis',
'db.statement': 'get dc-test-key',
}),
}),
expect.objectContaining({
description: 'dc-cache:test-key',
op: 'cache.get',
origin: 'auto.db.redis.diagnostic_channel',
data: expect.objectContaining({
'sentry.origin': 'auto.db.redis.diagnostic_channel',
'db.statement': 'get dc-cache:test-key',
'cache.hit': true,
'cache.key': ['dc-cache:test-key'],
'cache.item_size': 10,
}),
}),
expect.objectContaining({
description: 'dc-cache:unavailable-data',
op: 'cache.get',
origin: 'auto.db.redis.diagnostic_channel',
data: expect.objectContaining({
'sentry.origin': 'auto.db.redis.diagnostic_channel',
'db.statement': 'get dc-cache:unavailable-data',
'cache.hit': false,
'cache.key': ['dc-cache:unavailable-data'],
}),
}),
expect.objectContaining({
op: 'db.redis',
origin: 'auto.db.redis.diagnostic_channel',
data: expect.objectContaining({
'sentry.op': 'db.redis',
'sentry.origin': 'auto.db.redis.diagnostic_channel',
'db.system': 'redis',
'db.statement': 'mget [3 other arguments]',
}),
}),
]),
Comment thread
cursor[bot] marked this conversation as resolved.
};

const EXPECTED_CONNECT = {
transaction: 'redis-connect',
};

createEsmAndCjsTests(__dirname, 'scenario-ioredis-5-11.mjs', 'instrument.mjs', (createTestRunner, test) => {
test('creates spans for ioredis v5.11 commands via diagnostics_channel', { timeout: 75_000 }, async () => {
await createTestRunner()
.withDockerCompose({ workingDirectory: [__dirname] })
.expect({ transaction: EXPECTED_CONNECT })
.expect({ transaction: EXPECTED_TRANSACTION })
.start()
.completed();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ import {
} from './vendored/semconv';
import type { IORedisInstrumentationConfig } from './vendored/types';

// Channel names as published by node-redis >= 5.12.0.
// Hardcoded so we don't import `redis` at module-load time.
const CHANNEL_COMMAND = 'node-redis:command';
const CHANNEL_BATCH = 'node-redis:batch';
const CHANNEL_CONNECT = 'node-redis:connect';
// Channel names as published by node-redis >= 5.12.0 and ioredis >= 5.11.0.
const CHANNEL_REDIS_COMMAND = 'node-redis:command';
const CHANNEL_REDIS_BATCH = 'node-redis:batch';
const CHANNEL_REDIS_CONNECT = 'node-redis:connect';
const CHANNEL_IOREDIS_COMMAND = 'ioredis:command';
const CHANNEL_IOREDIS_CONNECT = 'ioredis:connect';

const ORIGIN = 'auto.db.redis.diagnostic_channel';

interface CommandData {
interface RedisCommandData {
command: string;
args: Array<string | Buffer>;
database?: number;
Expand All @@ -34,7 +35,19 @@ interface CommandData {
error?: Error;
}

interface BatchData {
interface IORedisCommandData {
command: string;
args: string[];
batchMode?: 'MULTI';
batchSize?: number;
database?: number;
serverAddress?: string;
serverPort?: number;
result?: unknown;
error?: Error;
}

interface RedisBatchData {
batchMode?: 'MULTI' | 'PIPELINE';
batchSize?: number;
database?: number;
Expand Down Expand Up @@ -73,22 +86,25 @@ export function subscribeRedisDiagnosticChannels(responseHook?: IORedisInstrumen
if (subscribed) return;

try {
setupCommandChannel();
setupBatchChannel();
setupConnectChannel();
setupCommandChannel<RedisCommandData>(CHANNEL_REDIS_COMMAND, data => data.args.slice(1));
setupBatchChannel(CHANNEL_REDIS_BATCH, data => (data.batchMode === 'PIPELINE' ? 'PIPELINE' : 'MULTI'));
setupConnectChannel(CHANNEL_REDIS_CONNECT);
setupCommandChannel<IORedisCommandData>(CHANNEL_IOREDIS_COMMAND, data => data.args);
setupConnectChannel(CHANNEL_IOREDIS_CONNECT);
subscribed = true;
} catch {
// tracingChannel from @sentry/opentelemetry requires `node:diagnostics_channel`.
// On runtimes where it isn't available, fail closed.
}
}

function setupCommandChannel(): void {
const channel = tracingChannel<CommandData>(CHANNEL_COMMAND, data => {
// node-redis >= 5.12.0 includes the command name as args[0] in the DC payload.
// Strip it so serialization and cache key extraction see only the actual arguments.
const actualArgs = data.args.slice(1);
const statement = safeSerialize(data.command, actualArgs);
function setupCommandChannel<T extends RedisCommandData | IORedisCommandData>(
channelName: string,
getCommandArgs: (data: T) => Array<string | Buffer>,
): void {
const channel = tracingChannel<T>(channelName, data => {
const args = getCommandArgs(data);
const statement = safeSerialize(data.command, args);
return startSpanManual(
{
name: `redis-${data.command}`,
Expand All @@ -113,8 +129,7 @@ function setupCommandChannel(): void {
const span = data._sentrySpan;
// only end if error handler isn't going to
if (!span || data.error) return;
// Same slice: strip command name from args before passing to the response hook.
runResponseHook(span, data.command, data.args.slice(1), data.result);
runResponseHook(span, data.command, getCommandArgs(data), data.result);
span.end();
},
error: data => {
Expand All @@ -128,13 +143,11 @@ function setupCommandChannel(): void {
});
}

function setupBatchChannel(): void {
const channel = tracingChannel<BatchData>(CHANNEL_BATCH, data => {
const operationName = data.batchMode === 'PIPELINE' ? 'PIPELINE' : 'MULTI';

function setupBatchChannel(channelName: string, getOperationName: (data: RedisBatchData) => string): void {
const channel = tracingChannel<RedisBatchData>(channelName, data => {
return startSpanManual(
{
name: operationName,
name: getOperationName(data),
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN,
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'db.redis',
Expand Down Expand Up @@ -167,8 +180,8 @@ function setupBatchChannel(): void {
});
}

function setupConnectChannel(): void {
const channel = tracingChannel<ConnectData>(CHANNEL_CONNECT, data => {
function setupConnectChannel(channelName: string): void {
const channel = tracingChannel<ConnectData>(channelName, data => {
return startSpanManual(
{
name: 'redis-connect',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export class IORedisInstrumentation extends InstrumentationBase<IORedisInstrumen
return [
new InstrumentationNodeModuleDefinition(
'ioredis',
['>=2.0.0 <6'],
['>=2.0.0 <5.11.0'],
(module: any, moduleVersion?: string) => {
const moduleExports =
module[Symbol.toStringTag] === 'Module'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ describe('IORedisInstrumentation', () => {
expect(defs[0]!.name).toBe('ioredis');
});

it('should support ioredis versions >=2.0.0 <6', () => {
it('should support ioredis versions >=2.0.0 <5.11.0', () => {
const defs = instrumentation.init();
const supportedVersions = defs[0]!.supportedVersions;
expect(supportedVersions).toContain('>=2.0.0 <6');
expect(supportedVersions).toContain('>=2.0.0 <5.11.0');
});
});

Expand Down
Loading
Loading