From 6ccce64658fee32a73a682da18312a1dc3828334 Mon Sep 17 00:00:00 2001 From: Vivek Date: Sun, 31 May 2026 12:30:24 +0530 Subject: [PATCH 1/2] feat: configure logging instrumentation scope --- .changelog/5261.added | 1 + .../opentelemetry/_logs/_internal/__init__.py | 35 ++++++++++++++++++- .../sdk/_logs/_internal/__init__.py | 29 +++++++++++---- 3 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 .changelog/5261.added diff --git a/.changelog/5261.added b/.changelog/5261.added new file mode 100644 index 00000000000..43aa8bbf201 --- /dev/null +++ b/.changelog/5261.added @@ -0,0 +1 @@ +Added support for configuring the instrumentation scope used by the SDK logger created by `LoggingHandler`. diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index 2319a461c9b..0bebd7578a9 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -28,7 +28,7 @@ from logging import getLogger from os import environ from time import time_ns -from typing import cast, overload +from typing import Any, cast, overload from typing_extensions import deprecated @@ -227,11 +227,13 @@ def __init__( # pylint: disable=super-init-not-called version: str | None = None, schema_url: str | None = None, attributes: _ExtendedAttributes | None = None, + instrumentation_scope: Any | None = None, ): self._name = name self._version = version self._schema_url = schema_url self._attributes = attributes + self._instrumentation_scope = instrumentation_scope self._real_logger: Logger | None = None self._noop_logger = NoOpLogger(name) @@ -241,11 +243,20 @@ def _logger(self) -> Logger: return self._real_logger if _LOGGER_PROVIDER: + if self._instrumentation_scope is None: + self._real_logger = _LOGGER_PROVIDER.get_logger( + self._name, + self._version, + self._schema_url, + self._attributes, + ) + return self._real_logger self._real_logger = _LOGGER_PROVIDER.get_logger( self._name, self._version, self._schema_url, self._attributes, + self._instrumentation_scope, ) return self._real_logger return self._noop_logger @@ -313,6 +324,7 @@ def get_logger( version: str | None = None, schema_url: str | None = None, attributes: _ExtendedAttributes | None = None, + instrumentation_scope: Any | None = None, ) -> Logger: """Returns a `Logger` for use by the given instrumentation library. @@ -352,8 +364,10 @@ def get_logger( version: str | None = None, schema_url: str | None = None, attributes: _ExtendedAttributes | None = None, + instrumentation_scope: Any | None = None, ) -> Logger: """Returns a NoOpLogger.""" + _ = instrumentation_scope return NoOpLogger( name, version=version, schema_url=schema_url, attributes=attributes ) @@ -366,19 +380,29 @@ def get_logger( version: str | None = None, schema_url: str | None = None, attributes: _ExtendedAttributes | None = None, + instrumentation_scope: Any | None = None, ) -> Logger: if _LOGGER_PROVIDER: + if instrumentation_scope is None: + return _LOGGER_PROVIDER.get_logger( + name, + version=version, + schema_url=schema_url, + attributes=attributes, + ) return _LOGGER_PROVIDER.get_logger( name, version=version, schema_url=schema_url, attributes=attributes, + instrumentation_scope=instrumentation_scope, ) return ProxyLogger( name, version=version, schema_url=schema_url, attributes=attributes, + instrumentation_scope=instrumentation_scope, ) @@ -429,6 +453,7 @@ def get_logger( logger_provider: LoggerProvider | None = None, schema_url: str | None = None, attributes: _ExtendedAttributes | None = None, + instrumentation_scope: Any | None = None, ) -> Logger: """Returns a `Logger` for use within a python process. @@ -439,9 +464,17 @@ def get_logger( """ if logger_provider is None: logger_provider = get_logger_provider() + if instrumentation_scope is None: + return logger_provider.get_logger( + instrumenting_module_name, + instrumenting_library_version, + schema_url, + attributes, + ) return logger_provider.get_logger( instrumenting_module_name, instrumenting_library_version, schema_url, attributes, + instrumentation_scope, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index b6b1ce88f69..db1b7a8188e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -544,9 +544,11 @@ def __init__( self, level: int = logging.NOTSET, logger_provider: APILoggerProvider | None = None, + instrumentation_scope: InstrumentationScope | None = None, ) -> None: super().__init__(level=level) self._logger_provider = logger_provider or get_logger_provider() + self._instrumentation_scope = instrumentation_scope warnings.warn( "`LoggingHandler` in `opentelemetry-sdk` is deprecated. Use the " @@ -638,7 +640,11 @@ def emit(self, record: logging.LogRecord) -> None: The record is translated to OTel format, and then sent across the pipeline. """ - logger = get_logger(record.name, logger_provider=self._logger_provider) + logger = get_logger( + record.name, + logger_provider=self._logger_provider, + instrumentation_scope=self._instrumentation_scope, + ) if not isinstance(logger, NoOpLogger): logger.emit(self._translate(record)) @@ -856,6 +862,7 @@ def get_logger( version: str | None = None, schema_url: str | None = None, attributes: _ExtendedAttributes | None = None, + instrumentation_scope: InstrumentationScope | None = None, ) -> APILogger: if self._disabled: return NoOpLogger( @@ -864,13 +871,21 @@ def get_logger( schema_url=schema_url, attributes=attributes, ) - logger = ( - self._get_logger_cached(name, version, schema_url) - if attributes is None - else self._get_logger_no_cache( - name, version, schema_url, attributes + if instrumentation_scope is None: + logger = ( + self._get_logger_cached(name, version, schema_url) + if attributes is None + else self._get_logger_no_cache( + name, version, schema_url, attributes + ) + ) + else: + logger = self._get_logger_no_cache( + instrumentation_scope.name, + instrumentation_scope.version, + instrumentation_scope.schema_url, + instrumentation_scope.attributes, ) - ) with self._active_loggers_lock: self._active_loggers.add(logger) return logger From 54f2ebdf947a67e4def33be30f71731e4eaa1d9a Mon Sep 17 00:00:00 2001 From: Vivek Date: Sun, 31 May 2026 12:30:30 +0530 Subject: [PATCH 2/2] test: cover logging instrumentation scope --- opentelemetry-api/tests/logs/test_proxy.py | 58 +++++++++++++++++++++ opentelemetry-sdk/tests/logs/test_export.py | 31 +++++++++++ opentelemetry-sdk/tests/logs/test_logs.py | 21 ++++++++ 3 files changed, 110 insertions(+) diff --git a/opentelemetry-api/tests/logs/test_proxy.py b/opentelemetry-api/tests/logs/test_proxy.py index 71772eb5a72..21c746f2521 100644 --- a/opentelemetry-api/tests/logs/test_proxy.py +++ b/opentelemetry-api/tests/logs/test_proxy.py @@ -3,6 +3,7 @@ # pylint: disable=W0212,W0222,W0221 import unittest +from typing import cast from unittest.mock import Mock import opentelemetry._logs._internal as _logs_internal @@ -18,7 +19,36 @@ def get_logger( version: str | None = None, schema_url: str | None = None, attributes: _ExtendedAttributes | None = None, + instrumentation_scope=None, ) -> _logs.Logger: + _ = instrumentation_scope + return LoggerTest(name) + + +class OldSignatureLoggerProvider: + def get_logger( # pylint: disable=no-self-use + self, + name: str, + version: str | None = None, + schema_url: str | None = None, + attributes: _ExtendedAttributes | None = None, + ) -> _logs.Logger: + return LoggerTest(name) + + +class LoggerProviderTest(_logs.NoOpLoggerProvider): + def __init__(self): + self.instrumentation_scope = None + + def get_logger( + self, + name: str, + version: str | None = None, + schema_url: str | None = None, + attributes: _ExtendedAttributes | None = None, + instrumentation_scope=None, + ) -> _logs.Logger: + self.instrumentation_scope = instrumentation_scope return LoggerTest(name) @@ -65,6 +95,34 @@ def test_proxy_logger(self): real_logger = provider.get_logger("proxy-test") self.assertIsInstance(real_logger, LoggerTest) + def test_proxy_logger_instrumentation_scope(self): + provider = _logs.get_logger_provider() + instrumentation_scope = object() + + logger = provider.get_logger( + "proxy-test", instrumentation_scope=instrumentation_scope + ) + + logger_provider = LoggerProviderTest() + _logs.set_logger_provider(logger_provider) + + proxy_logger = cast(_logs_internal.ProxyLogger, logger) + self.assertIsInstance(proxy_logger._logger, LoggerTest) + self.assertIs( + logger_provider.instrumentation_scope, instrumentation_scope + ) + + def test_get_logger_works_with_old_signature_provider(self): + logger_provider = cast( + _logs.LoggerProvider, OldSignatureLoggerProvider() + ) + + logger = _logs.get_logger( + "proxy-test", logger_provider=logger_provider + ) + + self.assertIsInstance(logger, LoggerTest) + def test_proxy_logger_forwards_record_with_exception(self): logger = _logs_internal.ProxyLogger("proxy-test") logger._real_logger = Mock(spec=LoggerTest("proxy-test")) diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index 06af6ef03a8..52a65cc722c 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -128,6 +128,37 @@ def test_simple_log_record_processor_default_level(self): finished_logs[0].instrumentation_scope.name, "default_level" ) + def test_simple_log_record_processor_instrumentation_scope(self): + exporter = InMemoryLogRecordExporter() + logger_provider = LoggerProvider() + instrumentation_scope = InstrumentationScope( + "custom_name", + "custom_version", + "custom_schema_url", + {"custom_key": "custom_value"}, + ) + + logger_provider.add_log_record_processor( + SimpleLogRecordProcessor(exporter) + ) + + logger = logging.getLogger("custom_instrumentation_scope") + logger.propagate = False + logger.addHandler( + LoggingHandler( + logger_provider=logger_provider, + instrumentation_scope=instrumentation_scope, + ) + ) + + logger.warning("Something is wrong") + finished_logs = exporter.get_finished_logs() + + self.assertEqual(len(finished_logs), 1) + self.assertEqual( + finished_logs[0].instrumentation_scope, instrumentation_scope + ) + def test_simple_log_record_processor_custom_level(self): exporter = InMemoryLogRecordExporter() logger_provider = LoggerProvider() diff --git a/opentelemetry-sdk/tests/logs/test_logs.py b/opentelemetry-sdk/tests/logs/test_logs.py index a9b171b6fd2..f2d1fe5a1ac 100644 --- a/opentelemetry-sdk/tests/logs/test_logs.py +++ b/opentelemetry-sdk/tests/logs/test_logs.py @@ -73,6 +73,27 @@ def test_get_logger(self): logger._instrumentation_scope.attributes, {"key": "value"} ) + def test_get_logger_with_instrumentation_scope(self): + """ + `LoggerProvider.get_logger` uses an explicitly provided + `InstrumentationScope` object on the created `Logger`. + """ + instrumentation_scope = InstrumentationScope( + "instrument_name", + "instrument_version", + "instrument_schema_url", + {"instrument_key": "instrument_value"}, + ) + logger = LoggerProvider().get_logger( + "name", + version="version", + schema_url="schema_url", + attributes={"key": "value"}, + instrumentation_scope=instrumentation_scope, + ) + + self.assertEqual(logger._instrumentation_scope, instrumentation_scope) + @patch.dict("os.environ", {OTEL_SDK_DISABLED: "true"}) def test_get_logger_with_sdk_disabled(self): logger = LoggerProvider().get_logger(Mock())