Skip to content

Commit 62861a2

Browse files
add log exporter and metric reader params to configure azure monitor (#44367)
* add log exporter and metric reader params * consistent ordering * added changelog * add some test configs * test fixes * fix pylint errors * Add more tests and rename * Remove duplicate metric reader * Add samples * Fix black * Fix format using tox * update test --------- Co-authored-by: Radhika Gupta <[email protected]>
1 parent 17c1e4e commit 62861a2

File tree

9 files changed

+151
-5
lines changed

9 files changed

+151
-5
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
## 1.8.4 (Unreleased)
44

55
### Features Added
6+
- Added ability to add additional Log Record Processors and Metric Readers via configure_azure_monitor
7+
([#44367](https://github.com/Azure/azure-sdk-for-python/pull/44367))
68

79
### Breaking Changes
810

sdk/monitor/azure-monitor-opentelemetry/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ You can use `configure_azure_monitor` to set up instrumentation for your app to
6868
| `resource` | Specifies the OpenTelemetry [Resource][ot_spec_resource] associated with your application. Passed in [Resource Attributes][ot_spec_resource_attributes] take priority over default attributes and those from [Resource Detectors][ot_python_resource_detectors]. | [OTEL_SERVICE_NAME][ot_spec_service_name], [OTEL_RESOURCE_ATTRIBUTES][ot_spec_resource_attributes], [OTEL_EXPERIMENTAL_RESOURCE_DETECTORS][ot_python_resource_detectors] |
6969
| `span_processors` | A list of [span processors][ot_span_processor] that will perform processing on each of your spans before they are exported. Useful for filtering/modifying telemetry. | `N/A` |
7070
| `views` | A list of [views][ot_view] that will be used to customize metrics exported by the SDK. | `N/A` |
71+
| `log_record_processors` | A list of [log record processors][ot_log_record_processor] that will process log records before they are exported. | `N/A` |
72+
| `metric_readers` | A list of [metric reader][ot_metric_reader] that will process metric readers before they are exported | `N/A` |
7173
| `traces_per_second` | Configures the Rate Limited sampler by specifying the maximum number of traces to sample per second. When set, this automatically enables the rate-limited sampler. Alternatively, you can configure sampling using the `OTEL_TRACES_SAMPLER` and `OTEL_TRACES_SAMPLER_ARG` environment variables as described in the table below. Please note that the sampling configuration via environment variables will have precedence over the sampling exporter/distro options. | `N/A`
7274

7375
You can configure further with [OpenTelemetry environment variables][ot_env_vars].
@@ -231,6 +233,7 @@ contact [[email protected]](mailto:[email protected]) with any additio
231233
[ot_sdk_python]: https://github.com/open-telemetry/opentelemetry-python
232234
[ot_sdk_python_metric_reader]: https://opentelemetry-python.readthedocs.io/en/latest/sdk/metrics.export.html#opentelemetry.sdk.metrics.export.MetricReader
233235
[ot_span_processor]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#span-processor
236+
[ot_log_record_processor]: https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/logs/sdk.md#log-record-processor
234237
[ot_view]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view
235238
[ot_sdk_python_view_examples]: https://github.com/open-telemetry/opentelemetry-python/tree/main/docs/examples/metrics/views
236239
[ot_instrumentation_django]: https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-django

sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
)
1313
from opentelemetry.metrics import set_meter_provider
1414
from opentelemetry.sdk.metrics import MeterProvider
15-
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
15+
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader, MetricReader
1616
from opentelemetry.sdk.metrics.view import View
1717
from opentelemetry.sdk.resources import Resource
1818
from opentelemetry.sdk.trace import TracerProvider
@@ -38,6 +38,8 @@
3838
SAMPLING_RATIO_ARG,
3939
SAMPLING_TRACES_PER_SECOND_ARG,
4040
SPAN_PROCESSORS_ARG,
41+
LOG_RECORD_PROCESSORS_ARG,
42+
METRIC_READERS_ARG,
4143
VIEWS_ARG,
4244
ENABLE_TRACE_BASED_SAMPLING_ARG,
4345
)
@@ -102,6 +104,10 @@ def configure_azure_monitor(**kwargs) -> None: # pylint: disable=C4758
102104
Attributes take priority over default attributes and those from Resource Detectors.
103105
:keyword list[~opentelemetry.sdk.trace.SpanProcessor] span_processors: List of `SpanProcessor` objects
104106
to process every span prior to exporting. Will be run sequentially.
107+
:keyword list[~opentelemetry.sdk._logs.LogRecordProcessor] log_record_processors: List of `LogRecordProcessor`
108+
objects to process every log record prior to exporting. Will be run sequentially.
109+
:keyword list[~opentelemetry.sdk.metrics.MetricReader] metric_readers: List of MetricReader objects to read and
110+
export metrics. Each reader can have its own exporter and collection interval.
105111
:keyword bool enable_live_metrics: Boolean value to determine whether to enable live metrics feature.
106112
Defaults to `False`.
107113
:keyword bool enable_performance_counters: Boolean value to determine whether to enable performance counters.
@@ -212,6 +218,8 @@ def _setup_logging(configurations: Dict[str, ConfigurationValue]):
212218
enable_performance_counters_config = configurations[ENABLE_PERFORMANCE_COUNTERS_ARG]
213219
logger_provider = LoggerProvider(resource=resource)
214220
enable_trace_based_sampling_for_logs = configurations[ENABLE_TRACE_BASED_SAMPLING_ARG]
221+
for custom_log_record_processor in configurations[LOG_RECORD_PROCESSORS_ARG]: # type: ignore
222+
logger_provider.add_log_record_processor(custom_log_record_processor) # type: ignore
215223
if configurations.get(ENABLE_LIVE_METRICS_ARG):
216224
qlp = _QuickpulseLogRecordProcessor()
217225
logger_provider.add_log_record_processor(qlp)
@@ -270,11 +278,12 @@ def _setup_logging(configurations: Dict[str, ConfigurationValue]):
270278
def _setup_metrics(configurations: Dict[str, ConfigurationValue]):
271279
resource: Resource = configurations[RESOURCE_ARG] # type: ignore
272280
views: List[View] = configurations[VIEWS_ARG] # type: ignore
281+
readers: list[MetricReader] = configurations[METRIC_READERS_ARG] # type: ignore
273282
enable_performance_counters_config = configurations[ENABLE_PERFORMANCE_COUNTERS_ARG]
274283
metric_exporter = AzureMonitorMetricExporter(**configurations)
275-
reader = PeriodicExportingMetricReader(metric_exporter)
284+
readers.append(PeriodicExportingMetricReader(metric_exporter))
276285
meter_provider = MeterProvider(
277-
metric_readers=[reader],
286+
metric_readers=readers,
278287
resource=resource,
279288
views=views,
280289
)

sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
RESOURCE_ARG = "resource"
2525
SAMPLING_RATIO_ARG = "sampling_ratio"
2626
SPAN_PROCESSORS_ARG = "span_processors"
27+
LOG_RECORD_PROCESSORS_ARG = "log_record_processors"
28+
METRIC_READERS_ARG = "metric_readers"
2729
VIEWS_ARG = "views"
2830
RATE_LIMITED_SAMPLER = "microsoft.rate_limited"
2931
FIXED_PERCENTAGE_SAMPLER = "microsoft.fixed.percentage"

sdk/monitor/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_utils/configurations.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
SAMPLING_RATIO_ARG,
4444
SAMPLING_TRACES_PER_SECOND_ARG,
4545
SPAN_PROCESSORS_ARG,
46+
LOG_RECORD_PROCESSORS_ARG,
47+
METRIC_READERS_ARG,
4648
VIEWS_ARG,
4749
RATE_LIMITED_SAMPLER,
4850
FIXED_PERCENTAGE_SAMPLER,
@@ -78,6 +80,8 @@ def _get_configurations(**kwargs) -> Dict[str, ConfigurationValue]:
7880
_default_sampling_ratio(configurations)
7981
_default_instrumentation_options(configurations)
8082
_default_span_processors(configurations)
83+
_default_log_record_processors(configurations)
84+
_default_metric_readers(configurations)
8185
_default_enable_live_metrics(configurations)
8286
_default_enable_performance_counters(configurations)
8387
_default_views(configurations)
@@ -225,6 +229,14 @@ def _default_span_processors(configurations):
225229
configurations.setdefault(SPAN_PROCESSORS_ARG, [])
226230

227231

232+
def _default_log_record_processors(configurations):
233+
configurations.setdefault(LOG_RECORD_PROCESSORS_ARG, [])
234+
235+
236+
def _default_metric_readers(configurations):
237+
configurations.setdefault(METRIC_READERS_ARG, [])
238+
239+
228240
def _default_enable_live_metrics(configurations):
229241
configurations.setdefault(ENABLE_LIVE_METRICS_ARG, False)
230242

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# -------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License in the project root for
4+
# license information.
5+
# --------------------------------------------------------------------------
6+
7+
import logging
8+
from logging import getLogger
9+
from azure.monitor.opentelemetry import configure_azure_monitor
10+
from azure.monitor.opentelemetry.exporter._generated.models import ContextTagKeys
11+
from opentelemetry import trace
12+
from opentelemetry.sdk._logs import LogRecordProcessor, ReadableLogRecord
13+
14+
logger = getLogger(__name__)
15+
logger.setLevel(logging.INFO)
16+
17+
18+
class LogRecordEnrichingProcessor(LogRecordProcessor):
19+
"""Enriches log records with operation name from the current span context."""
20+
21+
def on_emit(self, readable_log_record: ReadableLogRecord) -> None:
22+
current_span = trace.get_current_span()
23+
if current_span and getattr(current_span, "name", None):
24+
if readable_log_record.log_record.attributes is None:
25+
readable_log_record.log_record.attributes = {}
26+
readable_log_record.log_record.attributes[ContextTagKeys.AI_OPERATION_NAME] = current_span.name
27+
28+
def shutdown(self) -> None:
29+
pass
30+
31+
def force_flush(self, timeout_millis: int = 30000) -> bool:
32+
return True
33+
34+
35+
# Create the log record enriching processor
36+
log_enriching_processor = LogRecordEnrichingProcessor()
37+
38+
# Configure Azure Monitor with the custom log record processor
39+
configure_azure_monitor(log_record_processors=[log_enriching_processor])
40+
41+
tracer = trace.get_tracer(__name__)
42+
43+
with tracer.start_as_current_span("span-name-here"):
44+
logger.info("This log will be enriched with operation name")
45+
46+
input()
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# -------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License in the project root for
4+
# license information.
5+
# --------------------------------------------------------------------------
6+
7+
from time import sleep
8+
9+
from azure.monitor.opentelemetry import configure_azure_monitor
10+
from opentelemetry import metrics
11+
from opentelemetry.sdk.metrics.export import (
12+
MetricExportResult,
13+
MetricExporter,
14+
MetricsData,
15+
PeriodicExportingMetricReader,
16+
)
17+
18+
19+
class PrintMetricExporter(MetricExporter):
20+
"""Minimal exporter that prints metric data."""
21+
22+
def export(self, metrics_data: MetricsData, **kwargs) -> MetricExportResult: # type: ignore[override]
23+
# In a real exporter, send metrics_data to your backend
24+
print(f"exported metrics: {metrics_data}")
25+
return MetricExportResult.SUCCESS
26+
27+
def shutdown(self, timeout_millis: float = 30000, **kwargs) -> None: # type: ignore[override]
28+
return None
29+
30+
def force_flush(self, timeout_millis: float = 30000, **kwargs) -> bool: # type: ignore[override]
31+
return True
32+
33+
34+
# Add a custom reader; the SDK will append its own Azure Monitor reader
35+
custom_reader = PeriodicExportingMetricReader(
36+
PrintMetricExporter(),
37+
export_interval_millis=5000,
38+
)
39+
40+
configure_azure_monitor(
41+
enable_performance_counters=False,
42+
metric_readers=[custom_reader],
43+
)
44+
45+
meter = metrics.get_meter_provider().get_meter("metric-readers-sample")
46+
counter = meter.create_counter("example.counter")
47+
48+
for _ in range(3):
49+
counter.add(1)
50+
sleep(1)

sdk/monitor/azure-monitor-opentelemetry/tests/test_configure.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,7 @@ def test_setup_logging(self, get_logger_mock, pclp_mock):
536536
logging_handler_mock.return_value = logging_handler_init_mock
537537
logger_mock = Mock()
538538
logger_mock.handlers = []
539+
custom_lrp = Mock()
539540
get_logger_mock.return_value = logger_mock
540541
formatter_init_mock = Mock()
541542
elp_init_mock = Mock()
@@ -547,6 +548,7 @@ def test_setup_logging(self, get_logger_mock, pclp_mock):
547548
"enable_performance_counters": True,
548549
"logger_name": "test",
549550
"resource": TEST_RESOURCE,
551+
"log_record_processors": [custom_lrp],
550552
"logging_formatter": formatter_init_mock,
551553
"enable_trace_based_sampling_for_logs": False,
552554
}
@@ -572,7 +574,11 @@ def test_setup_logging(self, get_logger_mock, pclp_mock):
572574
set_logger_provider_mock.assert_called_once_with(lp_init_mock)
573575
log_exporter_mock.assert_called_once_with(**configurations)
574576
blrp_mock.assert_called_once_with(log_exp_init_mock, {"enable_trace_based_sampling_for_logs": False})
575-
self.assertEqual(lp_init_mock.add_log_record_processor.call_count, 2)
577+
self.assertEqual(lp_init_mock.add_log_record_processor.call_count, 3)
578+
lp_init_mock.add_log_record_processor.assert_has_calls(
579+
[call(custom_lrp), call(pclp_init_mock), call(blrp_init_mock)]
580+
)
581+
self.assertEqual(lp_init_mock.add_log_record_processor.call_count, 3)
576582
lp_init_mock.add_log_record_processor.assert_has_calls([call(pclp_init_mock), call(blrp_init_mock)])
577583
logging_handler_mock.assert_called_once_with(logger_provider=lp_init_mock)
578584
logging_handler_init_mock.setFormatter.assert_called_once_with(formatter_init_mock)
@@ -620,6 +626,7 @@ def test_setup_logging_duplicate_logger(self, get_logger_mock, instance_mock, pc
620626
"enable_performance_counters": True,
621627
"logger_name": "test",
622628
"resource": TEST_RESOURCE,
629+
"log_record_processors": [],
623630
"logging_formatter": None,
624631
"enable_trace_based_sampling_for_logs": True,
625632
}
@@ -686,6 +693,7 @@ def test_setup_logging_disable_performance_counters(self, get_logger_mock, pclp_
686693
"enable_performance_counters": False,
687694
"logger_name": "test",
688695
"resource": TEST_RESOURCE,
696+
"log_record_processors": [],
689697
"logging_formatter": formatter_init_mock,
690698
"enable_trace_based_sampling_for_logs": False,
691699
}
@@ -745,15 +753,20 @@ def test_setup_metrics(
745753
reader_init_mock = Mock()
746754
reader_mock.return_value = reader_init_mock
747755

756+
# Custom metric readers provided by user
757+
custom_reader_1 = Mock()
758+
custom_reader_2 = Mock()
759+
748760
configurations = {
749761
"connection_string": "test_cs",
750762
"enable_performance_counters": True,
751763
"resource": TEST_RESOURCE,
764+
"metric_readers": [custom_reader_1, custom_reader_2],
752765
"views": [],
753766
}
754767
_setup_metrics(configurations)
755768
mp_mock.assert_called_once_with(
756-
metric_readers=[reader_init_mock],
769+
metric_readers=[custom_reader_1, custom_reader_2, reader_init_mock],
757770
resource=TEST_RESOURCE,
758771
views=[],
759772
)
@@ -793,6 +806,7 @@ def test_setup_metrics_views(
793806
"connection_string": "test_cs",
794807
"enable_performance_counters": False,
795808
"resource": TEST_RESOURCE,
809+
"metric_readers": [],
796810
"views": [view_mock],
797811
}
798812
_setup_metrics(configurations)
@@ -836,6 +850,7 @@ def test_setup_metrics_perf_counters_disabled(
836850
"connection_string": "test_cs",
837851
"enable_performance_counters": False,
838852
"resource": TEST_RESOURCE,
853+
"metric_readers": [],
839854
"views": [],
840855
}
841856
_setup_metrics(configurations)

sdk/monitor/azure-monitor-opentelemetry/tests/utils/test_configurations.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
RATE_LIMITED_SAMPLER,
3434
FIXED_PERCENTAGE_SAMPLER,
3535
ENABLE_TRACE_BASED_SAMPLING_ARG,
36+
METRIC_READERS_ARG,
3637
)
3738
from opentelemetry.environment_variables import (
3839
OTEL_LOGS_EXPORTER,
@@ -76,6 +77,8 @@ def test_get_configurations(self, resource_create_mock):
7677
views=["test_view"],
7778
logger_name="test_logger",
7879
span_processors=["test_processor"],
80+
log_record_processors=["test_log_record_processor"],
81+
metric_readers=["test_metric_reader"],
7982
enable_trace_based_sampling_for_logs=True,
8083
)
8184

