diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md b/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md index b408f3b9d37c..a0e7f0db5e3c 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md @@ -7,6 +7,10 @@ ### Breaking Changes ### Bugs Fixed +- Update to the new stable OpenTelemetry database semantic conventions + (`db.system.name`, `db.query.text`, `db.operation.name`, `db.namespace`) when + mapping `CLIENT` spans to `RemoteDependencyData` + ([#48979](https://github.com/Azure/azure-sdk-for-python/pull/48979)) ### Other Changes diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_quickpulse/_types.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_quickpulse/_types.py index bac39d33976c..a053fa84faa2 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_quickpulse/_types.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_quickpulse/_types.py @@ -7,6 +7,11 @@ from opentelemetry._logs import LogRecord from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.semconv._incubating.attributes import gen_ai_attributes +from opentelemetry.semconv.attributes.db_attributes import ( + DB_OPERATION_NAME, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, +) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, @@ -114,16 +119,22 @@ def _from_span(span: ReadableSpan): url, ) data = url - elif SpanAttributes.DB_SYSTEM in attributes: - db_system = attributes[SpanAttributes.DB_SYSTEM] + elif DB_SYSTEM_NAME in attributes or SpanAttributes.DB_SYSTEM in attributes: + db_system = attributes.get(DB_SYSTEM_NAME) or attributes.get(SpanAttributes.DB_SYSTEM) dependency_type = db_system target = trace_utils._get_target_for_db_dependency( target, db_system, attributes, ) - if SpanAttributes.DB_STATEMENT in attributes: + # Use query text when available, otherwise fall back to the operation name. + # Support both stable and deprecated semantic conventions. + if DB_QUERY_TEXT in attributes: + data = attributes[DB_QUERY_TEXT] + elif SpanAttributes.DB_STATEMENT in attributes: data = attributes[SpanAttributes.DB_STATEMENT] + elif DB_OPERATION_NAME in attributes: + data = attributes[DB_OPERATION_NAME] elif SpanAttributes.DB_OPERATION in attributes: data = attributes[SpanAttributes.DB_OPERATION] elif SpanAttributes.MESSAGING_SYSTEM in attributes: diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_exporter.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_exporter.py index 651f618a9a93..536a20015efe 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_exporter.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_exporter.py @@ -8,6 +8,11 @@ from urllib.parse import urlparse from opentelemetry.semconv.attributes.client_attributes import CLIENT_ADDRESS +from opentelemetry.semconv.attributes.db_attributes import ( + DB_OPERATION_NAME, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, +) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, @@ -420,8 +425,9 @@ def _convert_span_to_envelope(span: ReadableSpan) -> TelemetryItem: else: status_code = 0 data.result_code = str(status_code) - elif SpanAttributes.DB_SYSTEM in span.attributes: # Database - db_system = span.attributes[SpanAttributes.DB_SYSTEM] + elif DB_SYSTEM_NAME in span.attributes or SpanAttributes.DB_SYSTEM in span.attributes: # Database + # Prefer the new stable `db.system.name`, fall back to the deprecated `db.system`. + db_system = span.attributes.get(DB_SYSTEM_NAME) or span.attributes.get(SpanAttributes.DB_SYSTEM) if db_system == DbSystemValues.MYSQL.value: data.type = "mysql" elif db_system == DbSystemValues.POSTGRESQL.value: @@ -434,9 +440,14 @@ def _convert_span_to_envelope(span: ReadableSpan) -> TelemetryItem: data.type = "SQL" else: data.type = db_system - # data is the full statement or operation - if SpanAttributes.DB_STATEMENT in span.attributes: + # Use query text when available, otherwise fall back to the operation name. + # Support both stable and deprecated semantic conventions. + if DB_QUERY_TEXT in span.attributes: + data.data = span.attributes[DB_QUERY_TEXT] + elif SpanAttributes.DB_STATEMENT in span.attributes: data.data = span.attributes[SpanAttributes.DB_STATEMENT] + elif DB_OPERATION_NAME in span.attributes: + data.data = span.attributes[DB_OPERATION_NAME] elif SpanAttributes.DB_OPERATION in span.attributes: data.data = span.attributes[SpanAttributes.DB_OPERATION] # db specific logic for target diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_utils.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_utils.py index e560376e73d1..c47bab8f6790 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_utils.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/trace/_utils.py @@ -7,10 +7,13 @@ from opentelemetry.semconv.attributes import ( client_attributes, + db_attributes, server_attributes, url_attributes, user_agent_attributes, ) + +from opentelemetry.semconv._incubating.attributes import db_attributes as db_attributes_incubating from opentelemetry.context import Context from opentelemetry.trace import get_current_span from opentelemetry.sdk.trace.sampling import ( @@ -38,16 +41,16 @@ def _get_default_port_db(db_system: str) -> int: return 9042 if db_system in (DbSystemValues.MARIADB.value, DbSystemValues.MYSQL.value): return 3306 - if db_system == DbSystemValues.MSSQL.value: + if db_system == DbSystemValues.MSSQL.value or db_system == db_attributes.DbSystemNameValues.MICROSOFT_SQL_SERVER.value: return 1433 # TODO: Add in memcached if db_system == "memcached": return 11211 - if db_system == DbSystemValues.DB2.value: + if db_system == DbSystemValues.DB2.value or db_system == db_attributes_incubating.DbSystemNameValues.IBM_DB2.value: return 50000 - if db_system == DbSystemValues.ORACLE.value: + if db_system == DbSystemValues.ORACLE.value or db_system == db_attributes_incubating.DbSystemNameValues.ORACLE_DB.value: return 1521 - if db_system == DbSystemValues.H2.value: + if db_system == DbSystemValues.H2.value or db_system == db_attributes_incubating.DbSystemNameValues.H2DATABASE.value: return 8082 if db_system == DbSystemValues.DERBY.value: return 1527 @@ -68,15 +71,19 @@ def _get_default_port_http(attributes: Attributes) -> int: def _is_sql_db(db_system: str) -> bool: return db_system in ( DbSystemValues.DB2.value, + db_attributes_incubating.DbSystemNameValues.IBM_DB2.value, DbSystemValues.DERBY.value, DbSystemValues.MARIADB.value, DbSystemValues.MSSQL.value, + db_attributes.DbSystemNameValues.MICROSOFT_SQL_SERVER.value, DbSystemValues.ORACLE.value, + db_attributes_incubating.DbSystemNameValues.ORACLE_DB.value, DbSystemValues.SQLITE.value, DbSystemValues.OTHER_SQL.value, # spell-checker:ignore HSQLDB DbSystemValues.HSQLDB.value, DbSystemValues.H2.value, + db_attributes_incubating.DbSystemNameValues.H2DATABASE.value, ) @@ -159,9 +166,11 @@ def _get_target_for_dependency_from_peer(attributes: Attributes) -> Optional[str port = attributes[SpanAttributes.NET_PEER_PORT] # TODO: check default port for rpc # This logic assumes default ports never conflict across dependency types - if port != _get_default_port_http(attributes) and port != _get_default_port_db( - str(attributes.get(SpanAttributes.DB_SYSTEM)) - ): + # Honor both the new stable `db.system.name` and the deprecated `db.system`. + db_system_for_port = attributes.get(db_attributes.DB_SYSTEM_NAME) or attributes.get( + SpanAttributes.DB_SYSTEM + ) + if port != _get_default_port_http(attributes) and port != _get_default_port_db(str(db_system_for_port)): target = "{}:{}".format(target, port) return target @@ -231,7 +240,15 @@ def _get_target_for_db_dependency( attributes: Attributes, ) -> Optional[str]: if attributes: - db_name = attributes.get(SpanAttributes.DB_NAME) + if not target: + server_address = attributes.get(server_attributes.SERVER_ADDRESS) + if server_address: + target = str(server_address) + server_port = attributes.get(server_attributes.SERVER_PORT) + if server_port and server_port != _get_default_port_db(str(db_system)): + target = "{}:{}".format(target, server_port) + # Prefer new stable `db.namespace`, fall back to deprecated `db.name`. + db_name = attributes.get(db_attributes.DB_NAMESPACE) or attributes.get(SpanAttributes.DB_NAME) if db_name: if not target: target = str(db_name) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/quickpulse/test_types.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/quickpulse/test_types.py index e63251d9dbfe..b4465dca1f19 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/quickpulse/test_types.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/quickpulse/test_types.py @@ -9,6 +9,12 @@ from opentelemetry._logs import LogRecord from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.semconv._incubating.attributes import gen_ai_attributes +from opentelemetry.semconv.attributes.db_attributes import ( + DB_NAMESPACE, + DB_OPERATION_NAME, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, +) from opentelemetry.semconv.attributes.http_attributes import ( HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE, @@ -184,6 +190,27 @@ def test_db_dependency(self): self.assertEqual(result.data, "SELECT * FROM table") self.assertEqual(result.target, "mysql") + def test_db_dependency_stable_semconv(self): + self.span.kind = SpanKind.CLIENT + self.span.attributes = { + DB_SYSTEM_NAME: "postgresql", + DB_QUERY_TEXT: "SELECT * FROM table", + DB_NAMESPACE: "database", + "server.address": "stable-server", + } + result = _DependencyData._from_span(self.span) + self.assertEqual(result.type, "postgresql") + self.assertEqual(result.data, "SELECT * FROM table") + self.assertEqual(result.target, "stable-server|database") + + self.span.attributes = { + DB_SYSTEM_NAME: "postgresql", + DB_OPERATION_NAME: "SELECT", + } + result = _DependencyData._from_span(self.span) + self.assertEqual(result.data, "SELECT") + self.assertEqual(result.target, "postgresql") + def test_messaging_dependency(self): self.span.kind = SpanKind.CLIENT self.span.attributes = {SpanAttributes.MESSAGING_SYSTEM: "kafka"} diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_trace.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_trace.py index 5f683cce320b..221003eacd61 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_trace.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_trace.py @@ -593,7 +593,7 @@ def test_span_to_envelope_client_db(self): start_time = 1575494316027613500 end_time = start_time + 1001000000 - # SpanKind.CLIENT Db + # SpanKind.CLIENT Db, deprecated semconv span = trace._Span( name="test", context=SpanContext( @@ -661,6 +661,26 @@ def test_span_to_envelope_client_db(self): envelope = exporter._span_to_envelope(span) self.assertEqual(envelope.data.base_data.target, "postgresql") + # Stable semconv + span._attributes = { + "db.system.name": "postgresql", + "db.query.text": "SELECT * from stable_test", + "db.namespace": "stableDb", + "server.address": "stable-server", + "server.port": 15432, + } + envelope = exporter._span_to_envelope(span) + self.assertEqual(envelope.data.base_data.type, "postgresql") + self.assertEqual(envelope.data.base_data.data, "SELECT * from stable_test") + self.assertEqual(envelope.data.base_data.target, "stable-server:15432|stableDb") + + span._attributes = { + "db.system.name": "postgresql", + "db.operation.name": "SELECT", + } + envelope = exporter._span_to_envelope(span) + self.assertEqual(envelope.data.base_data.data, "SELECT") + # Type span._attributes = { "db.system": "mssql", diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_trace_utils.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_trace_utils.py index ae4ddbef6d01..e12710ef5cf9 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_trace_utils.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/trace/test_trace_utils.py @@ -8,6 +8,8 @@ from azure.monitor.opentelemetry.exporter.export.trace._utils import ( _get_DJB2_sample_score, + _get_default_port_db, + _is_sql_db, ) from azure.monitor.opentelemetry.exporter._constants import ( @@ -17,6 +19,22 @@ ) +class TestDatabaseSystemNames(unittest.TestCase): + def test_legacy_and_stable_names(self): + test_cases = ( + ("h2", "h2database", 8082), + ("db2", "ibm.db2", 50000), + ("mssql", "microsoft.sql_server", 1433), + ("oracle", "oracle.db", 1521), + ) + + for legacy_name, stable_name, default_port in test_cases: + for db_system in (legacy_name, stable_name): + with self.subTest(db_system=db_system): + self.assertTrue(_is_sql_db(db_system)) + self.assertEqual(_get_default_port_db(db_system), default_port) + + class TestGetDJB2SampleScore(unittest.TestCase): """Test cases for _get_DJB2_sample_score function."""