From d6abf531d972c9005865b57b3b4b5ab31c15df40 Mon Sep 17 00:00:00 2001 From: Derwin Bell Date: Sat, 26 Sep 2026 22:33:01 -0400 Subject: [PATCH 1/3] Implement Logs Enabled filtering Add minimum-severity and trace-based filtering to Logger.enabled() and wire both options through declarative configuration. --- .changelog/5525.added | 1 + .../configuration/_logger_provider.py | 26 +++---- .../tests/test_logger_provider.py | 38 +++------- .../sdk/_logs/_internal/__init__.py | 13 ++++ opentelemetry-sdk/tests/logs/test_logs.py | 70 +++++++++++++++++++ 5 files changed, 106 insertions(+), 42 deletions(-) create mode 100644 .changelog/5525.added diff --git a/.changelog/5525.added b/.changelog/5525.added new file mode 100644 index 00000000000..abb4a6668df --- /dev/null +++ b/.changelog/5525.added @@ -0,0 +1 @@ +`opentelemetry-sdk`: make `Logger.enabled()` respect the configured minimum severity and trace-based filtering diff --git a/opentelemetry-configuration/src/opentelemetry/configuration/_logger_provider.py b/opentelemetry-configuration/src/opentelemetry/configuration/_logger_provider.py index 3adeacf9a04..89ab70ec99c 100644 --- a/opentelemetry-configuration/src/opentelemetry/configuration/_logger_provider.py +++ b/opentelemetry-configuration/src/opentelemetry/configuration/_logger_provider.py @@ -5,6 +5,7 @@ import logging +from opentelemetry._logs import SeverityNumber as SDKSeverityNumber from opentelemetry._logs import set_logger_provider from opentelemetry.configuration._common import ( _map_compression, @@ -266,24 +267,19 @@ def _create_log_record_processor( def _to_logger_config(config: LoggerConfigConfig | None) -> _LoggerConfig: """Map an experimental per-logger config to an SDK ``_LoggerConfig``. - Only ``enabled`` is honored. ``minimum_severity`` and ``trace_based`` are - accepted by the config schema but not supported by the Python SDK - ``_LoggerConfig``; the ones that are set are ignored with a warning naming - them. An absent ``enabled`` leaves the logger enabled. + Absent fields use the defaults defined by the Logs SDK specification. """ if config is None: return _LoggerConfig.default() - unsupported_fields = [ - field_name for field_name in ("minimum_severity", "trace_based") if getattr(config, field_name) is not None - ] - if unsupported_fields: - _logger.warning( - "Ignoring logger_configurator fields that are not supported by the Python SDK LoggerProvider: %s", - ", ".join(unsupported_fields), - ) - if config.enabled is None: - return _LoggerConfig.default() - return _LoggerConfig(is_enabled=config.enabled) + return _LoggerConfig( + is_enabled=config.enabled if config.enabled is not None else True, + minimum_severity=( + SDKSeverityNumber[config.minimum_severity.name.upper()] + if config.minimum_severity is not None + else SDKSeverityNumber.UNSPECIFIED + ), + trace_based=config.trace_based if config.trace_based is not None else False, + ) def _create_logger_configurator( diff --git a/opentelemetry-configuration/tests/test_logger_provider.py b/opentelemetry-configuration/tests/test_logger_provider.py index 2446a8f3e4e..e4f06bbf29d 100644 --- a/opentelemetry-configuration/tests/test_logger_provider.py +++ b/opentelemetry-configuration/tests/test_logger_provider.py @@ -10,6 +10,7 @@ from types import SimpleNamespace from unittest.mock import MagicMock, patch +from opentelemetry._logs import SeverityNumber as SDKSeverityNumber from opentelemetry._logs import get_logger_provider from opentelemetry.configuration._logger_provider import ( _DEFAULT_EXPORT_TIMEOUT_MILLIS, @@ -687,7 +688,7 @@ def test_absent_enabled_defaults_to_enabled(self): provider = create_logger_provider(config) self.assertTrue(self._enabled(provider, "any.scope")) - def test_unsupported_minimum_severity_logs_warning(self): + def test_minimum_severity_is_applied(self): config = LoggerProviderConfig( processors=[], logger_configurator_development=LoggerConfiguratorConfig( @@ -697,21 +698,14 @@ def test_unsupported_minimum_severity_logs_warning(self): ), ), ) - with self.assertLogs( - "opentelemetry.configuration._logger_provider", - level="WARNING", - ) as cm: - create_logger_provider(config) - self.assertTrue( - any("minimum_severity" in msg for msg in cm.output), - "Expected warning about unsupported minimum_severity", - ) - self.assertFalse( - any("trace_based" in msg for msg in cm.output), - "Warning must not name trace_based when it is not set", + provider = create_logger_provider(config) + + self.assertEqual( + provider._apply_logger_configurator(InstrumentationScope("any.scope")).minimum_severity, + SDKSeverityNumber.WARN, ) - def test_unsupported_trace_based_logs_warning(self): + def test_trace_based_is_applied(self): config = LoggerProviderConfig( processors=[], logger_configurator_development=LoggerConfiguratorConfig( @@ -721,19 +715,9 @@ def test_unsupported_trace_based_logs_warning(self): ), ), ) - with self.assertLogs( - "opentelemetry.configuration._logger_provider", - level="WARNING", - ) as cm: - create_logger_provider(config) - self.assertTrue( - any("trace_based" in msg for msg in cm.output), - "Expected warning about unsupported trace_based", - ) - self.assertFalse( - any("minimum_severity" in msg for msg in cm.output), - "Warning must not name minimum_severity when it is not set", - ) + provider = create_logger_provider(config) + + self.assertTrue(provider._apply_logger_configurator(InstrumentationScope("any.scope")).trace_based) if __name__ == "__main__": diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 5b543d01e44..dbc889d30e1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -78,6 +78,7 @@ from opentelemetry.trace import ( format_span_id, format_trace_id, + get_current_span, ) from opentelemetry.util.types import AnyValue, Attributes @@ -728,6 +729,8 @@ def flush(self) -> None: @dataclass class _LoggerConfig: is_enabled: bool = True + minimum_severity: SeverityNumber = SeverityNumber.UNSPECIFIED + trace_based: bool = False @classmethod def default(cls) -> _LoggerConfig: @@ -770,6 +773,16 @@ def enabled( ) -> bool: if not self._is_enabled(): return False + if ( + severity_number is not None + and severity_number is not SeverityNumber.UNSPECIFIED + and severity_number.value < self._logger_config.minimum_severity.value + ): + return False + if self._logger_config.trace_based: + span_context = get_current_span(context).get_span_context() + if span_context.is_valid and not span_context.trace_flags.sampled: + return False return self._multi_log_record_processor.enabled( context=context, instrumentation_scope=self._instrumentation_scope, diff --git a/opentelemetry-sdk/tests/logs/test_logs.py b/opentelemetry-sdk/tests/logs/test_logs.py index 38e38375a58..3e033e7e64d 100644 --- a/opentelemetry-sdk/tests/logs/test_logs.py +++ b/opentelemetry-sdk/tests/logs/test_logs.py @@ -38,6 +38,12 @@ _scope_name_matches_glob, ) from opentelemetry.semconv.attributes import exception_attributes +from opentelemetry.trace import ( + NonRecordingSpan, + SpanContext, + TraceFlags, + set_span_in_context, +) class TestLoggerProvider(unittest.TestCase): @@ -503,6 +509,70 @@ def test_enabled_disabled_logger_returns_false(self): logger = provider.get_logger("test") self.assertFalse(logger.enabled()) + def test_enabled_below_minimum_severity_returns_false(self): + logger, processor = self._get_logger() + logger._set_logger_config(_LoggerConfig(minimum_severity=SeverityNumber.WARN)) + + self.assertFalse(logger.enabled(severity_number=SeverityNumber.INFO4)) + processor.enabled.assert_not_called() + + def test_enabled_at_or_above_minimum_severity_returns_true(self): + logger, processor = self._get_logger() + logger._set_logger_config(_LoggerConfig(minimum_severity=SeverityNumber.WARN)) + processor.enabled.return_value = True + + self.assertTrue(logger.enabled(severity_number=SeverityNumber.WARN)) + self.assertTrue(logger.enabled(severity_number=SeverityNumber.ERROR)) + + def test_enabled_unspecified_severity_bypasses_minimum_severity(self): + logger, processor = self._get_logger() + logger._set_logger_config(_LoggerConfig(minimum_severity=SeverityNumber.WARN)) + processor.enabled.return_value = True + + self.assertTrue(logger.enabled()) + self.assertTrue(logger.enabled(severity_number=SeverityNumber.UNSPECIFIED)) + + def test_enabled_trace_based_unsampled_trace_returns_false(self): + logger, processor = self._get_logger() + logger._set_logger_config(_LoggerConfig(trace_based=True)) + context = set_span_in_context( + NonRecordingSpan( + SpanContext( + trace_id=1, + span_id=1, + is_remote=False, + trace_flags=TraceFlags.get_default(), + ) + ) + ) + + self.assertFalse(logger.enabled(context=context)) + processor.enabled.assert_not_called() + + def test_enabled_trace_based_sampled_trace_returns_true(self): + logger, processor = self._get_logger() + logger._set_logger_config(_LoggerConfig(trace_based=True)) + processor.enabled.return_value = True + context = set_span_in_context( + NonRecordingSpan( + SpanContext( + trace_id=1, + span_id=1, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ) + ) + ) + + self.assertTrue(logger.enabled(context=context)) + + def test_enabled_trace_based_without_trace_returns_true(self): + logger, processor = self._get_logger() + logger._set_logger_config(_LoggerConfig(trace_based=True)) + processor.enabled.return_value = True + + self.assertTrue(logger.enabled(context=get_current())) + def test_enabled_passes_args_to_processor(self): # pylint: disable=no-self-use provider = LoggerProvider() processor_mock = Mock() From 33295bbe08d6baa1a6f4ab2706f988957ce0e03f Mon Sep 17 00:00:00 2001 From: Derwin Bell Date: Thu, 1 Oct 2026 20:15:43 -0400 Subject: [PATCH 2/3] Rename changelog fragment for PR --- .changelog/{5525.added => 5724.added} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changelog/{5525.added => 5724.added} (100%) diff --git a/.changelog/5525.added b/.changelog/5724.added similarity index 100% rename from .changelog/5525.added rename to .changelog/5724.added From 4642b1f1432d90f4539b39db47ddccf99e697583 Mon Sep 17 00:00:00 2001 From: Derwin Bell Date: Tue, 20 Oct 2026 08:50:45 -0400 Subject: [PATCH 3/3] Limit Logs Enabled filtering to SDK scope Restore warnings for unsupported declarative minimum_severity and trace_based configuration until emit-time filtering is implemented. --- .../configuration/_logger_provider.py | 26 +++++++------ .../tests/test_logger_provider.py | 38 +++++++++++++------ 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/opentelemetry-configuration/src/opentelemetry/configuration/_logger_provider.py b/opentelemetry-configuration/src/opentelemetry/configuration/_logger_provider.py index 89ab70ec99c..3adeacf9a04 100644 --- a/opentelemetry-configuration/src/opentelemetry/configuration/_logger_provider.py +++ b/opentelemetry-configuration/src/opentelemetry/configuration/_logger_provider.py @@ -5,7 +5,6 @@ import logging -from opentelemetry._logs import SeverityNumber as SDKSeverityNumber from opentelemetry._logs import set_logger_provider from opentelemetry.configuration._common import ( _map_compression, @@ -267,19 +266,24 @@ def _create_log_record_processor( def _to_logger_config(config: LoggerConfigConfig | None) -> _LoggerConfig: """Map an experimental per-logger config to an SDK ``_LoggerConfig``. - Absent fields use the defaults defined by the Logs SDK specification. + Only ``enabled`` is honored. ``minimum_severity`` and ``trace_based`` are + accepted by the config schema but not supported by the Python SDK + ``_LoggerConfig``; the ones that are set are ignored with a warning naming + them. An absent ``enabled`` leaves the logger enabled. """ if config is None: return _LoggerConfig.default() - return _LoggerConfig( - is_enabled=config.enabled if config.enabled is not None else True, - minimum_severity=( - SDKSeverityNumber[config.minimum_severity.name.upper()] - if config.minimum_severity is not None - else SDKSeverityNumber.UNSPECIFIED - ), - trace_based=config.trace_based if config.trace_based is not None else False, - ) + unsupported_fields = [ + field_name for field_name in ("minimum_severity", "trace_based") if getattr(config, field_name) is not None + ] + if unsupported_fields: + _logger.warning( + "Ignoring logger_configurator fields that are not supported by the Python SDK LoggerProvider: %s", + ", ".join(unsupported_fields), + ) + if config.enabled is None: + return _LoggerConfig.default() + return _LoggerConfig(is_enabled=config.enabled) def _create_logger_configurator( diff --git a/opentelemetry-configuration/tests/test_logger_provider.py b/opentelemetry-configuration/tests/test_logger_provider.py index e4f06bbf29d..2446a8f3e4e 100644 --- a/opentelemetry-configuration/tests/test_logger_provider.py +++ b/opentelemetry-configuration/tests/test_logger_provider.py @@ -10,7 +10,6 @@ from types import SimpleNamespace from unittest.mock import MagicMock, patch -from opentelemetry._logs import SeverityNumber as SDKSeverityNumber from opentelemetry._logs import get_logger_provider from opentelemetry.configuration._logger_provider import ( _DEFAULT_EXPORT_TIMEOUT_MILLIS, @@ -688,7 +687,7 @@ def test_absent_enabled_defaults_to_enabled(self): provider = create_logger_provider(config) self.assertTrue(self._enabled(provider, "any.scope")) - def test_minimum_severity_is_applied(self): + def test_unsupported_minimum_severity_logs_warning(self): config = LoggerProviderConfig( processors=[], logger_configurator_development=LoggerConfiguratorConfig( @@ -698,14 +697,21 @@ def test_minimum_severity_is_applied(self): ), ), ) - provider = create_logger_provider(config) - - self.assertEqual( - provider._apply_logger_configurator(InstrumentationScope("any.scope")).minimum_severity, - SDKSeverityNumber.WARN, + with self.assertLogs( + "opentelemetry.configuration._logger_provider", + level="WARNING", + ) as cm: + create_logger_provider(config) + self.assertTrue( + any("minimum_severity" in msg for msg in cm.output), + "Expected warning about unsupported minimum_severity", + ) + self.assertFalse( + any("trace_based" in msg for msg in cm.output), + "Warning must not name trace_based when it is not set", ) - def test_trace_based_is_applied(self): + def test_unsupported_trace_based_logs_warning(self): config = LoggerProviderConfig( processors=[], logger_configurator_development=LoggerConfiguratorConfig( @@ -715,9 +721,19 @@ def test_trace_based_is_applied(self): ), ), ) - provider = create_logger_provider(config) - - self.assertTrue(provider._apply_logger_configurator(InstrumentationScope("any.scope")).trace_based) + with self.assertLogs( + "opentelemetry.configuration._logger_provider", + level="WARNING", + ) as cm: + create_logger_provider(config) + self.assertTrue( + any("trace_based" in msg for msg in cm.output), + "Expected warning about unsupported trace_based", + ) + self.assertFalse( + any("minimum_severity" in msg for msg in cm.output), + "Warning must not name minimum_severity when it is not set", + ) if __name__ == "__main__":