From 90aecf87b0c5f8c1a6624b1bc1153caa8563b79e Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 26 Jun 2026 18:53:34 -0600 Subject: [PATCH 1/4] logs: add Enabled support to Logger API, SDK, and LogRecordProcessor Implements the Enabled chain from the OpenTelemetry Logs specification: - API Logger.enabled(context, severity_number, event_name) -> bool: default True; NoOpLogger returns False; ProxyLogger delegates. - SDK LogRecordProcessor.enabled(...) -> bool: non-abstract default True so existing processors opt-in by overriding. - SDK SynchronousMultiLogRecordProcessor/ConcurrentMultiLogRecordProcessor: enabled() returns False when no processors are registered, False when all processors return False, otherwise True. - SDK Logger.enabled(): checks LoggerConfig.is_enabled first, then delegates to the multi-processor with the instrumentation scope. Closes #5360 --- .../opentelemetry/_logs/_internal/__init__.py | 39 ++++++++++ opentelemetry-api/tests/logs/test_proxy.py | 30 ++++++++ .../sdk/_logs/_internal/__init__.py | 73 +++++++++++++++++++ opentelemetry-sdk/tests/logs/test_logs.py | 59 +++++++++++++++ 4 files changed, 201 insertions(+) diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index 2319a461c9b..afbeeb68679 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -175,6 +175,23 @@ def emit( ) -> None: """Emits a :class:`LogRecord` representing a log to the processing pipeline.""" + def enabled( + self, + *, + context: Context | None = None, + severity_number: SeverityNumber | None = None, + event_name: str | None = None, + ) -> bool: + """Returns whether the logger is enabled for the given arguments. + + Instrumentation should call this before performing expensive work to + construct a log record, and skip that work if ``False`` is returned. + + The returned value may change over time and should be checked each time + before emitting a log record. + """ + return True + class NoOpLogger(Logger): """The default Logger used when no Logger implementation is available. @@ -219,6 +236,15 @@ def emit( ) -> None: pass + def enabled( + self, + *, + context: Context | None = None, + severity_number: SeverityNumber | None = None, + event_name: str | None = None, + ) -> bool: + return False + class ProxyLogger(Logger): def __init__( # pylint: disable=super-init-not-called @@ -300,6 +326,19 @@ def emit( exception=exception, ) + def enabled( + self, + *, + context: Context | None = None, + severity_number: SeverityNumber | None = None, + event_name: str | None = None, + ) -> bool: + return self._logger.enabled( + context=context, + severity_number=severity_number, + event_name=event_name, + ) + class LoggerProvider(ABC): """ diff --git a/opentelemetry-api/tests/logs/test_proxy.py b/opentelemetry-api/tests/logs/test_proxy.py index 71772eb5a72..0cf78c18645 100644 --- a/opentelemetry-api/tests/logs/test_proxy.py +++ b/opentelemetry-api/tests/logs/test_proxy.py @@ -74,3 +74,33 @@ def test_proxy_logger_forwards_record_with_exception(self): logger.emit(record) logger._real_logger.emit.assert_called_once_with(record) + + def test_proxy_logger_enabled_delegates_to_real_logger(self): + logger = _logs_internal.ProxyLogger("proxy-test") + real_logger = Mock(spec=LoggerTest("proxy-test")) + real_logger.enabled.return_value = True + logger._real_logger = real_logger + + result = logger.enabled( + severity_number=_logs.SeverityNumber.INFO, event_name="test" + ) + + self.assertTrue(result) + real_logger.enabled.assert_called_once_with( + context=None, + severity_number=_logs.SeverityNumber.INFO, + event_name="test", + ) + + def test_proxy_logger_enabled_falls_back_to_noop(self): + logger = _logs_internal.ProxyLogger("proxy-test") + self.assertFalse(logger.enabled()) + + def test_noop_logger_enabled_returns_false(self): + logger = _logs.NoOpLogger("noop-test") + self.assertFalse(logger.enabled()) + self.assertFalse( + logger.enabled( + severity_number=_logs.SeverityNumber.ERROR, event_name="e" + ) + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 5f40c5c8c70..0c00bc69a41 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -379,6 +379,21 @@ def on_emit(self, log_record: ReadWriteLogRecord) -> None: on error handling expectations. """ + def enabled( + self, + *, + context: Context | None = None, + instrumentation_scope: InstrumentationScope | None = None, + severity_number: SeverityNumber | None = None, + event_name: str | None = None, + ) -> bool: + """Returns whether this processor would emit a record for the given arguments. + + Processors that support filtering MAY override this method. The default + implementation returns ``True``. + """ + return True # pylint: disable=unused-argument + @abc.abstractmethod def shutdown(self) -> None: """Called when a :class:`opentelemetry.sdk._logs.Logger` is shutdown""" @@ -424,6 +439,27 @@ def on_emit(self, log_record: ReadWriteLogRecord) -> None: for lp in self._log_record_processors: lp.on_emit(log_record) + def enabled( + self, + *, + context: Context | None = None, + instrumentation_scope: InstrumentationScope | None = None, + severity_number: SeverityNumber | None = None, + event_name: str | None = None, + ) -> bool: + processors = self._log_record_processors + if not processors: + return False + return any( + lp.enabled( + context=context, + instrumentation_scope=instrumentation_scope, + severity_number=severity_number, + event_name=event_name, + ) + for lp in processors + ) + def shutdown(self) -> None: """Shutdown the log processors one by one""" for lp in self._log_record_processors: @@ -498,6 +534,27 @@ def _submit_and_wait( def on_emit(self, log_record: ReadWriteLogRecord) -> None: self._submit_and_wait(lambda lp: lp.on_emit, log_record) + def enabled( + self, + *, + context: Context | None = None, + instrumentation_scope: InstrumentationScope | None = None, + severity_number: SeverityNumber | None = None, + event_name: str | None = None, + ) -> bool: + processors = self._log_record_processors + if not processors: + return False + return any( + lp.enabled( + context=context, + instrumentation_scope=instrumentation_scope, + severity_number=severity_number, + event_name=event_name, + ) + for lp in processors + ) + def shutdown(self) -> None: self._submit_and_wait(lambda lp: lp.shutdown) @@ -719,6 +776,22 @@ def __init__( def _is_enabled(self) -> bool: return self._logger_config.is_enabled + def enabled( + self, + *, + context: Context | None = None, + severity_number: SeverityNumber | None = None, + event_name: str | None = None, + ) -> bool: + if not self._is_enabled(): + return False + return self._multi_log_record_processor.enabled( + context=context, + instrumentation_scope=self._instrumentation_scope, + severity_number=severity_number, + event_name=event_name, + ) + def _set_logger_config(self, logger_config: _LoggerConfig) -> None: self._logger_config = logger_config diff --git a/opentelemetry-sdk/tests/logs/test_logs.py b/opentelemetry-sdk/tests/logs/test_logs.py index 8100a684411..3bd320bccf6 100644 --- a/opentelemetry-sdk/tests/logs/test_logs.py +++ b/opentelemetry-sdk/tests/logs/test_logs.py @@ -551,3 +551,62 @@ def test_emit_readwrite_logrecord_uses_exception(self): self.assertEqual( attributes[exception_attributes.EXCEPTION_TYPE], "RuntimeError" ) + + def test_enabled_with_no_processors_returns_false(self): + provider = LoggerProvider() + logger = provider.get_logger("test") + self.assertFalse(logger.enabled()) + + def test_enabled_with_processor_returns_true(self): + provider = LoggerProvider() + provider.add_log_record_processor(Mock()) + logger = provider.get_logger("test") + self.assertTrue(logger.enabled()) + + def test_enabled_disabled_logger_returns_false(self): + provider = LoggerProvider( + _logger_configurator=_disable_logger_configurator + ) + provider.add_log_record_processor(Mock()) + logger = provider.get_logger("test") + self.assertFalse(logger.enabled()) + + def test_enabled_passes_args_to_processor(self): + provider = LoggerProvider() + processor_mock = Mock() + processor_mock.enabled.return_value = True + provider.add_log_record_processor(processor_mock) + logger = provider.get_logger("test") + + logger.enabled( + severity_number=SeverityNumber.INFO, event_name="my.event" + ) + + processor_mock.enabled.assert_called_once_with( + context=None, + instrumentation_scope=logger._instrumentation_scope, + severity_number=SeverityNumber.INFO, + event_name="my.event", + ) + + def test_enabled_all_processors_disabled_returns_false(self): + provider = LoggerProvider() + p1 = Mock() + p1.enabled.return_value = False + p2 = Mock() + p2.enabled.return_value = False + provider.add_log_record_processor(p1) + provider.add_log_record_processor(p2) + logger = provider.get_logger("test") + self.assertFalse(logger.enabled()) + + def test_enabled_one_processor_enabled_returns_true(self): + provider = LoggerProvider() + p1 = Mock() + p1.enabled.return_value = False + p2 = Mock() + p2.enabled.return_value = True + provider.add_log_record_processor(p1) + provider.add_log_record_processor(p2) + logger = provider.get_logger("test") + self.assertTrue(logger.enabled()) From f77a2512cf2164acb5caf91dec09f2fd326ee539 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 3 Jul 2026 17:52:45 -0600 Subject: [PATCH 2/4] fix(ci): resolve pylint no-self-use failures and add missing changelog fragment Add pylint disable=no-self-use annotations to the enabled() default implementations in the Logger API and LogRecordProcessor SDK base classes, matching the existing pattern used for force_flush(). Also add the missing .changelog/5380.added fragment required by the changelog CI check. --- .changelog/5380.added | 1 + opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py | 2 +- .../src/opentelemetry/sdk/_logs/_internal/__init__.py | 2 +- opentelemetry-sdk/tests/logs/test_logs.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 .changelog/5380.added diff --git a/.changelog/5380.added b/.changelog/5380.added new file mode 100644 index 00000000000..f13f195ee4e --- /dev/null +++ b/.changelog/5380.added @@ -0,0 +1 @@ +Add `enabled()` support to the Logger API, SDK, and `LogRecordProcessor` to let instrumentation skip expensive work when logging is disabled diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index afbeeb68679..e133bd757fc 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -175,7 +175,7 @@ def emit( ) -> None: """Emits a :class:`LogRecord` representing a log to the processing pipeline.""" - def enabled( + def enabled( # pylint: disable=no-self-use self, *, context: Context | None = None, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 0c00bc69a41..95d00e84124 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -379,7 +379,7 @@ def on_emit(self, log_record: ReadWriteLogRecord) -> None: on error handling expectations. """ - def enabled( + def enabled( # pylint: disable=no-self-use self, *, context: Context | None = None, diff --git a/opentelemetry-sdk/tests/logs/test_logs.py b/opentelemetry-sdk/tests/logs/test_logs.py index 3bd320bccf6..9795545a563 100644 --- a/opentelemetry-sdk/tests/logs/test_logs.py +++ b/opentelemetry-sdk/tests/logs/test_logs.py @@ -571,7 +571,7 @@ def test_enabled_disabled_logger_returns_false(self): logger = provider.get_logger("test") self.assertFalse(logger.enabled()) - def test_enabled_passes_args_to_processor(self): + def test_enabled_passes_args_to_processor(self): # pylint: disable=no-self-use provider = LoggerProvider() processor_mock = Mock() processor_mock.enabled.return_value = True From 5388e1b9e2b997863fa83e5113dba191e63568c7 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Wed, 15 Jul 2026 21:54:17 -0500 Subject: [PATCH 3/4] docs(changelog): prefix 5380 entry with affected package names --- .changelog/5380.added | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changelog/5380.added b/.changelog/5380.added index f13f195ee4e..a2380580b47 100644 --- a/.changelog/5380.added +++ b/.changelog/5380.added @@ -1 +1 @@ -Add `enabled()` support to the Logger API, SDK, and `LogRecordProcessor` to let instrumentation skip expensive work when logging is disabled +`opentelemetry-api`, `opentelemetry-sdk`: add `enabled()` support to the Logger API, SDK, and `LogRecordProcessor` to let instrumentation skip expensive work when logging is disabled From 9cd8f1485f6cc61e008bae1a0f774c063e1dbd06 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Fri, 17 Jul 2026 13:21:29 -0500 Subject: [PATCH 4/4] logs: make Logger.enabled abstract Since the logs API is not yet stable, adding enabled() as an abstract method does not break a released version. All concrete Logger subclasses (NoOpLogger, ProxyLogger, and the SDK Logger) already provide an implementation. --- .../src/opentelemetry/_logs/_internal/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index e133bd757fc..185bf6c1c32 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -175,7 +175,8 @@ def emit( ) -> None: """Emits a :class:`LogRecord` representing a log to the processing pipeline.""" - def enabled( # pylint: disable=no-self-use + @abstractmethod + def enabled( self, *, context: Context | None = None, @@ -190,7 +191,6 @@ def enabled( # pylint: disable=no-self-use The returned value may change over time and should be checked each time before emitting a log record. """ - return True class NoOpLogger(Logger):