Skip to content

Commit da258e2

Browse files
chargomeclaude
andcommitted
ref(node): Extract cacheResponseHook to a module free of OTel redis instrumentation
The orchestrion opt-in imports cacheResponseHook; importing it from redis/index transitively pulled the vendored OTel IORedisInstrumentation/RedisInstrumentation into the opt-in module graph. Move cacheResponseHook + _redisOptions into redis/cache.ts (no OTel instrumentation imports) so apps using injection only for mysql/lru-memoizer don't bundle the redis OTel instrumentation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5eb6e88 commit da258e2

3 files changed

Lines changed: 112 additions & 97 deletions

File tree

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import type { Span } from '@sentry/core';
2+
import {
3+
SEMANTIC_ATTRIBUTE_CACHE_HIT,
4+
SEMANTIC_ATTRIBUTE_CACHE_ITEM_SIZE,
5+
SEMANTIC_ATTRIBUTE_CACHE_KEY,
6+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
7+
spanToJSON,
8+
truncate,
9+
} from '@sentry/core';
10+
import type { IORedisCommandArgs } from '../../../utils/redisCache';
11+
import {
12+
calculateCacheItemSize,
13+
GET_COMMANDS,
14+
getCacheKeySafely,
15+
getCacheOperation,
16+
isInCommands,
17+
shouldConsiderForCache,
18+
} from '../../../utils/redisCache';
19+
import type { IORedisResponseCustomAttributeFunction } from './vendored/types';
20+
21+
// This module deliberately does NOT import the vendored OTel `IORedisInstrumentation`/
22+
// `RedisInstrumentation`, so the orchestrion opt-in can pull `cacheResponseHook`
23+
// without dragging the OTel redis instrumentation into its module graph.
24+
25+
export interface RedisOptions {
26+
/**
27+
* Define cache prefixes for cache keys that should be captured as a cache span.
28+
*
29+
* Setting this to, for example, `['user:']` will capture cache keys that start with `user:`.
30+
*/
31+
cachePrefixes?: string[];
32+
/**
33+
* Maximum length of the cache key added to the span description. If the key exceeds this length, it will be truncated.
34+
*
35+
* Passing `0` will use the full cache key without truncation.
36+
*
37+
* By default, the full cache key is used.
38+
*/
39+
maxCacheKeyLength?: number;
40+
}
41+
42+
/* Only exported for testing purposes */
43+
export let _redisOptions: RedisOptions = {};
44+
45+
/** Set the options consumed by {@link cacheResponseHook}. */
46+
export function setRedisOptions(options: RedisOptions): void {
47+
_redisOptions = options;
48+
}
49+
50+
/* Only exported for testing purposes */
51+
export const cacheResponseHook: IORedisResponseCustomAttributeFunction = (
52+
span: Span,
53+
redisCommand: string,
54+
cmdArgs: IORedisCommandArgs,
55+
response: unknown,
56+
) => {
57+
const safeKey = getCacheKeySafely(redisCommand, cmdArgs);
58+
const cacheOperation = getCacheOperation(redisCommand);
59+
60+
if (
61+
!safeKey ||
62+
!cacheOperation ||
63+
!_redisOptions.cachePrefixes ||
64+
!shouldConsiderForCache(redisCommand, safeKey, _redisOptions.cachePrefixes)
65+
) {
66+
// not relevant for cache
67+
return;
68+
}
69+
70+
// otel/ioredis seems to be using the old standard, as there was a change to those params: https://github.com/open-telemetry/opentelemetry-specification/issues/3199
71+
// We are using params based on the docs: https://opentelemetry.io/docs/specs/semconv/attributes-registry/network/
72+
// Fall back to stable semconv attributes (server.address/server.port) when
73+
// old-semconv ones are absent, eg OTEL_SEMCONV_STABILITY_OPT_IN=database
74+
// set for node-redis v4/v5.
75+
const spanData = spanToJSON(span).data;
76+
const networkPeerAddress = spanData['net.peer.name'] ?? spanData['server.address'];
77+
const networkPeerPort = spanData['net.peer.port'] ?? spanData['server.port'];
78+
if (networkPeerPort && networkPeerAddress) {
79+
span.setAttributes({ 'network.peer.address': networkPeerAddress, 'network.peer.port': networkPeerPort });
80+
}
81+
82+
const cacheItemSize = calculateCacheItemSize(response);
83+
84+
if (cacheItemSize) {
85+
span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_ITEM_SIZE, cacheItemSize);
86+
}
87+
88+
if (isInCommands(GET_COMMANDS, redisCommand) && cacheItemSize !== undefined) {
89+
span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_HIT, cacheItemSize > 0);
90+
}
91+
92+
span.setAttributes({
93+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: cacheOperation,
94+
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: safeKey,
95+
});
96+
97+
// todo: change to string[] once EAP supports it
98+
const spanDescription = safeKey.join(', ');
99+
100+
span.updateName(
101+
_redisOptions.maxCacheKeyLength ? truncate(spanDescription, _redisOptions.maxCacheKeyLength) : spanDescription,
102+
);
103+
};

