Skip to content

Commit 7a5af67

Browse files
committed
opentelemetry-instrumentation-redis: implement suppression of instrumentation in Redis commands and pipelines
- Added `suppress_instrumentation` context manager to allow selective disabling of instrumentation for Redis commands and pipelines. - Updated the Redis instrumentation to check if instrumentation is enabled before executing commands. - Added unit tests to verify the functionality of the suppression feature for both synchronous and asynchronous Redis operations.
1 parent 1d97282 commit 7a5af67

File tree

2 files changed

+151
-1
lines changed

2 files changed

+151
-1
lines changed

instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,10 @@ def response_hook(span, instance, response):
134134
_set_connection_attributes,
135135
)
136136
from opentelemetry.instrumentation.redis.version import __version__
137-
from opentelemetry.instrumentation.utils import unwrap
137+
from opentelemetry.instrumentation.utils import (
138+
is_instrumentation_enabled,
139+
unwrap,
140+
)
138141
from opentelemetry.semconv._incubating.attributes.db_attributes import (
139142
DB_STATEMENT,
140143
)
@@ -196,6 +199,9 @@ def _traced_execute_command(
196199
args: tuple[Any, ...],
197200
kwargs: dict[str, Any],
198201
) -> R:
202+
if not is_instrumentation_enabled():
203+
return func(*args, **kwargs)
204+
199205
query = _format_command_args(args)
200206
name = _build_span_name(instance, args)
201207
with tracer.start_as_current_span(
@@ -231,6 +237,9 @@ def _traced_execute_pipeline(
231237
args: tuple[Any, ...],
232238
kwargs: dict[str, Any],
233239
) -> R:
240+
if not is_instrumentation_enabled():
241+
return func(*args, **kwargs)
242+
234243
(
235244
command_stack,
236245
resource,
@@ -276,6 +285,9 @@ async def _async_traced_execute_command(
276285
args: tuple[Any, ...],
277286
kwargs: dict[str, Any],
278287
) -> Awaitable[R]:
288+
if not is_instrumentation_enabled():
289+
return await func(*args, **kwargs)
290+
279291
query = _format_command_args(args)
280292
name = _build_span_name(instance, args)
281293

@@ -307,6 +319,9 @@ async def _async_traced_execute_pipeline(
307319
args: tuple[Any, ...],
308320
kwargs: dict[str, Any],
309321
) -> Awaitable[R]:
322+
if not is_instrumentation_enabled():
323+
return await func(*args, **kwargs)
324+
310325
(
311326
command_stack,
312327
resource,

instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
from opentelemetry import trace
2727
from opentelemetry.instrumentation.redis import RedisInstrumentor
28+
from opentelemetry.instrumentation.utils import suppress_instrumentation
2829
from opentelemetry.semconv._incubating.attributes.db_attributes import (
2930
DB_REDIS_DATABASE_INDEX,
3031
DB_SYSTEM,
@@ -390,6 +391,7 @@ def redis_operations():
390391
self.assertEqual(span.kind, SpanKind.CLIENT)
391392
self.assertEqual(span.status.status_code, trace.StatusCode.UNSET)
392393

394+
393395
def test_span_name_empty_pipeline(self):
394396
redis_client = fakeredis.FakeStrictRedis()
395397
pipe = redis_client.pipeline()
@@ -401,6 +403,75 @@ def test_span_name_empty_pipeline(self):
401403
self.assertEqual(spans[0].kind, SpanKind.CLIENT)
402404
self.assertEqual(spans[0].status.status_code, trace.StatusCode.UNSET)
403405

406+
def test_suppress_instrumentation_command(self):
407+
redis_client = redis.Redis()
408+
409+
with mock.patch.object(redis_client, "connection"):
410+
# Execute command with suppression
411+
with suppress_instrumentation():
412+
redis_client.get("key")
413+
414+
# No spans should be created
415+
spans = self.memory_exporter.get_finished_spans()
416+
self.assertEqual(len(spans), 0)
417+
418+
# Verify that instrumentation works again after exiting the context
419+
with mock.patch.object(redis_client, "connection"):
420+
redis_client.get("key")
421+
422+
spans = self.memory_exporter.get_finished_spans()
423+
self.assertEqual(len(spans), 1)
424+
425+
def test_suppress_instrumentation_pipeline(self):
426+
redis_client = fakeredis.FakeStrictRedis()
427+
428+
with suppress_instrumentation():
429+
pipe = redis_client.pipeline()
430+
pipe.set("key1", "value1")
431+
pipe.set("key2", "value2")
432+
pipe.get("key1")
433+
pipe.execute()
434+
435+
# No spans should be created
436+
spans = self.memory_exporter.get_finished_spans()
437+
self.assertEqual(len(spans), 0)
438+
439+
# Verify that instrumentation works again after exiting the context
440+
pipe = redis_client.pipeline()
441+
pipe.set("key3", "value3")
442+
pipe.execute()
443+
444+
spans = self.memory_exporter.get_finished_spans()
445+
self.assertEqual(len(spans), 1)
446+
# Pipeline span could be "SET" or "redis.pipeline" depending on implementation
447+
self.assertIn(spans[0].name, ["SET", "redis.pipeline"])
448+
449+
def test_suppress_instrumentation_mixed(self):
450+
redis_client = redis.Redis()
451+
452+
# Regular instrumented call
453+
with mock.patch.object(redis_client, "connection"):
454+
redis_client.set("key1", "value1")
455+
456+
spans = self.memory_exporter.get_finished_spans()
457+
self.assertEqual(len(spans), 1)
458+
self.memory_exporter.clear()
459+
460+
# Suppressed call
461+
with suppress_instrumentation():
462+
with mock.patch.object(redis_client, "connection"):
463+
redis_client.set("key2", "value2")
464+
465+
spans = self.memory_exporter.get_finished_spans()
466+
self.assertEqual(len(spans), 0)
467+
468+
# Another regular instrumented call
469+
with mock.patch.object(redis_client, "connection"):
470+
redis_client.get("key1")
471+
472+
spans = self.memory_exporter.get_finished_spans()
473+
self.assertEqual(len(spans), 1)
474+
404475

405476
class TestRedisAsync(TestBase, IsolatedAsyncioTestCase):
406477
def assert_span_count(self, count: int):
@@ -570,6 +641,70 @@ async def test_span_name_empty_pipeline(self):
570641
self.assertEqual(spans[0].status.status_code, trace.StatusCode.UNSET)
571642
self.instrumentor.uninstrument_client(client=redis_client)
572643

644+
@pytest.mark.asyncio
645+
async def test_suppress_instrumentation_async_command(self):
646+
self.instrumentor.instrument(tracer_provider=self.tracer_provider)
647+
redis_client = FakeRedis()
648+
649+
# Execute command with suppression
650+
with suppress_instrumentation():
651+
await redis_client.get("key")
652+
653+
# No spans should be created
654+
self.assert_span_count(0)
655+
656+
# Verify that instrumentation works again after exiting the context
657+
await redis_client.set("key", "value")
658+
self.assert_span_count(1)
659+
self.instrumentor.uninstrument()
660+
661+
@pytest.mark.asyncio
662+
async def test_suppress_instrumentation_async_pipeline(self):
663+
self.instrumentor.instrument(tracer_provider=self.tracer_provider)
664+
redis_client = FakeRedis()
665+
666+
# Execute pipeline with suppression
667+
with suppress_instrumentation():
668+
async with redis_client.pipeline() as pipe:
669+
await pipe.set("key1", "value1")
670+
await pipe.set("key2", "value2")
671+
await pipe.get("key1")
672+
await pipe.execute()
673+
674+
# No spans should be created
675+
self.assert_span_count(0)
676+
677+
# Verify that instrumentation works again after exiting the context
678+
async with redis_client.pipeline() as pipe:
679+
await pipe.set("key3", "value3")
680+
await pipe.execute()
681+
682+
spans = self.assert_span_count(1)
683+
# Pipeline span could be "SET" or "redis.pipeline" depending on implementation
684+
self.assertIn(spans[0].name, ["SET", "redis.pipeline"])
685+
self.instrumentor.uninstrument()
686+
687+
@pytest.mark.asyncio
688+
async def test_suppress_instrumentation_async_mixed(self):
689+
self.instrumentor.instrument(tracer_provider=self.tracer_provider)
690+
redis_client = FakeRedis()
691+
692+
# Regular instrumented call
693+
await redis_client.set("key1", "value1")
694+
self.assert_span_count(1)
695+
self.memory_exporter.clear()
696+
697+
# Suppressed call
698+
with suppress_instrumentation():
699+
await redis_client.set("key2", "value2")
700+
701+
self.assert_span_count(0)
702+
703+
# Another regular instrumented call
704+
await redis_client.get("key1")
705+
self.assert_span_count(1)
706+
self.instrumentor.uninstrument()
707+
573708

574709
class TestRedisInstance(TestBase):
575710
def assert_span_count(self, count: int):

0 commit comments

Comments
 (0)