Skip to content

Commit 80d1049

Browse files
author
Amir Blum
authored
fix(plugin-ioredis): end span on response from the server and set span status according to response (open-telemetry#239)
* feat(plugin-ioredis): enahnce response handling - fix: end span on response from server, so duration accurately include the server processing and networking time - fix: set span status to error if server response with error - feat: add responseHook to plugin config so user can register hook to add custom attributes to span when successful response arrive from server * fix: add forgotten changes * style: fix lint * chore: remove reponseHook from PR * fix: remove unused import for build to pass * chore: lint * style: set span error with ternary * style: fix lint * style: fix lint * chore: fix lint * revert: lint change to graphql file * test(plugin-ioredis): make new test compatible with v0.15.0 * fix(plugin-ioredis): recordException on span when server returns error * fix: correct type for events in assertSpan
1 parent 234c42f commit 80d1049

File tree

3 files changed

+109
-15
lines changed

3 files changed

+109
-15
lines changed

packages/opentelemetry-test-utils/testUtils.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@
1515
*/
1616

1717
import * as childProcess from 'child_process';
18-
import { SpanKind, Attributes, Event, Span, Status } from '@opentelemetry/api';
18+
import {
19+
SpanKind,
20+
Attributes,
21+
Span,
22+
Status,
23+
TimedEvent,
24+
} from '@opentelemetry/api';
1925
import * as assert from 'assert';
2026
import { ReadableSpan } from '@opentelemetry/tracing';
2127
import {
@@ -78,7 +84,7 @@ export const assertSpan = (
7884
span: ReadableSpan,
7985
kind: SpanKind,
8086
attributes: Attributes,
81-
events: Event[],
87+
events: TimedEvent[],
8288
status: Status
8389
) => {
8490
assert.strictEqual(span.spanContext.traceId.length, 32);

plugins/node/opentelemetry-plugin-ioredis/src/utils.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636

3737
const endSpan = (span: Span, err: NodeJS.ErrnoException | null | undefined) => {
3838
if (err) {
39+
span.recordException(err);
3940
span.setStatus({
4041
code: StatusCode.ERROR,
4142
message: err.message,
@@ -116,7 +117,20 @@ export const traceSendCommand = (
116117

117118
try {
118119
const result = original.apply(this, arguments);
119-
endSpan(span, null);
120+
121+
const origResolve = cmd.resolve;
122+
/* eslint-disable @typescript-eslint/no-explicit-any */
123+
cmd.resolve = function (result: any) {
124+
endSpan(span, null);
125+
origResolve(result);
126+
};
127+
128+
const origReject = cmd.reject;
129+
cmd.reject = function (err: Error) {
130+
endSpan(span, err);
131+
origReject(err);
132+
};
133+
120134
return result;
121135
} catch (error) {
122136
endSpan(span, error);

plugins/node/opentelemetry-plugin-ioredis/test/ioredis.test.ts

+86-12
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
2828
import * as testUtils from '@opentelemetry/test-utils';
2929
import {
3030
InMemorySpanExporter,
31+
ReadableSpan,
3132
SimpleSpanProcessor,
3233
} from '@opentelemetry/tracing';
3334
import * as assert from 'assert';
@@ -36,6 +37,7 @@ import { IORedisPlugin, plugin } from '../src';
3637
import { IoredisPluginConfig, DbStatementSerializer } from '../src/types';
3738
import {
3839
DatabaseAttribute,
40+
ExceptionAttribute,
3941
GeneralAttribute,
4042
} from '@opentelemetry/semantic-conventions';
4143

@@ -59,6 +61,20 @@ const unsetStatus: Status = {
5961
code: StatusCode.UNSET,
6062
};
6163

64+
const predictableStackTrace =
65+
'-- Stack trace replaced by test to predictable value -- ';
66+
const sanitizeEventForAssertion = (span: ReadableSpan) => {
67+
span.events.forEach(e => {
68+
// stack trace includes data such as /user/{userName}/repos/{projectName}
69+
if (e.attributes?.[ExceptionAttribute.STACKTRACE]) {
70+
e.attributes[ExceptionAttribute.STACKTRACE] = predictableStackTrace;
71+
}
72+
73+
// since time will change on each test invocation, it is being replaced to predicable value
74+
e.time = [0, 0];
75+
});
76+
};
77+
6278
describe('ioredis', () => {
6379
const provider = new NodeTracerProvider();
6480
let ioredis: typeof ioredisTypes;
@@ -131,9 +147,11 @@ describe('ioredis', () => {
131147
assert.strictEqual(endedSpans.length, 3);
132148
assert.strictEqual(endedSpans[2].name, 'test span');
133149

134-
client.quit(done);
135-
assert.strictEqual(endedSpans.length, 4);
136-
assert.strictEqual(endedSpans[3].name, 'quit');
150+
client.quit(() => {
151+
assert.strictEqual(endedSpans.length, 4);
152+
assert.strictEqual(endedSpans[3].name, 'quit');
153+
done();
154+
});
137155
};
138156
const errorHandler = (err: Error) => {
139157
assert.ifError(err);
@@ -263,6 +281,38 @@ describe('ioredis', () => {
263281
});
264282
});
265283

284+
it('should set span with error when redis return reject', async () => {
285+
const span = provider.getTracer('ioredis-test').startSpan('test span');
286+
await context.with(setSpan(context.active(), span), async () => {
287+
await client.set('non-int-key', 'non-int-value');
288+
try {
289+
// should throw 'ReplyError: ERR value is not an integer or out of range'
290+
// because the value im the key is not numeric and we try to increment it
291+
await client.incr('non-int-key');
292+
} catch (ex) {
293+
const endedSpans = memoryExporter.getFinishedSpans();
294+
assert.strictEqual(endedSpans.length, 2);
295+
const ioredisSpan = endedSpans[1];
296+
// redis 'incr' operation failed with exception, so span should indicate it
297+
assert.strictEqual(ioredisSpan.status.code, StatusCode.ERROR);
298+
const exceptionEvent = ioredisSpan.events[0];
299+
assert.strictEqual(exceptionEvent.name, 'exception');
300+
assert.strictEqual(
301+
exceptionEvent.attributes?.[ExceptionAttribute.MESSAGE],
302+
ex.message
303+
);
304+
assert.strictEqual(
305+
exceptionEvent.attributes?.[ExceptionAttribute.STACKTRACE],
306+
ex.stack
307+
);
308+
assert.strictEqual(
309+
exceptionEvent.attributes?.[ExceptionAttribute.TYPE],
310+
ex.name
311+
);
312+
}
313+
});
314+
});
315+
266316
it('should create a child span for streamify scanning', done => {
267317
const attributes = {
268318
...DEFAULT_ATTRIBUTES,
@@ -322,10 +372,10 @@ describe('ioredis', () => {
322372
const spanNames = [
323373
'connect',
324374
'connect',
325-
'subscribe',
326375
'info',
327376
'info',
328377
'subscribe',
378+
'subscribe',
329379
'publish',
330380
'publish',
331381
'unsubscribe',
@@ -377,24 +427,48 @@ describe('ioredis', () => {
377427

378428
span.end();
379429
const endedSpans = memoryExporter.getFinishedSpans();
430+
const evalshaSpan = endedSpans[0];
380431
// the script may be already cached on server therefore we get either 2 or 3 spans
381432
if (endedSpans.length === 3) {
382433
assert.strictEqual(endedSpans[2].name, 'test span');
383434
assert.strictEqual(endedSpans[1].name, 'eval');
384435
assert.strictEqual(endedSpans[0].name, 'evalsha');
436+
// in this case, server returns NOSCRIPT error for evalsha,
437+
// telling the client to use EVAL instead
438+
sanitizeEventForAssertion(evalshaSpan);
439+
testUtils.assertSpan(
440+
evalshaSpan,
441+
SpanKind.CLIENT,
442+
attributes,
443+
[
444+
{
445+
attributes: {
446+
[ExceptionAttribute.MESSAGE]:
447+
'NOSCRIPT No matching script. Please use EVAL.',
448+
[ExceptionAttribute.STACKTRACE]: predictableStackTrace,
449+
[ExceptionAttribute.TYPE]: 'ReplyError',
450+
},
451+
name: 'exception',
452+
time: [0, 0],
453+
},
454+
],
455+
{
456+
code: StatusCode.ERROR,
457+
}
458+
);
385459
} else {
386460
assert.strictEqual(endedSpans.length, 2);
387461
assert.strictEqual(endedSpans[1].name, 'test span');
388462
assert.strictEqual(endedSpans[0].name, 'evalsha');
463+
testUtils.assertSpan(
464+
evalshaSpan,
465+
SpanKind.CLIENT,
466+
attributes,
467+
[],
468+
unsetStatus
469+
);
389470
}
390-
testUtils.assertSpan(
391-
endedSpans[0],
392-
SpanKind.CLIENT,
393-
attributes,
394-
[],
395-
unsetStatus
396-
);
397-
testUtils.assertPropagation(endedSpans[0], span);
471+
testUtils.assertPropagation(evalshaSpan, span);
398472
done();
399473
});
400474
});

0 commit comments

Comments
 (0)