diff --git a/CHANGELOG.md b/CHANGELOG.md index d9e48cd6df..12e214e7e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4049](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4049)) - `opentelemetry-instrumentation-sqlalchemy`: implement new semantic convention opt-in migration ([#4110](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4110)) +- `opentelemetry-instrumentation-redis`: implement new semantic convention opt-in migration + ([#4370](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4370)) ### Fixed diff --git a/instrumentation/README.md b/instrumentation/README.md index 8d9a247945..51a26d5e07 100644 --- a/instrumentation/README.md +++ b/instrumentation/README.md @@ -38,7 +38,7 @@ | [opentelemetry-instrumentation-pymssql](./opentelemetry-instrumentation-pymssql) | pymssql >= 2.1.5, < 3 | No | development | [opentelemetry-instrumentation-pymysql](./opentelemetry-instrumentation-pymysql) | PyMySQL < 2 | No | development | [opentelemetry-instrumentation-pyramid](./opentelemetry-instrumentation-pyramid) | pyramid >= 1.7 | Yes | migration -| [opentelemetry-instrumentation-redis](./opentelemetry-instrumentation-redis) | redis >= 2.6 | No | development +| [opentelemetry-instrumentation-redis](./opentelemetry-instrumentation-redis) | redis >= 2.6 | No | migration | [opentelemetry-instrumentation-remoulade](./opentelemetry-instrumentation-remoulade) | remoulade >= 0.50 | No | development | [opentelemetry-instrumentation-requests](./opentelemetry-instrumentation-requests) | requests ~= 2.0 | Yes | migration | [opentelemetry-instrumentation-sqlalchemy](./opentelemetry-instrumentation-sqlalchemy) | sqlalchemy >= 1.0.0, < 2.1.0 | Yes | migration diff --git a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py index 77c3ac31c3..8bbb506ea1 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py @@ -152,6 +152,12 @@ def response_hook(span, instance, response): from wrapt import wrap_function_wrapper from opentelemetry import trace +from opentelemetry.instrumentation._semconv import ( + _get_schema_url_for_signal_types, + _OpenTelemetrySemanticConventionStability, + _OpenTelemetryStabilitySignalType, + _set_db_statement, +) from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.redis.package import _instruments from opentelemetry.instrumentation.redis.util import ( @@ -167,9 +173,6 @@ def response_hook(span, instance, response): is_instrumentation_enabled, unwrap, ) -from opentelemetry.semconv._incubating.attributes.db_attributes import ( - DB_STATEMENT, -) from opentelemetry.trace import ( StatusCode, Tracer, @@ -222,6 +225,15 @@ def _traced_execute_factory( request_hook: RequestHook | None = None, response_hook: ResponseHook | None = None, ): + # Get semconv opt-in modes for database and HTTP signal types + _OpenTelemetrySemanticConventionStability._initialize() + db_sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.DATABASE + ) + http_sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.HTTP + ) + def _traced_execute_command( func: Callable[..., R], instance: RedisInstance, @@ -237,9 +249,20 @@ def _traced_execute_command( name, kind=trace.SpanKind.CLIENT ) as span: if span.is_recording(): - span.set_attribute(DB_STATEMENT, query) - _set_connection_attributes(span, instance) - span.set_attribute("db.redis.args_length", len(args)) + span_attrs = {} + _set_db_statement(span_attrs, query, db_sem_conv_opt_in_mode) + span_attrs["db.redis.args_length"] = len(args) + + # Set all DB attributes + for key, value in span_attrs.items(): + span.set_attribute(key, value) + + _set_connection_attributes( + span, + instance, + db_sem_conv_opt_in_mode, + http_sem_conv_opt_in_mode, + ) if span.name == "redis.create_index": _add_create_attributes(span, args) if callable(request_hook): @@ -260,6 +283,15 @@ def _traced_execute_pipeline_factory( request_hook: RequestHook | None = None, response_hook: ResponseHook | None = None, ): + # Get semconv opt-in modes for database and HTTP signal types + _OpenTelemetrySemanticConventionStability._initialize() + db_sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.DATABASE + ) + http_sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.HTTP + ) + def _traced_execute_pipeline( func: Callable[..., R], instance: PipelineInstance, @@ -279,10 +311,21 @@ def _traced_execute_pipeline( span_name, kind=trace.SpanKind.CLIENT ) as span: if span.is_recording(): - span.set_attribute(DB_STATEMENT, resource) - _set_connection_attributes(span, instance) - span.set_attribute( - "db.redis.pipeline_length", len(command_stack) + span_attrs = {} + _set_db_statement( + span_attrs, resource, db_sem_conv_opt_in_mode + ) + span_attrs["db.redis.pipeline_length"] = len(command_stack) + + # Set all DB attributes + for key, value in span_attrs.items(): + span.set_attribute(key, value) + + _set_connection_attributes( + span, + instance, + db_sem_conv_opt_in_mode, + http_sem_conv_opt_in_mode, ) response = None @@ -308,6 +351,15 @@ def _async_traced_execute_factory( request_hook: RequestHook | None = None, response_hook: ResponseHook | None = None, ): + # Get semconv opt-in modes for database and HTTP signal types + _OpenTelemetrySemanticConventionStability._initialize() + db_sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.DATABASE + ) + http_sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.HTTP + ) + async def _async_traced_execute_command( func: Callable[..., Awaitable[R]], instance: AsyncRedisInstance, @@ -324,9 +376,20 @@ async def _async_traced_execute_command( name, kind=trace.SpanKind.CLIENT ) as span: if span.is_recording(): - span.set_attribute(DB_STATEMENT, query) - _set_connection_attributes(span, instance) - span.set_attribute("db.redis.args_length", len(args)) + span_attrs = {} + _set_db_statement(span_attrs, query, db_sem_conv_opt_in_mode) + span_attrs["db.redis.args_length"] = len(args) + + # Set all DB attributes + for key, value in span_attrs.items(): + span.set_attribute(key, value) + + _set_connection_attributes( + span, + instance, + db_sem_conv_opt_in_mode, + http_sem_conv_opt_in_mode, + ) if callable(request_hook): request_hook(span, instance, args, kwargs) response = await func(*args, **kwargs) @@ -342,6 +405,15 @@ def _async_traced_execute_pipeline_factory( request_hook: RequestHook | None = None, response_hook: ResponseHook | None = None, ): + # Get semconv opt-in modes for database and HTTP signal types + _OpenTelemetrySemanticConventionStability._initialize() + db_sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.DATABASE + ) + http_sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.HTTP + ) + async def _async_traced_execute_pipeline( func: Callable[..., Awaitable[R]], instance: AsyncPipelineInstance, @@ -363,10 +435,21 @@ async def _async_traced_execute_pipeline( span_name, kind=trace.SpanKind.CLIENT ) as span: if span.is_recording(): - span.set_attribute(DB_STATEMENT, resource) - _set_connection_attributes(span, instance) - span.set_attribute( - "db.redis.pipeline_length", len(command_stack) + span_attrs = {} + _set_db_statement( + span_attrs, resource, db_sem_conv_opt_in_mode + ) + span_attrs["db.redis.pipeline_length"] = len(command_stack) + + # Set all DB attributes + for key, value in span_attrs.items(): + span.set_attribute(key, value) + + _set_connection_attributes( + span, + instance, + db_sem_conv_opt_in_mode, + http_sem_conv_opt_in_mode, ) response = None @@ -540,12 +623,20 @@ def _pipeline_wrapper(func, instance, args, kwargs): class RedisInstrumentor(BaseInstrumentor): @staticmethod def _get_tracer(**kwargs): + # Initialize semantic conventions opt-in if needed + _OpenTelemetrySemanticConventionStability._initialize() + # Redis instrumentation supports both DATABASE and HTTP signal types + signal_types = [ + _OpenTelemetryStabilitySignalType.DATABASE, + _OpenTelemetryStabilitySignalType.HTTP, + ] + tracer_provider = kwargs.get("tracer_provider") return get_tracer( __name__, __version__, tracer_provider=tracer_provider, - schema_url="https://opentelemetry.io/schemas/1.11.0", + schema_url=_get_schema_url_for_signal_types(signal_types), ) def instrument( diff --git a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/package.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/package.py index dd2efb37b0..e31b4a45db 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/package.py +++ b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/package.py @@ -14,3 +14,5 @@ _instruments = ("redis >= 2.6",) + +_semconv_status = "migration" diff --git a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py index 320758f842..b79759f184 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py +++ b/instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/util.py @@ -20,13 +20,15 @@ from typing import TYPE_CHECKING, Any +from opentelemetry.instrumentation._semconv import ( + _set_db_system, + _set_http_net_peer_name_client, + _set_http_peer_port_client, +) from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_REDIS_DATABASE_INDEX, - DB_SYSTEM, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( - NET_PEER_NAME, - NET_PEER_PORT, NET_TRANSPORT, ) from opentelemetry.semconv.trace import ( @@ -46,19 +48,33 @@ _FIELD_TYPES = ["NUMERIC", "TEXT", "GEO", "TAG", "VECTOR"] -def _extract_conn_attributes(conn_kwargs): +def _extract_conn_attributes( + conn_kwargs, db_sem_conv_opt_in_mode, http_sem_conv_opt_in_mode +): """Transform redis conn info into dict""" - attributes = { - DB_SYSTEM: DbSystemValues.REDIS.value, - } + attributes = {} + _set_db_system( + attributes, DbSystemValues.REDIS.value, db_sem_conv_opt_in_mode + ) + db = conn_kwargs.get("db", 0) attributes[DB_REDIS_DATABASE_INDEX] = db if "path" in conn_kwargs: - attributes[NET_PEER_NAME] = conn_kwargs.get("path", "") + _set_http_net_peer_name_client( + attributes, conn_kwargs.get("path", ""), http_sem_conv_opt_in_mode + ) attributes[NET_TRANSPORT] = NetTransportValues.OTHER.value else: - attributes[NET_PEER_NAME] = conn_kwargs.get("host", "localhost") - attributes[NET_PEER_PORT] = conn_kwargs.get("port", 6379) + _set_http_net_peer_name_client( + attributes, + conn_kwargs.get("host", "localhost"), + http_sem_conv_opt_in_mode, + ) + _set_http_peer_port_client( + attributes, + conn_kwargs.get("port", 6379), + http_sem_conv_opt_in_mode, + ) attributes[NET_TRANSPORT] = NetTransportValues.IP_TCP.value return attributes @@ -99,12 +115,17 @@ def _value_or_none(values, n): def _set_connection_attributes( - span: Span, conn: RedisInstance | AsyncRedisInstance + span: Span, + conn: RedisInstance | AsyncRedisInstance, + db_sem_conv_opt_in_mode, + http_sem_conv_opt_in_mode, ) -> None: if not span.is_recording() or not hasattr(conn, "connection_pool"): return for key, value in _extract_conn_attributes( - conn.connection_pool.connection_kwargs + conn.connection_pool.connection_kwargs, + db_sem_conv_opt_in_mode, + http_sem_conv_opt_in_mode, ).items(): span.set_attribute(key, value) diff --git a/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt index 627b13573c..4892d9ab78 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-redis/test-requirements.txt @@ -7,6 +7,7 @@ packaging==24.0 pluggy==1.6.0 py-cpuinfo==9.0.0 pytest==7.4.4 +pytest-asyncio==0.23.5 redis==5.0.1 tomli==2.0.1 typing_extensions==4.12.2 diff --git a/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py b/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py index 3e649fcef7..e621992c14 100644 --- a/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py +++ b/instrumentation/opentelemetry-instrumentation-redis/tests/test_redis.py @@ -11,9 +11,13 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +# pylint: disable=too-many-lines + import asyncio +import os from unittest import IsolatedAsyncioTestCase, mock -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch import fakeredis import pytest @@ -24,10 +28,15 @@ from redis.exceptions import WatchError from opentelemetry import trace +from opentelemetry.instrumentation._semconv import ( + OTEL_SEMCONV_STABILITY_OPT_IN, + _OpenTelemetrySemanticConventionStability, +) from opentelemetry.instrumentation.redis import RedisInstrumentor from opentelemetry.instrumentation.utils import suppress_instrumentation from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_REDIS_DATABASE_INDEX, + DB_STATEMENT, DB_SYSTEM, DbSystemValues, ) @@ -37,10 +46,28 @@ NET_TRANSPORT, NetTransportValues, ) +from opentelemetry.semconv.attributes.db_attributes import DB_QUERY_TEXT +from opentelemetry.semconv.attributes.server_attributes import ( + SERVER_ADDRESS, + SERVER_PORT, +) from opentelemetry.test.test_base import TestBase from opentelemetry.trace import SpanKind +def stability_mode(mode): + def decorator(test_case): + @patch.dict(os.environ, {OTEL_SEMCONV_STABILITY_OPT_IN: mode}) + def wrapper(*args, **kwargs): + _OpenTelemetrySemanticConventionStability._initialized = False + _OpenTelemetrySemanticConventionStability._initialize() + return test_case(*args, **kwargs) + + return wrapper + + return decorator + + # pylint: disable=too-many-public-methods class TestRedis(TestBase): def assert_span_count(self, count: int): @@ -773,3 +800,359 @@ def test_watch_error_sync_only_client(self): self.assertEqual(span.attributes.get("db.statement"), "SET ? ?") self.assertEqual(span.kind, SpanKind.CLIENT) self.assertEqual(span.status.status_code, trace.StatusCode.UNSET) + + +class TestRedisSemconvConfiguration(TestRedis): + """Tests semconv migration for both Redis pipeline and db_statement""" + + def re_instrument_and_clear_exporter(self): + # Re-instrument to pick up the environment variable change + RedisInstrumentor().uninstrument() + self.memory_exporter.clear() # Clear previous spans + RedisInstrumentor().instrument(tracer_provider=self.tracer_provider) + + @stability_mode("") + def test_pipeline_default_mode(self): + self.re_instrument_and_clear_exporter() + redis_client = fakeredis.FakeStrictRedis() + pipe = redis_client.pipeline() + pipe.get("key1") + pipe.set("key2", "value2") + pipe.execute() + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertIn(DB_STATEMENT, span.attributes) + self.assertIn("GET ?", span.attributes[DB_STATEMENT]) + self.assertIn("SET ? ?", span.attributes[DB_STATEMENT]) + self.assertNotIn(DB_QUERY_TEXT, span.attributes) + + @stability_mode("database") + def test_pipeline_database_stable_mode(self): + self.re_instrument_and_clear_exporter() + redis_client = fakeredis.FakeStrictRedis() + pipe = redis_client.pipeline() + pipe.get("key1") + pipe.set("key2", "value2") + pipe.execute() + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertNotIn(DB_STATEMENT, span.attributes) + self.assertIn(DB_QUERY_TEXT, span.attributes) + self.assertIn("GET ?", span.attributes[DB_QUERY_TEXT]) + self.assertIn("SET ? ?", span.attributes[DB_QUERY_TEXT]) + + @stability_mode("database/dup") + def test_pipeline_database_dup_mode(self): + self.re_instrument_and_clear_exporter() + redis_client = fakeredis.FakeStrictRedis() + pipe = redis_client.pipeline() + pipe.get("key1") + pipe.set("key2", "value2") + pipe.execute() + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertIn(DB_STATEMENT, span.attributes) + self.assertIn("GET ?", span.attributes[DB_STATEMENT]) + self.assertIn("SET ? ?", span.attributes[DB_STATEMENT]) + self.assertIn(DB_QUERY_TEXT, span.attributes) + self.assertIn("GET ?", span.attributes[DB_QUERY_TEXT]) + self.assertIn("SET ? ?", span.attributes[DB_QUERY_TEXT]) + + @stability_mode("") + def test_db_statement_default_mode(self): + self.re_instrument_and_clear_exporter() + redis_client = redis.Redis() + + with mock.patch.object(redis_client, "connection"): + redis_client.get("key") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertIn(DB_STATEMENT, span.attributes) + self.assertEqual(span.attributes[DB_STATEMENT], "GET ?") + self.assertNotIn(DB_QUERY_TEXT, span.attributes) + + @stability_mode("database") + def test_db_statement_database_stable_mode(self): + self.re_instrument_and_clear_exporter() + redis_client = redis.Redis() + + with mock.patch.object(redis_client, "connection"): + redis_client.get("key") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertNotIn(DB_STATEMENT, span.attributes) + self.assertIn(DB_QUERY_TEXT, span.attributes) + self.assertEqual(span.attributes[DB_QUERY_TEXT], "GET ?") + + @stability_mode("database/dup") + def test_db_statement_database_dup_mode(self): + self.re_instrument_and_clear_exporter() + redis_client = redis.Redis() + + with mock.patch.object(redis_client, "connection"): + redis_client.get("key") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertIn(DB_STATEMENT, span.attributes) + self.assertEqual(span.attributes[DB_STATEMENT], "GET ?") + self.assertIn(DB_QUERY_TEXT, span.attributes) + self.assertEqual(span.attributes[DB_QUERY_TEXT], "GET ?") + + @stability_mode("http") + def test_db_statement_http_stable_mode(self): + # HTTP signal type should not affect database attributes; they stay in default behavior + self.re_instrument_and_clear_exporter() + redis_client = redis.Redis() + + with mock.patch.object(redis_client, "connection"): + redis_client.get("key") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + # HTTP signal type doesn't affect database attributes - they remain in default mode + self.assertIn(DB_STATEMENT, span.attributes) + self.assertEqual(span.attributes[DB_STATEMENT], "GET ?") + self.assertNotIn(DB_QUERY_TEXT, span.attributes) + # Network attributes should still be present (HTTP signal type for network attributes) + self.assertIn(SERVER_ADDRESS, span.attributes) + self.assertIn(SERVER_PORT, span.attributes) + + @stability_mode("http/dup") + def test_db_statement_http_dup_mode(self): + # HTTP signal type should not affect database attributes; they stay in default behavior + self.re_instrument_and_clear_exporter() + redis_client = redis.Redis() + + with mock.patch.object(redis_client, "connection"): + redis_client.get("key") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + # HTTP signal type doesn't affect database attributes - they remain in default mode + self.assertIn(DB_STATEMENT, span.attributes) + self.assertEqual(span.attributes[DB_STATEMENT], "GET ?") + self.assertNotIn(DB_QUERY_TEXT, span.attributes) + # Network attributes should still be present (HTTP signal type for network attributes) + self.assertIn(SERVER_ADDRESS, span.attributes) + self.assertIn(SERVER_PORT, span.attributes) + + @stability_mode("http,database") + def test_db_statement_combined_http_database_mode(self): + # Both HTTP and DATABASE signal types should be active + self.re_instrument_and_clear_exporter() + redis_client = redis.Redis() + + with mock.patch.object(redis_client, "connection"): + redis_client.get("key") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + # DATABASE signal type should use stable attributes + self.assertNotIn(DB_STATEMENT, span.attributes) + self.assertIn(DB_QUERY_TEXT, span.attributes) + self.assertEqual(span.attributes[DB_QUERY_TEXT], "GET ?") + # Network attributes should still be present (HTTP signal type) + self.assertIn(SERVER_ADDRESS, span.attributes) + self.assertIn(SERVER_PORT, span.attributes) + + @stability_mode("database,http") + def test_db_statement_combined_database_http_mode(self): + # Both DATABASE and HTTP signal types should be active (order shouldn't matter) + self.re_instrument_and_clear_exporter() + redis_client = redis.Redis() + + with mock.patch.object(redis_client, "connection"): + redis_client.get("key") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + # DATABASE signal type should use stable attributes + self.assertNotIn(DB_STATEMENT, span.attributes) + self.assertIn(DB_QUERY_TEXT, span.attributes) + self.assertEqual(span.attributes[DB_QUERY_TEXT], "GET ?") + # Network attributes should still be present (HTTP signal type) + self.assertIn(SERVER_ADDRESS, span.attributes) + self.assertIn(SERVER_PORT, span.attributes) + + @stability_mode("database/dup,http") + def test_db_statement_combined_database_dup_http_mode(self): + # Both DATABASE (dup) and HTTP signal types should be active + self.re_instrument_and_clear_exporter() + redis_client = redis.Redis() + + with mock.patch.object(redis_client, "connection"): + redis_client.get("key") + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + # DATABASE signal type in dup mode should have both attributes + self.assertIn(DB_STATEMENT, span.attributes) + self.assertEqual(span.attributes[DB_STATEMENT], "GET ?") + self.assertIn(DB_QUERY_TEXT, span.attributes) + self.assertEqual(span.attributes[DB_QUERY_TEXT], "GET ?") + # Network attributes should still be present (HTTP signal type) + self.assertIn(SERVER_ADDRESS, span.attributes) + self.assertIn(SERVER_PORT, span.attributes) + + @stability_mode("http") + def test_pipeline_http_stable_mode(self): + # HTTP signal type should not affect database attributes in pipeline + self.re_instrument_and_clear_exporter() + redis_client = fakeredis.FakeStrictRedis() + pipe = redis_client.pipeline() + pipe.get("key1") + pipe.set("key2", "value2") + pipe.execute() + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + # HTTP signal type doesn't affect database attributes - they remain in default mode + self.assertIn(DB_STATEMENT, span.attributes) + self.assertIn("GET ?", span.attributes[DB_STATEMENT]) + self.assertIn("SET ? ?", span.attributes[DB_STATEMENT]) + self.assertNotIn(DB_QUERY_TEXT, span.attributes) + + @stability_mode("http,database") + def test_pipeline_combined_http_database_mode(self): + # Both HTTP and DATABASE signal types should be active in pipeline + self.re_instrument_and_clear_exporter() + redis_client = fakeredis.FakeStrictRedis() + pipe = redis_client.pipeline() + pipe.get("key1") + pipe.set("key2", "value2") + pipe.execute() + + spans = self.memory_exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + # DATABASE signal type should use stable attributes + self.assertNotIn(DB_STATEMENT, span.attributes) + self.assertIn(DB_QUERY_TEXT, span.attributes) + self.assertIn("GET ?", span.attributes[DB_QUERY_TEXT]) + self.assertIn("SET ? ?", span.attributes[DB_QUERY_TEXT]) + + @stability_mode("") + def test_schema_url_default_mode(self): + """Test schema URL assignment in default stability mode.""" + self.re_instrument_and_clear_exporter() + with mock.patch( + "opentelemetry.instrumentation.redis.get_tracer" + ) as mock_get_tracer: + mock_tracer = mock.Mock() + mock_get_tracer.return_value = mock_tracer + RedisInstrumentor._get_tracer(tracer_provider=self.tracer_provider) + + # Verify get_tracer was called with legacy schema URL in default mode + mock_get_tracer.assert_called_once() + call_args = mock_get_tracer.call_args + self.assertEqual( + call_args[1]["schema_url"], + "https://opentelemetry.io/schemas/1.11.0", + ) + + @stability_mode("database") + def test_schema_url_database_stable_mode(self): + """Test schema URL assignment in database stable mode.""" + self.re_instrument_and_clear_exporter() + with mock.patch( + "opentelemetry.instrumentation.redis.get_tracer" + ) as mock_get_tracer: + mock_tracer = mock.Mock() + mock_get_tracer.return_value = mock_tracer + RedisInstrumentor._get_tracer(tracer_provider=self.tracer_provider) + + # Verify get_tracer was called with stable schema URL + mock_get_tracer.assert_called_once() + call_args = mock_get_tracer.call_args + self.assertEqual( + call_args[1]["schema_url"], + "https://opentelemetry.io/schemas/1.25.0", + ) + + @stability_mode("database/dup") + def test_schema_url_database_dup_mode(self): + """Test schema URL assignment in database duplicate mode.""" + self.re_instrument_and_clear_exporter() + with mock.patch( + "opentelemetry.instrumentation.redis.get_tracer" + ) as mock_get_tracer: + mock_tracer = mock.Mock() + mock_get_tracer.return_value = mock_tracer + RedisInstrumentor._get_tracer(tracer_provider=self.tracer_provider) + + # Verify get_tracer was called with stable schema URL + mock_get_tracer.assert_called_once() + call_args = mock_get_tracer.call_args + self.assertEqual( + call_args[1]["schema_url"], + "https://opentelemetry.io/schemas/1.25.0", + ) + + @stability_mode("http") + def test_schema_url_http_mode(self): + """Test schema URL assignment in HTTP stability mode.""" + self.re_instrument_and_clear_exporter() + with mock.patch( + "opentelemetry.instrumentation.redis.get_tracer" + ) as mock_get_tracer: + mock_tracer = mock.Mock() + mock_get_tracer.return_value = mock_tracer + RedisInstrumentor._get_tracer(tracer_provider=self.tracer_provider) + + # Verify get_tracer was called with stable schema URL + mock_get_tracer.assert_called_once() + call_args = mock_get_tracer.call_args + self.assertEqual( + call_args[1]["schema_url"], + "https://opentelemetry.io/schemas/1.21.0", + ) + + @stability_mode("http,database") + def test_schema_url_combined_mode(self): + """Test schema URL assignment in combined HTTP and database mode.""" + self.re_instrument_and_clear_exporter() + with mock.patch( + "opentelemetry.instrumentation.redis.get_tracer" + ) as mock_get_tracer: + mock_tracer = mock.Mock() + mock_get_tracer.return_value = mock_tracer + RedisInstrumentor._get_tracer(tracer_provider=self.tracer_provider) + + # Verify get_tracer was called with stable schema URL + mock_get_tracer.assert_called_once() + call_args = mock_get_tracer.call_args + self.assertEqual( + call_args[1]["schema_url"], + "https://opentelemetry.io/schemas/1.25.0", + )