diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a095b3adb..10d29491542 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `opentelemetry-exporter-otlp-grpc` update SDK dependency to ~1.9. ([#2442](https://github.com/open-telemetry/opentelemetry-python/pull/2442)) +- bugfix(auto-instrumentation): attach OTLPHandler to root logger + ([#2450](https://github.com/open-telemetry/opentelemetry-python/pull/2450)) ## [1.9.1-0.28b1](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.9.1-0.28b1) - 2022-01-29 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index fdd3a25a7c1..124573c2543 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -17,6 +17,7 @@ OpenTelemetry SDK Configurator for Easy Instrumentation with Distros """ +import logging from abc import ABC, abstractmethod from os import environ from typing import Dict, Optional, Sequence, Tuple, Type @@ -31,6 +32,7 @@ ) from opentelemetry.sdk._logs import ( LogEmitterProvider, + OTLPHandler, set_log_emitter_provider, ) from opentelemetry.sdk._logs.export import BatchLogProcessor, LogExporter @@ -91,7 +93,7 @@ def _init_tracing( def _init_logging( - exporters: Dict[str, Sequence[LogExporter]], + exporters: Dict[str, Type[LogExporter]], auto_instrumentation_version: Optional[str] = None, ): # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name @@ -111,6 +113,11 @@ def _init_logging( BatchLogProcessor(exporter_class(**exporter_args)) ) + log_emitter = provider.get_log_emitter(__name__) + handler = OTLPHandler(level=logging.NOTSET, log_emitter=log_emitter) + + logging.getLogger().addHandler(handler) + def _import_config_components( selected_components, entry_point_name diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 32c939df148..e0dd1e7b4b3 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -13,6 +13,7 @@ # limitations under the License. # type: ignore +import logging from os import environ from unittest import TestCase from unittest.mock import patch @@ -26,8 +27,10 @@ _get_id_generator, _import_exporters, _import_id_generator, + _init_logging, _init_tracing, ) +from opentelemetry.sdk._logs import OTLPHandler from opentelemetry.sdk._logs.export import ConsoleLogExporter from opentelemetry.sdk._metrics.export import ConsoleMetricExporter from opentelemetry.sdk.resources import SERVICE_NAME, Resource @@ -45,6 +48,45 @@ def add_span_processor(self, processor): self.processor = processor +class DummyLogEmitterProvider: + def __init__(self, resource=None): + self.resource = resource + self.processor = DummyLogProcessor(DummyOTLPLogExporter()) + + def add_log_processor(self, processor): + self.processor = processor + + def get_log_emitter(self, name): + return DummyLogEmitter(name, self.resource, self.processor) + + +class DummyLogEmitter: + def __init__(self, name, resource, processor): + self.name = name + self.resource = resource + self.processor = processor + + def emit(self, record): + self.processor.emit(record) + + def flush(self): + pass + + +class DummyLogProcessor: + def __init__(self, exporter): + self.exporter = exporter + + def emit(self, record): + self.exporter.export([record]) + + def force_flush(self, time): + pass + + def shutdown(self): + pass + + class Processor: def __init__(self, exporter): self.exporter = exporter @@ -63,10 +105,21 @@ def shutdown(self): pass -class OTLPExporter: +class OTLPSpanExporter: pass +class DummyOTLPLogExporter: + def __init__(self, *args, **kwargs): + self.export_called = False + + def export(self, batch): + self.export_called = True + + def shutdown(self): + pass + + class CustomIdGenerator(IdGenerator): def generate_span_id(self): pass @@ -133,14 +186,14 @@ def test_trace_init_default(self): {"OTEL_RESOURCE_ATTRIBUTES": "service.name=my-otlp-test-service"}, ) def test_trace_init_otlp(self): - _init_tracing({"otlp": OTLPExporter}, RandomIdGenerator) + _init_tracing({"otlp": OTLPSpanExporter}, RandomIdGenerator) self.assertEqual(self.set_provider_mock.call_count, 1) provider = self.set_provider_mock.call_args[0][0] self.assertIsInstance(provider, Provider) self.assertIsInstance(provider.id_generator, RandomIdGenerator) self.assertIsInstance(provider.processor, Processor) - self.assertIsInstance(provider.processor.exporter, OTLPExporter) + self.assertIsInstance(provider.processor.exporter, OTLPSpanExporter) self.assertIsInstance(provider.resource, Resource) self.assertEqual( provider.resource.attributes.get("service.name"), @@ -163,6 +216,68 @@ def test_trace_init_custom_id_generator(self, mock_iter_entry_points): self.assertIsInstance(provider.id_generator, CustomIdGenerator) +class TestLoggingInit(TestCase): + def setUp(self): + self.processor_patch = patch( + "opentelemetry.sdk._configuration.BatchLogProcessor", + DummyLogProcessor, + ) + self.provider_patch = patch( + "opentelemetry.sdk._configuration.LogEmitterProvider", + DummyLogEmitterProvider, + ) + self.set_provider_patch = patch( + "opentelemetry.sdk._configuration.set_log_emitter_provider" + ) + + self.processor_mock = self.processor_patch.start() + self.provider_mock = self.provider_patch.start() + self.set_provider_mock = self.set_provider_patch.start() + + def tearDown(self): + self.processor_patch.stop() + self.set_provider_patch.stop() + self.provider_patch.stop() + root_logger = logging.getLogger("root") + root_logger.handlers = [ + handler + for handler in root_logger.handlers + if not isinstance(handler, OTLPHandler) + ] + + def test_logging_init_empty(self): + _init_logging({}, "auto-version") + self.assertEqual(self.set_provider_mock.call_count, 1) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider, DummyLogEmitterProvider) + self.assertIsInstance(provider.resource, Resource) + self.assertEqual( + provider.resource.attributes.get("telemetry.auto.version"), + "auto-version", + ) + + @patch.dict( + environ, + {"OTEL_RESOURCE_ATTRIBUTES": "service.name=otlp-service"}, + ) + def test_logging_init_exporter(self): + _init_logging({"otlp": DummyOTLPLogExporter}) + self.assertEqual(self.set_provider_mock.call_count, 1) + provider = self.set_provider_mock.call_args[0][0] + self.assertIsInstance(provider, DummyLogEmitterProvider) + self.assertIsInstance(provider.resource, Resource) + self.assertEqual( + provider.resource.attributes.get("service.name"), + "otlp-service", + ) + self.assertIsInstance(provider.processor, DummyLogProcessor) + self.assertIsInstance( + provider.processor.exporter, DummyOTLPLogExporter + ) + logging.getLogger(__name__).error("hello") + self.assertTrue(provider.processor.exporter.export_called) + + class TestExporterNames(TestCase): def test_otlp_exporter_overwrite(self): for exporter in [_EXPORTER_OTLP, _EXPORTER_OTLP_PROTO_GRPC]: