Skip to content

Commit fedac52

Browse files
committed
fix(redis): correct span handling for multi commands
1 parent e72c1b3 commit fedac52

File tree

2 files changed

+106
-13
lines changed

2 files changed

+106
-13
lines changed

packages/instrumentation-redis/src/v4-v5/instrumentation.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,15 @@ export class RedisInstrumentationV4_V5 extends InstrumentationBase<RedisInstrume
145145
this._wrap(
146146
redisClientMultiCommandPrototype,
147147
'exec',
148-
this._getPatchMultiCommandsExec()
148+
this._getPatchMultiCommandsExec(false)
149+
);
150+
if (isWrapped(redisClientMultiCommandPrototype?.execAsPipeline)) {
151+
this._unwrap(redisClientMultiCommandPrototype, 'execAsPipeline');
152+
}
153+
this._wrap(
154+
redisClientMultiCommandPrototype,
155+
'execAsPipeline',
156+
this._getPatchMultiCommandsExec(true)
149157
);
150158

151159
if (isWrapped(redisClientMultiCommandPrototype?.addCommand)) {
@@ -277,36 +285,36 @@ export class RedisInstrumentationV4_V5 extends InstrumentationBase<RedisInstrume
277285
};
278286
}
279287

280-
private _getPatchMultiCommandsExec() {
288+
private _getPatchMultiCommandsExec(isPipeline: boolean) {
281289
const plugin = this;
282290
return function execPatchWrapper(original: Function) {
283291
return function execPatch(this: any) {
284292
const execRes = original.apply(this, arguments);
285293
if (typeof execRes?.then !== 'function') {
286294
plugin._diag.error(
287-
'got non promise result when patching RedisClientMultiCommand.exec'
295+
'non-promise result when patching exec/execAsPipeline'
288296
);
289297
return execRes;
290298
}
291299

292300
return execRes
293301
.then((redisRes: unknown[]) => {
294302
const openSpans = this[OTEL_OPEN_SPANS];
295-
plugin._endSpansWithRedisReplies(openSpans, redisRes);
303+
plugin._endSpansWithRedisReplies(openSpans, redisRes, isPipeline);
296304
return redisRes;
297305
})
298306
.catch((err: Error) => {
299307
const openSpans = this[OTEL_OPEN_SPANS];
300308
if (!openSpans) {
301309
plugin._diag.error(
302-
'cannot find open spans to end for redis multi command'
310+
'cannot find open spans to end for multi/pipeline'
303311
);
304312
} else {
305313
const replies =
306314
err.constructor.name === 'MultiErrorReply'
307315
? (err as MultiErrorReply).replies
308316
: new Array(openSpans.length).fill(err);
309-
plugin._endSpansWithRedisReplies(openSpans, replies);
317+
plugin._endSpansWithRedisReplies(openSpans, replies, isPipeline);
310318
}
311319
return Promise.reject(err);
312320
});
@@ -472,27 +480,40 @@ export class RedisInstrumentationV4_V5 extends InstrumentationBase<RedisInstrume
472480

473481
private _endSpansWithRedisReplies(
474482
openSpans: Array<MutliCommandInfo>,
475-
replies: unknown[]
483+
replies: unknown[],
484+
isPipeline = false
476485
) {
477486
if (!openSpans) {
478487
return this._diag.error(
479-
'cannot find open spans to end for redis multi command'
488+
'cannot find open spans to end for redis multi/pipeline'
480489
);
481490
}
482491
if (replies.length !== openSpans.length) {
483492
return this._diag.error(
484493
'number of multi command spans does not match response from redis'
485494
);
486495
}
496+
// Determine a single operation name for the batch of commands.
497+
// If all commands are identical, include the command name (e.g., "MULTI SET").
498+
// Otherwise, use a generic "MULTI" or "PIPELINE" label for the span.
499+
const allCommands = openSpans.map(s => s.commandName);
500+
const allSameCommand = allCommands.every(cmd => cmd === allCommands[0]);
501+
const operationName = allSameCommand
502+
? (isPipeline ? 'PIPELINE ' : 'MULTI ') + allCommands[0]
503+
: isPipeline
504+
? 'PIPELINE'
505+
: 'MULTI';
487506

488507
for (let i = 0; i < openSpans.length; i++) {
489-
const { span, commandName, commandArgs } = openSpans[i];
508+
const { span, commandArgs } = openSpans[i];
490509
const currCommandRes = replies[i];
491510
const [res, err] =
492511
currCommandRes instanceof Error
493512
? [null, currCommandRes]
494513
: [currCommandRes, undefined];
495-
this._endSpanWithResponse(span, commandName, commandArgs, res, err);
514+
span.setAttribute(ATTR_DB_OPERATION_NAME, operationName);
515+
516+
this._endSpanWithResponse(span, allCommands[i], commandArgs, res, err);
496517
}
497518
}
498519

packages/instrumentation-redis/test/v4-v5/redis.test.ts

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ describe('redis v4-v5', () => {
452452
);
453453
assert.strictEqual(
454454
multiSetSpan?.attributes[ATTR_DB_OPERATION_NAME],
455-
'SET'
455+
'MULTI'
456456
);
457457

458458
assert.ok(multiGetSpan);
@@ -487,7 +487,7 @@ describe('redis v4-v5', () => {
487487
);
488488
assert.strictEqual(
489489
multiGetSpan?.attributes[ATTR_DB_OPERATION_NAME],
490-
'GET'
490+
'MULTI'
491491
);
492492
});
493493

@@ -530,7 +530,7 @@ describe('redis v4-v5', () => {
530530
);
531531
assert.strictEqual(
532532
multiSetSpan?.attributes[ATTR_DB_OPERATION_NAME],
533-
'SET'
533+
'MULTI SET'
534534
);
535535
});
536536

@@ -824,4 +824,76 @@ describe('redis v4-v5', () => {
824824
);
825825
});
826826
});
827+
describe('pipeline commands', () => {
828+
it('should trace all commands in a pipeline with a mixed set of commands', async () => {
829+
await client.set('another-key', 'another-value');
830+
831+
const [setKeyReply, otherKeyValue] = await client
832+
.multi()
833+
.set('key', 'value')
834+
.get('another-key')
835+
.execAsPipeline();
836+
837+
assert.strictEqual(setKeyReply, 'OK');
838+
assert.strictEqual(otherKeyValue, 'another-value');
839+
840+
const [setSpan, pipelineSetSpan, pipelineGetSpan] = getTestSpans();
841+
842+
assert.ok(setSpan);
843+
844+
assert.ok(pipelineSetSpan);
845+
assert.strictEqual(pipelineSetSpan.name, 'redis-SET');
846+
assert.strictEqual(
847+
pipelineSetSpan.attributes[ATTR_DB_STATEMENT],
848+
'SET key [1 other arguments]'
849+
);
850+
assert.strictEqual(
851+
pipelineSetSpan.attributes[ATTR_DB_QUERY_TEXT],
852+
'SET key [1 other arguments]'
853+
);
854+
assert.strictEqual(
855+
pipelineSetSpan.attributes[ATTR_DB_OPERATION_NAME],
856+
'PIPELINE'
857+
);
858+
859+
assert.ok(pipelineGetSpan);
860+
assert.strictEqual(pipelineGetSpan.name, 'redis-GET');
861+
assert.strictEqual(
862+
pipelineGetSpan.attributes[ATTR_DB_STATEMENT],
863+
'GET another-key'
864+
);
865+
assert.strictEqual(
866+
pipelineGetSpan.attributes[ATTR_DB_QUERY_TEXT],
867+
'GET another-key'
868+
);
869+
assert.strictEqual(
870+
pipelineGetSpan.attributes[ATTR_DB_OPERATION_NAME],
871+
'PIPELINE'
872+
);
873+
});
874+
875+
it('should trace all commands in a pipeline with a same set of commands', async () => {
876+
const [setReply] = await client
877+
.multi()
878+
.addCommand(['SET', 'key', 'value'])
879+
.execAsPipeline();
880+
881+
assert.strictEqual(setReply, 'OK');
882+
883+
const [pipelineSetSpan] = getTestSpans();
884+
assert.ok(pipelineSetSpan);
885+
assert.strictEqual(
886+
pipelineSetSpan.attributes[ATTR_DB_STATEMENT],
887+
'SET key [1 other arguments]'
888+
);
889+
assert.strictEqual(
890+
pipelineSetSpan.attributes[ATTR_DB_QUERY_TEXT],
891+
'SET key [1 other arguments]'
892+
);
893+
assert.strictEqual(
894+
pipelineSetSpan.attributes[ATTR_DB_OPERATION_NAME],
895+
'PIPELINE SET'
896+
);
897+
});
898+
});
827899
});

0 commit comments

Comments
 (0)