packages/node/src/integrations/tracing/redis/index.ts

Lines changed: 8 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,20 @@
1-
import type { IntegrationFn, Span } from '@sentry/core';
2-
import {
3-
defineIntegration,
4-
SEMANTIC_ATTRIBUTE_CACHE_HIT,
5-
SEMANTIC_ATTRIBUTE_CACHE_ITEM_SIZE,
6-
SEMANTIC_ATTRIBUTE_CACHE_KEY,
7-
SEMANTIC_ATTRIBUTE_SENTRY_OP,
8-
spanToJSON,
9-
truncate,
10-
waitForTracingChannelBinding,
11-
} from '@sentry/core';
1+
import type { IntegrationFn } from '@sentry/core';
2+
import { defineIntegration, waitForTracingChannelBinding } from '@sentry/core';
123
import * as dc from 'node:diagnostics_channel';
134
import { subscribeRedisDiagnosticChannels, type RedisTracingChannelFactory } from '@sentry/server-utils';
145
import { generateInstrumentOnce } from '@sentry/node-core';
156
import { isDiagnosticsChannelInjectionEnabled } from '../../../sdk/diagnosticsChannelInjection';
16-
import type { IORedisCommandArgs } from '../../../utils/redisCache';
17-
import {
18-
calculateCacheItemSize,
19-
GET_COMMANDS,
20-
getCacheKeySafely,
21-
getCacheOperation,
22-
isInCommands,
23-
shouldConsiderForCache,
24-
} from '../../../utils/redisCache';
25-
import type { IORedisResponseCustomAttributeFunction } from './vendored/types';
7+
import { cacheResponseHook, type RedisOptions, setRedisOptions } from './cache';
268
import { IORedisInstrumentation } from './vendored/ioredis-instrumentation';
279
import { RedisInstrumentation } from './vendored/redis-instrumentation';
2810

29-
interface RedisOptions {
30-
/**
31-
* Define cache prefixes for cache keys that should be captured as a cache span.
32-
*
33-
* Setting this to, for example, `['user:']` will capture cache keys that start with `user:`.
34-
*/
35-
cachePrefixes?: string[];
36-
/**
37-
* Maximum length of the cache key added to the span description. If the key exceeds this length, it will be truncated.
38-
*
39-
* Passing `0` will use the full cache key without truncation.
40-
*
41-
* By default, the full cache key is used.
42-
*/
43-
maxCacheKeyLength?: number;
44-
}
11+
// `cacheResponseHook`/`_redisOptions` live in `./cache` (which has no OTel
12+
// instrumentation imports) so the orchestrion opt-in can pull the hook without
13+
// dragging the OTel redis instrumentation in. Re-exported here for tests.
14+
export { _redisOptions, cacheResponseHook } from './cache';
4515

