From 190a23f4460b812106586785e43586561256e537 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Wed, 14 Oct 2026 21:12:40 -0400 Subject: [PATCH 1/6] opentelemetry-sdk: add additional logs benchmarks & make additional perf improvements --- .../benchmarks/logs/test_benchmark_logs.py | 60 ++++++++- .../logs/test_benchmark_logs_enabled.py | 117 ++++++++++++++++++ .../sdk/_logs/_internal/__init__.py | 22 +++- opentelemetry-sdk/tests/logs/test_logs.py | 96 +++++++++++--- .../tests/logs/test_sdk_metrics.py | 18 ++- 5 files changed, 289 insertions(+), 24 deletions(-) create mode 100644 opentelemetry-sdk/benchmarks/logs/test_benchmark_logs_enabled.py diff --git a/opentelemetry-sdk/benchmarks/logs/test_benchmark_logs.py b/opentelemetry-sdk/benchmarks/logs/test_benchmark_logs.py index 94d08cc09e0..5681638fff9 100644 --- a/opentelemetry-sdk/benchmarks/logs/test_benchmark_logs.py +++ b/opentelemetry-sdk/benchmarks/logs/test_benchmark_logs.py @@ -3,7 +3,7 @@ import pytest -from opentelemetry._logs import SeverityNumber +from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import ( BatchLogRecordProcessor, @@ -11,6 +11,7 @@ SimpleLogRecordProcessor, ) from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider resource = Resource( { @@ -70,3 +71,60 @@ def benchmark_get_logger(): ) benchmark(benchmark_get_logger) + + +def test_emit_with_exception(benchmark): + try: + raise ValueError("benchmark exception") + except ValueError as exc: + exception = exc + + def benchmark_emit(): + simple_logger.emit( + severity_number=SeverityNumber.ERROR, + body="benchmark log message", + # pylint: disable-next=used-before-assignment + exception=exception, + ) + + benchmark(benchmark_emit) + + +def test_emit_in_active_span(benchmark): + tracer = TracerProvider(resource=resource, shutdown_on_exit=False).get_tracer("benchmark_tracer") + + def benchmark_emit(): + simple_logger.emit( + severity_number=SeverityNumber.INFO, + body="benchmark log message", + event_name="test.event", + ) + + with tracer.start_as_current_span("benchmark_span"): + benchmark(benchmark_emit) + + +@pytest.mark.parametrize("style", ["kwargs", "log_record"]) +def test_emit_log_record_vs_kwargs(benchmark, style): + benchmark.group = "emit-log-record-vs-kwargs" + attributes = {f"key{i}": f"value{i}" for i in range(5)} + + def benchmark_emit_kwargs(): + simple_logger.emit( + severity_number=SeverityNumber.INFO, + body="benchmark log message", + attributes=attributes, + event_name="test.event", + ) + + def benchmark_emit_log_record(): + simple_logger.emit( + LogRecord( + severity_number=SeverityNumber.INFO, + body="benchmark log message", + attributes=attributes, + event_name="test.event", + ) + ) + + benchmark(benchmark_emit_kwargs if style == "kwargs" else benchmark_emit_log_record) diff --git a/opentelemetry-sdk/benchmarks/logs/test_benchmark_logs_enabled.py b/opentelemetry-sdk/benchmarks/logs/test_benchmark_logs_enabled.py new file mode 100644 index 00000000000..02009cc695b --- /dev/null +++ b/opentelemetry-sdk/benchmarks/logs/test_benchmark_logs_enabled.py @@ -0,0 +1,117 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import os +from unittest import mock + +import pytest + +from opentelemetry._logs import SeverityNumber +from opentelemetry._logs._internal import ProxyLogger +from opentelemetry.sdk._logs import LoggerProvider, LogRecordProcessor +from opentelemetry.sdk._logs._internal import _disable_logger_configurator +from opentelemetry.sdk.environment_variables import OTEL_SDK_DISABLED + + +class _SeverityFilteringProcessor(LogRecordProcessor): + """Processor that only accepts records at or above ``min_severity``. + + ``on_emit`` does no export work so the benchmarks measure the SDK + overhead of creating and dispatching the record. + """ + + def __init__(self, min_severity): + self._min_severity = min_severity + + def on_emit(self, log_record): + pass + + def enabled( + self, + *, + context=None, + instrumentation_scope=None, + severity_number=None, + event_name=None, + ): + return severity_number is None or severity_number.value >= self._min_severity.value + + def shutdown(self): + pass + + def force_flush(self, timeout_millis=30000): + return True + + +def _make_disabled_logger(mechanism): + """Returns a logger for which an INFO record is disabled.""" + if mechanism == "logger_config": + provider = LoggerProvider( + shutdown_on_exit=False, + _logger_configurator=_disable_logger_configurator, + ) + provider.add_log_record_processor(_SeverityFilteringProcessor(SeverityNumber.INFO)) + return provider.get_logger("bench") + if mechanism == "processor_filter": + provider = LoggerProvider(shutdown_on_exit=False) + provider.add_log_record_processor(_SeverityFilteringProcessor(SeverityNumber.WARN)) + return provider.get_logger("bench") + if mechanism == "no_processors": + return LoggerProvider(shutdown_on_exit=False).get_logger("bench") + if mechanism == "sdk_disabled": + with mock.patch.dict(os.environ, {OTEL_SDK_DISABLED: "true"}): + provider = LoggerProvider(shutdown_on_exit=False) + return provider.get_logger("bench") + if mechanism == "api_proxy_noop": + return ProxyLogger("bench") + raise ValueError(f"unknown mechanism: {mechanism}") + + +def _emit_unguarded(logger, index): + logger.emit( + severity_number=SeverityNumber.INFO, + body=f"benchmark log message {index}", + attributes={ + "http.request.method": "GET", + "http.route": "/users/{id}", + "http.response.status_code": 200, + "user.id": index, + "server.address": "example.com", + }, + event_name="test.event", + ) + + +def _emit_guarded(logger, index): + if logger.enabled(severity_number=SeverityNumber.INFO, event_name="test.event"): + _emit_unguarded(logger, index) + + +@pytest.mark.parametrize("guarded", [False, True], ids=["unguarded", "guarded"]) +@pytest.mark.parametrize( + "mechanism", + [ + "logger_config", + "processor_filter", + "no_processors", + "sdk_disabled", + "api_proxy_noop", + ], +) +def test_disabled_emit(benchmark, mechanism, guarded): + benchmark.group = f"disabled-{mechanism}" + logger = _make_disabled_logger(mechanism) + emit = _emit_guarded if guarded else _emit_unguarded + + benchmark(emit, logger, 42) + + +@pytest.mark.parametrize("guarded", [False, True], ids=["unguarded", "guarded"]) +def test_enabled_emit(benchmark, guarded): + benchmark.group = "enabled-guard-overhead" + provider = LoggerProvider(shutdown_on_exit=False) + provider.add_log_record_processor(_SeverityFilteringProcessor(SeverityNumber.INFO)) + logger = provider.get_logger("bench") + emit = _emit_guarded if guarded else _emit_unguarded + + benchmark(emit, logger, 42) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 5b543d01e44..164fe8779d4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -758,9 +758,6 @@ def __init__( self._logger_config = _logger_config self._log_record_limits = log_record_limits or LogRecordLimits() - def _is_enabled(self) -> bool: - return self._logger_config.is_enabled - def enabled( self, *, @@ -768,7 +765,7 @@ def enabled( severity_number: SeverityNumber | None = None, event_name: str | None = None, ) -> bool: - if not self._is_enabled(): + if not self._logger_config.is_enabled: return False return self._multi_log_record_processor.enabled( context=context, @@ -809,7 +806,21 @@ def emit( """Emits the :class:`ReadWriteLogRecord` by setting instrumentation scope and forwarding to the processor. """ - if not self._is_enabled(): + if not self._logger_config.is_enabled: + return + self._logger_metrics.emit_log() + if record is not None: + api_record = record.log_record if isinstance(record, ReadWriteLogRecord) else record + context = api_record.context + severity_number = api_record.severity_number + event_name = api_record.event_name + # Skip building the record when no processor would accept it. + if not self._multi_log_record_processor.enabled( + context=context, + instrumentation_scope=self._instrumentation_scope, + severity_number=severity_number, + event_name=event_name, + ): return # If a record is provided, use it directly if record is not None: @@ -847,7 +858,6 @@ def emit( limits=self._log_record_limits, ) - self._logger_metrics.emit_log() self._multi_log_record_processor.on_emit(writable_record) diff --git a/opentelemetry-sdk/tests/logs/test_logs.py b/opentelemetry-sdk/tests/logs/test_logs.py index 38e38375a58..e607ac4aa67 100644 --- a/opentelemetry-sdk/tests/logs/test_logs.py +++ b/opentelemetry-sdk/tests/logs/test_logs.py @@ -174,13 +174,13 @@ def test_default_logger_configurator(self): provider = LoggerProvider() logger = provider.get_logger("module_name", "1.0", "schema_url") other_logger = provider.get_logger("other_module_name", "1.0", "schema_url") - self.assertTrue(logger._is_enabled()) - self.assertTrue(other_logger._is_enabled()) + self.assertTrue(logger._logger_config.is_enabled) + self.assertTrue(other_logger._logger_config.is_enabled) def test_logger_provider_with_disabled_configurator(self): provider = LoggerProvider(_logger_configurator=_disable_logger_configurator) logger = provider.get_logger("test") - self.assertFalse(logger._is_enabled()) + self.assertFalse(logger._logger_config.is_enabled) def test_logger_provider_with_custom_configurator(self): def configurator(scope): @@ -191,22 +191,22 @@ def configurator(scope): provider = LoggerProvider(_logger_configurator=configurator) enabled = provider.get_logger("enabled_logger") disabled = provider.get_logger("disabled_logger") - self.assertTrue(enabled._is_enabled()) - self.assertFalse(disabled._is_enabled()) + self.assertTrue(enabled._logger_config.is_enabled) + self.assertFalse(disabled._logger_config.is_enabled) def test_set_logger_configurator_updates_existing_loggers(self): provider = LoggerProvider() logger = provider.get_logger("test") - self.assertTrue(logger._is_enabled()) + self.assertTrue(logger._logger_config.is_enabled) provider._set_logger_configurator(logger_configurator=_disable_logger_configurator) - self.assertFalse(logger._is_enabled()) + self.assertFalse(logger._logger_config.is_enabled) def test_set_logger_configurator_affects_new_loggers(self): provider = LoggerProvider() provider._set_logger_configurator(logger_configurator=_disable_logger_configurator) logger = provider.get_logger("new_logger") - self.assertFalse(logger._is_enabled()) + self.assertFalse(logger._logger_config.is_enabled) # pylint: disable-next=no-self-use def test_disabled_logger_skips_emit(self): @@ -235,13 +235,13 @@ def test_rule_based_logger_configurator(self): logger = provider.get_logger("module_name", "1.0", "schema_url") other_logger = provider.get_logger("other_module_name", "1.0", "schema_url") - self.assertTrue(logger._is_enabled()) - self.assertTrue(other_logger._is_enabled()) + self.assertTrue(logger._logger_config.is_enabled) + self.assertTrue(other_logger._logger_config.is_enabled) provider._set_logger_configurator(logger_configurator=configurator) - self.assertTrue(logger._is_enabled()) - self.assertFalse(other_logger._is_enabled()) + self.assertTrue(logger._logger_config.is_enabled) + self.assertFalse(other_logger._logger_config.is_enabled) def test_rule_based_logger_configurator_default_when_rules_dont_match( self, @@ -258,13 +258,13 @@ def test_rule_based_logger_configurator_default_when_rules_dont_match( logger = provider.get_logger("module_name", "1.0", "schema_url") other_logger = provider.get_logger("other_module_name", "1.0", "schema_url") - self.assertTrue(logger._is_enabled()) - self.assertTrue(other_logger._is_enabled()) + self.assertTrue(logger._logger_config.is_enabled) + self.assertTrue(other_logger._logger_config.is_enabled) provider._set_logger_configurator(logger_configurator=configurator) - self.assertFalse(logger._is_enabled()) - self.assertTrue(other_logger._is_enabled()) + self.assertFalse(logger._logger_config.is_enabled) + self.assertTrue(other_logger._logger_config.is_enabled) def test_rule_based_configurator_first_match_wins(self): disabled_config = _LoggerConfig(is_enabled=False) @@ -540,3 +540,67 @@ def test_enabled_one_processor_enabled_returns_true(self): provider.add_log_record_processor(p2) logger = provider.get_logger("test") self.assertTrue(logger.enabled()) + + def test_emit_skipped_when_all_processors_disabled(self): # pylint: disable=no-self-use + 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") + + logger.emit(body="a log line") + logger.emit(LogRecord(body="a log line")) + + p1.on_emit.assert_not_called() + p2.on_emit.assert_not_called() + + def test_emit_forwarded_when_one_processor_enabled(self): # pylint: disable=no-self-use + 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") + + logger.emit(body="a log line") + + p1.on_emit.assert_called_once() + p2.on_emit.assert_called_once() + + def test_emit_passes_args_to_processor_enabled(self): + provider = LoggerProvider() + processor_mock = Mock() + processor_mock.enabled.return_value = False + provider.add_log_record_processor(processor_mock) + logger = provider.get_logger("test") + context = get_current() + log_record = LogRecord( + context=context, + severity_number=SeverityNumber.WARN, + event_name="my.event", + ) + + cases = { + "kwargs": lambda: logger.emit( + context=context, + severity_number=SeverityNumber.WARN, + event_name="my.event", + ), + "log_record": lambda: logger.emit(log_record), + "read_write_log_record": lambda: logger.emit(ReadWriteLogRecord(log_record=log_record)), + } + for name, emit in cases.items(): + with self.subTest(name): + processor_mock.reset_mock() + emit() + processor_mock.enabled.assert_called_once_with( + context=context, + instrumentation_scope=logger._instrumentation_scope, + severity_number=SeverityNumber.WARN, + event_name="my.event", + ) diff --git a/opentelemetry-sdk/tests/logs/test_sdk_metrics.py b/opentelemetry-sdk/tests/logs/test_sdk_metrics.py index 8191039f847..78814220bb7 100644 --- a/opentelemetry-sdk/tests/logs/test_sdk_metrics.py +++ b/opentelemetry-sdk/tests/logs/test_sdk_metrics.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase -from unittest.mock import patch +from unittest.mock import Mock, patch from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk.environment_variables import ( @@ -46,6 +46,22 @@ def test_create_logs(self): {}, ) + def test_create_logs_counted_when_processors_disabled(self): + logger_provider = LoggerProvider(meter_provider=self.meter_provider) + processor = Mock() + processor.enabled.return_value = False + logger_provider.add_log_record_processor(processor) + logger = logger_provider.get_logger("test") + + logger.emit(body="log1") + + processor.on_emit.assert_not_called() + self.assert_created_logs( + self.metric_reader.get_metrics_data(), + 1, + {}, + ) + class TestLoggerProviderMetricsDisabled(TestCase): def test_disabled_by_default(self): From 0763d4fa8ddaf7298336a6a79b865b79c077372b Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Wed, 14 Oct 2026 21:53:06 -0400 Subject: [PATCH 2/6] add changelog fragment --- .changelog/5784.added | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changelog/5784.added diff --git a/.changelog/5784.added b/.changelog/5784.added new file mode 100644 index 00000000000..e6f98e80aaa --- /dev/null +++ b/.changelog/5784.added @@ -0,0 +1 @@ +`opentelemetry-sdk`: add additional logs benchmarks and improve performance for disabled loggers From c8027628de8b0f20416cee724dc7d20b7b8e32b3 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Wed, 14 Oct 2026 22:23:44 -0400 Subject: [PATCH 3/6] address co-pilot comment --- .../sdk/_logs/_internal/__init__.py | 9 +++-- opentelemetry-sdk/tests/logs/test_logs.py | 33 ++++++++++++++----- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 164fe8779d4..911a06ff2ed 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -809,15 +809,20 @@ def emit( if not self._logger_config.is_enabled: return self._logger_metrics.emit_log() + instrumentation_scope = self._instrumentation_scope if record is not None: - api_record = record.log_record if isinstance(record, ReadWriteLogRecord) else record + if isinstance(record, ReadWriteLogRecord): + instrumentation_scope = record.instrumentation_scope + api_record = record.log_record + else: + api_record = record context = api_record.context severity_number = api_record.severity_number event_name = api_record.event_name # Skip building the record when no processor would accept it. if not self._multi_log_record_processor.enabled( context=context, - instrumentation_scope=self._instrumentation_scope, + instrumentation_scope=instrumentation_scope, severity_number=severity_number, event_name=event_name, ): diff --git a/opentelemetry-sdk/tests/logs/test_logs.py b/opentelemetry-sdk/tests/logs/test_logs.py index e607ac4aa67..ba7beb6cedb 100644 --- a/opentelemetry-sdk/tests/logs/test_logs.py +++ b/opentelemetry-sdk/tests/logs/test_logs.py @@ -585,22 +585,39 @@ def test_emit_passes_args_to_processor_enabled(self): event_name="my.event", ) + other_scope = InstrumentationScope("other") + cases = { - "kwargs": lambda: logger.emit( - context=context, - severity_number=SeverityNumber.WARN, - event_name="my.event", + "kwargs": ( + lambda: logger.emit( + context=context, + severity_number=SeverityNumber.WARN, + event_name="my.event", + ), + logger._instrumentation_scope, + ), + "log_record": (lambda: logger.emit(log_record), logger._instrumentation_scope), + "read_write_log_record": ( + lambda: logger.emit( + ReadWriteLogRecord( + log_record=log_record, + instrumentation_scope=logger._instrumentation_scope, + ) + ), + logger._instrumentation_scope, + ), + "read_write_log_record_other_scope": ( + lambda: logger.emit(ReadWriteLogRecord(log_record=log_record, instrumentation_scope=other_scope)), + other_scope, ), - "log_record": lambda: logger.emit(log_record), - "read_write_log_record": lambda: logger.emit(ReadWriteLogRecord(log_record=log_record)), } - for name, emit in cases.items(): + for name, (emit, expected_scope) in cases.items(): with self.subTest(name): processor_mock.reset_mock() emit() processor_mock.enabled.assert_called_once_with( context=context, - instrumentation_scope=logger._instrumentation_scope, + instrumentation_scope=expected_scope, severity_number=SeverityNumber.WARN, event_name="my.event", ) From e7e4a98bd3d4da37cb8a856df1ca708355942bcc Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Thu, 15 Oct 2026 17:55:08 -0400 Subject: [PATCH 4/6] add memory/gc info to benchmarks + kwargs vs. log record emit --- .../logs/test_benchmark_logs_enabled.py | 76 ++++++++++++++++++- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/opentelemetry-sdk/benchmarks/logs/test_benchmark_logs_enabled.py b/opentelemetry-sdk/benchmarks/logs/test_benchmark_logs_enabled.py index 02009cc695b..38805184ac3 100644 --- a/opentelemetry-sdk/benchmarks/logs/test_benchmark_logs_enabled.py +++ b/opentelemetry-sdk/benchmarks/logs/test_benchmark_logs_enabled.py @@ -1,12 +1,14 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +import gc import os +import tracemalloc from unittest import mock import pytest -from opentelemetry._logs import SeverityNumber +from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry._logs._internal import ProxyLogger from opentelemetry.sdk._logs import LoggerProvider, LogRecordProcessor from opentelemetry.sdk._logs._internal import _disable_logger_configurator @@ -87,7 +89,72 @@ def _emit_guarded(logger, index): _emit_unguarded(logger, index) +def _emit_log_record_unguarded(logger, index): + logger.emit( + LogRecord( + severity_number=SeverityNumber.INFO, + body=f"benchmark log message {index}", + attributes={ + "http.request.method": "GET", + "http.route": "/users/{id}", + "http.response.status_code": 200, + "user.id": index, + "server.address": "example.com", + }, + event_name="test.event", + ) + ) + + +def _emit_log_record_guarded(logger, index): + if logger.enabled(severity_number=SeverityNumber.INFO, event_name="test.event"): + logger.emit( + LogRecord( + severity_number=SeverityNumber.INFO, + body=f"benchmark log message {index}", + attributes={ + "http.request.method": "GET", + "http.route": "/users/{id}", + "http.response.status_code": 200, + "user.id": index, + "server.address": "example.com", + }, + event_name="test.event", + ) + ) + + +_EMIT_FUNCTIONS = { + ("kwargs", False): _emit_unguarded, + ("kwargs", True): _emit_guarded, + ("log_record", False): _emit_log_record_unguarded, + ("log_record", True): _emit_log_record_guarded, +} + + +def _measure_allocations(benchmark, emit, logger): + for index in range(100): + emit(logger, index) + + peaks = [] + for index in range(200): + tracemalloc.start() + emit(logger, index) + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + peaks.append(peak) + benchmark.extra_info["mean_alloc_bytes"] = sum(peaks) / len(peaks) + + # Measure gen-0 collections before/after emitting 10,000 log records + gc.collect() + collections_before = gc.get_stats()[0]["collections"] + for index in range(10_000): + emit(logger, index) + benchmark.extra_info["gc_gen0_collections_per_10k"] = gc.get_stats()[0]["collections"] - collections_before + + @pytest.mark.parametrize("guarded", [False, True], ids=["unguarded", "guarded"]) +@pytest.mark.parametrize("style", ["kwargs", "log_record"]) @pytest.mark.parametrize( "mechanism", [ @@ -98,10 +165,11 @@ def _emit_guarded(logger, index): "api_proxy_noop", ], ) -def test_disabled_emit(benchmark, mechanism, guarded): - benchmark.group = f"disabled-{mechanism}" +def test_disabled_emit(benchmark, mechanism, style, guarded): + benchmark.group = f"disabled-{mechanism}-{style}" logger = _make_disabled_logger(mechanism) - emit = _emit_guarded if guarded else _emit_unguarded + emit = _EMIT_FUNCTIONS[(style, guarded)] + _measure_allocations(benchmark, emit, logger) benchmark(emit, logger, 42) From 960d76143add48175f00e0d4ad0fa391aa895e3c Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Thu, 15 Oct 2026 21:57:52 -0400 Subject: [PATCH 5/6] update logs classes to use __slots__ --- .../opentelemetry/_logs/_internal/__init__.py | 15 ++++++++ .../tests/logs/test_log_record.py | 36 +++++++++++++++++++ .../sdk/_logs/_internal/__init__.py | 33 +++++++++-------- .../tests/logs/test_log_record.py | 16 +++++++++ 4 files changed, 85 insertions(+), 15 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index 4c049f92e5e..203d0a9314b 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -53,6 +53,21 @@ class LogRecord(ABC): pertinent to the event being logged. """ + __slots__ = ( + "timestamp", + "observed_timestamp", + "context", + "trace_id", + "span_id", + "trace_flags", + "severity_text", + "severity_number", + "body", + "attributes", + "event_name", + "exception", + ) + @overload def __init__( self, diff --git a/opentelemetry-api/tests/logs/test_log_record.py b/opentelemetry-api/tests/logs/test_log_record.py index 2b6f893374c..9fa65140f48 100644 --- a/opentelemetry-api/tests/logs/test_log_record.py +++ b/opentelemetry-api/tests/logs/test_log_record.py @@ -5,8 +5,21 @@ from unittest.mock import patch from opentelemetry._logs import LogRecord +from opentelemetry.trace import ( + NonRecordingSpan, + SpanContext, + TraceFlags, + set_span_in_context, + use_span, +) OBSERVED_TIMESTAMP = "OBSERVED_TIMESTAMP" +SPAN_CONTEXT = SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), +) class TestLogRecord(unittest.TestCase): @@ -19,3 +32,26 @@ def test_log_record_exception(self): exc = ValueError("boom") log_record = LogRecord(exception=exc) self.assertIs(log_record.exception, exc) + + def test_log_record_span_context_fields_from_context(self): + log_record = LogRecord(context=set_span_in_context(NonRecordingSpan(SPAN_CONTEXT))) + self.assertEqual(log_record.trace_id, SPAN_CONTEXT.trace_id) + self.assertEqual(log_record.span_id, SPAN_CONTEXT.span_id) + self.assertEqual(log_record.trace_flags, SPAN_CONTEXT.trace_flags) + + def test_log_record_explicit_span_context_fields(self): + span_context = SpanContext( + trace_id=SPAN_CONTEXT.trace_id, + span_id=SPAN_CONTEXT.span_id, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.DEFAULT), + ) + with use_span(NonRecordingSpan(span_context)): + log_record = LogRecord( + trace_id=1, + span_id=2, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ) + self.assertEqual(log_record.trace_id, 1) + self.assertEqual(log_record.span_id, 2) + self.assertEqual(log_record.trace_flags, TraceFlags(TraceFlags.SAMPLED)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 911a06ff2ed..765f533be5e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -225,7 +225,7 @@ class LogLimits(LogRecordLimits): pass -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class ReadableLogRecord: """Readable LogRecord should be kept exactly in-sync with ReadWriteLogRecord, only difference is the frozen=True param.""" @@ -269,7 +269,7 @@ def to_json(self, indent: int | None = 4) -> str: ) -@dataclass +@dataclass(slots=True) class ReadWriteLogRecord: """A ReadWriteLogRecord instance represents an event being logged. ReadWriteLogRecord instances are created and emitted via `Logger` @@ -300,7 +300,12 @@ def __post_init__(self): def __eq__(self, other: object) -> bool: if not isinstance(other, ReadWriteLogRecord): return NotImplemented - return self.__dict__ == other.__dict__ + return ( + self.log_record == other.log_record + and self.resource == other.resource + and self.instrumentation_scope == other.instrumentation_scope + and self.limits == other.limits + ) @property def dropped_attributes(self) -> int: @@ -435,16 +440,15 @@ def enabled( severity_number: SeverityNumber | None = None, event_name: str | None = None, ) -> bool: - processors = self._log_record_processors - return any( - lp.enabled( + for lp in self._log_record_processors: + if lp.enabled( context=context, instrumentation_scope=instrumentation_scope, severity_number=severity_number, event_name=event_name, - ) - for lp in processors - ) + ): + return True + return False def shutdown(self) -> None: """Shutdown the log processors one by one""" @@ -524,16 +528,15 @@ def enabled( severity_number: SeverityNumber | None = None, event_name: str | None = None, ) -> bool: - processors = self._log_record_processors - return any( - lp.enabled( + for lp in self._log_record_processors: + if lp.enabled( context=context, instrumentation_scope=instrumentation_scope, severity_number=severity_number, event_name=event_name, - ) - for lp in processors - ) + ): + return True + return False def shutdown(self) -> None: self._submit_and_wait(lambda lp: lp.shutdown) diff --git a/opentelemetry-sdk/tests/logs/test_log_record.py b/opentelemetry-sdk/tests/logs/test_log_record.py index 78f24704b48..7eab1712b0b 100644 --- a/opentelemetry-sdk/tests/logs/test_log_record.py +++ b/opentelemetry-sdk/tests/logs/test_log_record.py @@ -209,3 +209,19 @@ def test_log_record_from_api_log_record(self): self.assertEqual(record.log_record.attributes, {"a": "b"}) self.assertEqual(record.log_record.event_name, "an.event") self.assertEqual(record.resource, resource) + + def test_read_write_log_record_equality(self): + log_record = LogRecord(body="a log line") + resource = Resource.create({"service.name": "foo"}) + limits = LogRecordLimits() + record = ReadWriteLogRecord(log_record=log_record, resource=resource, limits=limits) + + self.assertEqual( + record, + ReadWriteLogRecord(log_record=log_record, resource=resource, limits=limits), + ) + self.assertNotEqual( + record, + ReadWriteLogRecord(log_record=LogRecord(body="other"), resource=resource, limits=limits), + ) + self.assertNotEqual(record, object()) From 1db15f723372f66bc011962bb2148b96d362df6d Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Thu, 22 Oct 2026 16:10:08 -0400 Subject: [PATCH 6/6] update kwargs processor filtering to default to using current context when none is provided --- .../sdk/_logs/_internal/__init__.py | 2 ++ opentelemetry-sdk/tests/logs/test_logs.py | 23 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 765f533be5e..9a0aa4cbd9e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -822,6 +822,8 @@ def emit( context = api_record.context severity_number = api_record.severity_number event_name = api_record.event_name + else: + context = context or get_current() # Skip building the record when no processor would accept it. if not self._multi_log_record_processor.enabled( context=context, diff --git a/opentelemetry-sdk/tests/logs/test_logs.py b/opentelemetry-sdk/tests/logs/test_logs.py index ba7beb6cedb..032e0c28b32 100644 --- a/opentelemetry-sdk/tests/logs/test_logs.py +++ b/opentelemetry-sdk/tests/logs/test_logs.py @@ -13,7 +13,7 @@ from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.attributes import BoundedAttributes -from opentelemetry.context import get_current +from opentelemetry.context import attach, detach, get_current, set_value from opentelemetry.sdk._logs import ( Logger, LoggerProvider, @@ -621,3 +621,24 @@ def test_emit_passes_args_to_processor_enabled(self): severity_number=SeverityNumber.WARN, event_name="my.event", ) + + def test_emit_kwargs_passes_current_context_to_processor_enabled(self): # pylint: disable=no-self-use + provider = LoggerProvider() + processor_mock = Mock() + processor_mock.enabled.return_value = False + provider.add_log_record_processor(processor_mock) + logger = provider.get_logger("test") + + token = attach(set_value("key", "value")) + try: + logger.emit(body="a log line") + expected_context = get_current() + finally: + detach(token) + + processor_mock.enabled.assert_called_once_with( + context=expected_context, + instrumentation_scope=logger._instrumentation_scope, + severity_number=None, + event_name=None, + )