diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md b/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md index 4cb75b3f5e73..eedace1506c1 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md @@ -3,6 +3,8 @@ ## 1.0.0b47 (Unreleased) ### Features Added +- Add auto detection for application ID from connection string if not set + ([#44644](https://github.com/Azure/azure-sdk-for-python/pull/44644)) - Add support for user id and authId ([#44662](https://github.com/Azure/azure-sdk-for-python/pull/44662)) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_connection_string_parser.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_connection_string_parser.py index 80c341ee8d9e..2045a2b66c8e 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_connection_string_parser.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_connection_string_parser.py @@ -9,6 +9,7 @@ INSTRUMENTATION_KEY = "instrumentationkey" # cspell:disable-next-line AAD_AUDIENCE = "aadaudience" +APPLICATION_ID = "applicationid" # cspell:disable-line # Validate UUID format # Specs taken from https://tools.ietf.org/html/rfc4122 @@ -38,6 +39,7 @@ def __init__(self, connection_string: typing.Optional[str] = None) -> None: self._connection_string = connection_string self.aad_audience = "" self.region = "" + self.application_id = "" self._initialize() self._validate_instrumentation_key() @@ -73,6 +75,9 @@ def _initialize(self) -> None: # Extract region information self.region = self._extract_region() # type: ignore + # Extract application_id + self.application_id = code_cs.get(APPLICATION_ID) or env_cs.get(APPLICATION_ID) # type: ignore + def _extract_region(self) -> typing.Optional[str]: """Extract region from endpoint URL. diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_constants.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_constants.py index 27f4d76defdd..543210445eb2 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_constants.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_constants.py @@ -336,4 +336,7 @@ class _RP_Names(Enum): # Default message for messages(MessageData) with empty body _DEFAULT_LOG_MESSAGE = "n/a" +# Resource attribute applicationId +_APPLICATION_ID_RESOURCE_KEY = "microsoft.applicationId" + # cSpell:disable diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_utils.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_utils.py index 2432a96841ac..4d8e2ea129e4 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_utils.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_utils.py @@ -23,6 +23,7 @@ TelemetryItem, ) from azure.monitor.opentelemetry.exporter._version import VERSION as ext_version +from azure.monitor.opentelemetry.exporter._connection_string_parser import ConnectionStringParser from azure.monitor.opentelemetry.exporter._constants import ( _AKS_ARM_NAMESPACE_ID, _DEFAULT_AAD_SCOPE, @@ -438,3 +439,8 @@ def get_compute_type(): def _get_sha256_hash(input_str: str) -> str: return hashlib.sha256(input_str.encode("utf-8")).hexdigest() + + +def _get_application_id(connection_string: Optional[str]) -> Optional[str]: + parsed_connection_string = ConnectionStringParser(connection_string) + return parsed_connection_string.application_id 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 ecd2c452c34d..159998ccfc48 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 @@ -4,7 +4,7 @@ import json import logging from time import time_ns -from typing import no_type_check, Any, Dict, List, Sequence +from typing import no_type_check, Any, Dict, List, Sequence, Optional from urllib.parse import urlparse from opentelemetry.semconv.attributes.client_attributes import CLIENT_ADDRESS @@ -38,6 +38,7 @@ _REQUEST_ENVELOPE_NAME, _EXCEPTION_ENVELOPE_NAME, _REMOTE_DEPENDENCY_ENVELOPE_NAME, + _APPLICATION_ID_RESOURCE_KEY, ) from azure.monitor.opentelemetry.exporter import _utils from azure.monitor.opentelemetry.exporter._generated.models import ( @@ -124,6 +125,7 @@ class AzureMonitorTraceExporter(BaseExporter, SpanExporter): def __init__(self, **kwargs: Any): self._tracer_provider = kwargs.pop("tracer_provider", None) super().__init__(**kwargs) + self.application_id = _utils._get_application_id(self._connection_string) def export(self, spans: Sequence[ReadableSpan], **_kwargs: Any) -> SpanExportResult: """Export span data. @@ -134,12 +136,13 @@ def export(self, spans: Sequence[ReadableSpan], **_kwargs: Any) -> SpanExportRes :rtype: ~opentelemetry.sdk.trace.export.SpanExportResult """ envelopes = [] + if spans and self._should_collect_otel_resource_metric(): resource = None try: tracer_provider = self._tracer_provider or get_tracer_provider() resource = tracer_provider.resource # type: ignore - envelopes.append(self._get_otel_resource_envelope(resource)) + envelopes.append(self._get_otel_resource_envelope(resource, self.application_id)) except AttributeError as e: _logger.exception("Failed to derive Resource from Tracer Provider: %s", e) # pylint: disable=C4769 for span in spans: @@ -162,14 +165,18 @@ def shutdown(self) -> None: self.storage.close() # pylint: disable=protected-access - def _get_otel_resource_envelope(self, resource: Resource) -> TelemetryItem: - attributes: Dict[str, str] = {} + def _get_otel_resource_envelope(self, resource: Resource, application_id: Optional[str]) -> TelemetryItem: + attributes: Dict[str, Any] = {} if resource: - attributes = resource.attributes + attributes = dict(resource.attributes) envelope = _utils._create_telemetry_item(time_ns()) envelope.name = _METRIC_ENVELOPE_NAME envelope.tags.update(_utils._populate_part_a_fields(resource)) # pylint: disable=W0212 envelope.instrumentation_key = self._instrumentation_key + + if application_id and attributes.get(_APPLICATION_ID_RESOURCE_KEY) is None: + attributes[_APPLICATION_ID_RESOURCE_KEY] = application_id + data_point = MetricDataPoint( name="_OTELRESOURCE_"[:1024], value=0, diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/logs/test_logs.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/logs/test_logs.py index 50ded1df9cf4..67c769c1369b 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/logs/test_logs.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/logs/test_logs.py @@ -32,6 +32,7 @@ _APPLICATION_INSIGHTS_EVENT_MARKER_ATTRIBUTE, _MICROSOFT_CUSTOM_EVENT_NAME, _DEFAULT_LOG_MESSAGE, + _APPLICATION_ID_RESOURCE_KEY, ) from azure.monitor.opentelemetry.exporter._generated.models import ContextTagKeys from azure.monitor.opentelemetry.exporter._utils import ( @@ -128,7 +129,7 @@ def setUpClass(cls): body=None, attributes={"test": "attribute"}, ), - resource=Resource.create(attributes={"asd": "test_resource"}), + resource=Resource.create(attributes={"asd": "test_resource", "microsoft.applicationId": "app_id"}), instrumentation_scope=InstrumentationScope("test_name"), ) cls._log_data_complex_body = _logs.ReadWriteLogRecord( @@ -470,6 +471,7 @@ def test_log_to_envelope_log_none(self): self.assertEqual(envelope.name, "Microsoft.ApplicationInsights.Message") self.assertEqual(envelope.data.base_type, "MessageData") self.assertEqual(envelope.data.base_data.message, _DEFAULT_LOG_MESSAGE) + self.assertEqual(envelope.data.base_data.properties.get(_APPLICATION_ID_RESOURCE_KEY), None) def test_log_to_envelope_log_empty(self): exporter = self._exporter @@ -783,7 +785,6 @@ def test_get_severity_level(self): for sev_num in SeverityNumber: num = sev_num.value level = _get_severity_level(sev_num) - print(num) if num in range(0, 9): self.assertEqual(level, 0) elif num in range(9, 13): diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/test_connection_string_parser.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/test_connection_string_parser.py index d235c5dde117..431f7c8533c1 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/test_connection_string_parser.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/test_connection_string_parser.py @@ -357,3 +357,21 @@ def test_region_extraction_alphanumeric_regions(self): + f";IngestionEndpoint=https://{endpoint_suffix}" ) self.assertEqual(parser.region, expected_region) + + def test_application_id_extraction_from_connection_string(self): + parser = ConnectionStringParser( + connection_string="InstrumentationKey=" + + self._valid_instrumentation_key + + ";IngestionEndpoint=https://northeurope-999.in.applicationinsights.azure.com/" + + ";LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/;ApplicationId=3cd3dd3f-64cc-4d7c-9303-8d69a4bb8558" + ) + self.assertEqual(parser.application_id, "3cd3dd3f-64cc-4d7c-9303-8d69a4bb8558") + + def test_application_id_extraction_from_no_application_id(self): + parser = ConnectionStringParser( + connection_string="InstrumentationKey=" + + self._valid_instrumentation_key + + ";IngestionEndpoint=https://northeurope-999.in.applicationinsights.azure.com/" + + ";LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/" + ) + self.assertEqual(parser.application_id, None) 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 aa0d8002b13c..31cd5b5ac031 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 @@ -41,6 +41,7 @@ _AZURE_SDK_NAMESPACE_NAME, _AZURE_SDK_OPENTELEMETRY_NAME, _AZURE_AI_SDK_NAME, + _APPLICATION_ID_RESOURCE_KEY, ) from azure.monitor.opentelemetry.exporter._generated.models import ContextTagKeys from azure.monitor.opentelemetry.exporter._utils import azure_monitor_context @@ -133,6 +134,7 @@ def test_export_failure(self): transmit.return_value = ExportResult.FAILED_RETRYABLE storage_mock = mock.Mock() exporter.storage.put = storage_mock + exporter._connection_string = "InstrumentationKey=4321abcd-5678-4efa-8abc-1234567890ab;IngestionEndpoint=https://eastus-8.in.applicationinsights.azure.com/;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/;ApplicationId=4321abcd-5678-4efa-8abc-1234567890ab" result = exporter.export([test_span]) self.assertEqual(result, SpanExportResult.FAILURE) self.assertEqual(storage_mock.call_count, 1) @@ -188,7 +190,7 @@ def test_export_with_tracer_provider(self): "azure.monitor.opentelemetry.exporter.AzureMonitorTraceExporter._get_otel_resource_envelope" ) as resource_patch: # noqa: E501 result = exporter.export([test_span]) - resource_patch.assert_called_once_with(mock_resource) + resource_patch.assert_called_once_with(mock_resource, None) self.assertEqual(result, SpanExportResult.SUCCESS) self.assertEqual(storage_mock.call_count, 1) @@ -221,7 +223,7 @@ def test_export_with_tracer_provider_global(self): "azure.monitor.opentelemetry.exporter.AzureMonitorTraceExporter._get_otel_resource_envelope" ) as resource_patch: # noqa: E501 result = exporter.export([test_span]) - resource_patch.assert_called_once_with(mock_resource) + resource_patch.assert_called_once_with(mock_resource, None) self.assertEqual(result, SpanExportResult.SUCCESS) self.assertEqual(storage_mock.call_count, 1) @@ -1768,7 +1770,7 @@ def test_export_otel_resource_metric(self, mock_get_tracer_provider): mock_get_otel_resource_envelope.return_value = "test_envelope" result = exporter.export([test_span]) self.assertEqual(result, SpanExportResult.SUCCESS) - mock_get_otel_resource_envelope.assert_called_once_with(test_resource) + mock_get_otel_resource_envelope.assert_called_once_with(test_resource, None) envelopes = ["test_envelope", exporter._span_to_envelope(test_span)] transmit.assert_called_once_with(envelopes) @@ -1781,9 +1783,10 @@ def test_get_otel_resource_envelope(self): "bool_test_key": False, "float_test_key": 0.5, "sequence_test_key": ["a", "b"], + "microsoft.applicationId": "test_app_id", } ) - envelope = exporter._get_otel_resource_envelope(test_resource) + envelope = exporter._get_otel_resource_envelope(test_resource, "test_app_id") metric_name = envelope.name self.assertEqual(metric_name, "Microsoft.ApplicationInsights.Metric") instrumentation_key = envelope.instrumentation_key