Skip to content

Commit 92ef979

Browse files
committed
Implement filtering logic for min_severity and trace_based parameters
1 parent 6f97ed8 commit 92ef979

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
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414

1515
- Add `rstcheck` to pre-commit to stop introducing invalid RST
1616
([#4755](https://github.com/open-telemetry/opentelemetry-python/pull/4755))
17+
- Add `minimum_severity` and `trace_based` logger parameters to filter logs
1718

1819
## Version 1.37.0/0.58b0 (2025-09-11)
1920

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,8 @@ def __init__(
677677
ConcurrentMultiLogRecordProcessor,
678678
],
679679
instrumentation_scope: InstrumentationScope,
680+
min_severity_level: SeverityNumber = SeverityNumber.UNSPECIFIED,
681+
trace_based: bool = False,
680682
):
681683
super().__init__(
682684
instrumentation_scope.name,
@@ -687,6 +689,8 @@ def __init__(
687689
self._resource = resource
688690
self._multi_log_record_processor = multi_log_record_processor
689691
self._instrumentation_scope = instrumentation_scope
692+
self._min_severity_level = min_severity_level
693+
self._trace_based = trace_based
690694

691695
@property
692696
def resource(self):
@@ -701,6 +705,10 @@ def emit(self, record: APILogRecord):
701705
record = LogRecord._from_api_log_record(
702706
record=record, resource=self._resource
703707
)
708+
if is_less_than_min_severity(record, self._min_severity_level):
709+
return
710+
if should_drop_logs_for_trace_based(record, self._trace_based):
711+
return
704712
log_data = LogData(record, self._instrumentation_scope)
705713
self._multi_log_record_processor.on_emit(log_data)
706714

@@ -713,6 +721,8 @@ def __init__(
713721
multi_log_record_processor: SynchronousMultiLogRecordProcessor
714722
| ConcurrentMultiLogRecordProcessor
715723
| None = None,
724+
min_severity_level: SeverityNumber = SeverityNumber.UNSPECIFIED,
725+
trace_based: bool = False,
716726
):
717727
if resource is None:
718728
self._resource = Resource.create({})
@@ -728,6 +738,8 @@ def __init__(
728738
self._at_exit_handler = atexit.register(self.shutdown)
729739
self._logger_cache = {}
730740
self._logger_cache_lock = Lock()
741+
self._min_severity_level = min_severity_level
742+
self._trace_based = trace_based
731743

732744
@property
733745
def resource(self):
@@ -749,6 +761,8 @@ def _get_logger_no_cache(
749761
schema_url,
750762
attributes,
751763
),
764+
self._min_severity_level,
765+
self._trace_based,
752766
)
753767

754768
def _get_logger_cached(
@@ -875,3 +889,18 @@ def std_to_otel(levelno: int) -> SeverityNumber:
875889
if levelno > 53:
876890
return SeverityNumber.FATAL4
877891
return _STD_TO_OTEL[levelno]
892+
893+
def is_less_than_min_severity(record: LogRecord, min_severity: SeverityNumber) -> bool:
894+
if record.severity_number is not None:
895+
if min_severity is not None and min_severity != SeverityNumber.UNSPECIFIED and record.severity_number.value < min_severity.value:
896+
return True
897+
return False
898+
899+
def should_drop_logs_for_trace_based(record: LogRecord, trace_state_enabled: bool) -> bool:
900+
if trace_state_enabled:
901+
if record.context is not None:
902+
span = get_current_span(record.context)
903+
span_context = span.get_span_context()
904+
if span_context.is_valid and not span_context.trace_flags.sampled:
905+
return True
906+
return False

opentelemetry-sdk/tests/logs/test_logs.py

Lines changed: 198 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
NoOpLogger,
2424
SynchronousMultiLogRecordProcessor,
2525
)
26+
from opentelemetry._logs import (
27+
SeverityNumber,
28+
)
2629
from opentelemetry.sdk.environment_variables import OTEL_SDK_DISABLED
2730
from opentelemetry.sdk.resources import Resource
2831
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
@@ -68,6 +71,8 @@ def test_get_logger(self):
6871
self.assertEqual(
6972
logger._instrumentation_scope.attributes, {"key": "value"}
7073
)
74+
self.assertEqual(logger._min_severity_level, SeverityNumber.UNSPECIFIED)
75+
self.assertFalse(logger._trace_based)
7176

7277
@patch.dict("os.environ", {OTEL_SDK_DISABLED: "true"})
7378
def test_get_logger_with_sdk_disabled(self):
@@ -77,7 +82,7 @@ def test_get_logger_with_sdk_disabled(self):
7782

7883
@patch.object(Resource, "create")
7984
def test_logger_provider_init(self, resource_patch):
80-
logger_provider = LoggerProvider()
85+
logger_provider = LoggerProvider(min_severity_level=SeverityNumber.DEBUG4, trace_based=True)
8186
resource_patch.assert_called_once()
8287
self.assertIsNotNone(logger_provider._resource)
8388
self.assertTrue(
@@ -86,6 +91,8 @@ def test_logger_provider_init(self, resource_patch):
8691
SynchronousMultiLogRecordProcessor,
8792
)
8893
)
94+
self.assertEqual(logger_provider._min_severity_level, SeverityNumber.DEBUG4)
95+
self.assertTrue(logger_provider._trace_based)
8996
self.assertIsNotNone(logger_provider._at_exit_handler)
9097

9198

@@ -127,3 +134,193 @@ def test_can_emit_api_logrecord(self):
127134
log_record_processor_mock.on_emit.assert_called_once()
128135
log_data = log_record_processor_mock.on_emit.call_args.args[0]
129136
self.assertTrue(isinstance(log_data.log_record, LogRecord))
137+
138+
def test_emit_logrecord_with_min_severity_filtering(self):
139+
"""Test that logs below minimum severity are filtered out"""
140+
logger, log_record_processor_mock = self._get_logger()
141+
logger._min_severity_level = SeverityNumber.DEBUG4
142+
143+
log_record_info = LogRecord(
144+
observed_timestamp=0,
145+
body="info log line",
146+
severity_number=SeverityNumber.DEBUG,
147+
severity_text="DEBUG",
148+
)
149+
150+
logger.emit(log_record_info)
151+
log_record_processor_mock.on_emit.assert_not_called()
152+
153+
log_record_processor_mock.reset_mock()
154+
155+
log_record_error = LogRecord(
156+
observed_timestamp=0,
157+
body="error log line",
158+
severity_number=SeverityNumber.ERROR,
159+
severity_text="ERROR",
160+
)
161+
162+
logger.emit(log_record_error)
163+
164+
log_record_processor_mock.on_emit.assert_called_once()
165+
log_data = log_record_processor_mock.on_emit.call_args.args[0]
166+
self.assertTrue(isinstance(log_data.log_record, LogRecord))
167+
self.assertEqual(log_data.log_record.severity_number, SeverityNumber.ERROR)
168+
169+
def test_emit_logrecord_with_min_severity_unspecified(self):
170+
"""Test that when min severity is UNSPECIFIED, all logs are emitted"""
171+
logger, log_record_processor_mock = self._get_logger()
172+
log_record = LogRecord(
173+
observed_timestamp=0,
174+
body="debug log line",
175+
severity_number=SeverityNumber.DEBUG,
176+
severity_text="DEBUG",
177+
)
178+
logger.emit(log_record)
179+
log_record_processor_mock.on_emit.assert_called_once()
180+
181+
def test_emit_logrecord_with_trace_based_filtering(self):
182+
"""Test that logs are filtered based on trace sampling state"""
183+
logger, log_record_processor_mock = self._get_logger()
184+
logger._trace_based = True
185+
186+
mock_span_context = Mock()
187+
mock_span_context.is_valid = True
188+
mock_span_context.trace_flags.sampled = False
189+
190+
mock_span = Mock()
191+
mock_span.get_span_context.return_value = mock_span_context
192+
193+
mock_context = Mock()
194+
195+
with patch('opentelemetry.sdk._logs._internal.get_current_span', return_value=mock_span):
196+
log_record = LogRecord(
197+
observed_timestamp=0,
198+
body="should be dropped",
199+
severity_number=SeverityNumber.INFO,
200+
severity_text="INFO",
201+
context=mock_context,
202+
)
203+
204+
logger.emit(log_record)
205+
log_record_processor_mock.on_emit.assert_not_called()
206+
207+
log_record_processor_mock.reset_mock()
208+
209+
mock_span_context = Mock()
210+
mock_span_context.is_valid = True
211+
mock_span_context.trace_flags.sampled = True
212+
213+
mock_span = Mock()
214+
mock_span.get_span_context.return_value = mock_span_context
215+
216+
def test_emit_logrecord_trace_filtering_disabled(self):
217+
"""Test that when trace-based filtering is disabled, all logs are emitted"""
218+
logger, log_record_processor_mock = self._get_logger()
219+
220+
mock_span_context = Mock()
221+
mock_span_context.is_valid = False
222+
mock_span_context.trace_flags.sampled = False
223+
224+
mock_span = Mock()
225+
mock_span.get_span_context.return_value = mock_span_context
226+
227+
mock_context = Mock()
228+
229+
with patch('opentelemetry.sdk._logs._internal.get_current_span', return_value=mock_span):
230+
log_record = LogRecord(
231+
observed_timestamp=0,
232+
body="should be emitted when filtering disabled",
233+
severity_number=SeverityNumber.INFO,
234+
severity_text="INFO",
235+
context=mock_context,
236+
)
237+
238+
logger.emit(log_record)
239+
log_record_processor_mock.on_emit.assert_called_once()
240+
241+
def test_emit_logrecord_trace_filtering_edge_cases(self):
242+
"""Test edge cases for trace-based filtering"""
243+
logger, log_record_processor_mock = self._get_logger()
244+
logger._trace_based = True
245+
246+
mock_span_context = Mock()
247+
mock_span_context.is_valid = False
248+
mock_span_context.trace_flags.sampled = True
249+
250+
mock_span = Mock()
251+
mock_span.get_span_context.return_value = mock_span_context
252+
253+
mock_context = Mock()
254+
255+
with patch('opentelemetry.sdk._logs._internal.get_current_span', return_value=mock_span):
256+
log_record = LogRecord(
257+
observed_timestamp=0,
258+
body="invalid but sampled",
259+
severity_number=SeverityNumber.INFO,
260+
severity_text="INFO",
261+
context=mock_context,
262+
)
263+
264+
logger.emit(log_record)
265+
log_record_processor_mock.on_emit.assert_called_once()
266+
267+
log_record_processor_mock.reset_mock()
268+
269+
mock_span_context = Mock()
270+
mock_span_context.is_valid = True
271+
mock_span_context.trace_flags.sampled = False
272+
273+
mock_span = Mock()
274+
mock_span.get_span_context.return_value = mock_span_context
275+
276+
with patch('opentelemetry.sdk._logs._internal.get_current_span', return_value=mock_span):
277+
log_record = LogRecord(
278+
observed_timestamp=0,
279+
body="valid but not sampled",
280+
severity_number=SeverityNumber.INFO,
281+
severity_text="INFO",
282+
context=mock_context,
283+
)
284+
285+
logger.emit(log_record)
286+
log_record_processor_mock.on_emit.assert_not_called()
287+
288+
def test_emit_both_min_severity_and_trace_based_filtering(self):
289+
"""Test that both min severity and trace-based filtering work together"""
290+
logger, log_record_processor_mock = self._get_logger()
291+
logger._min_severity_level = SeverityNumber.WARN
292+
logger._trace_based = True
293+
294+
mock_span_context = Mock()
295+
mock_span_context.is_valid = True
296+
mock_span_context.trace_flags.sampled = True
297+
298+
mock_span = Mock()
299+
mock_span.get_span_context.return_value = mock_span_context
300+
301+
mock_context = Mock()
302+
303+
with patch('opentelemetry.sdk._logs._internal.get_current_span', return_value=mock_span):
304+
log_record_info = LogRecord(
305+
observed_timestamp=0,
306+
body="info log line",
307+
severity_number=SeverityNumber.INFO,
308+
severity_text="INFO",
309+
context=mock_context,
310+
)
311+
312+
logger.emit(log_record_info)
313+
log_record_processor_mock.on_emit.assert_not_called()
314+
315+
log_record_processor_mock.reset_mock()
316+
317+
log_record_error = LogRecord(
318+
observed_timestamp=0,
319+
body="error log line",
320+
severity_number=SeverityNumber.ERROR,
321+
severity_text="ERROR",
322+
context=mock_context,
323+
)
324+
325+
logger.emit(log_record_error)
326+
log_record_processor_mock.on_emit.assert_called_once()

0 commit comments

Comments
 (0)