Skip to content

Commit 7019263

Browse files
authored
feat(node): Add maxCacheKeyLength to Redis integration (remove truncation) (#18045)
This removes the automatic truncation of the span description. Now, all cache keys are set in the description. part of #17389
1 parent 10211f4 commit 7019263

File tree

2 files changed

+105
-5
lines changed

2 files changed

+105
-5
lines changed

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,34 @@ 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+
* Passing `0` will use the full cache key without truncation.
37+
*
38+
* By default, the full cache key is used.
39+
*/
40+
maxCacheKeyLength?: number;
2841
}
2942

3043
const INTEGRATION_NAME = 'Redis';
3144

32-
let _redisOptions: RedisOptions = {};
45+
/* Only exported for testing purposes */
46+
export let _redisOptions: RedisOptions = {};
3347

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

3757
const safeKey = getCacheKeySafely(redisCommand, cmdArgs);
@@ -70,9 +90,12 @@ const cacheResponseHook: RedisResponseCustomAttributeFunction = (span: Span, red
7090
[SEMANTIC_ATTRIBUTE_CACHE_KEY]: safeKey,
7191
});
7292

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

75-
span.updateName(truncate(spanDescription, 1024));
96+
span.updateName(
97+
_redisOptions.maxCacheKeyLength ? truncate(spanDescription, _redisOptions.maxCacheKeyLength) : spanDescription,
98+
);
7699
};
77100

78101
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+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
31+
Object.keys(_redisOptions).forEach(key => delete (_redisOptions as any)[key]);
32+
Object.assign(_redisOptions, originalRedisOptions);
33+
});
34+
35+
describe('early returns', () => {
36+
it.each([
37+
{ desc: 'no args', cmd: 'get', args: [], response: 'test' },
38+
{ desc: 'unsupported command', cmd: 'exists', args: ['key'], response: 'test' },
39+
{ desc: 'no cache prefixes', cmd: 'get', args: ['key'], response: 'test', options: {} },
40+
{ desc: 'non-matching prefix', cmd: 'get', args: ['key'], response: 'test', options: { cachePrefixes: ['c'] } },
41+
])('should always set sentry.origin but return early when $desc', ({ cmd, args, response, options = {} }) => {
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)