diff --git a/CHANGELOG.md b/CHANGELOG.md index 461b1fdc..3844e028 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ ([#210](https://github.com/microsoft/ApplicationInsights-Python/pull/210)) - Updated setup.py, directory structure ([#214](https://github.com/microsoft/ApplicationInsights-Python/pull/214)) +- Introduce Distro API + ([#215](https://github.com/microsoft/ApplicationInsights-Python/pull/215)) ## [1.0.0b8](https://github.com/microsoft/ApplicationInsights-Python/releases/tag/v1.0.0b8) - 2022-09-26 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 e69de29b..ec52d899 100644 --- a/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/__init__.py +++ b/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/__init__.py @@ -0,0 +1,44 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- +from azure.monitor.opentelemetry.distro.util import get_configurations +from azure.monitor.opentelemetry.exporter import AzureMonitorTraceExporter +from opentelemetry import trace +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.semconv.resource import ResourceAttributes + + +def configure_opentelemetry(**kwargs): + """ + This function works as a configuration layer that allows the + end user to configure OpenTelemetry and Azure monitor components. The + configuration can be done via environment variables or + via arguments passed to this function. Each argument has a 1:1 + correspondence with an environment variable. + """ + + configurations = get_configurations(**kwargs) + connection_string = configurations["connection_string"] + service_name = configurations["service_name"] + service_namespace = configurations["service_namespace"] + service_instance_id = configurations["service_instance_id"] + disable_tracing = configurations["disable_tracing"] + + if not disable_tracing: + resource = Resource.create( + { + ResourceAttributes.SERVICE_NAME: service_name, + ResourceAttributes.SERVICE_NAMESPACE: service_namespace, + ResourceAttributes.SERVICE_INSTANCE_ID: service_instance_id, + } + ) + trace.set_tracer_provider(TracerProvider(resource=resource)) + exporter = AzureMonitorTraceExporter( + connection_string=connection_string + ) + span_processor = BatchSpanProcessor(exporter) + trace.get_tracer_provider().add_span_processor(span_processor) diff --git a/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/distro.py b/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/distro.py index e11c765e..b87f02db 100644 --- a/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/distro.py +++ b/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/distro.py @@ -3,7 +3,7 @@ # Licensed under the MIT License. See License in the project root for # license information. # -------------------------------------------------------------------------- - +import logging from os import environ from opentelemetry.environment_variables import ( @@ -12,14 +12,27 @@ ) from opentelemetry.instrumentation.distro import BaseDistro +_logger = logging.getLogger(__name__) + class AzureMonitorDistro(BaseDistro): - def _configure(self, **kwargs): - # TODO: Uncomment when logging is out of preview - # environ.setdefault(OTEL_LOGS_EXPORTER, "azure_monitor_opentelemetry_exporter") - environ.setdefault( - OTEL_METRICS_EXPORTER, "azure_monitor_opentelemetry_exporter" - ) - environ.setdefault( - OTEL_TRACES_EXPORTER, "azure_monitor_opentelemetry_exporter" - ) + def _configure(self, **kwargs) -> None: + try: + _configure_auto_instrumentation() + except Exception as ex: + _logger.exception( + ("Error occured auto-instrumenting AzureMonitorDistro") + ) + raise ex + + +def _configure_auto_instrumentation() -> None: + # TODO: support configuration via env vars + # TODO: Uncomment when logging is out of preview + # environ.setdefault(OTEL_LOGS_EXPORTER, "azure_monitor_opentelemetry_exporter") + environ.setdefault( + OTEL_METRICS_EXPORTER, "azure_monitor_opentelemetry_exporter" + ) + environ.setdefault( + OTEL_TRACES_EXPORTER, "azure_monitor_opentelemetry_exporter" + ) diff --git a/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/util/__init__.py b/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/util/__init__.py new file mode 100644 index 00000000..46de6634 --- /dev/null +++ b/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/util/__init__.py @@ -0,0 +1,26 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +from typing import Any, Dict + + +def get_configurations(**kwargs) -> Dict[str, Any]: + configurations = {} + + # In-code configurations take priority + configurations["connection_string"] = kwargs.get("connection_string", None) + configurations["disable_tracing"] = kwargs.get("disable_tracing", False) + configurations["service_name"] = kwargs.get("service_name", "") + configurations["service_namespace"] = kwargs.get("service_namespace", "") + configurations["service_instance_id"] = kwargs.get( + "service_instance_id", "" + ) + + # TODO: Support addtional env vars configurations + # if configurations.get("disable_tracing") is None: + # configurations["disable_tracing"] = False + + return configurations diff --git a/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/version.py b/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/version.py new file mode 100644 index 00000000..24ef9d09 --- /dev/null +++ b/azure-monitor-opentelemetry-distro/azure/monitor/opentelemetry/distro/version.py @@ -0,0 +1,7 @@ +# ------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License in the project root for +# license information. +# -------------------------------------------------------------------------- + +VERSION = "1.0.0b8" diff --git a/azure-monitor-opentelemetry-distro/samples/simple.py b/azure-monitor-opentelemetry-distro/samples/simple.py index 9853862d..f8d7178a 100644 --- a/azure-monitor-opentelemetry-distro/samples/simple.py +++ b/azure-monitor-opentelemetry-distro/samples/simple.py @@ -4,7 +4,14 @@ # license information. # -------------------------------------------------------------------------- -from azure.monitor.opentelemetry.distro.distro import AzureMonitorDistro +from azure.monitor.opentelemetry.distro import configure_opentelemetry +from opentelemetry import trace -distro = AzureMonitorDistro() -distro.configure() +configure_opentelemetry() + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("hello"): + print("Hello, World!") + +input() diff --git a/azure-monitor-opentelemetry-distro/tests/configuration/test_configure.py b/azure-monitor-opentelemetry-distro/tests/configuration/test_configure.py new file mode 100644 index 00000000..16173423 --- /dev/null +++ b/azure-monitor-opentelemetry-distro/tests/configuration/test_configure.py @@ -0,0 +1,106 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from unittest.mock import Mock, patch + +from azure.monitor.opentelemetry.distro import configure_opentelemetry +from opentelemetry.semconv.resource import ResourceAttributes + + +class TestConfigure(unittest.TestCase): + @patch( + "azure.monitor.opentelemetry.distro.BatchSpanProcessor", + ) + @patch( + "azure.monitor.opentelemetry.distro.AzureMonitorTraceExporter", + ) + @patch( + "azure.monitor.opentelemetry.distro.TracerProvider", + autospec=True, + ) + @patch( + "azure.monitor.opentelemetry.distro.Resource", + ) + @patch( + "azure.monitor.opentelemetry.distro.trace", + ) + def test_configure_opentelemetry( + self, + trace_mock, + resource_mock, + tp_mock, + exporter_mock, + bsp_mock, + ): + tp_init_mock = Mock() + tp_mock.return_value = tp_init_mock + exp_init_mock = Mock() + exporter_mock.return_value = exp_init_mock + resource_init_mock = Mock() + resource_mock.create.return_value = resource_init_mock + bsp_init_mock = Mock() + bsp_mock.return_value = bsp_init_mock + configure_opentelemetry( + connection_string="test_cs", + disable_tracing=False, + service_name="test_service_name", + service_namespace="test_namespace", + service_instance_id="test_id", + ) + resource_mock.create.assert_called_once_with( + { + ResourceAttributes.SERVICE_NAME: "test_service_name", + ResourceAttributes.SERVICE_NAMESPACE: "test_namespace", + ResourceAttributes.SERVICE_INSTANCE_ID: "test_id", + } + ) + tp_mock.assert_called_once_with(resource=resource_init_mock) + trace_mock.set_tracer_provider.assert_called_once_with(tp_init_mock) + exporter_mock.assert_called_once_with(connection_string="test_cs") + bsp_mock.assert_called_once_with(exp_init_mock) + + @patch( + "azure.monitor.opentelemetry.distro.BatchSpanProcessor", + ) + @patch( + "azure.monitor.opentelemetry.distro.AzureMonitorTraceExporter", + ) + @patch( + "azure.monitor.opentelemetry.distro.TracerProvider", + autospec=True, + ) + @patch( + "azure.monitor.opentelemetry.distro.Resource", + ) + @patch( + "azure.monitor.opentelemetry.distro.trace", + ) + def test_configure_opentelemetry_disable_tracing( + self, + trace_mock, + resource_mock, + tp_mock, + exporter_mock, + bsp_mock, + ): + configure_opentelemetry( + connection_string="test_cs", + disable_tracing=True, + ) + resource_mock.assert_not_called() + tp_mock.assert_not_called() + trace_mock.set_tracer_provider.assert_not_called() + exporter_mock.assert_not_called() + bsp_mock.assert_not_called() diff --git a/azure-monitor-opentelemetry-distro/tests/configuration/test_util.py b/azure-monitor-opentelemetry-distro/tests/configuration/test_util.py new file mode 100644 index 00000000..9524cd1a --- /dev/null +++ b/azure-monitor-opentelemetry-distro/tests/configuration/test_util.py @@ -0,0 +1,42 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from azure.monitor.opentelemetry.distro.util import get_configurations + + +class TestUtil(unittest.TestCase): + def test_get_configurations(self): + configurations = get_configurations( + connection_string="test_cs", + disable_tracing="test_disable", + service_name="test_service_name", + service_namespace="test_namespace", + service_instance_id="test_id", + ) + + self.assertEqual(configurations["connection_string"], "test_cs") + self.assertEqual(configurations["disable_tracing"], "test_disable") + self.assertEqual(configurations["service_name"], "test_service_name") + self.assertEqual(configurations["service_namespace"], "test_namespace") + self.assertEqual(configurations["service_instance_id"], "test_id") + + def test_get_configurations_default(self): + configurations = get_configurations() + self.assertEqual(configurations["connection_string"], None) + self.assertEqual(configurations["disable_tracing"], False) + self.assertEqual(configurations["service_name"], "") + self.assertEqual(configurations["service_namespace"], "") + self.assertEqual(configurations["service_instance_id"], "")