@@ -27,6 +27,7 @@ import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
2727import * as testUtils from '@opentelemetry/test-utils' ;
2828import {
2929 InMemorySpanExporter ,
30+ ReadableSpan ,
3031 SimpleSpanProcessor ,
3132} from '@opentelemetry/tracing' ;
3233import * as assert from 'assert' ;
@@ -38,6 +39,7 @@ import {
3839} from '../src/types' ;
3940import {
4041 DatabaseAttribute ,
42+ ExceptionAttribute ,
4143 GeneralAttribute ,
4244} from '@opentelemetry/semantic-conventions' ;
4345
@@ -61,6 +63,20 @@ const unsetStatus: Status = {
6163 code : StatusCode . UNSET ,
6264} ;
6365
66+ const predictableStackTrace =
67+ '-- Stack trace replaced by test to predictable value -- ' ;
68+ const sanitizeEventForAssertion = ( span : ReadableSpan ) => {
69+ span . events . forEach ( e => {
70+ // stack trace includes data such as /user/{userName}/repos/{projectName}
71+ if ( e . attributes ?. [ ExceptionAttribute . STACKTRACE ] ) {
72+ e . attributes [ ExceptionAttribute . STACKTRACE ] = predictableStackTrace ;
73+ }
74+
75+ // since time will change on each test invocation, it is being replaced to predicable value
76+ e . time = [ 0 , 0 ] ;
77+ } ) ;
78+ } ;
79+
6480describe ( 'ioredis' , ( ) => {
6581 const provider = new NodeTracerProvider ( ) ;
6682 let ioredis : typeof ioredisTypes ;
@@ -138,9 +154,11 @@ describe('ioredis', () => {
138154 assert . strictEqual ( endedSpans . length , 3 ) ;
139155 assert . strictEqual ( endedSpans [ 2 ] . name , 'test span' ) ;
140156
141- client . quit ( done ) ;
142- assert . strictEqual ( endedSpans . length , 4 ) ;
143- assert . strictEqual ( endedSpans [ 3 ] . name , 'quit' ) ;
157+ client . quit ( ( ) => {
158+ assert . strictEqual ( endedSpans . length , 4 ) ;
159+ assert . strictEqual ( endedSpans [ 3 ] . name , 'quit' ) ;
160+ done ( ) ;
161+ } ) ;
144162 } ;
145163 const errorHandler = ( err : Error ) => {
146164 assert . ifError ( err ) ;
@@ -270,6 +288,38 @@ describe('ioredis', () => {
270288 } ) ;
271289 } ) ;
272290
291+ it ( 'should set span with error when redis return reject' , async ( ) => {
292+ const span = provider . getTracer ( 'ioredis-test' ) . startSpan ( 'test span' ) ;
293+ await context . with ( setSpan ( context . active ( ) , span ) , async ( ) => {
294+ await client . set ( 'non-int-key' , 'non-int-value' ) ;
295+ try {
296+ // should throw 'ReplyError: ERR value is not an integer or out of range'
297+ // because the value im the key is not numeric and we try to increment it
298+ await client . incr ( 'non-int-key' ) ;
299+ } catch ( ex ) {
300+ const endedSpans = memoryExporter . getFinishedSpans ( ) ;
301+ assert . strictEqual ( endedSpans . length , 2 ) ;
302+ const ioredisSpan = endedSpans [ 1 ] ;
303+ // redis 'incr' operation failed with exception, so span should indicate it
304+ assert . strictEqual ( ioredisSpan . status . code , StatusCode . ERROR ) ;
305+ const exceptionEvent = ioredisSpan . events [ 0 ] ;
306+ assert . strictEqual ( exceptionEvent . name , 'exception' ) ;
307+ assert . strictEqual (
308+ exceptionEvent . attributes ?. [ ExceptionAttribute . MESSAGE ] ,
309+ ex . message
310+ ) ;
311+ assert . strictEqual (
312+ exceptionEvent . attributes ?. [ ExceptionAttribute . STACKTRACE ] ,
313+ ex . stack
314+ ) ;
315+ assert . strictEqual (
316+ exceptionEvent . attributes ?. [ ExceptionAttribute . TYPE ] ,
317+ ex . name
318+ ) ;
319+ }
320+ } ) ;
321+ } ) ;
322+
273323 it ( 'should create a child span for streamify scanning' , done => {
274324 const attributes = {
275325 ...DEFAULT_ATTRIBUTES ,
@@ -329,10 +379,10 @@ describe('ioredis', () => {
329379 const spanNames = [
330380 'connect' ,
331381 'connect' ,
332- 'subscribe' ,
333382 'info' ,
334383 'info' ,
335384 'subscribe' ,
385+ 'subscribe' ,
336386 'publish' ,
337387 'publish' ,
338388 'unsubscribe' ,
@@ -384,24 +434,48 @@ describe('ioredis', () => {
384434
385435 span . end ( ) ;
386436 const endedSpans = memoryExporter . getFinishedSpans ( ) ;
437+ const evalshaSpan = endedSpans [ 0 ] ;
387438 // the script may be already cached on server therefore we get either 2 or 3 spans
388439 if ( endedSpans . length === 3 ) {
389440 assert . strictEqual ( endedSpans [ 2 ] . name , 'test span' ) ;
390441 assert . strictEqual ( endedSpans [ 1 ] . name , 'eval' ) ;
391442 assert . strictEqual ( endedSpans [ 0 ] . name , 'evalsha' ) ;
443+ // in this case, server returns NOSCRIPT error for evalsha,
444+ // telling the client to use EVAL instead
445+ sanitizeEventForAssertion ( evalshaSpan ) ;
446+ testUtils . assertSpan (
447+ evalshaSpan ,
448+ SpanKind . CLIENT ,
449+ attributes ,
450+ [
451+ {
452+ attributes : {
453+ [ ExceptionAttribute . MESSAGE ] :
454+ 'NOSCRIPT No matching script. Please use EVAL.' ,
455+ [ ExceptionAttribute . STACKTRACE ] : predictableStackTrace ,
456+ [ ExceptionAttribute . TYPE ] : 'ReplyError' ,
457+ } ,
458+ name : 'exception' ,
459+ time : [ 0 , 0 ] ,
460+ } ,
461+ ] ,
462+ {
463+ code : StatusCode . ERROR ,
464+ }
465+ ) ;
392466 } else {
393467 assert . strictEqual ( endedSpans . length , 2 ) ;
394468 assert . strictEqual ( endedSpans [ 1 ] . name , 'test span' ) ;
395469 assert . strictEqual ( endedSpans [ 0 ] . name , 'evalsha' ) ;
470+ testUtils . assertSpan (
471+ evalshaSpan ,
472+ SpanKind . CLIENT ,
473+ attributes ,
474+ [ ] ,
475+ unsetStatus
476+ ) ;
396477 }
397- testUtils . assertSpan (
398- endedSpans [ 0 ] ,
399- SpanKind . CLIENT ,
400- attributes ,
401- [ ] ,
402- unsetStatus
403- ) ;
404- testUtils . assertPropagation ( endedSpans [ 0 ] , span ) ;
478+ testUtils . assertPropagation ( evalshaSpan , span ) ;
405479 done ( ) ;
406480 } ) ;
407481 } ) ;
0 commit comments