diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md b/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md index 5ea624ceb339..905a8101f2bc 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md @@ -10,6 +10,8 @@ ### Other Changes +- Support AI Foundry by Handling GEN_AI_SYSTEM Attributes with [Spec](https://github.com/aep-health-and-standards/Telemetry-Collection-Spec/blob/main/ApplicationInsights/genai_semconv_mapping.md) ([#41705](https://github.com/Azure/azure-sdk-for-python/pull/41705)) + ## 1.0.0b39 (2025-06-25) ### Bugs Fixed 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 b0643499c978..206d0b2dd064 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 @@ -96,6 +96,8 @@ _SAMPLE_RATE_KEY, ] +_GEN_AI_ATTRIBUTE_PREFIX = "GenAI | {}" + class AzureMonitorTraceExporter(BaseExporter, SpanExporter): """Azure Monitor Trace exporter for OpenTelemetry.""" @@ -333,6 +335,13 @@ def _convert_span_to_envelope(span: ReadableSpan) -> TelemetryItem: envelope.data = MonitorBase(base_data=data, base_type="RemoteDependencyData") target = trace_utils._get_target_for_dependency_from_peer(span.attributes) if span.kind is SpanKind.CLIENT: + gen_ai_attributes_val = "" + # gen_ai take precedence over other mappings (ex. HTTP) even if their attributes are also present on the span. + # following mappings will override the type + if gen_ai_attributes.GEN_AI_SYSTEM in span.attributes: # GenAI + gen_ai_attributes_val = span.attributes[gen_ai_attributes.GEN_AI_SYSTEM] + if gen_ai_attributes_val: + data.type = _GEN_AI_ATTRIBUTE_PREFIX.format(gen_ai_attributes_val) if _AZURE_SDK_NAMESPACE_NAME in span.attributes: # Azure specific resources # Currently only eventhub and servicebus are supported # https://github.com/Azure/azure-sdk-for-python/issues/9256 @@ -406,10 +415,12 @@ def _convert_span_to_envelope(span: ReadableSpan) -> TelemetryItem: target, # type: ignore span.attributes, ) - elif gen_ai_attributes.GEN_AI_SYSTEM in span.attributes: # GenAI - data.type = span.attributes[gen_ai_attributes.GEN_AI_SYSTEM] else: data.type = "N/A" + + # If no fields are available to set target using standard rules, set Dependency Target to gen_ai.system if present + if not target and gen_ai_attributes_val: + target = gen_ai_attributes_val elif span.kind is SpanKind.PRODUCER: # Messaging # Currently only eventhub and servicebus are supported that produce PRODUCER spans if _AZURE_SDK_NAMESPACE_NAME in span.attributes: @@ -426,7 +437,9 @@ def _convert_span_to_envelope(span: ReadableSpan) -> TelemetryItem: ) else: # SpanKind.INTERNAL data.type = "InProc" - if _AZURE_SDK_NAMESPACE_NAME in span.attributes: + if gen_ai_attributes.GEN_AI_SYSTEM in span.attributes: # GenAI + data.type = _GEN_AI_ATTRIBUTE_PREFIX.format(span.attributes[gen_ai_attributes.GEN_AI_SYSTEM]) + elif _AZURE_SDK_NAMESPACE_NAME in span.attributes: data.type += " | {}".format(span.attributes[_AZURE_SDK_NAMESPACE_NAME]) # Apply truncation # See https://github.com/MohanGsk/ApplicationInsights-Home/tree/master/EndpointSpecs/Schemas/Bond 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 8ebe8b221a03..d90eacc3a0ef 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 @@ -438,6 +438,24 @@ def test_span_to_envelope_client_http(self): envelope = exporter._span_to_envelope(span) self.assertEqual(envelope.data.base_data.target, "www.example.com") + span._attributes = { + "http.request.method": "GET", + "gen_ai.system": "az.ai.inference" + } + envelope = exporter._span_to_envelope(span) + self.assertEqual(envelope.data.base_data.target, "az.ai.inference") + self.assertEqual(envelope.data.base_data.name, "GET /") + + span._attributes = { + "http.request.method": "GET", + "server.address": "www.example.com", + "server.port": 80, + "url.scheme": "http", + "gen_ai.system": "az.ai.inference" + } + envelope = exporter._span_to_envelope(span) + self.assertEqual(envelope.data.base_data.target, "www.example.com") + # url # spell-checker:ignore ddds span._attributes = { @@ -760,8 +778,57 @@ def test_span_to_envelope_client_gen_ai(self): self.assertEqual(envelope.data.base_data.result_code, "0") self.assertEqual(envelope.data.base_type, "RemoteDependencyData") - self.assertEqual(envelope.data.base_data.type, "az.ai.inference") + self.assertEqual(envelope.data.base_data.type, "N/A") self.assertEqual(len(envelope.data.base_data.properties), 1) + + def test_span_to_envelope_client_internal_gen_ai_type(self): + exporter = self._exporter + start_time = 1575494316027613500 + end_time = start_time + 1001000000 + + span = trace._Span( + name="test", + context=SpanContext( + trace_id=36873507687745823477771305566750195431, + span_id=12030755672171557337, + is_remote=False, + ), + attributes={ + "gen_ai.system": "az.ai.inference", + }, + kind=SpanKind.INTERNAL, + ) + span.start(start_time=start_time) + span.end(end_time=end_time) + span._status = Status(status_code=StatusCode.UNSET) + envelope = exporter._span_to_envelope(span) + + self.assertEqual(envelope.data.base_data.type, "GenAI | az.ai.inference") + + def test_span_to_envelope_client_mutiple_types_with_gen_ai(self): + exporter = self._exporter + start_time = 1575494316027613500 + end_time = start_time + 1001000000 + + span = trace._Span( + name="test", + context=SpanContext( + trace_id=36873507687745823477771305566750195431, + span_id=12030755672171557337, + is_remote=False, + ), + attributes={ + "gen_ai.system": "az.ai.inference", + "az.namespace": "Microsoft.EventHub", + }, + kind=SpanKind.INTERNAL, + ) + span.start(start_time=start_time) + span.end(end_time=end_time) + span._status = Status(status_code=StatusCode.UNSET) + envelope = exporter._span_to_envelope(span) + + self.assertEqual(envelope.data.base_data.type, "GenAI | az.ai.inference") def test_span_to_envelope_client_azure(self): exporter = self._exporter