Skip to content

Commit 4421670

Browse files
Elyahouemdnetoxrmx
authored
opentelemetry-instrumentation-redis: implement suppression of instrumentation in Redis commands and pipelines (#3955)
* 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. * Doc * Ruff * Disable too-many-public-methods for tests file * Update instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py --------- Co-authored-by: Emídio Neto <[email protected]> Co-authored-by: Riccardo Magliocchetti <[email protected]>
1 parent 03c39cf commit 4421670

File tree

3 files changed

+181
-1
lines changed

3 files changed

+181
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2727
([#3884](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3884))
2828
- `opentelemetry-instrumentation-aiohttp-server`: add support for custom header captures via `OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST` and `OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`
2929
([#3916](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3916))
30+
- `opentelemetry-instrumentation-redis`: add support for `suppress_instrumentation` context manager for both sync and async Redis clients and pipelines
3031

3132
### Fixed
3233

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

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,35 @@ def response_hook(span, instance, response):
110110
client = redis.StrictRedis(host="localhost", port=6379)
111111
client.get("my-key")
112112
113+
Suppress Instrumentation
114+
------------------------
115+
116+
You can use the ``suppress_instrumentation`` context manager to prevent instrumentation
117+
from being applied to specific Redis operations. This is useful when you want to avoid
118+
creating spans for internal operations, health checks, or during specific code paths.
119+
120+
.. code:: python
121+
122+
from opentelemetry.instrumentation.redis import RedisInstrumentor
123+
from opentelemetry.instrumentation.utils import suppress_instrumentation
124+
import redis
125+
126+
# Instrument redis
127+
RedisInstrumentor().instrument()
128+
129+
client = redis.StrictRedis(host="localhost", port=6379)
130+
131+
# This will report a span
132+
client.get("my-key")
133+
134+
# This will NOT report a span
135+
with suppress_instrumentation():
136+
client.get("internal-key")
137+
client.set("cache-key", "value")
138+
139+
# This will report a span again
140+
client.get("another-key")
141+
113142
API
114143
---
115144
"""
@@ -134,7 +163,10 @@ def response_hook(span, instance, response):
134163
_set_connection_attributes,
135164
)
136165
from opentelemetry.instrumentation.redis.version import __version__
137-
from opentelemetry.instrumentation.utils import unwrap
166+
from opentelemetry.instrumentation.utils import (
167+
is_instrumentation_enabled,
168+
unwrap,
169+
)
138170
from opentelemetry.semconv._incubating.attributes.db_attributes import (
139171
DB_STATEMENT,
140172
)
@@ -196,6 +228,9 @@ def _traced_execute_command(
196228
args: tuple[Any, ...],
197229
kwargs: dict[str, Any],
198230
) -> R:
231+
if not is_instrumentation_enabled():
232+
return func(*args, **kwargs)
233+
199234
query = _format_command_args(args)
200235
name = _build_span_name(instance, args)
201236
with tracer.start_as_current_span(
@@ -231,6 +266,9 @@ def _traced_execute_pipeline(
231266
args: tuple[Any, ...],
232267
kwargs: dict[str, Any],
233268
) -> R:
269+
if not is_instrumentation_enabled():
270+
return func(*args, **kwargs)
271+
234272
(
235273
command_stack,
236274
resource,
@@ -276,6 +314,9 @@ async def _async_traced_execute_command(
276314
args: tuple[Any, ...],
277315
kwargs: dict[str, Any],
278316
) -> Awaitable[R]:
317+
if not is_instrumentation_enabled():
318+
return await func(*args, **kwargs)
319+
279320
query = _format_command_args(args)
280321
name = _build_span_name(instance, args)
281322

@@ -307,6 +348,9 @@ async def _async_traced_execute_pipeline(
307348
args: tuple[Any, ...],
308349
kwargs: dict[str, Any],
309350
) -> Awaitable[R]:
351+
if not is_instrumentation_enabled():
352+
return await func(*args, **kwargs)
353+
310354
(
311355
command_stack,
312356
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,
@@ -40,6 +41,7 @@
4041
from opentelemetry.trace import SpanKind
4142

4243

44+
# pylint: disable=too-many-public-methods
4345
class TestRedis(TestBase):
4446
def assert_span_count(self, count: int):
4547
"""
@@ -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)