@@ -110,6 +113,8 @@ def test_get_configurations(self, resource_create_mock):
110113
self.assertEqual(configurations["views"], ["test_view"])
111114
self.assertEqual(configurations["logger_name"], "test_logger")
112115
self.assertEqual(configurations["span_processors"], ["test_processor"])
116+
self.assertEqual(configurations["log_record_processors"], ["test_log_record_processor"])
117+
self.assertEqual(configurations[METRIC_READERS_ARG], ["test_metric_reader"])
113118
self.assertEqual(configurations[ENABLE_TRACE_BASED_SAMPLING_ARG], True)
114119

115120
@patch.dict("os.environ", {}, clear=True)
@@ -144,6 +149,8 @@ def test_get_configurations_defaults(self, resource_create_mock):
144149
self.assertEqual(configurations["enable_performance_counters"], True)
145150
self.assertEqual(configurations["logger_name"], "")
146151
self.assertEqual(configurations["span_processors"], [])
152+
self.assertEqual(configurations["log_record_processors"], [])
153+
self.assertEqual(configurations["metric_readers"], [])
147154
self.assertEqual(configurations["views"], [])
148155
self.assertEqual(configurations[ENABLE_TRACE_BASED_SAMPLING_ARG], False)
149156

0 commit comments

Comments
 (0)