Repository: open-telemetry/opentelemetry-python

Issue:
Cannot set the OTLP log exporter with auto instrumentation.
Hi! I have a simple flask app that I am already instrumentation via auto instrumentation (version 0.28b1). In my setup, my app is running alongside a collector that has the otlp receiver configured for metrics, log and traces.

I would like to get my app to send the logs to the collector. For this, I am using the following command line:

```
OTEL_LOGS_EXPORTER=otlp_proto_grpc \
OTEL_PYTHON_LOG_CORRELATION=true \
OTEL_SERVICE_NAME=julien-otel-app \ 
OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317/" \
opentelemetry-instrument python server.py
```

Log correlation is working and I am seeing this in the console:

```
2022-02-08 08:56:49,120 WARNING [server] [server.py:21] [trace_id=c68de9a4f11119ddddee944c48fb317f span_id=cc531047ecdc589d resource.service.name=julien-otel-app] - request received
```

However, the collector is not picking up any logs. There is no error message in the logs. Any idea what is going on? Thanks!

[Related slack thread](https://cloud-native.slack.com/archives/C01PD4HUVBL/p1644267735647029).


Discussion:

Resolving patch:
diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py
--- 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


Test patch:
diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py
--- 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]: