Skip to content

Commit 7f030e8

Browse files
committed
feat(node): Add maxCacheKeyLength to Redis integration (remove truncation)
1 parent fe97d67 commit 7f030e8

File tree

2 files changed

+103
-5
lines changed

2 files changed

+103
-5
lines changed

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

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,32 @@ import {
2424
} from '../../utils/redisCache';
2525

2626
interface RedisOptions {
27+
/**
28+
* Define cache prefixes for cache keys that should be captured as a cache span.
29+
*
30+
* Setting this to, for example, `['user:']` will capture cache keys that start with `user:`.
31+
*/
2732
cachePrefixes?: string[];
33+
/**
34+
* Maximum length of the cache key added to the span description. If the key exceeds this length, it will be truncated.
35+
*
36+
* By default, the full cache key is used.
37+
*/
38+
maxCacheKeyLength?: number;
2839
}
2940

3041
const INTEGRATION_NAME = 'Redis';
3142

32-
let _redisOptions: RedisOptions = {};
43+
/* Only exported for testing purposes */
44+
export let _redisOptions: RedisOptions = {};
3345

34-
const cacheResponseHook: RedisResponseCustomAttributeFunction = (span: Span, redisCommand, cmdArgs, response) => {
46+
/* Only exported for testing purposes */
47+
export const cacheResponseHook: RedisResponseCustomAttributeFunction = (
48+
span: Span,
49+
redisCommand,
50+
cmdArgs,
51+
response,
52+
) => {
3553
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.db.otel.redis');
3654

3755
const safeKey = getCacheKeySafely(redisCommand, cmdArgs);
@@ -70,9 +88,12 @@ const cacheResponseHook: RedisResponseCustomAttributeFunction = (span: Span, red
7088
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: safeKey,
7189
});
7290

91+
// todo: change to string[] once EAP supports it
7392
const spanDescription = safeKey.join(', ');
7493

75-
span.updateName(truncate(spanDescription, 1024));
94+
span.updateName(
95+
_redisOptions.maxCacheKeyLength ? truncate(spanDescription, _redisOptions.maxCacheKeyLength) : spanDescription,
96+
);
7697
};
7798

7899
const instrumentIORedis = generateInstrumentOnce(`${INTEGRATION_NAME}.IORedis`, () => {

packages/node/test/integrations/tracing/redis.test.ts

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { describe, expect, it } from 'vitest';
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { _redisOptions, cacheResponseHook } from '../../../src/integrations/tracing/redis';
23
import {
34
calculateCacheItemSize,
45
GET_COMMANDS,
@@ -8,6 +9,82 @@ import {
89
} from '../../../src/utils/redisCache';
910

1011
describe('Redis', () => {
12+
describe('cacheResponseHook', () => {
13+
let mockSpan: any;
14+
let originalRedisOptions: any;
15+
16+
beforeEach(() => {
17+
mockSpan = {
18+
setAttribute: vi.fn(),
19+
setAttributes: vi.fn(),
20+
updateName: vi.fn(),
21+
spanContext: () => ({ spanId: 'test-span-id', traceId: 'test-trace-id' }),
22+
};
23+
24+
originalRedisOptions = { ..._redisOptions };
25+
});
26+
27+
afterEach(() => {
28+
vi.restoreAllMocks();
29+
// Reset redis options by clearing all properties first, then restoring original ones
30+
Object.keys(_redisOptions).forEach(key => delete (_redisOptions as any)[key]);
31+
Object.assign(_redisOptions, originalRedisOptions);
32+
});
33+
34+
describe('early returns', () => {
35+
it.each([
36+
{ desc: 'no args', cmd: 'get', args: [], response: 'test' },
37+
{ desc: 'unsupported command', cmd: 'exists', args: ['key'], response: 'test' },
38+
{ desc: 'no cache prefixes', cmd: 'get', args: ['key'], response: 'test', options: {} },
39+
{ desc: 'non-matching prefix', cmd: 'get', args: ['key'], response: 'test', options: { cachePrefixes: ['c'] } },
40+
])('should always set sentry.origin but return early when $desc', ({ cmd, args, response, options = {} }) => {
41+
vi.clearAllMocks();
42+
Object.assign(_redisOptions, options);
43+
44+
cacheResponseHook(mockSpan, cmd, args, response);
45+
46+
expect(mockSpan.setAttribute).toHaveBeenCalledWith('sentry.origin', 'auto.db.otel.redis');
47+
expect(mockSpan.setAttributes).not.toHaveBeenCalled();
48+
expect(mockSpan.updateName).not.toHaveBeenCalled();
49+
});
50+
});
51+
52+
describe('span name truncation', () => {
53+
beforeEach(() => {
54+
Object.assign(_redisOptions, { cachePrefixes: ['cache:'] });
55+
});
56+
57+
it('should not truncate span name when maxCacheKeyLength is not set', () => {
58+
cacheResponseHook(
59+
mockSpan,
60+
'mget',
61+
['cache:very-long-key-name', 'cache:very-long-key-name-2', 'cache:very-long-key-name-3'],
62+
'value',
63+
);
64+
65+
expect(mockSpan.updateName).toHaveBeenCalledWith(
66+
'cache:very-long-key-name, cache:very-long-key-name-2, cache:very-long-key-name-3',
67+
);
68+
});
69+
70+
it('should truncate span name when maxCacheKeyLength is set', () => {
71+
Object.assign(_redisOptions, { maxCacheKeyLength: 10 });
72+
73+
cacheResponseHook(mockSpan, 'get', ['cache:very-long-key-name'], 'value');
74+
75+
expect(mockSpan.updateName).toHaveBeenCalledWith('cache:very...');
76+
});
77+
78+
it('should truncate multiple keys joined with commas', () => {
79+
Object.assign(_redisOptions, { maxCacheKeyLength: 20 });
80+
81+
cacheResponseHook(mockSpan, 'mget', ['cache:key1', 'cache:key2', 'cache:key3'], ['val1', 'val2', 'val3']);
82+
83+
expect(mockSpan.updateName).toHaveBeenCalledWith('cache:key1, cache:ke...');
84+
});
85+
});
86+
});
87+
1188
describe('getCacheKeySafely (single arg)', () => {
1289
it('should return an empty string if there are no command arguments', () => {
1390
const result = getCacheKeySafely('get', []);
@@ -26,7 +103,7 @@ describe('Redis', () => {
26103
expect(result).toStrictEqual(['key1']);
27104
});
28105

29-
it('should return only the key for multiple arguments', () => {
106+
it('should return only the first key for commands that only accept a singe key (get)', () => {
30107
const cmdArgs = ['key1', 'the-value'];
31108
const result = getCacheKeySafely('get', cmdArgs);
32109
expect(result).toStrictEqual(['key1']);

0 commit comments

Comments
 (0)