Skip to content

Commit

Permalink
fix(instrumentation-redis-4): fix multi.exec() instrumentation for re…
Browse files Browse the repository at this point in the history
…dis >=4.6.12

In [email protected] 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
  • Loading branch information
trentm committed Jan 16, 2024
1 parent c365375 commit f78fbc6
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { defaultDbStatementSerializer } from '@opentelemetry/redis-common';
import { RedisInstrumentationConfig } from './types';
import { VERSION } from './version';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import type { MultiErrorReply } from './internal-types';

const OTEL_OPEN_SPANS = Symbol(
'opentelemetry.instruemntation.redis.open_spans'
Expand Down Expand Up @@ -289,56 +290,31 @@ export class RedisInstrumentation extends InstrumentationBase<any> {
return execRes
.then((redisRes: unknown[]) => {
const openSpans = this[OTEL_OPEN_SPANS];
if (!openSpans) {
return plugin._diag.error(
'cannot find open spans to end for redis multi command'
);
}
if (redisRes.length !== openSpans.length) {
return plugin._diag.error(
'number of multi command spans does not match response from redis'
);
}
for (let i = 0; i < openSpans.length; i++) {
const { span, commandName, commandArgs } = openSpans[i];
const currCommandRes = redisRes[i];
if (currCommandRes instanceof Error) {
plugin._endSpanWithResponse(
span,
commandName,
commandArgs,
null,
currCommandRes
plugin._endSpansWithRedisReplies(openSpans, redisRes);
return redisRes;
})
.catch((err: Error) => {
const openSpans = this[OTEL_OPEN_SPANS];
if (err.constructor.name === 'MultiErrorReply') {
const multiErr = err as MultiErrorReply;
plugin._endSpansWithRedisReplies(openSpans, multiErr.replies);
} else {
if (!openSpans) {
return plugin._diag.error(
'cannot find open spans to end for redis multi command'
);
} else {
}
for (let i = 0; i < openSpans.length; i++) {
const { span, commandName, commandArgs } = openSpans[i];
plugin._endSpanWithResponse(
span,
commandName,
commandArgs,
currCommandRes,
undefined
null,
err
);
}
}
return redisRes;
})
.catch((err: Error) => {
const openSpans = this[OTEL_OPEN_SPANS];
if (!openSpans) {
return plugin._diag.error(
'cannot find open spans to end for redis multi command'
);
}
for (let i = 0; i < openSpans.length; i++) {
const { span, commandName, commandArgs } = openSpans[i];
plugin._endSpanWithResponse(
span,
commandName,
commandArgs,
null,
err
);
}
return Promise.reject(err);
});
};
Expand Down Expand Up @@ -487,6 +463,43 @@ export class RedisInstrumentation extends InstrumentationBase<any> {
return res;
}

private _endSpansWithRedisReplies(
openSpans: Array<MutliCommandInfo>,
replies: unknown[]
) {
if (!openSpans) {
return this._diag.error(
'cannot find open spans to end for redis multi command'
);
}
if (replies.length !== openSpans.length) {
return this._diag.error(
'number of multi command spans does not match response from redis'
);
}
for (let i = 0; i < openSpans.length; i++) {
const { span, commandName, commandArgs } = openSpans[i];
const currCommandRes = replies[i];
if (currCommandRes instanceof Error) {
this._endSpanWithResponse(
span,
commandName,
commandArgs,
null,
currCommandRes
);
} else {
this._endSpanWithResponse(
span,
commandName,
commandArgs,
currCommandRes,
undefined
);
}
}
}

private _endSpanWithResponse(
span: Span,
commandName: string,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// Error class introduced in [email protected].
// https://github.com/redis/node-redis/blob/[email protected]/packages/client/lib/errors.ts#L69-L84
export interface MultiErrorReply extends Error {
replies: unknown[];
errorIndexes: Array<number>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const instrumentation = registerInstrumentationTesting(
new RedisInstrumentation()
);

import { createClient, WatchError } from 'redis';
import * as redis from 'redis';
import {
Span,
SpanKind,
Expand Down Expand Up @@ -68,7 +68,7 @@ describe('redis@^4.0.0', () => {
let client: any;

beforeEach(async () => {
client = createClient({
client = redis.createClient({
url: redisTestUrl,
});
await context.with(suppressTracing(context.active()), async () => {
Expand Down Expand Up @@ -187,7 +187,7 @@ describe('redis@^4.0.0', () => {

describe('client connect', () => {
it('produces a span', async () => {
const newClient = createClient({
const newClient = redis.createClient({
url: redisTestUrl,
});

Expand Down Expand Up @@ -223,7 +223,7 @@ describe('redis@^4.0.0', () => {
const redisURL = `redis://${redisTestConfig.host}:${
redisTestConfig.port + 1
}`;
const newClient = createClient({
const newClient = redis.createClient({
url: redisURL,
});

Expand All @@ -246,7 +246,7 @@ describe('redis@^4.0.0', () => {
const expectAttributeConnString = `redis://${redisTestConfig.host}:${
redisTestConfig.port + 1
}`;
const newClient = createClient({
const newClient = redis.createClient({
url: redisURL,
});

Expand All @@ -273,7 +273,7 @@ describe('redis@^4.0.0', () => {
const expectAttributeConnString = `redis://${redisTestConfig.host}:${
redisTestConfig.port + 1
}?db=mydb`;
const newClient = createClient({
const newClient = redis.createClient({
url: redisURL,
});

Expand Down Expand Up @@ -377,11 +377,17 @@ describe('redis@^4.0.0', () => {
});

it('multi command with error', async () => {
const [setReply, incrReply] = await client
.multi()
.set('key', 'value')
.incr('key')
.exec(); // ['OK', 'ReplyError']
let replies;
try {
replies = await client.multi().set('key', 'value').incr('key').exec();
} catch (err) {
// Starting in [email protected] `multi().exec()` with *throw* a
// MultiErrorReply, with `err.replies`, if any of the commands error.
assert.ok(err instanceof redis.MultiErrorReply);
replies = err.replies;
}
const [setReply, incrReply] = replies;

assert.strictEqual(setReply, 'OK'); // verify we did not screw up the normal functionality
assert.ok(incrReply instanceof Error); // verify we did not screw up the normal functionality

Expand All @@ -406,7 +412,7 @@ describe('redis@^4.0.0', () => {
await client.multi().get(watchedKey).exec();
assert.fail('expected WatchError to be thrown and caught in try/catch');
} catch (error) {
assert.ok(error instanceof WatchError);
assert.ok(error instanceof redis.WatchError);
}

// All the multi spans' status are set to ERROR.
Expand Down

0 comments on commit f78fbc6

Please sign in to comment.