4616
const INTEGRATION_NAME = 'Redis' as const;
4717

48-
/* Only exported for testing purposes */
49-
export let _redisOptions: RedisOptions = {};
50-
51-
/* Only exported for testing purposes */
52-
export const cacheResponseHook: IORedisResponseCustomAttributeFunction = (
53-
span: Span,
54-
redisCommand: string,
55-
cmdArgs: IORedisCommandArgs,
56-
response: unknown,
57-
) => {
58-
const safeKey = getCacheKeySafely(redisCommand, cmdArgs);
59-
const cacheOperation = getCacheOperation(redisCommand);
60-
61-
if (
62-
!safeKey ||
63-
!cacheOperation ||
64-
!_redisOptions.cachePrefixes ||
65-
!shouldConsiderForCache(redisCommand, safeKey, _redisOptions.cachePrefixes)
66-
) {
67-
// not relevant for cache
68-
return;
69-
}
70-
71-
// otel/ioredis seems to be using the old standard, as there was a change to those params: https://github.com/open-telemetry/opentelemetry-specification/issues/3199
72-
// We are using params based on the docs: https://opentelemetry.io/docs/specs/semconv/attributes-registry/network/
73-
// Fall back to stable semconv attributes (server.address/server.port) when
74-
// old-semconv ones are absent, eg OTEL_SEMCONV_STABILITY_OPT_IN=database
75-
// set for node-redis v4/v5.
76-
const spanData = spanToJSON(span).data;
77-
const networkPeerAddress = spanData['net.peer.name'] ?? spanData['server.address'];
78-
const networkPeerPort = spanData['net.peer.port'] ?? spanData['server.port'];
79-
if (networkPeerPort && networkPeerAddress) {
80-
span.setAttributes({ 'network.peer.address': networkPeerAddress, 'network.peer.port': networkPeerPort });
81-
}
82-
83-
const cacheItemSize = calculateCacheItemSize(response);
84-
85-
if (cacheItemSize) {
86-
span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_ITEM_SIZE, cacheItemSize);
87-
}
88-
89-
if (isInCommands(GET_COMMANDS, redisCommand) && cacheItemSize !== undefined) {
90-
span.setAttribute(SEMANTIC_ATTRIBUTE_CACHE_HIT, cacheItemSize > 0);
91-
}
92-
93-
span.setAttributes({
94-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: cacheOperation,
95-
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: safeKey,
96-
});
97-
98-
// todo: change to string[] once EAP supports it
99-
const spanDescription = safeKey.join(', ');
100-
101-
span.updateName(
102-
_redisOptions.maxCacheKeyLength ? truncate(spanDescription, _redisOptions.maxCacheKeyLength) : spanDescription,
103-
);
104-
};
105-
10618
const instrumentIORedis = generateInstrumentOnce(`${INTEGRATION_NAME}.IORedis`, () => {
10719
return new IORedisInstrumentation({
10820
responseHook: cacheResponseHook,
@@ -151,7 +63,7 @@ const _redisIntegration = ((options: RedisOptions = {}) => {
15163
return {
15264
name: INTEGRATION_NAME,
15365
setupOnce() {
154-
_redisOptions = options;
66+
setRedisOptions(options);
15567
instrumentRedis();
15668
},
15769
};

packages/node/src/sdk/experimentalUseDiagnosticsChannelInjection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
detectOrchestrionSetup,
66
} from '@sentry/server-utils/orchestrion';
77
import { registerDiagnosticsChannelInjection } from '@sentry/server-utils/orchestrion/register';
8-
import { cacheResponseHook } from '../integrations/tracing/redis';
8+
import { cacheResponseHook } from '../integrations/tracing/redis/cache';
99
import type { DiagnosticsChannelInjection } from './diagnosticsChannelInjection';
1010
import { setDiagnosticsChannelInjectionLoader } from './diagnosticsChannelInjection';
1111

0 commit comments

Comments
 (0)