1010require_relative '../../../../../lib/opentelemetry/instrumentation/redis/patches/redis_v4_client'
1111
1212describe OpenTelemetry ::Instrumentation ::Redis ::Patches ::RedisV4Client do
13+ # NOTE: These tests should be run for redis v4 and redis v5, even though the patches won't be installed on v5.
14+ # Perhaps these tests should live in a different file?
1315 let ( :instrumentation ) { OpenTelemetry ::Instrumentation ::Redis ::Instrumentation . instance }
1416 let ( :exporter ) { EXPORTER }
1517 let ( :password ) { 'passw0rd' }
2123 # will generate one extra span on connect because the Redis client will
2224 # send an AUTH command before doing anything else.
2325 def redis_with_auth ( redis_options = { } )
24- redis_options [ :password ] = password
25- redis_options [ :host ] = redis_host
26- redis_options [ :port ] = redis_port
26+ redis_options [ :password ] || = password
27+ redis_options [ :host ] || = redis_host
28+ redis_options [ :port ] || = redis_port
2729 Redis . new ( redis_options )
2830 end
2931
32+ def redis_version
33+ Gem . loaded_specs [ 'redis' ] &.version
34+ end
35+
36+ def redis_version_major
37+ redis_version &.segments &.first
38+ end
39+
40+ def redis_gte_5?
41+ redis_version_major &.>=( 5 )
42+ end
43+
3044 before do
3145 # ensure obfuscation is off if it was previously set in a different test
3246 config = { db_statement : :include }
@@ -95,19 +109,49 @@ def redis_with_auth(redis_options = {})
95109 end
96110
97111 it 'reflects db index' do
112+ skip if redis_gte_5?
113+
98114 redis = redis_with_auth ( db : 1 )
99115 redis . get ( 'K' )
100116
101117 _ ( exporter . finished_spans . size ) . must_equal 3
102118
103119 select_span = exporter . finished_spans [ 1 ]
104120 _ ( select_span . name ) . must_equal 'SELECT'
105- _ ( select_span . attributes [ 'db.system' ] ) . must_equal 'redis'
106121 _ ( select_span . attributes [ 'db.statement' ] ) . must_equal ( 'SELECT 1' )
122+
123+ get_span = exporter . finished_spans . last
124+
125+ _ ( select_span . attributes [ 'db.system' ] ) . must_equal 'redis'
107126 _ ( select_span . attributes [ 'net.peer.name' ] ) . must_equal redis_host
108127 _ ( select_span . attributes [ 'net.peer.port' ] ) . must_equal redis_port
128+ _ ( select_span . attributes [ 'db.redis.database_index' ] ) . must_equal 1
109129
130+ _ ( get_span . name ) . must_equal 'GET'
131+ _ ( get_span . attributes [ 'db.system' ] ) . must_equal 'redis'
132+ _ ( get_span . attributes [ 'db.statement' ] ) . must_equal ( 'GET K' )
133+ _ ( get_span . attributes [ 'db.redis.database_index' ] ) . must_equal 1
134+ _ ( get_span . attributes [ 'net.peer.name' ] ) . must_equal redis_host
135+ _ ( get_span . attributes [ 'net.peer.port' ] ) . must_equal redis_port
136+ end
137+
138+ it 'reflects db index v5' do
139+ skip unless redis_gte_5?
140+
141+ redis = redis_with_auth ( db : 1 )
142+ redis . get ( 'K' )
143+
144+ _ ( exporter . finished_spans . size ) . must_equal 2
145+ select_span = exporter . finished_spans . first
110146 get_span = exporter . finished_spans . last
147+ _ ( select_span . name ) . must_equal 'PIPELINED'
148+ _ ( select_span . attributes [ 'db.statement' ] ) . must_equal ( "AUTH ?\n SELECT 1" )
149+
150+ _ ( select_span . attributes [ 'db.system' ] ) . must_equal 'redis'
151+ _ ( select_span . attributes [ 'net.peer.name' ] ) . must_equal redis_host
152+ _ ( select_span . attributes [ 'net.peer.port' ] ) . must_equal redis_port
153+ _ ( select_span . attributes [ 'db.redis.database_index' ] ) . must_equal 1
154+
111155 _ ( get_span . name ) . must_equal 'GET'
112156 _ ( get_span . attributes [ 'db.system' ] ) . must_equal 'redis'
113157 _ ( get_span . attributes [ 'db.statement' ] ) . must_equal ( 'GET K' )
@@ -134,6 +178,8 @@ def redis_with_auth(redis_options = {})
134178 end
135179
136180 it 'records exceptions' do
181+ skip if redis_gte_5?
182+
137183 expect do
138184 redis = redis_with_auth
139185 redis . call 'THIS_IS_NOT_A_REDIS_FUNC' , 'THIS_IS_NOT_A_VALID_ARG'
@@ -150,21 +196,68 @@ def redis_with_auth(redis_options = {})
150196 _ ( last_span . status . code ) . must_equal (
151197 OpenTelemetry ::Trace ::Status ::ERROR
152198 )
199+
153200 _ ( last_span . status . description . tr ( '`' , "'" ) ) . must_include (
154201 "ERR unknown command 'THIS_IS_NOT_A_REDIS_FUNC', with args beginning with: 'THIS_IS_NOT_A_VALID_ARG"
155202 )
156203 end
157204
158- it 'records net.peer.name and net.peer.port attributes' do
205+ it 'records exceptions v5' do
206+ skip unless redis_gte_5?
207+
159208 expect do
160- Redis . new ( host : 'example.com' , port : 8321 , timeout : 0.01 ) . auth ( password )
161- end . must_raise Redis ::CannotConnectError
209+ redis = redis_with_auth
210+ redis . call 'THIS_IS_NOT_A_REDIS_FUNC' , 'THIS_IS_NOT_A_VALID_ARG'
211+ end . must_raise Redis ::CommandError
162212
163- _ ( last_span . name ) . must_equal 'AUTH'
213+ _ ( exporter . finished_spans . size ) . must_equal 2
214+ _ ( last_span . name ) . must_equal 'THIS_IS_NOT_A_REDIS_FUNC'
164215 _ ( last_span . attributes [ 'db.system' ] ) . must_equal 'redis'
165- _ ( last_span . attributes [ 'db.statement' ] ) . must_equal 'AUTH ?'
166- _ ( last_span . attributes [ 'net.peer.name' ] ) . must_equal 'example.com'
167- _ ( last_span . attributes [ 'net.peer.port' ] ) . must_equal 8321
216+ _ ( last_span . attributes [ 'db.statement' ] ) . must_equal (
217+ 'THIS_IS_NOT_A_REDIS_FUNC THIS_IS_NOT_A_VALID_ARG'
218+ )
219+ _ ( last_span . attributes [ 'net.peer.name' ] ) . must_equal redis_host
220+ _ ( last_span . attributes [ 'net.peer.port' ] ) . must_equal redis_port
221+ _ ( last_span . status . code ) . must_equal (
222+ OpenTelemetry ::Trace ::Status ::ERROR
223+ )
224+
225+ _ ( last_span . status . description . tr ( '`' , "'" ) ) . must_include (
226+ 'Unhandled exception of type: RedisClient::CommandError'
227+ )
228+ end
229+
230+ it 'records net.peer.name and net.peer.port attributes' do
231+ skip if redis_gte_5?
232+
233+ client = Redis . new ( host : 'example.com' , port : 8321 , timeout : 0.01 )
234+ expect { client . auth ( password ) } . must_raise Redis ::CannotConnectError
235+
236+ if redis_gte_5?
237+ skip (
238+ 'Redis 5 is a wrapper around RedisClient, which calls' \
239+ '`ensure_connected` before any of the middlewares are invoked.' \
240+ 'This is more appropriately instrumented via a `#connect` hook in the middleware.'
241+ )
242+ else
243+ _ ( last_span . name ) . must_equal 'AUTH'
244+ _ ( last_span . attributes [ 'db.system' ] ) . must_equal 'redis'
245+ _ ( last_span . attributes [ 'db.statement' ] ) . must_equal 'AUTH ?'
246+ _ ( last_span . attributes [ 'net.peer.name' ] ) . must_equal 'example.com'
247+ _ ( last_span . attributes [ 'net.peer.port' ] ) . must_equal 8321
248+ end
249+ end
250+
251+ it 'records net.peer.name and net.peer.port attributes v5' do
252+ skip unless redis_gte_5?
253+
254+ client = Redis . new ( host : 'example.com' , port : 8321 , timeout : 0.01 )
255+ expect { client . auth ( password ) } . must_raise Redis ::CannotConnectError
256+
257+ # NOTE: Redis 5 is a wrapper around RedisClient, which calls
258+ # ensure_connected` before any of the middlewares are invoked, so we don't
259+ # have a span here. This is more appropriately instrumented via a `#connect`
260+ # hook in the middleware.
168261 end
169262
170263 it 'traces pipelined commands' do
@@ -185,10 +278,11 @@ def redis_with_auth(redis_options = {})
185278
186279 it 'traces pipelined commands on commit' do
187280 redis = redis_with_auth
188- redis . queue ( [ :set , 'v1' , '0' ] )
189- redis . queue ( [ :incr , 'v1' ] )
190- redis . queue ( [ :get , 'v1' ] )
191- redis . commit
281+ redis . pipelined do |pipeline |
282+ pipeline . set ( 'v1' , '0' )
283+ pipeline . incr ( 'v1' )
284+ pipeline . get ( 'v1' )
285+ end
192286
193287 _ ( exporter . finished_spans . size ) . must_equal 2
194288 _ ( last_span . name ) . must_equal 'PIPELINED'
@@ -225,16 +319,17 @@ def redis_with_auth(redis_options = {})
225319 it 'truncates long db.statements' do
226320 redis = redis_with_auth
227321 the_long_value = 'y' * 100
228- redis . queue ( [ :set , 'v1' , the_long_value ] )
229- redis . queue ( [ :set , 'v1' , the_long_value ] )
230- redis . queue ( [ :set , 'v1' , the_long_value ] )
231- redis . queue ( [ :set , 'v1' , the_long_value ] )
232- redis . queue ( [ :set , 'v1' , the_long_value ] )
233- redis . queue ( [ :set , 'v1' , the_long_value ] )
234- redis . queue ( [ :set , 'v1' , the_long_value ] )
235- redis . queue ( [ :set , 'v1' , the_long_value ] )
236- redis . queue ( [ :set , 'v1' , the_long_value ] )
237- redis . commit
322+ redis . pipelined do |pipeline |
323+ pipeline . set ( 'v1' , the_long_value )
324+ pipeline . set ( 'v1' , the_long_value )
325+ pipeline . set ( 'v1' , the_long_value )
326+ pipeline . set ( 'v1' , the_long_value )
327+ pipeline . set ( 'v1' , the_long_value )
328+ pipeline . set ( 'v1' , the_long_value )
329+ pipeline . set ( 'v1' , the_long_value )
330+ pipeline . set ( 'v1' , the_long_value )
331+ pipeline . set ( 'v1' , the_long_value )
332+ end
238333
239334 expected_db_statement = <<~HEREDOC . chomp
240335 SET v1 yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
@@ -291,13 +386,39 @@ def redis_with_auth(redis_options = {})
291386 end
292387
293388 it 'omits db.statement attribute' do
389+ skip if redis_gte_5?
390+
391+ redis = redis_with_auth
392+ _ ( redis . set ( 'K' , 'xyz' ) ) . must_equal 'OK'
393+ _ ( redis . get ( 'K' ) ) . must_equal 'xyz'
394+ _ ( exporter . finished_spans . size ) . must_equal 3
395+
396+ set_span = exporter . finished_spans [ 0 ]
397+ _ ( set_span . name ) . must_equal ( 'AUTH' )
398+ _ ( set_span . attributes [ 'db.system' ] ) . must_equal 'redis'
399+ _ ( set_span . attributes ) . wont_include ( 'db.statement' )
400+
401+ set_span = exporter . finished_spans [ 1 ]
402+ _ ( set_span . name ) . must_equal 'SET'
403+ _ ( set_span . attributes [ 'db.system' ] ) . must_equal 'redis'
404+ _ ( set_span . attributes ) . wont_include ( 'db.statement' )
405+
406+ set_span = exporter . finished_spans [ 2 ]
407+ _ ( set_span . name ) . must_equal 'GET'
408+ _ ( set_span . attributes [ 'db.system' ] ) . must_equal 'redis'
409+ _ ( set_span . attributes ) . wont_include ( 'db.statement' )
410+ end
411+
412+ it 'omits db.statement attribute v5' do
413+ skip unless redis_gte_5?
414+
294415 redis = redis_with_auth
295416 _ ( redis . set ( 'K' , 'xyz' ) ) . must_equal 'OK'
296417 _ ( redis . get ( 'K' ) ) . must_equal 'xyz'
297418 _ ( exporter . finished_spans . size ) . must_equal 3
298419
299420 set_span = exporter . finished_spans [ 0 ]
300- _ ( set_span . name ) . must_equal 'AUTH'
421+ _ ( set_span . name ) . must_equal ( 'PIPELINED' )
301422 _ ( set_span . attributes [ 'db.system' ] ) . must_equal 'redis'
302423 _ ( set_span . attributes ) . wont_include ( 'db.statement' )
303424
@@ -320,13 +441,45 @@ def redis_with_auth(redis_options = {})
320441 end
321442
322443 it 'obfuscates arguments in db.statement' do
444+ skip if redis_gte_5?
445+
446+ redis = redis_with_auth
447+ _ ( redis . set ( 'K' , 'xyz' ) ) . must_equal 'OK'
448+ _ ( redis . get ( 'K' ) ) . must_equal 'xyz'
449+ _ ( exporter . finished_spans . size ) . must_equal 3
450+
451+ set_span = exporter . finished_spans [ 0 ]
452+ _ ( set_span . name ) . must_equal ( 'AUTH' )
453+ _ ( set_span . attributes [ 'db.system' ] ) . must_equal 'redis'
454+ _ ( set_span . attributes [ 'db.statement' ] ) . must_equal (
455+ 'AUTH ?'
456+ )
457+
458+ set_span = exporter . finished_spans [ 1 ]
459+ _ ( set_span . name ) . must_equal 'SET'
460+ _ ( set_span . attributes [ 'db.system' ] ) . must_equal 'redis'
461+ _ ( set_span . attributes [ 'db.statement' ] ) . must_equal (
462+ 'SET ? ?'
463+ )
464+
465+ set_span = exporter . finished_spans [ 2 ]
466+ _ ( set_span . name ) . must_equal 'GET'
467+ _ ( set_span . attributes [ 'db.system' ] ) . must_equal 'redis'
468+ _ ( set_span . attributes [ 'db.statement' ] ) . must_equal (
469+ 'GET ?'
470+ )
471+ end
472+
473+ it 'obfuscates arguments in db.statement v5' do
474+ skip unless redis_gte_5?
475+
323476 redis = redis_with_auth
324477 _ ( redis . set ( 'K' , 'xyz' ) ) . must_equal 'OK'
325478 _ ( redis . get ( 'K' ) ) . must_equal 'xyz'
326479 _ ( exporter . finished_spans . size ) . must_equal 3
327480
328481 set_span = exporter . finished_spans [ 0 ]
329- _ ( set_span . name ) . must_equal 'AUTH'
482+ _ ( set_span . name ) . must_equal ( 'PIPELINED' )
330483 _ ( set_span . attributes [ 'db.system' ] ) . must_equal 'redis'
331484 _ ( set_span . attributes [ 'db.statement' ] ) . must_equal (
332485 'AUTH ?'
0 commit comments