diff --git a/CHANGELOG.md b/CHANGELOG.md index 13ffca69..fe88282c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Enable custom exporters + ([#288](https://github.com/microsoft/ApplicationInsights-Python/pull/288)) - Update samples ([#281](https://github.com/microsoft/ApplicationInsights-Python/pull/281)) diff --git a/azure-monitor-opentelemetry/README.md b/azure-monitor-opentelemetry/README.md index d82a850e..9b72cb1e 100644 --- a/azure-monitor-opentelemetry/README.md +++ b/azure-monitor-opentelemetry/README.md @@ -61,9 +61,9 @@ You can configure further with [OpenTelemetry environment variables][ot_env_vars | Environment Variable | Description | |-------------|----------------------| | [OTEL_SERVICE_NAME][opentelemetry_spec_service_name], [OTEL_RESOURCE_ATTRIBUTES][opentelemetry_spec_resource_attributes] | Specifies the OpenTelemetry [resource][opentelemetry_spec_resource] associated with your application. | -| `OTEL_LOGS_EXPORTER` | If set to `None`, disables collection and export of logging telemetry. | -| `OTEL_METRICS_EXPORTER` | If set to `None`, disables collection and export of metric telemetry. | -| `OTEL_TRACES_EXPORTER` | If set to `None`, disables collection and export of distributed tracing telemetry. | +| `OTEL_LOGS_EXPORTER` | Specifies additional logs exporters by a comma separated list of entry point names. For example, to enable the ConsoleLogExporter in addition to the Azure Monitor OpenTelemetry Exporter, set `OTEL_LOGS_EXPORTER=console`. If set to `None`, disables collection and export of logging telemetry. | +| `OTEL_METRICS_EXPORTER` | Specifies additional metrics exporters by a comma separated list of entry point names. For example, to enable the ConsoleMetricExporter in addition to the Azure Monitor OpenTelemetry Exporter, set `OTEL_METRICS_EXPORTER=console`. If set to `None`, disables collection and export of metric telemetry. | +| `OTEL_TRACES_EXPORTER` | Specifies additional traces exporters by a comma separated list of entry point names. For example, to enable the ConsoleSpanExporter in addition to the Azure Monitor OpenTelemetry Exporter, set `OTEL_TRACES_EXPORTER=console`. If set to `None`, disables collection and export of distributed tracing telemetry. | | `OTEL_BLRP_SCHEDULE_DELAY` | Specifies the logging export interval in milliseconds. Defaults to 5000. | | `OTEL_BSP_SCHEDULE_DELAY` | Specifies the distributed tracing export interval in milliseconds. Defaults to 5000. | | `OTEL_TRACES_SAMPLER_ARG` | Specifies the ratio of distributed tracing telemetry to be [sampled][application_insights_sampling]. Accepted values are in the range [0,1]. Defaults to 1.0, meaning no telemetry is sampled out. | diff --git a/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py b/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py index 47873cbc..22a961d4 100644 --- a/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py +++ b/azure-monitor-opentelemetry/azure/monitor/opentelemetry/_configure.py @@ -4,6 +4,7 @@ # license information. # -------------------------------------------------------------------------- from logging import getLogger +from os import getenv from typing import Dict from azure.monitor.opentelemetry._constants import ( @@ -22,6 +23,11 @@ ) from azure.monitor.opentelemetry.util.configurations import _get_configurations from opentelemetry._logs import get_logger_provider, set_logger_provider +from opentelemetry.environment_variables import ( + OTEL_LOGS_EXPORTER, + OTEL_METRICS_EXPORTER, + OTEL_TRACES_EXPORTER, +) from opentelemetry.instrumentation.dependencies import ( get_dist_dependency_conflicts, ) @@ -97,6 +103,11 @@ def _setup_tracing(configurations: Dict[str, ConfigurationValue]): trace_exporter, ) get_tracer_provider().add_span_processor(span_processor) + for traces_exporter in _get_extra_exporters( + "opentelemetry_traces_exporter", OTEL_TRACES_EXPORTER + ): + span_processor = BatchSpanProcessor(traces_exporter) + get_tracer_provider().add_span_processor(span_processor) def _setup_logging(configurations: Dict[str, ConfigurationValue]): @@ -110,15 +121,30 @@ def _setup_logging(configurations: Dict[str, ConfigurationValue]): schedule_delay_millis=logging_export_interval_ms, ) get_logger_provider().add_log_record_processor(log_record_processor) + for logs_exporter in _get_extra_exporters( + "opentelemetry_logs_exporter", OTEL_LOGS_EXPORTER + ): + log_record_processor = BatchLogRecordProcessor( + logs_exporter, + schedule_delay_millis=logging_export_interval_ms, + ) + get_logger_provider().add_log_record_processor(log_record_processor) handler = LoggingHandler(logger_provider=get_logger_provider()) getLogger().addHandler(handler) def _setup_metrics(configurations: Dict[str, ConfigurationValue]): - metric_exporter = AzureMonitorMetricExporter(**configurations) - reader = PeriodicExportingMetricReader(metric_exporter) + metric_readers = [ + PeriodicExportingMetricReader( + AzureMonitorMetricExporter(**configurations) + ) + ] + for metrics_exporter in _get_extra_exporters( + "opentelemetry_metrics_exporter", OTEL_METRICS_EXPORTER + ): + metric_readers.append(PeriodicExportingMetricReader(metrics_exporter)) meter_provider = MeterProvider( - metric_readers=[reader], + metric_readers=metric_readers, ) set_meter_provider(meter_provider) @@ -149,3 +175,16 @@ def _setup_instrumentations(): lib_name, exc_info=ex, ) + + +def _get_extra_exporters(entry_point_group, env_var): + exporter_entry_points = iter_entry_points(entry_point_group) + selected_exporter_names_env_var = getenv(env_var, "").lower().strip() + selected_exporter_names = selected_exporter_names_env_var.split(",") + exporters = [] + for ep in exporter_entry_points: + if ep.name == "azure_monitor_opentelemetry_exporter": + continue + if ep.name in selected_exporter_names: + exporters.append(ep.load()()) + return exporters diff --git a/azure-monitor-opentelemetry/tests/configuration/test_configure.py b/azure-monitor-opentelemetry/tests/configuration/test_configure.py index a313e634..2cca16da 100644 --- a/azure-monitor-opentelemetry/tests/configuration/test_configure.py +++ b/azure-monitor-opentelemetry/tests/configuration/test_configure.py @@ -13,10 +13,11 @@ # limitations under the License. import unittest -from unittest.mock import Mock, patch +from unittest.mock import Mock, call, patch from azure.monitor.opentelemetry._configure import ( _SUPPORTED_INSTRUMENTED_LIBRARIES, + _get_extra_exporters, _setup_instrumentations, _setup_logging, _setup_metrics, @@ -162,6 +163,9 @@ def test_configure_azure_monitor_disable_metrics( metrics_mock.assert_not_called() instrumentation_mock.assert_called_once_with() + @patch( + "azure.monitor.opentelemetry._configure._get_extra_exporters", + ) @patch( "azure.monitor.opentelemetry._configure.BatchSpanProcessor", ) @@ -189,6 +193,7 @@ def test_setup_tracing( get_tracer_provider_mock, trace_exporter_mock, bsp_mock, + extra_exporters_mock, ): sampler_init_mock = Mock() sampler_mock.return_value = sampler_init_mock @@ -199,6 +204,12 @@ def test_setup_tracing( trace_exporter_mock.return_value = trace_exp_init_mock bsp_init_mock = Mock() bsp_mock.return_value = bsp_init_mock + custom_exporter_mock1 = Mock() + custom_exporter_mock2 = Mock() + extra_exporters_mock.return_value = [ + custom_exporter_mock1, + custom_exporter_mock2, + ] configurations = { "connection_string": "test_cs", @@ -212,9 +223,20 @@ def test_setup_tracing( set_tracer_provider_mock.assert_called_once_with(tp_init_mock) get_tracer_provider_mock.assert_called() trace_exporter_mock.assert_called_once_with(**configurations) - bsp_mock.assert_called_once_with(trace_exp_init_mock) - tp_init_mock.add_span_processor.assert_called_once_with(bsp_init_mock) + bsp_mock.assert_has_calls( + [ + call(trace_exp_init_mock), + call(custom_exporter_mock1), + call(custom_exporter_mock2), + ] + ) + tp_init_mock.add_span_processor.assert_has_calls( + [call(bsp_init_mock), call(bsp_init_mock), call(bsp_init_mock)] + ) + @patch( + "azure.monitor.opentelemetry._configure._get_extra_exporters", + ) @patch( "azure.monitor.opentelemetry._configure.getLogger", ) @@ -246,6 +268,7 @@ def test_setup_logging( blrp_mock, logging_handler_mock, get_logger_mock, + extra_exporters_mock, ): lp_init_mock = Mock() lp_mock.return_value = lp_init_mock @@ -258,6 +281,12 @@ def test_setup_logging( logging_handler_mock.return_value = logging_handler_init_mock logger_mock = Mock() get_logger_mock.return_value = logger_mock + custom_exporter_mock1 = Mock() + custom_exporter_mock2 = Mock() + extra_exporters_mock.return_value = [ + custom_exporter_mock1, + custom_exporter_mock2, + ] configurations = { "connection_string": "test_cs", @@ -269,11 +298,15 @@ def test_setup_logging( set_logger_provider_mock.assert_called_once_with(lp_init_mock) get_logger_provider_mock.assert_called() log_exporter_mock.assert_called_once_with(**configurations) - blrp_mock.assert_called_once_with( - log_exp_init_mock, schedule_delay_millis=10000 + blrp_mock.assert_has_calls( + [ + call(log_exp_init_mock, schedule_delay_millis=10000), + call(custom_exporter_mock1, schedule_delay_millis=10000), + call(custom_exporter_mock2, schedule_delay_millis=10000), + ] ) - lp_init_mock.add_log_record_processor.assert_called_once_with( - blrp_init_mock + lp_init_mock.add_log_record_processor.assert_has_calls( + [call(blrp_init_mock), call(blrp_init_mock), call(blrp_init_mock)] ) logging_handler_mock.assert_called_once_with( logger_provider=lp_init_mock @@ -283,6 +316,9 @@ def test_setup_logging( logging_handler_init_mock ) + @patch( + "azure.monitor.opentelemetry._configure._get_extra_exporters", + ) @patch( "azure.monitor.opentelemetry._configure.PeriodicExportingMetricReader", ) @@ -302,6 +338,7 @@ def test_setup_metrics( set_meter_provider_mock, metric_exporter_mock, reader_mock, + extra_exporters_mock, ): mp_init_mock = Mock() mp_mock.return_value = mp_init_mock @@ -309,17 +346,33 @@ def test_setup_metrics( metric_exporter_mock.return_value = metric_exp_init_mock reader_init_mock = Mock() reader_mock.return_value = reader_init_mock + custom_exporter_mock1 = Mock() + custom_exporter_mock2 = Mock() + extra_exporters_mock.return_value = [ + custom_exporter_mock1, + custom_exporter_mock2, + ] configurations = { "connection_string": "test_cs", } _setup_metrics(configurations) mp_mock.assert_called_once_with( - metric_readers=[reader_init_mock], + metric_readers=[ + reader_init_mock, + reader_init_mock, + reader_init_mock, + ], ) set_meter_provider_mock.assert_called_once_with(mp_init_mock) metric_exporter_mock.assert_called_once_with(**configurations) - reader_mock.assert_called_once_with(metric_exp_init_mock) + reader_mock.assert_has_calls( + [ + call(metric_exp_init_mock), + call(custom_exporter_mock1), + call(custom_exporter_mock2), + ] + ) @patch( "azure.monitor.opentelemetry._configure.get_dist_dependency_conflicts" @@ -395,3 +448,32 @@ def test_setup_instrumentations_exception( ep_mock.load.assert_called_once() instrumentor_mock.instrument.assert_not_called() logger_mock.warning.assert_called_once() + + @patch.dict( + "os.environ", + { + "EXPORTER_ENV_VAR": "custom_exporter1,azure_monitor_opentelemetry_exporter,custom_exporter2" + }, + ) + @patch("azure.monitor.opentelemetry._configure.iter_entry_points") + def test_extra_exporters(self, iter_mock): + ep_mock1 = Mock() + ep_mock1.name = "custom_exporter1" + exp_mock1 = Mock() + ep_mock1.load.return_value = exp_mock1 + ep_mock_azmon = Mock() + ep_mock_azmon.name = "azure_monitor_opentelemetry_exporter" + exp_mock_azmon = Mock() + ep_mock_azmon.load.return_value = exp_mock_azmon + ep_mock2 = Mock() + ep_mock2.name = "custom_exporter2" + exp_mock2 = Mock() + ep_mock2.load.return_value = exp_mock2 + iter_mock.return_value = (ep_mock_azmon, ep_mock2, ep_mock1) + exporter_entry_point_group = "exporter_entry_point_group" + self.assertEquals( + _get_extra_exporters( + exporter_entry_point_group, "EXPORTER_ENV_VAR" + ), + [exp_mock2(), exp_mock1()], + )