From dcfad094300df3051412720e5a3b16c4c5454aff Mon Sep 17 00:00:00 2001 From: Leighton Chen Date: Wed, 13 Apr 2022 10:01:35 -0700 Subject: [PATCH] Implement metrics exporter (#23960) --- .../CHANGELOG.md | 2 + .../opentelemetry/exporter/__init__.py | 7 +- .../exporter/export/metrics/__init__.py | 0 .../exporter/export/metrics/_exporter.py | 132 ++++++++ .../samples/logs/sample_correlate.py | 9 +- .../samples/logs/sample_exception.py | 10 +- .../samples/logs/sample_log.py | 10 +- .../samples/logs/sample_properties.py | 10 +- .../samples/metrics/README.md | 50 +++ .../samples/metrics/sample_instruments.py | 64 ++++ .../samples/metrics/sample_metrics.py | 26 ++ .../setup.py | 4 +- .../tests/logs/test_logs.py | 10 +- .../tests/metrics/__init__.py | 2 + .../tests/metrics/test_metrics.py | 312 ++++++++++++++++++ shared_requirements.txt | 4 +- 16 files changed, 623 insertions(+), 29 deletions(-) create mode 100644 sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/metrics/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/metrics/_exporter.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry-exporter/samples/metrics/README.md create mode 100644 sdk/monitor/azure-monitor-opentelemetry-exporter/samples/metrics/sample_instruments.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry-exporter/samples/metrics/sample_metrics.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry-exporter/tests/metrics/__init__.py create mode 100644 sdk/monitor/azure-monitor-opentelemetry-exporter/tests/metrics/test_metrics.py diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md b/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md index d7d519568646..4db8328ba988 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md @@ -9,6 +9,8 @@ ([#23633](https://github.com/Azure/azure-sdk-for-python/pull/23633)) - Implement exporting span events as message/exception telemetry ([#23708](https://github.com/Azure/azure-sdk-for-python/pull/23708)) +- Implement metrics exporter using experimental OT metrics sdk + ([#23960](https://github.com/Azure/azure-sdk-for-python/pull/23960)) ### Breaking Changes diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/__init__.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/__init__.py index 56b3f96ef11e..f556e141fc69 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/__init__.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/__init__.py @@ -5,8 +5,13 @@ # -------------------------------------------------------------------------- from azure.monitor.opentelemetry.exporter.export.logs._exporter import AzureMonitorLogExporter +from azure.monitor.opentelemetry.exporter.export.metrics._exporter import AzureMonitorMetricExporter from azure.monitor.opentelemetry.exporter.export.trace._exporter import AzureMonitorTraceExporter from ._version import VERSION -__all__ = ["AzureMonitorLogExporter", "AzureMonitorTraceExporter"] +__all__ = [ + "AzureMonitorMetricExporter", + "AzureMonitorLogExporter", + "AzureMonitorTraceExporter", +] __version__ = VERSION diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/metrics/__init__.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/metrics/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 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 new file mode 100644 index 000000000000..b87f683af65b --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/metrics/_exporter.py @@ -0,0 +1,132 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import logging +from typing import Sequence, Any + +from opentelemetry.sdk._metrics.export import MetricExporter, MetricExportResult +from opentelemetry.sdk._metrics.point import ( + Gauge, + Histogram, + Metric, + Sum, +) + +from azure.monitor.opentelemetry.exporter import _utils +from azure.monitor.opentelemetry.exporter._generated.models import ( + MetricDataPoint, + MetricsData, + MonitorBase, + TelemetryItem, +) +from azure.monitor.opentelemetry.exporter.export._base import ( + BaseExporter, + ExportResult, +) + +_logger = logging.getLogger(__name__) + +__all__ = ["AzureMonitorMetricExporter"] + + +class AzureMonitorMetricExporter(BaseExporter, MetricExporter): + """Azure Monitor Metric exporter for OpenTelemetry.""" + + def export( + self, metrics: Sequence[Metric], **kwargs: Any # pylint: disable=unused-argument + ) -> MetricExportResult: + """Exports a batch of metric data + :param metrics: Open Telemetry Metric(s) to export. + :type metrics: Sequence[~opentelemetry._metrics.point.Metric] + :rtype: ~opentelemetry.sdk._metrics.export.MetricExportResult + """ + envelopes = [self._metric_to_envelope(metric) for metric in metrics] + try: + result = self._transmit(envelopes) + if result == ExportResult.FAILED_RETRYABLE: + envelopes_to_store = [x.as_dict() for x in envelopes] + self.storage.put(envelopes_to_store, 1) + if result == ExportResult.SUCCESS: + # Try to send any cached events + self._transmit_from_storage() + return _get_metric_export_result(result) + except Exception: # pylint: disable=broad-except + _logger.exception("Exception occurred while exporting the data.") + return _get_metric_export_result(ExportResult.FAILED_NOT_RETRYABLE) + + def shutdown(self) -> None: + """Shuts down the exporter. + + Called when the SDK is shut down. + """ + self.storage.close() + + def _metric_to_envelope(self, metric: Metric) -> TelemetryItem: + if not metric: + return None + envelope = _convert_metric_to_envelope(metric) + envelope.instrumentation_key = self._instrumentation_key + return envelope + + @classmethod + def from_connection_string( + cls, conn_str: str, **kwargs: Any + ) -> "AzureMonitorMetricExporter": + """ + Create an AzureMonitorMetricExporter from a connection string. + + This is the recommended way of instantation if a connection string is passed in explicitly. + If a user wants to use a connection string provided by environment variable, the constructor + of the exporter can be called directly. + + :param str conn_str: The connection string to be used for authentication. + :keyword str api_version: The service API version used. Defaults to latest. + :returns an instance of ~AzureMonitorMetricExporter + """ + return cls(connection_string=conn_str, **kwargs) + + +# pylint: disable=protected-access +def _convert_metric_to_envelope(metric: Metric) -> TelemetryItem: + point = metric.point + envelope = _utils._create_telemetry_item(point.time_unix_nano) + envelope.name = "Microsoft.ApplicationInsights.Metric" + envelope.tags.update(_utils._populate_part_a_fields(metric.resource)) + properties = metric.attributes + value = 0 + # TODO + count = 1 + # min = None + # max = None + # std_dev = None + + if isinstance(point, (Gauge, Sum)): + value = point.value + elif isinstance(point, Histogram): + value = sum(point.bucket_counts) + count = sum(point.bucket_counts) + + data_point = MetricDataPoint( + name=metric.name, + value=value, + data_point_type="Aggregation", + count=count, + ) + data = MetricsData( + properties=properties, + metrics=[data_point], + ) + + envelope.data = MonitorBase(base_data=data, base_type="MetricData") + + return envelope + + +def _get_metric_export_result(result: ExportResult) -> MetricExportResult: + if result == ExportResult.SUCCESS: + return MetricExportResult.SUCCESS + if result in ( + ExportResult.FAILED_RETRYABLE, + ExportResult.FAILED_NOT_RETRYABLE, + ): + return MetricExportResult.FAILURE + return None diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/logs/sample_correlate.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/logs/sample_correlate.py index 83b78ae87292..31838bb8b7c9 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/logs/sample_correlate.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/logs/sample_correlate.py @@ -10,6 +10,7 @@ from opentelemetry.sdk._logs import ( LogEmitterProvider, OTLPHandler, + get_log_emitter_provider, set_log_emitter_provider, ) from opentelemetry.sdk._logs.export import BatchLogProcessor @@ -19,17 +20,15 @@ trace.set_tracer_provider(TracerProvider()) tracer = trace.get_tracer(__name__) -log_emitter_provider = LogEmitterProvider() -set_log_emitter_provider(log_emitter_provider) +set_log_emitter_provider(LogEmitterProvider()) exporter = AzureMonitorLogExporter.from_connection_string( os.environ["APPLICATIONINSIGHTS_CONNECTION_STRING"] ) - -log_emitter_provider.add_log_processor(BatchLogProcessor(exporter)) -handler = OTLPHandler() +get_log_emitter_provider().add_log_processor(BatchLogProcessor(exporter)) # Attach OTel handler to namespaced logger +handler = OTLPHandler() logger = logging.getLogger(__name__) logger.addHandler(handler) logger.setLevel(logging.NOTSET) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/logs/sample_exception.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/logs/sample_exception.py index 21a2dc58a7bb..8e28ff2b4425 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/logs/sample_exception.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/logs/sample_exception.py @@ -9,23 +9,21 @@ from opentelemetry.sdk._logs import ( LogEmitterProvider, OTLPHandler, + get_log_emitter_provider, set_log_emitter_provider, ) from opentelemetry.sdk._logs.export import BatchLogProcessor from azure.monitor.opentelemetry.exporter import AzureMonitorLogExporter -log_emitter_provider = LogEmitterProvider() -set_log_emitter_provider(log_emitter_provider) - +set_log_emitter_provider(LogEmitterProvider()) exporter = AzureMonitorLogExporter.from_connection_string( os.environ["APPLICATIONINSIGHTS_CONNECTION_STRING"] ) - -log_emitter_provider.add_log_processor(BatchLogProcessor(exporter)) -handler = OTLPHandler() +get_log_emitter_provider().add_log_processor(BatchLogProcessor(exporter)) # Attach OTel handler to namespaced logger +handler = OTLPHandler() logger = logging.getLogger(__name__) logger.addHandler(handler) logger.setLevel(logging.NOTSET) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/logs/sample_log.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/logs/sample_log.py index 3285e1e9334d..fbfd64f10d0f 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/logs/sample_log.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/logs/sample_log.py @@ -10,23 +10,21 @@ from opentelemetry.sdk._logs import ( LogEmitterProvider, OTLPHandler, + get_log_emitter_provider, set_log_emitter_provider, ) from opentelemetry.sdk._logs.export import BatchLogProcessor from azure.monitor.opentelemetry.exporter import AzureMonitorLogExporter -log_emitter_provider = LogEmitterProvider() -set_log_emitter_provider(log_emitter_provider) - +set_log_emitter_provider(LogEmitterProvider()) exporter = AzureMonitorLogExporter.from_connection_string( os.environ["APPLICATIONINSIGHTS_CONNECTION_STRING"] ) - -log_emitter_provider.add_log_processor(BatchLogProcessor(exporter)) -handler = OTLPHandler() +get_log_emitter_provider().add_log_processor(BatchLogProcessor(exporter)) # Attach OTel handler to namespaced logger +handler = OTLPHandler() logger = logging.getLogger(__name__) logger.addHandler(handler) logger.setLevel(logging.NOTSET) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/logs/sample_properties.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/logs/sample_properties.py index b7740f1b0688..61f819237a5e 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/logs/sample_properties.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/logs/sample_properties.py @@ -9,23 +9,21 @@ from opentelemetry.sdk._logs import ( LogEmitterProvider, OTLPHandler, + get_log_emitter_provider, set_log_emitter_provider, ) from opentelemetry.sdk._logs.export import BatchLogProcessor from azure.monitor.opentelemetry.exporter import AzureMonitorLogExporter -log_emitter_provider = LogEmitterProvider() -set_log_emitter_provider(log_emitter_provider) - +set_log_emitter_provider(LogEmitterProvider()) exporter = AzureMonitorLogExporter.from_connection_string( os.environ["APPLICATIONINSIGHTS_CONNECTION_STRING"] ) - -log_emitter_provider.add_log_processor(BatchLogProcessor(exporter)) -handler = OTLPHandler() +get_log_emitter_provider().add_log_processor(BatchLogProcessor(exporter)) # Attach OTel handler to namespaced logger +handler = OTLPHandler() logger = logging.getLogger(__name__) logger.addHandler(handler) logger.setLevel(logging.NOTSET) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/metrics/README.md b/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/metrics/README.md new file mode 100644 index 000000000000..6f1d5cfbedaf --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/metrics/README.md @@ -0,0 +1,50 @@ +--- +page_type: sample +languages: + - python +products: + - azure-monitor +--- + +# Microsoft Azure Monitor Opentelemetry Exporter Metric Python Samples + +These code samples show common champion scenario operations with the AzureMonitorMetricExporter. + +* Metrics: [sample_metrics.py](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/metrics/sample_metrics.py) +* Instruments: [sample_instruments.py](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/metrics/sample_instruments.py) + + +## Installation + +```sh +$ pip install azure-monitor-opentelemetry-exporter --pre +``` + +## Run the Applications + +### Metrics + +* Update `APPLICATIONINSIGHTS_CONNECTION_STRING` environment variable + +* Run the sample + +```sh +$ # from this directory +$ python sample_metrics.py +``` + +### Instrument usage + +* Update `APPLICATIONINSIGHTS_CONNECTION_STRING` environment variable + +* Run the sample + +```sh +$ # from this directory +$ python sample_instruments.py +``` + +## Explore the data + +After running the applications, data would be available in [Azure]( +https://docs.microsoft.com/azure/azure-monitor/app/app-insights-overview#where-do-i-see-my-telemetry) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/metrics/sample_instruments.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/metrics/sample_instruments.py new file mode 100644 index 000000000000..2c6eb32c6f36 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/metrics/sample_instruments.py @@ -0,0 +1,64 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +""" +An example to show an application using all instruments in the OpenTelemetry SDK. Metrics created +and recorded using the sdk are tracked and telemetry is exported to application insights with the +AzureMonitorMetricsExporter. +""" +import os + +from opentelemetry import _metrics +from opentelemetry._metrics.measurement import Measurement +from opentelemetry.sdk._metrics import MeterProvider +from opentelemetry.sdk._metrics.export import PeriodicExportingMetricReader + +from azure.monitor.opentelemetry.exporter import AzureMonitorMetricExporter + +exporter = AzureMonitorMetricExporter.from_connection_string( + os.environ["APPLICATIONINSIGHTS_CONNECTION_STRING"] +) +reader = PeriodicExportingMetricReader(exporter, export_interval_millis=5000) +_metrics.set_meter_provider(MeterProvider(metric_readers=[reader])) + +# Create a namespaced meter +meter = _metrics.get_meter_provider().get_meter("sample") + +# Callback functions for observable instruments +def observable_counter_func(): + yield Measurement(1, {}) + + +def observable_up_down_counter_func(): + yield Measurement(-10, {}) + + +def observable_gauge_func(): + yield Measurement(9, {}) + +# Counter +counter = meter.create_counter("counter") +counter.add(1) + +# Async Counter +observable_counter = meter.create_observable_counter( + "observable_counter", observable_counter_func +) + +# UpDownCounter +updown_counter = meter.create_up_down_counter("updown_counter") +updown_counter.add(1) +updown_counter.add(-5) + +# Async UpDownCounter +observable_updown_counter = meter.create_observable_up_down_counter( + "observable_updown_counter", observable_up_down_counter_func +) + +# Histogram +histogram = meter.create_histogram("histogram") +histogram.record(99.9) + +# Async Gauge +gauge = meter.create_observable_gauge("gauge", observable_gauge_func) + +input(...) \ No newline at end of file diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/metrics/sample_metrics.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/metrics/sample_metrics.py new file mode 100644 index 000000000000..b910fc8b1b55 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/samples/metrics/sample_metrics.py @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +""" +An example to show an application using Opentelemetry metrics sdk. Metrics created and recorded using the +sdk are tracked and telemetry is exported to application insights with the AzureMonitorMetricsExporter. +""" +import os + +from opentelemetry import _metrics +from opentelemetry.sdk._metrics import MeterProvider +from opentelemetry.sdk._metrics.export import PeriodicExportingMetricReader + +from azure.monitor.opentelemetry.exporter import AzureMonitorMetricExporter + +exporter = AzureMonitorMetricExporter.from_connection_string( + os.environ["APPLICATIONINSIGHTS_CONNECTION_STRING"] +) +reader = PeriodicExportingMetricReader(exporter, export_interval_millis=5000) +_metrics.set_meter_provider(MeterProvider(metric_readers=[reader])) + +# Create a namespaced meter +meter = _metrics.get_meter_provider().get_meter("sample") + +# Create Counter instrument with the meter +counter = meter.create_counter("counter") +counter.add(1) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/setup.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/setup.py index 2a2dc9d0a2a5..e54c642f5258 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/setup.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/setup.py @@ -78,7 +78,7 @@ install_requires=[ "azure-core<2.0.0,>=1.23.0", "msrest>=0.6.10", - "opentelemetry-api<2.0.0,>=1.9.0,!=1.10a0", - "opentelemetry-sdk<2.0.0,>=1.9.0,!=1.10a0", + "opentelemetry-api<2.0.0,>=1.10.0,!=1.10a0", + "opentelemetry-sdk<2.0.0,>=1.10.0,!=1.10a0", ], ) 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 5d6ac8e5ecfc..bf2e341d38f7 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 @@ -20,7 +20,10 @@ _get_log_export_result, _get_severity_level, ) -from azure.monitor.opentelemetry.exporter._utils import azure_monitor_context +from azure.monitor.opentelemetry.exporter._utils import ( + azure_monitor_context, + ns_to_iso_str, +) def throw(exc_type, *args, **kwargs): @@ -200,6 +203,8 @@ def test_log_to_envelope_log(self): exporter = self._exporter envelope = exporter._log_to_envelope(self._log_data) record = self._log_data.log_record + self.assertEqual(envelope.name, 'Microsoft.ApplicationInsights.Message') + self.assertEqual(envelope.time, ns_to_iso_str(record.timestamp)) self.assertEqual(envelope.data.base_type, 'MessageData') self.assertEqual(envelope.data.base_data.message, record.body) self.assertEqual(envelope.data.base_data.severity_level, 2) @@ -208,6 +213,9 @@ def test_log_to_envelope_log(self): def test_log_to_envelope_exception(self): exporter = self._exporter envelope = exporter._log_to_envelope(self._exc_data) + record = self._log_data.log_record + self.assertEqual(envelope.name, 'Microsoft.ApplicationInsights.Exception') + self.assertEqual(envelope.time, ns_to_iso_str(record.timestamp)) self.assertEqual(envelope.data.base_type, 'ExceptionData') self.assertEqual(envelope.data.base_data.severity_level, 4) self.assertEqual(envelope.data.base_data.properties["test"], "attribute") diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/metrics/__init__.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/metrics/__init__.py new file mode 100644 index 000000000000..5b7f7a925cc0 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/metrics/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. 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 new file mode 100644 index 000000000000..34ac4df12641 --- /dev/null +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/metrics/test_metrics.py @@ -0,0 +1,312 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import os +import platform +import shutil +import unittest +from unittest import mock + +# pylint: disable=import-error +from opentelemetry.sdk.util.instrumentation import InstrumentationInfo +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk._metrics.export import MetricExportResult +from opentelemetry.sdk._metrics.point import ( + AggregationTemporality, + Gauge, + Histogram, + Metric, + Sum, +) + +from azure.monitor.opentelemetry.exporter.export._base import ExportResult +from azure.monitor.opentelemetry.exporter.export.metrics._exporter import ( + AzureMonitorMetricExporter, + _get_metric_export_result, +) +from azure.monitor.opentelemetry.exporter._utils import ( + azure_monitor_context, + ns_to_iso_str, +) + + +def throw(exc_type, *args, **kwargs): + def func(*_args, **_kwargs): + raise exc_type(*args, **kwargs) + + return func + + +# pylint: disable=import-error +# pylint: disable=protected-access +# pylint: disable=too-many-lines +class TestAzureMetricExporter(unittest.TestCase): + @classmethod + def setUpClass(cls): + os.environ.clear() + os.environ[ + "APPINSIGHTS_INSTRUMENTATIONKEY" + ] = "1234abcd-5678-4efa-8abc-1234567890ab" + cls._exporter = AzureMonitorMetricExporter() + cls._metric = Metric( + attributes={ + "test": "attribute" + }, + description="test description", + instrumentation_info=InstrumentationInfo("test_name"), + name="test name", + resource = Resource.create( + attributes={"asd":"test_resource"} + ), + unit="ms", + point=Sum( + start_time_unix_nano=1646865018558419456, + time_unix_nano=1646865018558419457, + value=10, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=False, + ) + ) + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls._exporter.storage._path, True) + + def test_constructor(self): + """Test the constructor.""" + exporter = AzureMonitorMetricExporter( + connection_string="InstrumentationKey=4321abcd-5678-4efa-8abc-1234567890ab", + ) + self.assertEqual( + exporter._instrumentation_key, + "4321abcd-5678-4efa-8abc-1234567890ab", + ) + + def test_from_connection_string(self): + exporter = AzureMonitorMetricExporter.from_connection_string( + "InstrumentationKey=4321abcd-5678-4efa-8abc-1234567890ab" + ) + self.assertTrue(isinstance(exporter, AzureMonitorMetricExporter)) + self.assertEqual( + exporter._instrumentation_key, + "4321abcd-5678-4efa-8abc-1234567890ab", + ) + + def test_export_empty(self): + exporter = self._exporter + result = exporter.export([]) + self.assertEqual(result, MetricExportResult.SUCCESS) + + def test_export_failure(self): + exporter = self._exporter + with mock.patch( + "azure.monitor.opentelemetry.exporter.AzureMonitorMetricExporter._transmit" + ) as transmit: # noqa: E501 + transmit.return_value = ExportResult.FAILED_RETRYABLE + storage_mock = mock.Mock() + exporter.storage.put = storage_mock + result = exporter.export([self._metric]) + self.assertEqual(result, MetricExportResult.FAILURE) + self.assertEqual(storage_mock.call_count, 1) + + def test_export_success(self): + exporter = self._exporter + with mock.patch( + "azure.monitor.opentelemetry.exporter.AzureMonitorMetricExporter._transmit" + ) as transmit: # noqa: E501 + transmit.return_value = ExportResult.SUCCESS + storage_mock = mock.Mock() + exporter._transmit_from_storage = storage_mock + result = exporter.export([self._metric]) + self.assertEqual(result, MetricExportResult.SUCCESS) + self.assertEqual(storage_mock.call_count, 1) + + @mock.patch("azure.monitor.opentelemetry.exporter.export.metrics._exporter._logger") + def test_export_exception(self, logger_mock): + exporter = self._exporter + with mock.patch( + "azure.monitor.opentelemetry.exporter.AzureMonitorMetricExporter._transmit", + throw(Exception), + ): # noqa: E501 + result = exporter.export([self._metric]) + self.assertEqual(result, MetricExportResult.FAILURE) + self.assertEqual(logger_mock.exception.called, True) + + def test_export_not_retryable(self): + exporter = self._exporter + with mock.patch( + "azure.monitor.opentelemetry.exporter.AzureMonitorMetricExporter._transmit" + ) as transmit: # noqa: E501 + transmit.return_value = ExportResult.FAILED_NOT_RETRYABLE + result = exporter.export([self._metric]) + self.assertEqual(result, MetricExportResult.FAILURE) + + def test_metric_to_envelope_partA(self): + exporter = self._exporter + resource = Resource( + {"service.name": "testServiceName", + "service.namespace": "testServiceNamespace", + "service.instance.id": "testServiceInstanceId"}) + _metric = Metric( + attributes={ + "test": "attribute" + }, + description="test description", + instrumentation_info=InstrumentationInfo("test_name"), + name="test name", + resource = resource, + unit="ms", + point=Sum( + start_time_unix_nano=1646865018558419456, + time_unix_nano=1646865018558419457, + value=10, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=False, + ) + ) + envelope = exporter._metric_to_envelope(_metric) + + self.assertEqual(envelope.instrumentation_key, + "1234abcd-5678-4efa-8abc-1234567890ab") + self.assertIsNotNone(envelope.tags) + self.assertEqual(envelope.tags.get("ai.device.id"), azure_monitor_context["ai.device.id"]) + self.assertEqual(envelope.tags.get("ai.device.locale"), azure_monitor_context["ai.device.locale"]) + self.assertEqual(envelope.tags.get("ai.device.osVersion"), azure_monitor_context["ai.device.osVersion"]) + self.assertEqual(envelope.tags.get("ai.device.type"), azure_monitor_context["ai.device.type"]) + self.assertEqual(envelope.tags.get("ai.internal.sdkVersion"), azure_monitor_context["ai.internal.sdkVersion"]) + + self.assertEqual(envelope.tags.get("ai.cloud.role"), "testServiceNamespace.testServiceName") + self.assertEqual(envelope.tags.get("ai.cloud.roleInstance"), "testServiceInstanceId") + self.assertEqual(envelope.tags.get("ai.internal.nodeName"), "testServiceInstanceId") + + def test_metric_to_envelope_partA_default(self): + exporter = self._exporter + resource = Resource( + {"service.name": "testServiceName"}) + _metric = Metric( + attributes={ + "test": "attribute" + }, + description="test description", + instrumentation_info=InstrumentationInfo("test_name"), + name="test name", + resource = resource, + unit="ms", + point=Sum( + start_time_unix_nano=1646865018558419456, + time_unix_nano=1646865018558419457, + value=10, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=False, + ) + ) + envelope = exporter._metric_to_envelope(_metric) + self.assertEqual(envelope.tags.get("ai.cloud.role"), "testServiceName") + self.assertEqual(envelope.tags.get("ai.cloud.roleInstance"), platform.node()) + self.assertEqual(envelope.tags.get("ai.internal.nodeName"), envelope.tags.get("ai.cloud.roleInstance")) + + def test_metric_to_envelope_sum(self): + exporter = self._exporter + _metric = Metric( + attributes={ + "test": "attribute" + }, + description="test description", + instrumentation_info=InstrumentationInfo("test_name"), + name="test name", + resource=None, + unit="ms", + point=Sum( + start_time_unix_nano=1646865018558419456, + time_unix_nano=1646865018558419457, + value=10, + aggregation_temporality=AggregationTemporality.CUMULATIVE, + is_monotonic=False, + ) + ) + envelope = exporter._metric_to_envelope(_metric) + self.assertEqual(envelope.name, 'Microsoft.ApplicationInsights.Metric') + self.assertEqual(envelope.time, ns_to_iso_str(_metric.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].value, 10) + self.assertEqual(envelope.data.base_data.metrics[0].data_point_type, "Aggregation") + self.assertEqual(envelope.data.base_data.metrics[0].count, 1) + + def test_metric_to_envelope_gauge(self): + exporter = self._exporter + _metric = Metric( + attributes={ + "test": "attribute" + }, + description="test description", + instrumentation_info=InstrumentationInfo("test_name"), + name="test name", + resource=None, + unit="ms", + point=Gauge( + time_unix_nano=1646865018558419457, + value=100, + ) + ) + envelope = exporter._metric_to_envelope(_metric) + self.assertEqual(envelope.name, 'Microsoft.ApplicationInsights.Metric') + self.assertEqual(envelope.time, ns_to_iso_str(_metric.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].value, 100) + self.assertEqual(envelope.data.base_data.metrics[0].data_point_type, "Aggregation") + self.assertEqual(envelope.data.base_data.metrics[0].count, 1) + + def test_metric_to_envelope_histogram(self): + exporter = self._exporter + _metric = Metric( + attributes={ + "test": "attribute" + }, + description="test description", + instrumentation_info=InstrumentationInfo("test_name"), + name="test name", + resource=None, + unit="ms", + point=Histogram( + aggregation_temporality=AggregationTemporality.DELTA, + bucket_counts=[0,3,4], + explicit_bounds=[0,5,10,0], + start_time_unix_nano=1646865018558419456, + time_unix_nano=1646865018558419457, + sum=31, + ) + ) + envelope = exporter._metric_to_envelope(_metric) + self.assertEqual(envelope.name, 'Microsoft.ApplicationInsights.Metric') + self.assertEqual(envelope.time, ns_to_iso_str(_metric.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].value, 7) + self.assertEqual(envelope.data.base_data.metrics[0].data_point_type, "Aggregation") + self.assertEqual(envelope.data.base_data.metrics[0].count, 7) + +class TestAzureMetricExporterUtils(unittest.TestCase): + def test_get_metric_export_result(self): + self.assertEqual( + _get_metric_export_result(ExportResult.SUCCESS), + MetricExportResult.SUCCESS, + ) + self.assertEqual( + _get_metric_export_result(ExportResult.FAILED_NOT_RETRYABLE), + MetricExportResult.FAILURE, + ) + self.assertEqual( + _get_metric_export_result(ExportResult.FAILED_RETRYABLE), + MetricExportResult.FAILURE, + ) + self.assertEqual(_get_metric_export_result(None), None) diff --git a/shared_requirements.txt b/shared_requirements.txt index 0191ab78f871..3235bc641356 100644 --- a/shared_requirements.txt +++ b/shared_requirements.txt @@ -199,8 +199,8 @@ opentelemetry-sdk<2.0.0,>=1.5.0,!=1.10a0 #override azure-ai-translation-document azure-core<2.0.0,>=1.14.0 #override azure-monitor-opentelemetry-exporter azure-core<2.0.0,>=1.23.0 #override azure-monitor-opentelemetry-exporter msrest>=0.6.10 -#override azure-monitor-opentelemetry-exporter opentelemetry-api<2.0.0,>=1.9.0,!=1.10a0 -#override azure-monitor-opentelemetry-exporter opentelemetry-sdk<2.0.0,>=1.9.0,!=1.10a0 +#override azure-monitor-opentelemetry-exporter opentelemetry-api<2.0.0,>=1.10.0,!=1.10a0 +#override azure-monitor-opentelemetry-exporter opentelemetry-sdk<2.0.0,>=1.10.0,!=1.10a0 #override azure-core-tracing-opentelemetry opentelemetry-api<2.0.0,>=1.0.0 #override azure-identity six>=1.12.0 #override azure-keyvault-keys six>=1.12.0