Skip to content

Commit 7567efa

Browse files
authored
Handle redis.exceptions.WatchError as a non-error event in instrumentation (#2668)
1 parent 432d6f5 commit 7567efa

File tree

4 files changed

+135
-5
lines changed

4 files changed

+135
-5
lines changed

Diff for: CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4040
([#2682](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2682))
4141

4242
### Fixed
43-
43+
- Handle `redis.exceptions.WatchError` as a non-error event in redis instrumentation
44+
([#2668](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2668))
4445
- `opentelemetry-instrumentation-httpx` Ensure httpx.get or httpx.request like methods are instrumented
4546
([#2538](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2538))
4647
- Add Python 3.12 support

Diff for: instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def response_hook(span, instance, response):
106106
from opentelemetry.instrumentation.redis.version import __version__
107107
from opentelemetry.instrumentation.utils import unwrap
108108
from opentelemetry.semconv.trace import SpanAttributes
109-
from opentelemetry.trace import Span
109+
from opentelemetry.trace import Span, StatusCode
110110

111111
_DEFAULT_SERVICE = "redis"
112112

@@ -212,9 +212,16 @@ def _traced_execute_pipeline(func, instance, args, kwargs):
212212
span.set_attribute(
213213
"db.redis.pipeline_length", len(command_stack)
214214
)
215-
response = func(*args, **kwargs)
215+
216+
response = None
217+
try:
218+
response = func(*args, **kwargs)
219+
except redis.WatchError:
220+
span.set_status(StatusCode.UNSET)
221+
216222
if callable(response_hook):
217223
response_hook(span, instance, response)
224+
218225
return response
219226

220227
pipeline_class = (
@@ -281,7 +288,13 @@ async def _async_traced_execute_pipeline(func, instance, args, kwargs):
281288
span.set_attribute(
282289
"db.redis.pipeline_length", len(command_stack)
283290
)
284-
response = await func(*args, **kwargs)
291+
292+
response = None
293+
try:
294+
response = await func(*args, **kwargs)
295+
except redis.WatchError:
296+
span.set_status(StatusCode.UNSET)
297+
285298
if callable(response_hook):
286299
response_hook(span, instance, response)
287300
return response

Diff for: instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
asgiref==3.7.2
22
async-timeout==4.0.3
33
Deprecated==1.2.14
4+
fakeredis==2.23.3
45
importlib-metadata==6.11.0
56
iniconfig==2.0.0
67
packaging==24.0

Diff for: instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py

+116-1
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,16 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
import asyncio
15-
from unittest import mock
15+
from unittest import IsolatedAsyncioTestCase, mock
1616
from unittest.mock import AsyncMock
1717

18+
import fakeredis
19+
import pytest
1820
import redis
1921
import redis.asyncio
22+
from fakeredis.aioredis import FakeRedis
23+
from redis.exceptions import ConnectionError as redis_ConnectionError
24+
from redis.exceptions import WatchError
2025

2126
from opentelemetry import trace
2227
from opentelemetry.instrumentation.redis import RedisInstrumentor
@@ -311,3 +316,113 @@ def test_attributes_unix_socket(self):
311316
span.attributes[SpanAttributes.NET_TRANSPORT],
312317
NetTransportValues.OTHER.value,
313318
)
319+
320+
def test_connection_error(self):
321+
server = fakeredis.FakeServer()
322+
server.connected = False
323+
redis_client = fakeredis.FakeStrictRedis(server=server)
324+
try:
325+
redis_client.set("foo", "bar")
326+
except redis_ConnectionError:
327+
pass
328+
329+
spans = self.memory_exporter.get_finished_spans()
330+
self.assertEqual(len(spans), 1)
331+
span = spans[0]
332+
333+
self.assertEqual(span.name, "SET")
334+
self.assertEqual(span.kind, SpanKind.CLIENT)
335+
self.assertEqual(span.status.status_code, trace.StatusCode.ERROR)
336+
337+
def test_response_error(self):
338+
redis_client = fakeredis.FakeStrictRedis()
339+
redis_client.lpush("mylist", "value")
340+
try:
341+
redis_client.incr(
342+
"mylist"
343+
) # Trying to increment a list, which is invalid
344+
except redis.ResponseError:
345+
pass
346+
347+
spans = self.memory_exporter.get_finished_spans()
348+
self.assertEqual(len(spans), 2)
349+
350+
span = spans[0]
351+
self.assertEqual(span.name, "LPUSH")
352+
self.assertEqual(span.kind, SpanKind.CLIENT)
353+
self.assertEqual(span.status.status_code, trace.StatusCode.UNSET)
354+
355+
span = spans[1]
356+
self.assertEqual(span.name, "INCRBY")
357+
self.assertEqual(span.kind, SpanKind.CLIENT)
358+
self.assertEqual(span.status.status_code, trace.StatusCode.ERROR)
359+
360+
def test_watch_error_sync(self):
361+
def redis_operations():
362+
try:
363+
redis_client = fakeredis.FakeStrictRedis()
364+
pipe = redis_client.pipeline(transaction=True)
365+
pipe.watch("a")
366+
redis_client.set("a", "bad") # This will cause the WatchError
367+
pipe.multi()
368+
pipe.set("a", "1")
369+
pipe.execute()
370+
except WatchError:
371+
pass
372+
373+
redis_operations()
374+
375+
spans = self.memory_exporter.get_finished_spans()
376+
self.assertEqual(len(spans), 3)
377+
378+
# there should be 3 tests, we start watch operation and have 2 set operation on same key
379+
self.assertEqual(len(spans), 3)
380+
381+
self.assertEqual(spans[0].attributes.get("db.statement"), "WATCH ?")
382+
self.assertEqual(spans[0].kind, SpanKind.CLIENT)
383+
self.assertEqual(spans[0].status.status_code, trace.StatusCode.UNSET)
384+
385+
for span in spans[1:]:
386+
self.assertEqual(span.attributes.get("db.statement"), "SET ? ?")
387+
self.assertEqual(span.kind, SpanKind.CLIENT)
388+
self.assertEqual(span.status.status_code, trace.StatusCode.UNSET)
389+
390+
391+
class TestRedisAsync(TestBase, IsolatedAsyncioTestCase):
392+
def setUp(self):
393+
super().setUp()
394+
RedisInstrumentor().instrument(tracer_provider=self.tracer_provider)
395+
396+
def tearDown(self):
397+
super().tearDown()
398+
RedisInstrumentor().uninstrument()
399+
400+
@pytest.mark.asyncio
401+
async def test_watch_error_async(self):
402+
async def redis_operations():
403+
try:
404+
redis_client = FakeRedis()
405+
async with redis_client.pipeline(transaction=False) as pipe:
406+
await pipe.watch("a")
407+
await redis_client.set("a", "bad")
408+
pipe.multi()
409+
await pipe.set("a", "1")
410+
await pipe.execute()
411+
except WatchError:
412+
pass
413+
414+
await redis_operations()
415+
416+
spans = self.memory_exporter.get_finished_spans()
417+
418+
# there should be 3 tests, we start watch operation and have 2 set operation on same key
419+
self.assertEqual(len(spans), 3)
420+
421+
self.assertEqual(spans[0].attributes.get("db.statement"), "WATCH ?")
422+
self.assertEqual(spans[0].kind, SpanKind.CLIENT)
423+
self.assertEqual(spans[0].status.status_code, trace.StatusCode.UNSET)
424+
425+
for span in spans[1:]:
426+
self.assertEqual(span.attributes.get("db.statement"), "SET ? ?")
427+
self.assertEqual(span.kind, SpanKind.CLIENT)
428+
self.assertEqual(span.status.status_code, trace.StatusCode.UNSET)

0 commit comments

Comments
 (0)