From 6536338263c7fd90c1f273e689e8f810ac2561f4 Mon Sep 17 00:00:00 2001 From: Derwin Bell Date: Sat, 26 Sep 2026 22:33:01 -0400 Subject: [PATCH 1/2] 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 7f43da40af3..5c83be84adf 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, @@ -239,24 +240,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 098c9bc3fd2..083899fd66b 100644 --- a/opentelemetry-configuration/tests/test_logger_provider.py +++ b/opentelemetry-configuration/tests/test_logger_provider.py @@ -9,6 +9,7 @@ import unittest 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, @@ -608,7 +609,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( @@ -618,21 +619,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( @@ -642,19 +636,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 5ee6a847e38dc3de5f9b18911a9a4f8b5eb8dc98 Mon Sep 17 00:00:00 2001 From: Derwin Bell Date: Thu, 1 Oct 2026 20:15:43 -0400 Subject: [PATCH 2/2] 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