Skip to content

Commit 265b57b

Browse files
committed
feat(redis): add unit tests for semconv opt-in stable Redis attributes
1 parent 2b2670d commit 265b57b

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import os
2+
from unittest.mock import patch
3+
4+
import fakeredis
5+
from redis.exceptions import ResponseError, WatchError
6+
7+
from opentelemetry import trace
8+
from opentelemetry.instrumentation.redis import RedisInstrumentor
9+
from opentelemetry.semconv._incubating.attributes.db_attributes import (
10+
DB_STATEMENT,
11+
DB_SYSTEM,
12+
)
13+
from opentelemetry.test.test_base import TestBase
14+
15+
16+
class TestRedisSemConv(TestBase):
17+
def setUp(self):
18+
super().setUp()
19+
self.conn_patcher = patch(
20+
"opentelemetry.instrumentation.redis._get_redis_conn_info",
21+
return_value=("localhost", 6379, 0, None),
22+
)
23+
self.conn_patcher.start()
24+
RedisInstrumentor().instrument(tracer_provider=self.tracer_provider)
25+
self.redis_client = fakeredis.FakeStrictRedis()
26+
self.env_mode = os.getenv("OTEL_SEMCONV_STABILITY_OPT_IN")
27+
28+
def tearDown(self):
29+
super().tearDown()
30+
self.conn_patcher.stop()
31+
RedisInstrumentor().uninstrument()
32+
33+
def test_single_command(self):
34+
"""Tests attributes for a regular single command."""
35+
self.redis_client.set("key", "value")
36+
spans = self.memory_exporter.get_finished_spans()
37+
self.assertEqual(len(spans), 1)
38+
span = spans[0]
39+
40+
if self.env_mode == "database":
41+
self.assertEqual(span.attributes.get("db.system.name"), "redis")
42+
self.assertEqual(span.attributes.get("db.operation.name"), "SET")
43+
self.assertEqual(span.attributes.get("db.namespace"), "0")
44+
self.assertEqual(span.attributes.get("db.query.text"), "SET ? ?")
45+
self.assertNotIn(DB_SYSTEM, span.attributes)
46+
self.assertNotIn(DB_STATEMENT, span.attributes)
47+
elif self.env_mode == "database/dup":
48+
self.assertEqual(span.attributes.get("db.system.name"), "redis")
49+
self.assertEqual(span.attributes.get(DB_SYSTEM), "redis")
50+
self.assertEqual(span.attributes.get(DB_STATEMENT), "SET ? ?")
51+
else: # Default (old) behavior
52+
self.assertEqual(span.attributes.get(DB_SYSTEM), "redis")
53+
self.assertEqual(span.attributes.get(DB_STATEMENT), "SET ? ?")
54+
self.assertNotIn("db.system.name", span.attributes)
55+
56+
def test_pipeline_command(self):
57+
"""Tests attributes for a pipeline command."""
58+
with self.redis_client.pipeline(transaction=False) as pipe:
59+
pipe.set("a", 1)
60+
pipe.get("a")
61+
pipe.execute()
62+
63+
spans = self.memory_exporter.get_finished_spans()
64+
self.assertEqual(len(spans), 1)
65+
span = spans[0]
66+
67+
if self.env_mode in ("database", "database/dup"):
68+
self.assertEqual(span.attributes.get("db.operation.name"), "PIPELINE")
69+
self.assertEqual(span.attributes.get("db.operation.batch.size"), 2)
70+
71+
def test_stored_procedure_command(self):
72+
"""Tests attributes for a stored procedure command."""
73+
with patch.object(self.redis_client, "parse_response", return_value=b"ok"):
74+
self.redis_client.evalsha("some-sha", 0, "key", "value")
75+
76+
spans = self.memory_exporter.get_finished_spans()
77+
self.assertEqual(len(spans), 1)
78+
span = spans[0]
79+
80+
if self.env_mode in ("database", "database/dup"):
81+
self.assertEqual(span.attributes.get("db.stored_procedure.name"), "some-sha")
82+
else:
83+
self.assertNotIn("db.stored_procedure.name", span.attributes)
84+
85+
def test_generic_error(self):
86+
"""Tests attributes for a generic error."""
87+
with patch.object(
88+
self.redis_client,
89+
"parse_response",
90+
side_effect=ResponseError("ERR unknown command"),
91+
):
92+
with self.assertRaises(ResponseError):
93+
self.redis_client.execute_command("INVALID_COMMAND")
94+
95+
spans = self.memory_exporter.get_finished_spans()
96+
self.assertEqual(len(spans), 1)
97+
span = spans[0]
98+
self.assertEqual(span.status.status_code, trace.StatusCode.ERROR)
99+
100+
if self.env_mode in ("database", "database/dup"):
101+
self.assertEqual("ERR", span.attributes.get("error.type"))
102+
self.assertEqual("ERR", span.attributes.get("db.response.status_code"))
103+
else:
104+
self.assertNotIn("error.type", span.attributes)
105+
self.assertNotIn("db.response.status_code", span.attributes)
106+
107+
def test_watch_error(self):
108+
"""Tests attributes for a WatchError."""
109+
with self.assertRaises(WatchError):
110+
with self.redis_client.pipeline(transaction=True) as pipe:
111+
pipe.watch("watched_key")
112+
self.redis_client.set("watched_key", "modified-externally")
113+
pipe.multi()
114+
pipe.set("watched_key", "new-value")
115+
pipe.get("watched_key")
116+
with patch.object(pipe, "execute", side_effect=WatchError):
117+
pipe.execute()
118+
119+
spans = self.memory_exporter.get_finished_spans()
120+
self.assertEqual(len(spans), 2)
121+
122+
failed_pipeline_span = spans[-1]
123+
self.assertEqual(failed_pipeline_span.status.status_code, trace.StatusCode.UNSET)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import os
2+
import subprocess
3+
import sys
4+
5+
import pytest
6+
7+
# The full relative path from the project root
8+
HELPER_PATH = "instrumentation/opentelemetry-instrumentation-redis/tests/_test_semconv_helper.py"
9+
10+
11+
@pytest.mark.parametrize(
12+
"semconv_opt_in, test_name",
13+
[
14+
(None, "default"),
15+
("database", "new"),
16+
("database/dup", "dup"),
17+
],
18+
)
19+
def test_run_in_subprocess(semconv_opt_in, test_name):
20+
"""
21+
Runs the semantic convention test in a clean subprocess.
22+
The `semconv_opt_in` value is used to set the env var.
23+
"""
24+
env = os.environ.copy()
25+
26+
if semconv_opt_in is None:
27+
if "OTEL_SEMCONV_STABILITY_OPT_IN" in env:
28+
del env["OTEL_SEMCONV_STABILITY_OPT_IN"]
29+
else:
30+
env["OTEL_SEMCONV_STABILITY_OPT_IN"] = semconv_opt_in
31+
32+
result = subprocess.run(
33+
[sys.executable, "-m", "pytest", HELPER_PATH],
34+
capture_output=True,
35+
text=True,
36+
env=env,
37+
check=False, # Explicitly set check=False to satisfy pylint
38+
)
39+
# Use a standard assert
40+
assert (
41+
result.returncode == 0
42+
), f"Subprocess for '{test_name}' mode failed with stdout:\n{result.stdout}\nstderr:\n{result.stderr}"

0 commit comments

Comments
 (0)