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
1 change: 1 addition & 0 deletions dev-packages/node-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"prisma": "6.15.0",
"proxy": "^2.1.1",
"redis-4": "npm:redis@^4.6.14",
"redis-5": "npm:redis@^5.12.0",
"reflect-metadata": "0.2.1",
"rxjs": "^7.8.2",
"tedious": "^19.2.1",
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-redis-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,44 @@
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
const Sentry = require('@sentry/node');

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
integrations: [Sentry.redisIntegration({ cachePrefixes: ['dc-cache:'] })],
});

// Stop the process from exiting before the transaction is sent
setInterval(() => {}, 1000);

const { createClient } = require('redis-5');

async function run() {
const redisClient = await createClient({ socket: { host: '127.0.0.1', port: 6379 } }).connect();

await Sentry.startSpan(
{
name: 'Test Span Redis 5 DC',
op: 'test-span-redis-5-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();
110 changes: 110 additions & 0 deletions dev-packages/node-integration-tests/suites/tracing/redis-dc/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { afterAll, describe, expect, test } from 'vitest';
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';

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

test('should create spans for redis v5 commands via diagnostics_channel', { timeout: 60_000 }, async () => {
const EXPECTED_TRANSACTION = {
transaction: 'Test Span Redis 5 DC',
spans: expect.arrayContaining([
expect.objectContaining({
op: 'db.redis',
origin: 'auto.db.otel.redis',
data: expect.objectContaining({
'sentry.op': 'db.redis',
'sentry.origin': 'auto.db.otel.redis',
'db.system': 'redis',
'db.statement': 'SET dc-test-key [1 other arguments]',
}),
}),
// cache SET: span name updated to key by cacheResponseHook
expect.objectContaining({
description: 'dc-cache:test-key',
op: 'cache.put',
origin: 'auto.db.otel.redis',
data: expect.objectContaining({
'sentry.origin': 'auto.db.otel.redis',
'db.statement': 'SET dc-cache:test-key [1 other arguments]',
'cache.key': ['dc-cache:test-key'],
'cache.item_size': 2,
}),
}),
// cache SET with EX option: redis v5 sends SET key value EX 10 as the command
expect.objectContaining({
description: 'dc-cache:test-key-ex',
op: 'cache.put',
origin: 'auto.db.otel.redis',
data: expect.objectContaining({
'sentry.origin': 'auto.db.otel.redis',
'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.otel.redis',
data: expect.objectContaining({
'sentry.op': 'db.redis',
'sentry.origin': 'auto.db.otel.redis',
'db.system': 'redis',
'db.statement': 'GET dc-test-key',
}),
}),
// cache GET (hit)
expect.objectContaining({
description: 'dc-cache:test-key',
op: 'cache.get',
origin: 'auto.db.otel.redis',
data: expect.objectContaining({
'sentry.origin': 'auto.db.otel.redis',
'db.statement': 'GET dc-cache:test-key',
'cache.hit': true,
'cache.key': ['dc-cache:test-key'],
'cache.item_size': 10,
}),
}),
// cache GET (miss)
expect.objectContaining({
description: 'dc-cache:unavailable-data',
op: 'cache.get',
origin: 'auto.db.otel.redis',
data: expect.objectContaining({
'sentry.origin': 'auto.db.otel.redis',
'db.statement': 'GET dc-cache:unavailable-data',
'cache.hit': false,
'cache.key': ['dc-cache:unavailable-data'],
}),
}),
// MGET: mixed cache/non-cache keys, span name is all keys joined
expect.objectContaining({
description: 'dc-test-key, dc-cache:test-key, dc-cache:unavailable-data',
op: 'cache.get',
origin: 'auto.db.otel.redis',
data: expect.objectContaining({
'sentry.origin': 'auto.db.otel.redis',
'db.statement': 'MGET [3 other arguments]',
'cache.hit': true,
'cache.key': ['dc-test-key', 'dc-cache:test-key', 'dc-cache:unavailable-data'],
}),
}),
]),
};

// node-redis emits a node-redis:connect DC event for the initial connection.
// That fires before startSpan so it becomes its own root transaction, received after the main one.
const EXPECTED_CONNECT = {
transaction: 'redis-connect',
};

await createRunner(__dirname, 'scenario-redis-5.js')
.withDockerCompose({ workingDirectory: [__dirname] })
.expect({ transaction: EXPECTED_TRANSACTION })
.expect({ transaction: EXPECTED_CONNECT })
.start()
.completed();
});
});
1 change: 1 addition & 0 deletions packages/node/rollup.npm.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default [
makeBaseNPMConfig({
entrypoints: ['src/index.ts', 'src/init.ts', 'src/preload.ts'],
packageSpecificConfig: {
external: [/^@sentry\/opentelemetry/],
output: {
// set exports to 'named' or 'auto' so that rollup doesn't warn
exports: 'named',
Expand Down
6 changes: 6 additions & 0 deletions packages/node/src/integrations/tracing/redis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import type { IORedisResponseCustomAttributeFunction } from './vendored/types';
import { IORedisInstrumentation } from './vendored/ioredis-instrumentation';
import { RedisInstrumentation } from './vendored/redis-instrumentation';
import { subscribeRedisDiagnosticChannels } from './redis-dc-subscriber';

interface RedisOptions {
/**
Expand Down Expand Up @@ -120,6 +121,11 @@ export const instrumentRedis = Object.assign(
(): void => {
instrumentIORedis();
instrumentRedisModule();
// node-redis >= 5.12.0 publishes via diagnostics_channel. The subscriber uses
// `@sentry/opentelemetry/tracing-channel`, which needs the Sentry OTel context manager
// to be registered before it can `bindStore`. `initOpenTelemetry()` runs after integration
// `setupOnce`, so defer to the next tick.
void Promise.resolve().then(() => subscribeRedisDiagnosticChannels(cacheResponseHook));

// todo: implement them gradually
// new LegacyRedisInstrumentation({}),
Expand Down
Loading
Loading