Skip to content

Commit dcfad09

Browse files
authored
Implement metrics exporter (#23960)
1 parent d3ab6fa commit dcfad09

16 files changed

Lines changed: 623 additions & 29 deletions

File tree

sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
([#23633](https://github.com/Azure/azure-sdk-for-python/pull/23633))
1010
- Implement exporting span events as message/exception telemetry
1111
([#23708](https://github.com/Azure/azure-sdk-for-python/pull/23708))
12+
- Implement metrics exporter using experimental OT metrics sdk
13+
([#23960](https://github.com/Azure/azure-sdk-for-python/pull/23960))
1214

1315
### Breaking Changes
1416

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@
55
# --------------------------------------------------------------------------
66

77
from azure.monitor.opentelemetry.exporter.export.logs._exporter import AzureMonitorLogExporter
8+
from azure.monitor.opentelemetry.exporter.export.metrics._exporter import AzureMonitorMetricExporter
89
from azure.monitor.opentelemetry.exporter.export.trace._exporter import AzureMonitorTraceExporter
910
from ._version import VERSION
1011

11-
__all__ = ["AzureMonitorLogExporter", "AzureMonitorTraceExporter"]
12+
__all__ = [
13+
"AzureMonitorMetricExporter",
14+
"AzureMonitorLogExporter",
15+
"AzureMonitorTraceExporter",
16+
]
1217
__version__ = VERSION

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/metrics/__init__.py

Whitespace-only changes.
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
import logging
4+
from typing import Sequence, Any
5+
6+
from opentelemetry.sdk._metrics.export import MetricExporter, MetricExportResult
7+
from opentelemetry.sdk._metrics.point import (
8+
Gauge,
9+
Histogram,
10+
Metric,
11+
Sum,
12+
)
13+
14+
from azure.monitor.opentelemetry.exporter import _utils
15+
from azure.monitor.opentelemetry.exporter._generated.models import (
16+
MetricDataPoint,
17+
MetricsData,
18+
MonitorBase,
19+
TelemetryItem,
20+
)
21+
from azure.monitor.opentelemetry.exporter.export._base import (
22+
BaseExporter,
23+
ExportResult,
24+
)
25+
26+
_logger = logging.getLogger(__name__)
27+
28+
__all__ = ["AzureMonitorMetricExporter"]
29+
30+
31+
class AzureMonitorMetricExporter(BaseExporter, MetricExporter):
32+
"""Azure Monitor Metric exporter for OpenTelemetry."""
33+
34+
def export(
35+
self, metrics: Sequence[Metric], **kwargs: Any # pylint: disable=unused-argument
36+
) -> MetricExportResult:
37+
"""Exports a batch of metric data
38+
:param metrics: Open Telemetry Metric(s) to export.
39+
:type metrics: Sequence[~opentelemetry._metrics.point.Metric]
40+
:rtype: ~opentelemetry.sdk._metrics.export.MetricExportResult
41+
"""
42+
envelopes = [self._metric_to_envelope(metric) for metric in metrics]
43+
try:
44+
result = self._transmit(envelopes)
45+
if result == ExportResult.FAILED_RETRYABLE:
46+
envelopes_to_store = [x.as_dict() for x in envelopes]
47+
self.storage.put(envelopes_to_store, 1)
48+
if result == ExportResult.SUCCESS:
49+
# Try to send any cached events
50+
self._transmit_from_storage()
51+
return _get_metric_export_result(result)
52+
except Exception: # pylint: disable=broad-except
53+
_logger.exception("Exception occurred while exporting the data.")
54+
return _get_metric_export_result(ExportResult.FAILED_NOT_RETRYABLE)
55+
56+
def shutdown(self) -> None:
57+
"""Shuts down the exporter.
58+
59+
Called when the SDK is shut down.
60+
"""
61+
self.storage.close()
62+
63+
def _metric_to_envelope(self, metric: Metric) -> TelemetryItem:
64+
if not metric:
65+
return None
66+
envelope = _convert_metric_to_envelope(metric)
67+
envelope.instrumentation_key = self._instrumentation_key
68+
return envelope
69+
70+
@classmethod
71+
def from_connection_string(
72+
cls, conn_str: str, **kwargs: Any
73+
) -> "AzureMonitorMetricExporter":
74+
"""
75+
Create an AzureMonitorMetricExporter from a connection string.
76+
77+
This is the recommended way of instantation if a connection string is passed in explicitly.
78+
If a user wants to use a connection string provided by environment variable, the constructor
79+
of the exporter can be called directly.
80+
81+
:param str conn_str: The connection string to be used for authentication.
82+
:keyword str api_version: The service API version used. Defaults to latest.
83+
:returns an instance of ~AzureMonitorMetricExporter
84+
"""
85+
return cls(connection_string=conn_str, **kwargs)
86+
87+
88+
# pylint: disable=protected-access
89+
def _convert_metric_to_envelope(metric: Metric) -> TelemetryItem:
90+
point = metric.point
91+
envelope = _utils._create_telemetry_item(point.time_unix_nano)
92+
envelope.name = "Microsoft.ApplicationInsights.Metric"
93+
envelope.tags.update(_utils._populate_part_a_fields(metric.resource))
94+
properties = metric.attributes
95+
value = 0
96+
# TODO
97+
count = 1
98+
# min = None
99+
# max = None
100+
# std_dev = None
101+
102+
if isinstance(point, (Gauge, Sum)):
103+
value = point.value
104+
elif isinstance(point, Histogram):
105+
value = sum(point.bucket_counts)
106+
count = sum(point.bucket_counts)
107+
108+
data_point = MetricDataPoint(
109+
name=metric.name,
110+
value=value,
111+
data_point_type="Aggregation",
112+
count=count,
113+
)
114+
data = MetricsData(
115+
properties=properties,
116+
metrics=[data_point],
117+
)
118+
119+
envelope.data = MonitorBase(base_data=data, base_type="MetricData")
120+
121+
return envelope
122+
123+
124+
def _get_metric_export_result(result: ExportResult) -> MetricExportResult:
125+
if result == ExportResult.SUCCESS:
126+
return MetricExportResult.SUCCESS
127+
if result in (
128+
ExportResult.FAILED_RETRYABLE,
129+
ExportResult.FAILED_NOT_RETRYABLE,
130+
):
131+
return MetricExportResult.FAILURE
132+
return None

sdk/monitor/azure-monitor-opentelemetry-exporter/samples/logs/sample_correlate.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from opentelemetry.sdk._logs import (
1111
LogEmitterProvider,
1212
OTLPHandler,
13+
get_log_emitter_provider,
1314
set_log_emitter_provider,
1415
)
1516
from opentelemetry.sdk._logs.export import BatchLogProcessor
@@ -19,17 +20,15 @@
1920

2021
trace.set_tracer_provider(TracerProvider())
2122
tracer = trace.get_tracer(__name__)
22-
log_emitter_provider = LogEmitterProvider()
23-
set_log_emitter_provider(log_emitter_provider)
23+
set_log_emitter_provider(LogEmitterProvider())
2424

2525
exporter = AzureMonitorLogExporter.from_connection_string(
2626
os.environ["APPLICATIONINSIGHTS_CONNECTION_STRING"]
2727
)
28-
29-
log_emitter_provider.add_log_processor(BatchLogProcessor(exporter))
30-
handler = OTLPHandler()
28+
get_log_emitter_provider().add_log_processor(BatchLogProcessor(exporter))
3129

3230
# Attach OTel handler to namespaced logger
31+
handler = OTLPHandler()
3332
logger = logging.getLogger(__name__)
3433
logger.addHandler(handler)
3534
logger.setLevel(logging.NOTSET)

sdk/monitor/azure-monitor-opentelemetry-exporter/samples/logs/sample_exception.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,21 @@
99
from opentelemetry.sdk._logs import (
1010
LogEmitterProvider,
1111
OTLPHandler,
12+
get_log_emitter_provider,
1213
set_log_emitter_provider,
1314
)
1415
from opentelemetry.sdk._logs.export import BatchLogProcessor
1516

1617
from azure.monitor.opentelemetry.exporter import AzureMonitorLogExporter
1718

18-
log_emitter_provider = LogEmitterProvider()
19-
set_log_emitter_provider(log_emitter_provider)
20-
19+
set_log_emitter_provider(LogEmitterProvider())
2120
exporter = AzureMonitorLogExporter.from_connection_string(
2221
os.environ["APPLICATIONINSIGHTS_CONNECTION_STRING"]
2322
)
24-
25-
log_emitter_provider.add_log_processor(BatchLogProcessor(exporter))
26-
handler = OTLPHandler()
23+
get_log_emitter_provider().add_log_processor(BatchLogProcessor(exporter))
2724

2825
# Attach OTel handler to namespaced logger
26+
handler = OTLPHandler()
2927
logger = logging.getLogger(__name__)
3028
logger.addHandler(handler)
3129
logger.setLevel(logging.NOTSET)

sdk/monitor/azure-monitor-opentelemetry-exporter/samples/logs/sample_log.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,21 @@
1010
from opentelemetry.sdk._logs import (
1111
LogEmitterProvider,
1212
OTLPHandler,
13+
get_log_emitter_provider,
1314
set_log_emitter_provider,
1415
)
1516
from opentelemetry.sdk._logs.export import BatchLogProcessor
1617

1718
from azure.monitor.opentelemetry.exporter import AzureMonitorLogExporter
1819

19-
log_emitter_provider = LogEmitterProvider()
20-
set_log_emitter_provider(log_emitter_provider)
21-
20+
set_log_emitter_provider(LogEmitterProvider())
2221
exporter = AzureMonitorLogExporter.from_connection_string(
2322
os.environ["APPLICATIONINSIGHTS_CONNECTION_STRING"]
2423
)
25-
26-
log_emitter_provider.add_log_processor(BatchLogProcessor(exporter))
27-
handler = OTLPHandler()
24+
get_log_emitter_provider().add_log_processor(BatchLogProcessor(exporter))
2825

2926
# Attach OTel handler to namespaced logger
27+
handler = OTLPHandler()
3028
logger = logging.getLogger(__name__)
3129
logger.addHandler(handler)
3230
logger.setLevel(logging.NOTSET)

sdk/monitor/azure-monitor-opentelemetry-exporter/samples/logs/sample_properties.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,21 @@
99
from opentelemetry.sdk._logs import (
1010
LogEmitterProvider,
1111
OTLPHandler,
12+
get_log_emitter_provider,
1213
set_log_emitter_provider,
1314
)
1415
from opentelemetry.sdk._logs.export import BatchLogProcessor
1516

1617
from azure.monitor.opentelemetry.exporter import AzureMonitorLogExporter
1718

18-
log_emitter_provider = LogEmitterProvider()
19-
set_log_emitter_provider(log_emitter_provider)
20-
19+
set_log_emitter_provider(LogEmitterProvider())
2120
exporter = AzureMonitorLogExporter.from_connection_string(
2221
os.environ["APPLICATIONINSIGHTS_CONNECTION_STRING"]
2322
)
24-
25-
log_emitter_provider.add_log_processor(BatchLogProcessor(exporter))
26-
handler = OTLPHandler()
23+
get_log_emitter_provider().add_log_processor(BatchLogProcessor(exporter))
2724

2825
# Attach OTel handler to namespaced logger
26+
handler = OTLPHandler()
2927
logger = logging.getLogger(__name__)
3028
logger.addHandler(handler)
3129
logger.setLevel(logging.NOTSET)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
page_type: sample
3+
languages:
4+
- python
5+
products:
6+
- azure-monitor
7+
---
8+
9+
# Microsoft Azure Monitor Opentelemetry Exporter Metric Python Samples
10+
11+
These code samples show common champion scenario operations with the AzureMonitorMetricExporter.
12+
13+
* 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)
14+
* 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)
15+
16+
17+
## Installation
18+
19+
```sh
20+
$ pip install azure-monitor-opentelemetry-exporter --pre
21+
```
22+
23+
## Run the Applications
24+
25+
### Metrics
26+
27+
* Update `APPLICATIONINSIGHTS_CONNECTION_STRING` environment variable
28+
29+
* Run the sample
30+
31+
```sh
32+
$ # from this directory
33+
$ python sample_metrics.py
34+
```
35+
36+
### Instrument usage
37+
38+
* Update `APPLICATIONINSIGHTS_CONNECTION_STRING` environment variable
39+
40+
* Run the sample
41+
42+
```sh
43+
$ # from this directory
44+
$ python sample_instruments.py
45+
```
46+
47+
## Explore the data
48+
49+
After running the applications, data would be available in [Azure](
50+
https://docs.microsoft.com/azure/azure-monitor/app/app-insights-overview#where-do-i-see-my-telemetry)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
"""
4+
An example to show an application using all instruments in the OpenTelemetry SDK. Metrics created
5+
and recorded using the sdk are tracked and telemetry is exported to application insights with the
6+
AzureMonitorMetricsExporter.
7+
"""
8+
import os
9+
10+
from opentelemetry import _metrics
11+
from opentelemetry._metrics.measurement import Measurement
12+
from opentelemetry.sdk._metrics import MeterProvider
13+
from opentelemetry.sdk._metrics.export import PeriodicExportingMetricReader
14+
15+
from azure.monitor.opentelemetry.exporter import AzureMonitorMetricExporter
16+
17+
exporter = AzureMonitorMetricExporter.from_connection_string(
18+
os.environ["APPLICATIONINSIGHTS_CONNECTION_STRING"]
19+
)
20+
reader = PeriodicExportingMetricReader(exporter, export_interval_millis=5000)
21+
_metrics.set_meter_provider(MeterProvider(metric_readers=[reader]))
22+
23+
# Create a namespaced meter
24+
meter = _metrics.get_meter_provider().get_meter("sample")
25+
26+
# Callback functions for observable instruments
27+
def observable_counter_func():
28+
yield Measurement(1, {})
29+
30+
31+
def observable_up_down_counter_func():
32+
yield Measurement(-10, {})
33+
34+
35+
def observable_gauge_func():
36+
yield Measurement(9, {})
37+
38+
# Counter
39+
counter = meter.create_counter("counter")
40+
counter.add(1)
41+
42+
# Async Counter
43+
observable_counter = meter.create_observable_counter(
44+
"observable_counter", observable_counter_func
45+
)
46+
47+
# UpDownCounter
48+
updown_counter = meter.create_up_down_counter("updown_counter")
49+
updown_counter.add(1)
50+
updown_counter.add(-5)
51+
52+
# Async UpDownCounter
53+
observable_updown_counter = meter.create_observable_up_down_counter(
54+
"observable_updown_counter", observable_up_down_counter_func
55+
)
56+
57+
# Histogram
58+
histogram = meter.create_histogram("histogram")
59+
histogram.record(99.9)
60+
61+
# Async Gauge
62+
gauge = meter.create_observable_gauge("gauge", observable_gauge_func)
63+
64+
input(...)

0 commit comments

Comments
 (0)