diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a4f9471..64e6af0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ ([#227](https://github.com/microsoft/ApplicationInsights-Python/pull/227)) - Add metric configuration to distro api ([#232](https://github.com/microsoft/ApplicationInsights-Python/pull/232)) +- Add ability to pass custom configuration into instrumentations + ([#235](https://github.com/microsoft/ApplicationInsights-Python/pull/235)) ## [1.0.0b8](https://github.com/microsoft/ApplicationInsights-Python/releases/tag/v1.0.0b8) - 2022-09-26 diff --git a/azure-monitor-opentelemetry-distro/README.md b/azure-monitor-opentelemetry-distro/README.md index 7abc6231..8d630f9d 100644 --- a/azure-monitor-opentelemetry-distro/README.md +++ b/azure-monitor-opentelemetry-distro/README.md @@ -56,9 +56,37 @@ You can use `configure_azure_monitor` to set up instrumentation for your app to * sampling_ratio - 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. * tracing_export_interval_millis - Specifies the distributed tracing export interval in milliseconds. Defaults to 30,000. -See additional [configuration related to exporting here][exporter_configuration_docs]. +#### Exporter configurations -### Example code +You can pass exporter configuration parameters directly into `configure_azure_monitor`. See additional [configuration related to exporting here][exporter_configuration_docs]. + +```python +... +configure_azure_monitor( + connection_string="", + disable_offline_storage=True, +) +... +``` + +#### Instrumentation configurations + +You can pass in instrumentation specific configuration into `configure_azure_monitor` with the key `_config` and value as a dictionary representing `kwargs` for the corresponding instrumentation. Note the instrumented library must also be enabled through the `instrumentations` configuration. + +```python +... +configure_azure_monitor( + connection_string="", + instrumentations=["flask", "requests"], + flask_config={"excluded_urls": "http://localhost:8080/ignore"}, + requests_config={"excluded_urls": "http://example.com"}, +) +... +``` + +Take a look at the specific [instrumenation][ot_instrumentations] documentation for available configurations. + +### Samples Samples are available [here][samples] to demonstrate how to utilize the above configuration options. diff --git a/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/__init__.py b/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/__init__.py index 2bc29d82..118b8c0e 100644 --- a/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/__init__.py +++ b/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/__init__.py @@ -33,12 +33,13 @@ _logger = getLogger(__name__) -_SUPPORTED_INSTRUMENTED_LIBRARIES = { +_INSTRUMENTATION_CONFIG_SUFFIX = "_config" +_SUPPORTED_INSTRUMENTED_LIBRARIES = ( "django", "flask", "psycopg2", "requests", -} +) def configure_azure_monitor(**kwargs): @@ -141,6 +142,15 @@ def _setup_metrics(resource: Resource, configurations: Dict[str, Any]): def _setup_instrumentations(configurations: Dict[str, Any]): instrumentations = configurations.get("instrumentations", []) + instrumentation_configs = {} + + # Instrumentation specific configs + # Format is {"": {"":}} + for k, v in configurations.items(): + if k.endswith(_INSTRUMENTATION_CONFIG_SUFFIX): + lib_name = k.partition(_INSTRUMENTATION_CONFIG_SUFFIX)[0] + instrumentation_configs[lib_name] = v + for lib_name in instrumentations: if lib_name in _SUPPORTED_INSTRUMENTED_LIBRARIES: try: @@ -158,7 +168,8 @@ def _setup_instrumentations(configurations: Dict[str, Any]): lib_name.capitalize() ) class_ = getattr(module, instrumentor_name) - class_().instrument() + config = instrumentation_configs.get(lib_name, {}) + class_().instrument(**config) except ImportError: _logger.warning( "Unable to import %s. Please make sure it is installed.", diff --git a/azure-monitor-opentelemetry-distro/samples/metrics/instruments.py b/azure-monitor-opentelemetry-distro/samples/metrics/instruments.py index d9b6f1b8..33caa6b4 100644 --- a/azure-monitor-opentelemetry-distro/samples/metrics/instruments.py +++ b/azure-monitor-opentelemetry-distro/samples/metrics/instruments.py @@ -14,8 +14,6 @@ disable_tracing=True, ) -# Create a namespaced meter -meter = metrics.get_meter_provider().get_meter("sample") # Callback functions for observable instruments def observable_counter_func(options: CallbackOptions) -> Iterable[Observation]: @@ -32,6 +30,9 @@ def observable_gauge_func(options: CallbackOptions) -> Iterable[Observation]: yield Observation(9, {}) +# Create a namespaced meter +meter = metrics.get_meter_provider().get_meter("sample") + # Counter counter = meter.create_counter("counter") counter.add(1) diff --git a/azure-monitor-opentelemetry-distro/samples/tracing/client.py b/azure-monitor-opentelemetry-distro/samples/tracing/client.py index fd56a788..0a7a97a8 100644 --- a/azure-monitor-opentelemetry-distro/samples/tracing/client.py +++ b/azure-monitor-opentelemetry-distro/samples/tracing/client.py @@ -18,6 +18,7 @@ disable_logging=True, disable_metrics=True, instrumentations=["requests"], + requests_config={"excluded_urls": "http://example.com"}, tracing_export_interval_millis=15000, ) @@ -26,6 +27,8 @@ try: # Requests made using the requests library will be automatically captured response = requests.get("https://azure.microsoft.com/", timeout=5) + # This request will not be tracked due to the excluded_urls configuration + response = requests.get("http://example.com", timeout=5) logger.warning("Request sent") except Exception as ex: # If an exception occurs, this can be manually recorded on the parent span diff --git a/azure-monitor-opentelemetry-distro/samples/tracing/django/sample/example/views.py b/azure-monitor-opentelemetry-distro/samples/tracing/django/sample/example/views.py index 618e3f23..cf279665 100644 --- a/azure-monitor-opentelemetry-distro/samples/tracing/django/sample/example/views.py +++ b/azure-monitor-opentelemetry-distro/samples/tracing/django/sample/example/views.py @@ -9,7 +9,7 @@ # Configure Azure monitor collection telemetry pipeline configure_azure_monitor( - # connection_string="", + connection_string="", service_name="django_service_name", instrumentations=["django"], disable_logging=True, @@ -17,6 +17,7 @@ tracing_export_interval_millis=15000, ) + # Requests sent to the django application will be automatically captured def index(request): return HttpResponse("Hello, world.") diff --git a/azure-monitor-opentelemetry-distro/samples/tracing/server_flask.py b/azure-monitor-opentelemetry-distro/samples/tracing/server_flask.py index 6fef59b9..c1c09de3 100644 --- a/azure-monitor-opentelemetry-distro/samples/tracing/server_flask.py +++ b/azure-monitor-opentelemetry-distro/samples/tracing/server_flask.py @@ -13,11 +13,13 @@ disable_logging=True, disable_metrics=True, instrumentations=["flask"], + flask_config={"excluded_urls": "http://localhost:8080/ignore"}, tracing_export_interval_millis=15000, ) app = flask.Flask(__name__) + # Requests sent to the flask application will be automatically captured @app.route("/") def test(): @@ -30,5 +32,12 @@ def exception(): raise Exception("Hit an exception") +# Requests sent to this endpoint will not be tracked due to +# flask_config configuration +@app.route("/ignore") +def ignore(): + return "Request received but not tracked." + + if __name__ == "__main__": app.run(host="localhost", port=8080) diff --git a/azure-monitor-opentelemetry-distro/tests/configuration/test_configure.py b/azure-monitor-opentelemetry-distro/tests/configuration/test_configure.py index da7cceec..786aa8fc 100644 --- a/azure-monitor-opentelemetry-distro/tests/configuration/test_configure.py +++ b/azure-monitor-opentelemetry-distro/tests/configuration/test_configure.py @@ -505,3 +505,73 @@ def test_setup_instrumentations_failed_general( ) instrumentor_mock.assert_not_called() instrument_mock.instrument.assert_not_called() + + @patch("azure.monitor.opentelemetry.distro.getattr") + def test_setup_instrumentations_custom_configuration( + self, + getattr_mock, + ): + for lib_name in _SUPPORTED_INSTRUMENTED_LIBRARIES: + with patch("importlib.import_module") as import_module_mock: + configurations = { + "instrumentations": [lib_name], + lib_name + "_config": {"test_key": "test_value"}, + } + instrument_mock = Mock() + instrumentor_mock = Mock() + instrumentor_mock.return_value = instrument_mock + getattr_mock.return_value = instrumentor_mock + _setup_instrumentations(configurations) + self.assertEqual(import_module_mock.call_count, 2) + instr_lib_name = "opentelemetry.instrumentation." + lib_name + import_module_mock.assert_has_calls( + [call(lib_name), call(instr_lib_name)] + ) + instrumentor_mock.assert_called_once() + instrument_mock.instrument.assert_called_once_with( + **{"test_key": "test_value"} + ) + + @patch("azure.monitor.opentelemetry.distro.getattr") + def test_setup_instrumentations_custom_configuration_not_enabled( + self, + getattr_mock, + ): + with patch("importlib.import_module") as import_module_mock: + lib_name = list(_SUPPORTED_INSTRUMENTED_LIBRARIES)[0] + configurations = { + "instrumentations": [], + lib_name + "_config": {"test_key": "test_value"}, + } + instrument_mock = Mock() + instrumentor_mock = Mock() + instrumentor_mock.return_value = instrument_mock + getattr_mock.return_value = instrumentor_mock + _setup_instrumentations(configurations) + import_module_mock.assert_not_called() + instrumentor_mock.assert_not_called() + instrument_mock.instrument.assert_not_called() + + @patch("azure.monitor.opentelemetry.distro.getattr") + def test_setup_instrumentations_custom_configuration_incorrect( + self, + getattr_mock, + ): + with patch("importlib.import_module") as import_module_mock: + lib_name = list(_SUPPORTED_INSTRUMENTED_LIBRARIES)[0] + configurations = { + "instrumentations": [lib_name], + lib_name + "error_config": {"test_key": "test_value"}, + } + instrument_mock = Mock() + instrumentor_mock = Mock() + instrumentor_mock.return_value = instrument_mock + getattr_mock.return_value = instrumentor_mock + _setup_instrumentations(configurations) + self.assertEqual(import_module_mock.call_count, 2) + instr_lib_name = "opentelemetry.instrumentation." + lib_name + import_module_mock.assert_has_calls( + [call(lib_name), call(instr_lib_name)] + ) + instrumentor_mock.assert_called_once() + instrument_mock.instrument.assert_called_once_with(**{}) diff --git a/azure-monitor-opentelemetry-distro/tests/configuration/test_util.py b/azure-monitor-opentelemetry-distro/tests/configuration/test_util.py index eb8a8308..8c70dcf0 100644 --- a/azure-monitor-opentelemetry-distro/tests/configuration/test_util.py +++ b/azure-monitor-opentelemetry-distro/tests/configuration/test_util.py @@ -23,6 +23,7 @@ def test_get_configurations(self): connection_string="test_cs", disable_logging="test_disable_logging", disable_tracing="test_disable_tracing", + instrumentations=["test_instrumentation"], logging_level="test_logging_level", logger_name="test_logger_name", service_name="test_service_name", @@ -31,6 +32,7 @@ def test_get_configurations(self): sampling_ratio="test_sample_ratio", tracing_export_interval="test_tracing_interval", logging_export_interval="test_logging_interval", + views=("test_view"), ) self.assertEqual(configurations["connection_string"], "test_cs") @@ -40,6 +42,9 @@ def test_get_configurations(self): self.assertEqual( configurations["disable_tracing"], "test_disable_tracing" ) + self.assertEqual( + configurations["instrumentations"], ["test_instrumentation"] + ) self.assertEqual(configurations["logging_level"], "test_logging_level") self.assertEqual(configurations["logger_name"], "test_logger_name") self.assertEqual(configurations["service_name"], "test_service_name") @@ -52,3 +57,4 @@ def test_get_configurations(self): self.assertEqual( configurations["logging_export_interval"], "test_logging_interval" ) + self.assertEqual(configurations["views"], ("test_view"))