diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md b/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md index ef92eac283c5..0088906b327e 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md @@ -10,6 +10,8 @@ ([#34141](https://github.com/Azure/azure-sdk-for-python/pull/34141)) - Add application.ver to part A fields ([#34401](https://github.com/Azure/azure-sdk-for-python/pull/34401)) +- Add `APPLICATIONINSIGHTS_METRIC_NAMESPACE_OPT_IN` + ([#34463](https://github.com/Azure/azure-sdk-for-python/pull/34463)) ### Breaking Changes 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 8fffcb3e424f..e66fac012d9e 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 @@ -9,6 +9,7 @@ _APPLICATIONINSIGHTS_STATSBEAT_DISABLED_ALL = "APPLICATIONINSIGHTS_STATSBEAT_DISABLED_ALL" _APPLICATIONINSIGHTS_OPENTELEMETRY_RESOURCE_METRIC_DISABLED = \ "APPLICATIONINSIGHTS_OPENTELEMETRY_RESOURCE_METRIC_DISABLED" +_APPLICATIONINSIGHTS_METRIC_NAMESPACE_OPT_IN = "APPLICATIONINSIGHTS_METRIC_NAMESPACE_OPT_IN" _WEBSITE_SITE_NAME = "WEBSITE_SITE_NAME" _WEBSITE_HOME_STAMPNAME = "WEBSITE_HOME_STAMPNAME" _WEBSITE_HOSTNAME = "WEBSITE_HOSTNAME" diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/metrics/_exporter.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/metrics/_exporter.py index 859b85e48357..0485a20fb49a 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/metrics/_exporter.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/metrics/_exporter.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import logging +import os from typing import Dict, Optional, Union, Any @@ -23,8 +24,10 @@ NumberDataPoint, ) from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope from azure.monitor.opentelemetry.exporter._constants import ( + _APPLICATIONINSIGHTS_METRIC_NAMESPACE_OPT_IN, _AUTOCOLLECTED_INSTRUMENT_NAMES, _METRIC_ENVELOPE_NAME, ) @@ -94,6 +97,7 @@ def export( point, metric.name, resource_metric.resource, + scope_metric.scope, ) if envelope is not None: envelopes.append(envelope) @@ -133,8 +137,9 @@ def _point_to_envelope( point: DataPointT, name: str, resource: Optional[Resource] = None, + scope: Optional[InstrumentationScope] = None, ) -> Optional[TelemetryItem]: - envelope = _convert_point_to_envelope(point, name, resource) + envelope = _convert_point_to_envelope(point, name, resource, scope) if name in _AUTOCOLLECTED_INSTRUMENT_NAMES: envelope = _handle_std_metric_envelope(envelope, name, point.attributes) # type: ignore if envelope is not None: @@ -168,10 +173,14 @@ def _convert_point_to_envelope( point: DataPointT, name: str, resource: Optional[Resource] = None, + scope: Optional[InstrumentationScope] = None ) -> TelemetryItem: envelope = _utils._create_telemetry_item(point.time_unix_nano) envelope.name = _METRIC_ENVELOPE_NAME envelope.tags.update(_utils._populate_part_a_fields(resource)) # type: ignore + namespace = None + if scope is not None and _is_metric_namespace_opted_in(): + namespace = str(scope.name)[:256] value: Union[int, float] = 0 count = 1 min_ = None @@ -191,6 +200,7 @@ def _convert_point_to_envelope( data_point = MetricDataPoint( name=str(name)[:1024], + namespace=namespace, value=value, count=count, min=min_, @@ -266,6 +276,8 @@ def _handle_std_metric_envelope( def _is_status_code_success(status_code: Optional[str], threshold: int) -> bool: return status_code is not None and int(status_code) < threshold +def _is_metric_namespace_opted_in() -> bool: + return os.environ.get(_APPLICATIONINSIGHTS_METRIC_NAMESPACE_OPT_IN, "False").lower() == "true" def _get_metric_export_result(result: ExportResult) -> MetricExportResult: if result == ExportResult.SUCCESS: diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/statsbeat/_exporter.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/statsbeat/_exporter.py index bde8c807389d..e711731f9ca6 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/statsbeat/_exporter.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/statsbeat/_exporter.py @@ -3,6 +3,7 @@ from typing import Optional from opentelemetry.sdk.metrics.export import DataPointT from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.util.instrumentation import InstrumentationScope from azure.monitor.opentelemetry.exporter._generated.models import TelemetryItem from azure.monitor.opentelemetry.exporter import AzureMonitorMetricExporter @@ -17,6 +18,7 @@ def _point_to_envelope( point: DataPointT, name: str, resource: Optional[Resource] = None, + scope: Optional[InstrumentationScope] = None ) -> Optional[TelemetryItem]: # map statsbeat name from OpenTelemetry name name = _STATSBEAT_METRIC_NAME_MAPPINGS[name] @@ -24,4 +26,5 @@ def _point_to_envelope( point, name, resource, + None, ) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/metrics/test_metrics.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/metrics/test_metrics.py index 4869f9d300da..024c358e6b16 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/metrics/test_metrics.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/metrics/test_metrics.py @@ -213,6 +213,7 @@ def test_point_to_envelope_partA_default(self): def test_point_to_envelope_number(self): exporter = self._exporter resource = Resource.create(attributes={"asd":"test_resource"}) + scope = InstrumentationScope("test_scope") point=NumberDataPoint( attributes={ "test": "attribute", @@ -221,7 +222,7 @@ def test_point_to_envelope_number(self): time_unix_nano=1646865018558419457, value=10, ) - envelope = exporter._point_to_envelope(point, "test name", resource) + envelope = exporter._point_to_envelope(point, "test name", resource, scope) self.assertEqual(envelope.instrumentation_key, exporter._instrumentation_key) self.assertEqual(envelope.name, 'Microsoft.ApplicationInsights.Metric') self.assertEqual(envelope.time, ns_to_iso_str(point.time_unix_nano)) @@ -230,6 +231,7 @@ def test_point_to_envelope_number(self): self.assertEqual(envelope.data.base_data.properties['test'], 'attribute') self.assertEqual(len(envelope.data.base_data.metrics), 1) self.assertEqual(envelope.data.base_data.metrics[0].name, "test name") + self.assertEqual(envelope.data.base_data.metrics[0].namespace, None) self.assertEqual(envelope.data.base_data.metrics[0].value, 10) self.assertEqual(envelope.data.base_data.metrics[0].count, 1) @@ -261,6 +263,37 @@ def test_point_to_envelope_histogram(self): self.assertEqual(envelope.data.base_data.metrics[0].value, 31) self.assertEqual(envelope.data.base_data.metrics[0].count, 7) + @mock.patch.dict( + "os.environ", + { + "APPLICATIONINSIGHTS_METRIC_NAMESPACE_OPT_IN": "True", + }, + ) + def test_point_to_envelope_metric_namespace(self): + exporter = self._exporter + resource = Resource.create(attributes={"asd":"test_resource"}) + scope = InstrumentationScope("test_scope") + point=NumberDataPoint( + attributes={ + "test": "attribute", + }, + start_time_unix_nano=1646865018558419456, + time_unix_nano=1646865018558419457, + value=10, + ) + envelope = exporter._point_to_envelope(point, "test name", resource, scope) + self.assertEqual(envelope.instrumentation_key, exporter._instrumentation_key) + self.assertEqual(envelope.name, 'Microsoft.ApplicationInsights.Metric') + self.assertEqual(envelope.time, ns_to_iso_str(point.time_unix_nano)) + self.assertEqual(envelope.data.base_type, 'MetricData') + self.assertEqual(len(envelope.data.base_data.properties), 1) + self.assertEqual(envelope.data.base_data.properties['test'], 'attribute') + self.assertEqual(len(envelope.data.base_data.metrics), 1) + self.assertEqual(envelope.data.base_data.metrics[0].name, "test name") + self.assertEqual(envelope.data.base_data.metrics[0].namespace, "test_scope") + self.assertEqual(envelope.data.base_data.metrics[0].value, 10) + self.assertEqual(envelope.data.base_data.metrics[0].count, 1) + def test_point_to_envelope_std_metric_client_duration(self): exporter = self._exporter resource = Resource(