Skip to content

Commit 9cf1425

Browse files
committed
Implement filtering logic for min_severity and trace_based parameters
1 parent daae23b commit 9cf1425

File tree

3 files changed

+228
-1
lines changed

3 files changed

+228
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
([#4755](https://github.com/open-telemetry/opentelemetry-python/pull/4755))
1717
- logs: extend Logger.emit to accept separated keyword arguments
1818
([#4737](https://github.com/open-telemetry/opentelemetry-python/pull/4737))
19+
- Add `minimum_severity` and `trace_based` logger parameters to filter logs
1920

2021
## Version 1.37.0/0.58b0 (2025-09-11)
2122

opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,8 @@ def __init__(
675675
ConcurrentMultiLogRecordProcessor,
676676
],
677677
instrumentation_scope: InstrumentationScope,
678+
min_severity_level: SeverityNumber = SeverityNumber.UNSPECIFIED,
679+
trace_based: bool = False,
678680
):
679681
super().__init__(
680682
instrumentation_scope.name,
@@ -685,6 +687,8 @@ def __init__(
685687
self._resource = resource
686688
self._multi_log_record_processor = multi_log_record_processor
687689
self._instrumentation_scope = instrumentation_scope
690+
self._min_severity_level = min_severity_level
691+
self._trace_based = trace_based
688692

689693
@property
690694
def resource(self):
@@ -744,6 +748,10 @@ def emit(
744748
record = LogRecord._from_api_log_record(
745749
record=record, resource=self._resource
746750
)
751+
if is_less_than_min_severity(record, self._min_severity_level):
752+
return
753+
if should_drop_logs_for_trace_based(record, self._trace_based):
754+
return
747755

748756
log_data = LogData(record, self._instrumentation_scope)
749757

@@ -758,6 +766,8 @@ def __init__(
758766
multi_log_record_processor: SynchronousMultiLogRecordProcessor
759767
| ConcurrentMultiLogRecordProcessor
760768
| None = None,
769+
min_severity_level: SeverityNumber = SeverityNumber.UNSPECIFIED,
770+
trace_based: bool = False,
761771
):
762772
if resource is None:
763773
self._resource = Resource.create({})
@@ -773,6 +783,8 @@ def __init__(
773783
self._at_exit_handler = atexit.register(self.shutdown)
774784
self._logger_cache = {}
775785
self._logger_cache_lock = Lock()
786+
self._min_severity_level = min_severity_level
787+
self._trace_based = trace_based
776788

777789
@property
778790
def resource(self):
@@ -794,6 +806,8 @@ def _get_logger_no_cache(
794806
schema_url,
795807
attributes,
796808
),
809+
self._min_severity_level,
810+
self._trace_based,
797811
)
798812

799813
def _get_logger_cached(
@@ -920,3 +934,18 @@ def std_to_otel(levelno: int) -> SeverityNumber:
920934
if levelno > 53:
921935
return SeverityNumber.FATAL4
922936
return _STD_TO_OTEL[levelno]
937+
938+
def is_less_than_min_severity(record: LogRecord, min_severity: SeverityNumber) -> bool:
939+
if record.severity_number is not None:
940+
if min_severity is not None and min_severity != SeverityNumber.UNSPECIFIED and record.severity_number.value < min_severity.value:
941+
return True
942+
return False
943+
944+
def should_drop_logs_for_trace_based(record: LogRecord, trace_state_enabled: bool) -> bool:
945+
if trace_state_enabled:
946+
if record.context is not None:
947+
span = get_current_span(record.context)
948+
span_context = span.get_span_context()
949+
if span_context.is_valid and not span_context.trace_flags.sampled:
950+
return True
951+
return False

opentelemetry-sdk/tests/logs/test_logs.py

Lines changed: 198 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
NoOpLogger,
3030
SynchronousMultiLogRecordProcessor,
3131
)
32+
from opentelemetry._logs import (
33+
SeverityNumber,
34+
)
3235
from opentelemetry.sdk.environment_variables import OTEL_SDK_DISABLED
3336
from opentelemetry.sdk.resources import Resource
3437
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
@@ -74,6 +77,8 @@ def test_get_logger(self):
7477
self.assertEqual(
7578
logger._instrumentation_scope.attributes, {"key": "value"}
7679
)
80+
self.assertEqual(logger._min_severity_level, SeverityNumber.UNSPECIFIED)
81+
self.assertFalse(logger._trace_based)
7782

7883
@patch.dict("os.environ", {OTEL_SDK_DISABLED: "true"})
7984
def test_get_logger_with_sdk_disabled(self):
@@ -83,7 +88,7 @@ def test_get_logger_with_sdk_disabled(self):
8388

8489
@patch.object(Resource, "create")
8590
def test_logger_provider_init(self, resource_patch):
86-
logger_provider = LoggerProvider()
91+
logger_provider = LoggerProvider(min_severity_level=SeverityNumber.DEBUG4, trace_based=True)
8792
resource_patch.assert_called_once()
8893
self.assertIsNotNone(logger_provider._resource)
8994
self.assertTrue(
@@ -92,6 +97,8 @@ def test_logger_provider_init(self, resource_patch):
9297
SynchronousMultiLogRecordProcessor,
9398
)
9499
)
100+
self.assertEqual(logger_provider._min_severity_level, SeverityNumber.DEBUG4)
101+
self.assertTrue(logger_provider._trace_based)
95102
self.assertIsNotNone(logger_provider._at_exit_handler)
96103

97104

@@ -171,3 +178,193 @@ def test_can_emit_with_keywords_arguments(self):
171178
self.assertEqual(log_record.attributes, {"some": "attributes"})
172179
self.assertEqual(log_record.event_name, "event_name")
173180
self.assertEqual(log_record.resource, logger.resource)
181+
182+
def test_emit_logrecord_with_min_severity_filtering(self):
183+
"""Test that logs below minimum severity are filtered out"""
184+
logger, log_record_processor_mock = self._get_logger()
185+
logger._min_severity_level = SeverityNumber.DEBUG4
186+
187+
log_record_info = LogRecord(
188+
observed_timestamp=0,
189+
body="info log line",
190+
severity_number=SeverityNumber.DEBUG,
191+
severity_text="DEBUG",
192+
)
193+
194+
logger.emit(log_record_info)
195+
log_record_processor_mock.on_emit.assert_not_called()
196+
197+
log_record_processor_mock.reset_mock()
198+
199+
log_record_error = LogRecord(
200+
observed_timestamp=0,
201+
body="error log line",
202+
severity_number=SeverityNumber.ERROR,
203+
severity_text="ERROR",
204+
)
205+
206+
logger.emit(log_record_error)
207+
208+
log_record_processor_mock.on_emit.assert_called_once()
209+
log_data = log_record_processor_mock.on_emit.call_args.args[0]
210+
self.assertTrue(isinstance(log_data.log_record, LogRecord))
211+
self.assertEqual(log_data.log_record.severity_number, SeverityNumber.ERROR)
212+
213+
def test_emit_logrecord_with_min_severity_unspecified(self):
214+
"""Test that when min severity is UNSPECIFIED, all logs are emitted"""
215+
logger, log_record_processor_mock = self._get_logger()
216+
log_record = LogRecord(
217+
observed_timestamp=0,
218+
body="debug log line",
219+
severity_number=SeverityNumber.DEBUG,
220+
severity_text="DEBUG",
221+
)
222+
logger.emit(log_record)
223+
log_record_processor_mock.on_emit.assert_called_once()
224+
225+
def test_emit_logrecord_with_trace_based_filtering(self):
226+
"""Test that logs are filtered based on trace sampling state"""
227+
logger, log_record_processor_mock = self._get_logger()
228+
logger._trace_based = True
229+
230+
mock_span_context = Mock()
231+
mock_span_context.is_valid = True
232+
mock_span_context.trace_flags.sampled = False
233+
234+
mock_span = Mock()
235+
mock_span.get_span_context.return_value = mock_span_context
236+
237+
mock_context = Mock()
238+
239+
with patch('opentelemetry.sdk._logs._internal.get_current_span', return_value=mock_span):
240+
log_record = LogRecord(
241+
observed_timestamp=0,
242+
body="should be dropped",
243+
severity_number=SeverityNumber.INFO,
244+
severity_text="INFO",
245+
context=mock_context,
246+
)
247+
248+
logger.emit(log_record)
249+
log_record_processor_mock.on_emit.assert_not_called()
250+
251+
log_record_processor_mock.reset_mock()
252+
253+
mock_span_context = Mock()
254+
mock_span_context.is_valid = True
255+
mock_span_context.trace_flags.sampled = True
256+
257+
mock_span = Mock()
258+
mock_span.get_span_context.return_value = mock_span_context
259+
260+
def test_emit_logrecord_trace_filtering_disabled(self):
261+
"""Test that when trace-based filtering is disabled, all logs are emitted"""
262+
logger, log_record_processor_mock = self._get_logger()
263+
264+
mock_span_context = Mock()
265+
mock_span_context.is_valid = False
266+
mock_span_context.trace_flags.sampled = False
267+
268+
mock_span = Mock()
269+
mock_span.get_span_context.return_value = mock_span_context
270+
271+
mock_context = Mock()
272+
273+
with patch('opentelemetry.sdk._logs._internal.get_current_span', return_value=mock_span):
274+
log_record = LogRecord(
275+
observed_timestamp=0,
276+
body="should be emitted when filtering disabled",
277+
severity_number=SeverityNumber.INFO,
278+
severity_text="INFO",
279+
context=mock_context,
280+
)
281+
282+
logger.emit(log_record)
283+
log_record_processor_mock.on_emit.assert_called_once()
284+
285+
def test_emit_logrecord_trace_filtering_edge_cases(self):
286+
"""Test edge cases for trace-based filtering"""
287+
logger, log_record_processor_mock = self._get_logger()
288+
logger._trace_based = True
289+
290+
mock_span_context = Mock()
291+
mock_span_context.is_valid = False
292+
mock_span_context.trace_flags.sampled = True
293+
294+
mock_span = Mock()
295+
mock_span.get_span_context.return_value = mock_span_context
296+
297+
mock_context = Mock()
298+
299+
with patch('opentelemetry.sdk._logs._internal.get_current_span', return_value=mock_span):
300+
log_record = LogRecord(
301+
observed_timestamp=0,
302+
body="invalid but sampled",
303+
severity_number=SeverityNumber.INFO,
304+
severity_text="INFO",
305+
context=mock_context,
306+
)
307+
308+
logger.emit(log_record)
309+
log_record_processor_mock.on_emit.assert_called_once()
310+
311+
log_record_processor_mock.reset_mock()
312+
313+
mock_span_context = Mock()
314+
mock_span_context.is_valid = True
315+
mock_span_context.trace_flags.sampled = False
316+
317+
mock_span = Mock()
318+
mock_span.get_span_context.return_value = mock_span_context
319+
320+
with patch('opentelemetry.sdk._logs._internal.get_current_span', return_value=mock_span):
321+
log_record = LogRecord(
322+
observed_timestamp=0,
323+
body="valid but not sampled",
324+
severity_number=SeverityNumber.INFO,
325+
severity_text="INFO",
326+
context=mock_context,
327+
)
328+
329+
logger.emit(log_record)
330+
log_record_processor_mock.on_emit.assert_not_called()
331+
332+
def test_emit_both_min_severity_and_trace_based_filtering(self):
333+
"""Test that both min severity and trace-based filtering work together"""
334+
logger, log_record_processor_mock = self._get_logger()
335+
logger._min_severity_level = SeverityNumber.WARN
336+
logger._trace_based = True
337+
338+
mock_span_context = Mock()
339+
mock_span_context.is_valid = True
340+
mock_span_context.trace_flags.sampled = True
341+
342+
mock_span = Mock()
343+
mock_span.get_span_context.return_value = mock_span_context
344+
345+
mock_context = Mock()
346+
347+
with patch('opentelemetry.sdk._logs._internal.get_current_span', return_value=mock_span):
348+
log_record_info = LogRecord(
349+
observed_timestamp=0,
350+
body="info log line",
351+
severity_number=SeverityNumber.INFO,
352+
severity_text="INFO",
353+
context=mock_context,
354+
)
355+
356+
logger.emit(log_record_info)
357+
log_record_processor_mock.on_emit.assert_not_called()
358+
359+
log_record_processor_mock.reset_mock()
360+
361+
log_record_error = LogRecord(
362+
observed_timestamp=0,
363+
body="error log line",
364+
severity_number=SeverityNumber.ERROR,
365+
severity_text="ERROR",
366+
context=mock_context,
367+
)
368+
369+
logger.emit(log_record_error)
370+
log_record_processor_mock.on_emit.assert_called_once()

0 commit comments

Comments
 (0)