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 2319a461c9b..e133bd757fc 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( # pylint: disable=no-self-use + 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 74b759c328c..f99fc23c27d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -347,6 +347,21 @@ def on_emit(self, log_record: ReadWriteLogRecord) -> None: on error handling expectations. """ + def enabled( # pylint: disable=no-self-use + 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""" @@ -392,6 +407,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: @@ -466,6 +502,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) @@ -687,6 +744,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 a9b171b6fd2..14fd7d3ea26 100644 --- a/opentelemetry-sdk/tests/logs/test_logs.py +++ b/opentelemetry-sdk/tests/logs/test_logs.py @@ -451,3 +451,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): # pylint: disable=no-self-use + 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())