@@ -28,6 +28,7 @@ import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
28
28
import * as testUtils from '@opentelemetry/test-utils' ;
29
29
import {
30
30
InMemorySpanExporter ,
31
+ ReadableSpan ,
31
32
SimpleSpanProcessor ,
32
33
} from '@opentelemetry/tracing' ;
33
34
import * as assert from 'assert' ;
@@ -36,6 +37,7 @@ import { IORedisPlugin, plugin } from '../src';
36
37
import { IoredisPluginConfig , DbStatementSerializer } from '../src/types' ;
37
38
import {
38
39
DatabaseAttribute ,
40
+ ExceptionAttribute ,
39
41
GeneralAttribute ,
40
42
} from '@opentelemetry/semantic-conventions' ;
41
43
@@ -59,6 +61,20 @@ const unsetStatus: Status = {
59
61
code : StatusCode . UNSET ,
60
62
} ;
61
63
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
+
62
78
describe ( 'ioredis' , ( ) => {
63
79
const provider = new NodeTracerProvider ( ) ;
64
80
let ioredis : typeof ioredisTypes ;
@@ -131,9 +147,11 @@ describe('ioredis', () => {
131
147
assert . strictEqual ( endedSpans . length , 3 ) ;
132
148
assert . strictEqual ( endedSpans [ 2 ] . name , 'test span' ) ;
133
149
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
+ } ) ;
137
155
} ;
138
156
const errorHandler = ( err : Error ) => {
139
157
assert . ifError ( err ) ;
@@ -263,6 +281,38 @@ describe('ioredis', () => {
263
281
} ) ;
264
282
} ) ;
265
283
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
+
266
316
it ( 'should create a child span for streamify scanning' , done => {
267
317
const attributes = {
268
318
...DEFAULT_ATTRIBUTES ,
@@ -322,10 +372,10 @@ describe('ioredis', () => {
322
372
const spanNames = [
323
373
'connect' ,
324
374
'connect' ,
325
- 'subscribe' ,
326
375
'info' ,
327
376
'info' ,
328
377
'subscribe' ,
378
+ 'subscribe' ,
329
379
'publish' ,
330
380
'publish' ,
331
381
'unsubscribe' ,
@@ -377,24 +427,48 @@ describe('ioredis', () => {
377
427
378
428
span . end ( ) ;
379
429
const endedSpans = memoryExporter . getFinishedSpans ( ) ;
430
+ const evalshaSpan = endedSpans [ 0 ] ;
380
431
// the script may be already cached on server therefore we get either 2 or 3 spans
381
432
if ( endedSpans . length === 3 ) {
382
433
assert . strictEqual ( endedSpans [ 2 ] . name , 'test span' ) ;
383
434
assert . strictEqual ( endedSpans [ 1 ] . name , 'eval' ) ;
384
435
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
+ ) ;
385
459
} else {
386
460
assert . strictEqual ( endedSpans . length , 2 ) ;
387
461
assert . strictEqual ( endedSpans [ 1 ] . name , 'test span' ) ;
388
462
assert . strictEqual ( endedSpans [ 0 ] . name , 'evalsha' ) ;
463
+ testUtils . assertSpan (
464
+ evalshaSpan ,
465
+ SpanKind . CLIENT ,
466
+ attributes ,
467
+ [ ] ,
468
+ unsetStatus
469
+ ) ;
389
470
}
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 ) ;
398
472
done ( ) ;
399
473
} ) ;
400
474
} ) ;
0 commit comments