Skip to content
Merged
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 numberDiff line numberDiff 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",
Comment thread
isaacs marked this conversation as resolved.
"reflect-metadata": "0.2.1",
"rxjs": "^7.8.2",
"tedious": "^19.2.1",
Expand Down
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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);

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

const { createClient } = require('redis-5');
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 numberDiff line numberDiff 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.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]',
}),
}),
// cache SET: span name updated to key by cacheResponseHook
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,
}),
}),
// 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.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',
}),
}),
// cache GET (hit)
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,
}),
}),
// cache GET (miss)
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'],
}),
}),
// MGET: node-redis sanitizes args for diagnostics_channel (keys become '?'),
// so cache detection cannot match prefixes — remains a plain db.redis span.
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]',
}),
}),
]),
};

// node-redis emits a node-redis:connect DC event for the initial connection.
// That fires before startSpan so it arrives as the first envelope.
const EXPECTED_CONNECT = {
transaction: 'redis-connect',
};

await createRunner(__dirname, 'scenario-redis-5.js')
.withDockerCompose({ workingDirectory: [__dirname] })
.expect({ transaction: EXPECTED_CONNECT })
.expect({ transaction: EXPECTED_TRANSACTION })
.start()
.completed();
});
});
1 change: 1 addition & 0 deletions packages/node/rollup.npm.config.mjs
Original file line numberDiff line numberDiff 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
9 changes: 6 additions & 3 deletions packages/node/src/integrations/tracing/redis/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,6 @@ import {
SEMANTIC_ATTRIBUTE_CACHE_ITEM_SIZE,
SEMANTIC_ATTRIBUTE_CACHE_KEY,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
spanToJSON,
truncate,
} from '@sentry/core';
Expand All@@ -23,6 +22,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 DownExpand Up@@ -53,8 +53,6 @@ export const cacheResponseHook: IORedisResponseCustomAttributeFunction = (
cmdArgs: IORedisCommandArgs,
response: unknown,
) => {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.redis');

Comment thread
cursor[bot] marked this conversation as resolved.
const safeKey = getCacheKeySafely(redisCommand, cmdArgs);
const cacheOperation = getCacheOperation(redisCommand);

Expand DownExpand Up@@ -120,6 +118,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));
Comment thread
isaacs marked this conversation as resolved.
Comment thread
isaacs marked this conversation as resolved.

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