Skip to content

Commit fce7d3b

Browse files
trentmdyladan
andauthored
fix(instrumentation-redis-4): fix multi.exec() instrumentation for redis >=4.6.12 (#1904)
In redis@4.6.12 the behaviour of multi.exec() changed to *throw* a MultiErrorReply if any of the commands errored out. The individual command replies are available at 'err.replies', instead of as the promise result. This adjusts the instrumentation to generate spans as before: only setting SpanStatusCode.ERROR and calling span.recordException for the individual commands that failed. Fixes: #1874 Co-authored-by: Daniel Dyla <dyladan@users.noreply.github.com>
1 parent f65f2f1 commit fce7d3b

File tree

3 files changed

+67
-47
lines changed

3 files changed

+67
-47
lines changed

plugins/node/opentelemetry-instrumentation-redis-4/src/instrumentation.ts

+34-42
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { defaultDbStatementSerializer } from '@opentelemetry/redis-common';
3333
import { RedisInstrumentationConfig } from './types';
3434
import { VERSION } from './version';
3535
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
36+
import type { MultiErrorReply } from './internal-types';
3637

3738
const OTEL_OPEN_SPANS = Symbol(
3839
'opentelemetry.instruemntation.redis.open_spans'
@@ -289,55 +290,21 @@ export class RedisInstrumentation extends InstrumentationBase<any> {
289290
return execRes
290291
.then((redisRes: unknown[]) => {
291292
const openSpans = this[OTEL_OPEN_SPANS];
292-
if (!openSpans) {
293-
return plugin._diag.error(
294-
'cannot find open spans to end for redis multi command'
295-
);
296-
}
297-
if (redisRes.length !== openSpans.length) {
298-
return plugin._diag.error(
299-
'number of multi command spans does not match response from redis'
300-
);
301-
}
302-
for (let i = 0; i < openSpans.length; i++) {
303-
const { span, commandName, commandArgs } = openSpans[i];
304-
const currCommandRes = redisRes[i];
305-
if (currCommandRes instanceof Error) {
306-
plugin._endSpanWithResponse(
307-
span,
308-
commandName,
309-
commandArgs,
310-
null,
311-
currCommandRes
312-
);
313-
} else {
314-
plugin._endSpanWithResponse(
315-
span,
316-
commandName,
317-
commandArgs,
318-
currCommandRes,
319-
undefined
320-
);
321-
}
322-
}
293+
plugin._endSpansWithRedisReplies(openSpans, redisRes);
323294
return redisRes;
324295
})
325296
.catch((err: Error) => {
326297
const openSpans = this[OTEL_OPEN_SPANS];
327298
if (!openSpans) {
328-
return plugin._diag.error(
299+
plugin._diag.error(
329300
'cannot find open spans to end for redis multi command'
330301
);
331-
}
332-
for (let i = 0; i < openSpans.length; i++) {
333-
const { span, commandName, commandArgs } = openSpans[i];
334-
plugin._endSpanWithResponse(
335-
span,
336-
commandName,
337-
commandArgs,
338-
null,
339-
err
340-
);
302+
} else {
303+
const replies =
304+
err.constructor.name === 'MultiErrorReply'
305+
? (err as MultiErrorReply).replies
306+
: new Array(openSpans.length).fill(err);
307+
plugin._endSpansWithRedisReplies(openSpans, replies);
341308
}
342309
return Promise.reject(err);
343310
});
@@ -487,6 +454,31 @@ export class RedisInstrumentation extends InstrumentationBase<any> {
487454
return res;
488455
}
489456

457+
private _endSpansWithRedisReplies(
458+
openSpans: Array<MutliCommandInfo>,
459+
replies: unknown[]
460+
) {
461+
if (!openSpans) {
462+
return this._diag.error(
463+
'cannot find open spans to end for redis multi command'
464+
);
465+
}
466+
if (replies.length !== openSpans.length) {
467+
return this._diag.error(
468+
'number of multi command spans does not match response from redis'
469+
);
470+
}
471+
for (let i = 0; i < openSpans.length; i++) {
472+
const { span, commandName, commandArgs } = openSpans[i];
473+
const currCommandRes = replies[i];
474+
const [res, err] =
475+
currCommandRes instanceof Error
476+
? [null, currCommandRes]
477+
: [currCommandRes, undefined];
478+
this._endSpanWithResponse(span, commandName, commandArgs, res, err);
479+
}
480+
}
481+
490482
private _endSpanWithResponse(
491483
span: Span,
492484
commandName: string,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// Error class introduced in redis@4.6.12.
18+
// https://github.com/redis/node-redis/blob/redis@4.6.12/packages/client/lib/errors.ts#L69-L84
19+
export interface MultiErrorReply extends Error {
20+
replies: unknown[];
21+
errorIndexes: Array<number>;
22+
}

plugins/node/opentelemetry-instrumentation-redis-4/test/redis.test.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
registerInstrumentationTesting,
1919
} from '@opentelemetry/contrib-test-utils';
2020
import { RedisInstrumentation } from '../src';
21+
import type { MultiErrorReply } from '../src/internal-types';
2122
import * as assert from 'assert';
2223

2324
import {
@@ -377,11 +378,16 @@ describe('redis@^4.0.0', () => {
377378
});
378379

379380
it('multi command with error', async () => {
380-
const [setReply, incrReply] = await client
381-
.multi()
382-
.set('key', 'value')
383-
.incr('key')
384-
.exec(); // ['OK', 'ReplyError']
381+
let replies;
382+
try {
383+
replies = await client.multi().set('key', 'value').incr('key').exec();
384+
} catch (err) {
385+
// Starting in redis@4.6.12 `multi().exec()` will *throw* a
386+
// MultiErrorReply, with `err.replies`, if any of the commands error.
387+
replies = (err as MultiErrorReply).replies;
388+
}
389+
const [setReply, incrReply] = replies;
390+
385391
assert.strictEqual(setReply, 'OK'); // verify we did not screw up the normal functionality
386392
assert.ok(incrReply instanceof Error); // verify we did not screw up the normal functionality
387393

0 commit comments

Comments
 (0)