From 9c21864fb01bc2519d8de2711e12fd4d82fd7ebe Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Wed, 11 Feb 2026 18:40:43 +0100 Subject: [PATCH 1/4] Add logger exception support for logs API/SDK Signed-off-by: Israel Blancas --- CHANGELOG.md | 2 + .../opentelemetry/_logs/_internal/__init__.py | 11 ++++ .../tests/logs/test_log_record.py | 5 ++ opentelemetry-api/tests/logs/test_proxy.py | 1 + .../sdk/_logs/_internal/__init__.py | 58 +++++++++++++++++ opentelemetry-sdk/tests/logs/test_logs.py | 64 +++++++++++++++++++ 6 files changed, 141 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f125b47e997..970a2a59ec4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- logs: add exception support to Logger emit and LogRecord attributes + ([#4907](https://github.com/open-telemetry/opentelemetry-python/issues/4907)) - `opentelemetry-exporter-otlp-proto-grpc`: Fix re-initialization of gRPC channel on UNAVAILABLE error ([#4825](https://github.com/open-telemetry/opentelemetry-python/pull/4825)) - `opentelemetry-exporter-prometheus`: Fix duplicate HELP/TYPE declarations for metrics with different label sets diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index bbcfcddc846..dc3e351ac80 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -76,6 +76,7 @@ def __init__( body: AnyValue = None, attributes: Optional[_ExtendedAttributes] = None, event_name: Optional[str] = None, + exception: Optional[BaseException] = None, ) -> None: ... @overload @@ -94,6 +95,7 @@ def __init__( severity_number: Optional[SeverityNumber] = None, body: AnyValue = None, attributes: Optional[_ExtendedAttributes] = None, + exception: Optional[BaseException] = None, ) -> None: ... def __init__( @@ -110,6 +112,7 @@ def __init__( body: AnyValue = None, attributes: Optional[_ExtendedAttributes] = None, event_name: Optional[str] = None, + exception: Optional[BaseException] = None, ) -> None: if not context: context = get_current() @@ -127,6 +130,7 @@ def __init__( self.body = body self.attributes = attributes self.event_name = event_name + self.exception = exception class Logger(ABC): @@ -157,6 +161,7 @@ def emit( body: AnyValue | None = None, attributes: _ExtendedAttributes | None = None, event_name: str | None = None, + exception: BaseException | None = None, ) -> None: ... @overload @@ -178,6 +183,7 @@ def emit( body: AnyValue | None = None, attributes: _ExtendedAttributes | None = None, event_name: str | None = None, + exception: BaseException | None = None, ) -> None: """Emits a :class:`LogRecord` representing a log to the processing pipeline.""" @@ -200,6 +206,7 @@ def emit( body: AnyValue | None = None, attributes: _ExtendedAttributes | None = None, event_name: str | None = None, + exception: BaseException | None = None, ) -> None: ... @overload @@ -220,6 +227,7 @@ def emit( body: AnyValue | None = None, attributes: _ExtendedAttributes | None = None, event_name: str | None = None, + exception: BaseException | None = None, ) -> None: pass @@ -266,6 +274,7 @@ def emit( body: AnyValue | None = None, attributes: _ExtendedAttributes | None = None, event_name: str | None = None, + exception: BaseException | None = None, ) -> None: ... @overload @@ -286,6 +295,7 @@ def emit( body: AnyValue | None = None, attributes: _ExtendedAttributes | None = None, event_name: str | None = None, + exception: BaseException | None = None, ) -> None: if record: self._logger.emit(record) @@ -299,6 +309,7 @@ def emit( body=body, attributes=attributes, event_name=event_name, + exception=exception, ) diff --git a/opentelemetry-api/tests/logs/test_log_record.py b/opentelemetry-api/tests/logs/test_log_record.py index a06ed8dabfc..da0f41e3b6b 100644 --- a/opentelemetry-api/tests/logs/test_log_record.py +++ b/opentelemetry-api/tests/logs/test_log_record.py @@ -25,3 +25,8 @@ class TestLogRecord(unittest.TestCase): def test_log_record_observed_timestamp_default(self, time_ns_mock): # type: ignore time_ns_mock.return_value = OBSERVED_TIMESTAMP self.assertEqual(LogRecord().observed_timestamp, OBSERVED_TIMESTAMP) + + def test_log_record_exception(self): + exc = ValueError("boom") + log_record = LogRecord(exception=exc) + self.assertIs(log_record.exception, exc) diff --git a/opentelemetry-api/tests/logs/test_proxy.py b/opentelemetry-api/tests/logs/test_proxy.py index d72ccc7c6b2..120908e7ff0 100644 --- a/opentelemetry-api/tests/logs/test_proxy.py +++ b/opentelemetry-api/tests/logs/test_proxy.py @@ -46,6 +46,7 @@ def emit( body=None, attributes=None, event_name=None, + exception: typing.Optional[BaseException] = None, ) -> None: pass diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index d775dd44555..dc8bbf40daa 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -482,6 +482,50 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: ) +def _get_exception_attributes( + exception: BaseException, +) -> dict[str, AnyValue]: + stacktrace = "".join( + traceback.format_exception( + type(exception), value=exception, tb=exception.__traceback__ + ) + ) + module = type(exception).__module__ + qualname = type(exception).__qualname__ + exception_type = ( + f"{module}.{qualname}" if module and module != "builtins" else qualname + ) + return { + exception_attributes.EXCEPTION_TYPE: exception_type, + exception_attributes.EXCEPTION_MESSAGE: str(exception), + exception_attributes.EXCEPTION_STACKTRACE: stacktrace, + } + + +def _apply_exception_attributes( + log_record: LogRecord, + exception: BaseException | None, +) -> None: + if exception is None: + return + + exception_attributes_map = _get_exception_attributes(exception) + attributes = log_record.attributes + if attributes: + if isinstance(attributes, BoundedAttributes): + for key, value in exception_attributes_map.items(): + if key not in attributes: + attributes[key] = value + return + merged = dict(attributes) + for key, value in exception_attributes_map.items(): + merged.setdefault(key, value) + log_record.attributes = merged + return + + log_record.attributes = exception_attributes_map + + class LoggingHandler(logging.Handler): """A handler class which writes logging records, in OTLP format, to a network destination or file. Supports signals from the `logging` module. @@ -628,13 +672,22 @@ def emit( body: AnyValue | None = None, attributes: _ExtendedAttributes | None = None, event_name: str | None = None, + exception: BaseException | None = None, ) -> None: """Emits the :class:`ReadWriteLogRecord` by setting instrumentation scope and forwarding to the processor. """ # If a record is provided, use it directly if record is not None: + record_exception = exception or getattr(record, "exception", None) + if record_exception is None and isinstance( + record, ReadWriteLogRecord + ): + record_exception = getattr( + record.log_record, "exception", None + ) if not isinstance(record, ReadWriteLogRecord): + _apply_exception_attributes(record, record_exception) # pylint:disable=protected-access writable_record = ReadWriteLogRecord._from_api_log_record( record=record, @@ -642,6 +695,9 @@ def emit( instrumentation_scope=self._instrumentation_scope, ) else: + _apply_exception_attributes( + record.log_record, record_exception + ) writable_record = record else: # Create a record from individual parameters @@ -654,7 +710,9 @@ def emit( body=body, attributes=attributes, event_name=event_name, + exception=exception, ) + _apply_exception_attributes(log_record, exception) # pylint:disable=protected-access writable_record = ReadWriteLogRecord._from_api_log_record( record=log_record, diff --git a/opentelemetry-sdk/tests/logs/test_logs.py b/opentelemetry-sdk/tests/logs/test_logs.py index 70811260ae4..edf8e97e49f 100644 --- a/opentelemetry-sdk/tests/logs/test_logs.py +++ b/opentelemetry-sdk/tests/logs/test_logs.py @@ -23,6 +23,7 @@ Logger, LoggerProvider, ReadableLogRecord, + ReadWriteLogRecord, ) from opentelemetry.sdk._logs._internal import ( NoOpLogger, @@ -31,6 +32,7 @@ from opentelemetry.sdk.environment_variables import OTEL_SDK_DISABLED from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope +from opentelemetry.semconv.attributes import exception_attributes class TestLoggerProvider(unittest.TestCase): @@ -214,3 +216,65 @@ def test_can_emit_with_keywords_arguments(self): self.assertEqual(result_log_record.attributes, {"some": "attributes"}) self.assertEqual(result_log_record.event_name, "event_name") self.assertEqual(log_data.resource, logger.resource) + + def test_emit_with_exception_adds_attributes(self): + logger, log_record_processor_mock = self._get_logger() + exc = ValueError("boom") + + logger.emit(body="a log line", exception=exc) + log_record_processor_mock.on_emit.assert_called_once() + log_data = log_record_processor_mock.on_emit.call_args.args[0] + attributes = dict(log_data.log_record.attributes) + self.assertEqual( + attributes[exception_attributes.EXCEPTION_TYPE], "ValueError" + ) + self.assertEqual( + attributes[exception_attributes.EXCEPTION_MESSAGE], "boom" + ) + self.assertIn( + "ValueError: boom", + attributes[exception_attributes.EXCEPTION_STACKTRACE], + ) + + def test_emit_logrecord_exception_preserves_user_attributes(self): + logger, log_record_processor_mock = self._get_logger() + exc = ValueError("boom") + log_record = LogRecord( + observed_timestamp=0, + body="a log line", + attributes={exception_attributes.EXCEPTION_TYPE: "custom"}, + exception=exc, + ) + + logger.emit(log_record) + log_record_processor_mock.on_emit.assert_called_once() + log_data = log_record_processor_mock.on_emit.call_args.args[0] + attributes = dict(log_data.log_record.attributes) + self.assertEqual( + attributes[exception_attributes.EXCEPTION_TYPE], "custom" + ) + self.assertEqual( + attributes[exception_attributes.EXCEPTION_MESSAGE], "boom" + ) + + def test_emit_readwrite_logrecord_uses_exception(self): + logger, log_record_processor_mock = self._get_logger() + exc = RuntimeError("kaput") + log_record = LogRecord( + observed_timestamp=0, + body="a log line", + exception=exc, + ) + readwrite = ReadWriteLogRecord( + log_record=log_record, + resource=Resource.create({}), + instrumentation_scope=logger._instrumentation_scope, + ) + + logger.emit(readwrite) + log_record_processor_mock.on_emit.assert_called_once() + log_data = log_record_processor_mock.on_emit.call_args.args[0] + attributes = dict(log_data.log_record.attributes) + self.assertEqual( + attributes[exception_attributes.EXCEPTION_TYPE], "RuntimeError" + ) From 9ca005dfab02fca447dfa0282e0d3c4fb36019e6 Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Thu, 12 Mar 2026 16:26:16 +0100 Subject: [PATCH 2/4] Apply changes requested in code review Signed-off-by: Israel Blancas --- .../opentelemetry/_logs/_internal/__init__.py | 3 +- opentelemetry-api/tests/logs/test_proxy.py | 13 ++++ .../sdk/_logs/_internal/__init__.py | 74 +++++++++++++------ opentelemetry-sdk/tests/logs/test_logs.py | 45 +++++++++++ 4 files changed, 111 insertions(+), 24 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index dc3e351ac80..b437c1755cb 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -95,7 +95,6 @@ def __init__( severity_number: Optional[SeverityNumber] = None, body: AnyValue = None, attributes: Optional[_ExtendedAttributes] = None, - exception: Optional[BaseException] = None, ) -> None: ... def __init__( @@ -298,7 +297,7 @@ def emit( exception: BaseException | None = None, ) -> None: if record: - self._logger.emit(record) + self._logger.emit(record, exception=exception) else: self._logger.emit( timestamp=timestamp, diff --git a/opentelemetry-api/tests/logs/test_proxy.py b/opentelemetry-api/tests/logs/test_proxy.py index 120908e7ff0..3f6a723cbaa 100644 --- a/opentelemetry-api/tests/logs/test_proxy.py +++ b/opentelemetry-api/tests/logs/test_proxy.py @@ -15,6 +15,7 @@ # pylint: disable=W0212,W0222,W0221 import typing import unittest +from unittest.mock import Mock import opentelemetry._logs._internal as _logs_internal from opentelemetry import _logs @@ -75,3 +76,15 @@ def test_proxy_logger(self): # references to the old provider still work but return real logger now real_logger = provider.get_logger("proxy-test") self.assertIsInstance(real_logger, LoggerTest) + + def test_proxy_logger_forwards_exception_with_record(self): + logger = _logs_internal.ProxyLogger("proxy-test") + logger._real_logger = Mock(spec=LoggerTest("proxy-test")) + record = _logs.LogRecord() + exception = ValueError("boom") + + logger.emit(record, exception=exception) + + logger._real_logger.emit.assert_called_once_with( + record, exception=exception + ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 71581a58cfa..6794a435c80 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -534,28 +534,50 @@ def _get_exception_attributes( } -def _apply_exception_attributes( - log_record: LogRecord, +def _get_attributes_with_exception( + attributes: _ExtendedAttributes | None, exception: BaseException | None, -) -> None: +) -> _ExtendedAttributes | None: if exception is None: - return + return attributes exception_attributes_map = _get_exception_attributes(exception) - attributes = log_record.attributes - if attributes: - if isinstance(attributes, BoundedAttributes): - for key, value in exception_attributes_map.items(): - if key not in attributes: - attributes[key] = value - return - merged = dict(attributes) + attributes = attributes or {} + if isinstance(attributes, BoundedAttributes): + merged = BoundedAttributes( + maxlen=attributes.maxlen, + attributes=attributes, + immutable=False, + max_value_len=attributes.max_value_len, + extended_attributes=attributes._extended_attributes, # pylint: disable=protected-access + ) + merged.dropped = attributes.dropped for key, value in exception_attributes_map.items(): - merged.setdefault(key, value) - log_record.attributes = merged - return - - log_record.attributes = exception_attributes_map + if key not in merged: + merged[key] = value + return merged + + return exception_attributes_map | dict(attributes) + + +def _copy_log_record( + record: LogRecord, + attributes: _ExtendedAttributes | None, +) -> LogRecord: + return LogRecord( + timestamp=record.timestamp, + observed_timestamp=record.observed_timestamp, + context=record.context, + trace_id=record.trace_id, + span_id=record.span_id, + trace_flags=record.trace_flags, + severity_text=record.severity_text, + severity_number=record.severity_number, + body=record.body, + attributes=attributes, + event_name=record.event_name, + exception=getattr(record, "exception", None), + ) class LoggingHandler(logging.Handler): @@ -725,7 +747,13 @@ def emit( record.log_record, "exception", None ) if not isinstance(record, ReadWriteLogRecord): - _apply_exception_attributes(record, record_exception) + if record_exception is not None: + record = _copy_log_record( + record, + _get_attributes_with_exception( + record.attributes, record_exception + ), + ) # pylint:disable=protected-access writable_record = ReadWriteLogRecord._from_api_log_record( record=record, @@ -733,12 +761,15 @@ def emit( instrumentation_scope=self._instrumentation_scope, ) else: - _apply_exception_attributes( - record.log_record, record_exception + record.log_record.attributes = _get_attributes_with_exception( + record.log_record.attributes, record_exception ) writable_record = record else: # Create a record from individual parameters + log_record_attributes = _get_attributes_with_exception( + attributes, exception + ) log_record = LogRecord( timestamp=timestamp, observed_timestamp=observed_timestamp, @@ -746,11 +777,10 @@ def emit( severity_number=severity_number, severity_text=severity_text, body=body, - attributes=attributes, + attributes=log_record_attributes, event_name=event_name, exception=exception, ) - _apply_exception_attributes(log_record, exception) # pylint:disable=protected-access writable_record = ReadWriteLogRecord._from_api_log_record( record=log_record, diff --git a/opentelemetry-sdk/tests/logs/test_logs.py b/opentelemetry-sdk/tests/logs/test_logs.py index edf8e97e49f..957ba989109 100644 --- a/opentelemetry-sdk/tests/logs/test_logs.py +++ b/opentelemetry-sdk/tests/logs/test_logs.py @@ -18,6 +18,7 @@ from unittest.mock import Mock, patch from opentelemetry._logs import LogRecord, SeverityNumber +from opentelemetry.attributes import BoundedAttributes from opentelemetry.context import get_current from opentelemetry.sdk._logs import ( Logger, @@ -236,6 +237,22 @@ def test_emit_with_exception_adds_attributes(self): attributes[exception_attributes.EXCEPTION_STACKTRACE], ) + def test_emit_with_raised_exception_has_stacktrace(self): + logger, log_record_processor_mock = self._get_logger() + + try: + raise ValueError("boom") + except ValueError as exc: + logger.emit(body="error", exception=exc) + + log_record_processor_mock.on_emit.assert_called_once() + log_data = log_record_processor_mock.on_emit.call_args.args[0] + stacktrace = dict(log_data.log_record.attributes)[ + exception_attributes.EXCEPTION_STACKTRACE + ] + self.assertIn("Traceback (most recent call last)", stacktrace) + self.assertIn("raise ValueError", stacktrace) + def test_emit_logrecord_exception_preserves_user_attributes(self): logger, log_record_processor_mock = self._get_logger() exc = ValueError("boom") @@ -257,6 +274,34 @@ def test_emit_logrecord_exception_preserves_user_attributes(self): attributes[exception_attributes.EXCEPTION_MESSAGE], "boom" ) + def test_emit_logrecord_exception_with_immutable_attributes(self): + logger, log_record_processor_mock = self._get_logger() + exc = ValueError("boom") + original_attributes = BoundedAttributes( + attributes={"custom": "value"}, + immutable=True, + extended_attributes=True, + ) + log_record = LogRecord( + observed_timestamp=0, + body="a log line", + attributes=original_attributes, + exception=exc, + ) + + logger.emit(log_record) + + self.assertNotIn( + exception_attributes.EXCEPTION_TYPE, log_record.attributes + ) + log_record_processor_mock.on_emit.assert_called_once() + log_data = log_record_processor_mock.on_emit.call_args.args[0] + attributes = dict(log_data.log_record.attributes) + self.assertEqual(attributes["custom"], "value") + self.assertEqual( + attributes[exception_attributes.EXCEPTION_TYPE], "ValueError" + ) + def test_emit_readwrite_logrecord_uses_exception(self): logger, log_record_processor_mock = self._get_logger() exc = RuntimeError("kaput") From bf107968a355ae17abe2946a2f06f20a17fd7bdf Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Fri, 13 Mar 2026 11:15:44 +0100 Subject: [PATCH 3/4] Fix CI Signed-off-by: Israel Blancas --- .../opentelemetry/_logs/_internal/__init__.py | 6 ++++++ .../sdk/_logs/_internal/__init__.py | 21 ++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index b437c1755cb..b7ee546508d 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -167,6 +167,8 @@ def emit( def emit( self, record: LogRecord, + *, + exception: BaseException | None = None, ) -> None: ... @abstractmethod @@ -212,6 +214,8 @@ def emit( def emit( # pylint:disable=arguments-differ self, record: LogRecord, + *, + exception: BaseException | None = None, ) -> None: ... def emit( @@ -280,6 +284,8 @@ def emit( def emit( # pylint:disable=arguments-differ self, record: LogRecord, + *, + exception: BaseException | None = None, ) -> None: ... def emit( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 72625b8c349..54e94a933ca 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -544,22 +544,27 @@ def _get_attributes_with_exception( return attributes exception_attributes_map = _get_exception_attributes(exception) - attributes = attributes or {} - if isinstance(attributes, BoundedAttributes): + if attributes is None: + attributes_map: _ExtendedAttributes = {} + else: + attributes_map = attributes + + if isinstance(attributes_map, BoundedAttributes): + bounded_attributes = attributes_map merged = BoundedAttributes( - maxlen=attributes.maxlen, - attributes=attributes, + maxlen=bounded_attributes.maxlen, + attributes=bounded_attributes, immutable=False, - max_value_len=attributes.max_value_len, - extended_attributes=attributes._extended_attributes, # pylint: disable=protected-access + max_value_len=bounded_attributes.max_value_len, + extended_attributes=bounded_attributes._extended_attributes, # pylint: disable=protected-access ) - merged.dropped = attributes.dropped + merged.dropped = bounded_attributes.dropped for key, value in exception_attributes_map.items(): if key not in merged: merged[key] = value return merged - return exception_attributes_map | dict(attributes) + return exception_attributes_map | dict(attributes_map.items()) def _copy_log_record( From b1d7f0dd5a778b706ee039432c5ed44ca3ad5a55 Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Fri, 13 Mar 2026 11:27:40 +0100 Subject: [PATCH 4/4] Fix ci Signed-off-by: Israel Blancas --- .../src/opentelemetry/sdk/_logs/_internal/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 54e94a933ca..f48a6a93d02 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -571,13 +571,10 @@ def _copy_log_record( record: LogRecord, attributes: _ExtendedAttributes | None, ) -> LogRecord: - return LogRecord( + copied_record = LogRecord( timestamp=record.timestamp, observed_timestamp=record.observed_timestamp, context=record.context, - trace_id=record.trace_id, - span_id=record.span_id, - trace_flags=record.trace_flags, severity_text=record.severity_text, severity_number=record.severity_number, body=record.body, @@ -585,6 +582,10 @@ def _copy_log_record( event_name=record.event_name, exception=getattr(record, "exception", None), ) + copied_record.trace_id = record.trace_id + copied_record.span_id = record.span_id + copied_record.trace_flags = record.trace_flags + return copied_record class LoggingHandler(logging.Handler):