From 816ce49c1faa2c5dadb702428303029976286c37 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Thu, 22 May 2025 12:33:30 -0700 Subject: [PATCH 01/10] Logs API/SDK accepts additional otel context --- .../src/opentelemetry/_logs/_internal/__init__.py | 3 +++ .../src/opentelemetry/sdk/_logs/_internal/__init__.py | 6 ++++++ opentelemetry-sdk/tests/logs/test_log_record.py | 3 ++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index f969f7db73..9d91a92f3d 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -40,6 +40,7 @@ from typing import Optional, cast from opentelemetry._logs.severity import SeverityNumber +from opentelemetry.context.context import Context from opentelemetry.environment_variables import _OTEL_PYTHON_LOGGER_PROVIDER from opentelemetry.trace.span import TraceFlags from opentelemetry.util._once import Once @@ -61,6 +62,7 @@ def __init__( self, timestamp: Optional[int] = None, observed_timestamp: Optional[int] = None, + context: Optional[Context] = None, trace_id: Optional[int] = None, span_id: Optional[int] = None, trace_flags: Optional["TraceFlags"] = None, @@ -73,6 +75,7 @@ def __init__( if observed_timestamp is None: observed_timestamp = time_ns() self.observed_timestamp = observed_timestamp + self.context = context self.trace_id = trace_id self.span_id = span_id self.trace_flags = trace_flags diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 9060e49aac..9e5977f797 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -37,6 +37,7 @@ std_to_otel, ) from opentelemetry.attributes import _VALID_ANY_VALUE_TYPES, BoundedAttributes +from opentelemetry.context.context import Context from opentelemetry.sdk.environment_variables import ( OTEL_ATTRIBUTE_COUNT_LIMIT, OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, @@ -176,6 +177,7 @@ def __init__( self, timestamp: int | None = None, observed_timestamp: int | None = None, + context: Context | None = None, trace_id: int | None = None, span_id: int | None = None, trace_flags: TraceFlags | None = None, @@ -190,6 +192,7 @@ def __init__( **{ "timestamp": timestamp, "observed_timestamp": observed_timestamp, + "context": context, "trace_id": trace_id, "span_id": span_id, "trace_flags": trace_flags, @@ -234,6 +237,9 @@ def to_json(self, indent: int | None = 4) -> str: "dropped_attributes": self.dropped_attributes, "timestamp": ns_to_iso_str(self.timestamp), "observed_timestamp": ns_to_iso_str(self.observed_timestamp), + "context": ( + dict(self.context) if self.context is not None else "" + ), "trace_id": ( f"0x{format_trace_id(self.trace_id)}" if self.trace_id is not None diff --git a/opentelemetry-sdk/tests/logs/test_log_record.py b/opentelemetry-sdk/tests/logs/test_log_record.py index 4a0d58dc9b..a5413f282a 100644 --- a/opentelemetry-sdk/tests/logs/test_log_record.py +++ b/opentelemetry-sdk/tests/logs/test_log_record.py @@ -42,6 +42,7 @@ def test_log_record_to_json(self): "dropped_attributes": 0, "timestamp": "1970-01-01T00:00:00.000000Z", "observed_timestamp": "1970-01-01T00:00:00.000000Z", + "context": "", "trace_id": "", "span_id": "", "trace_flags": None, @@ -68,7 +69,7 @@ def test_log_record_to_json(self): self.assertEqual(expected, actual.to_json(indent=4)) self.assertEqual( actual.to_json(indent=None), - '{"body": "a log line", "severity_number": null, "severity_text": null, "attributes": {"mapping": {"key": "value"}, "none": null, "sequence": [1, 2], "str": "string"}, "dropped_attributes": 0, "timestamp": "1970-01-01T00:00:00.000000Z", "observed_timestamp": "1970-01-01T00:00:00.000000Z", "trace_id": "", "span_id": "", "trace_flags": null, "resource": {"attributes": {"service.name": "foo"}, "schema_url": ""}}', + '{"body": "a log line", "severity_number": null, "severity_text": null, "attributes": {"mapping": {"key": "value"}, "none": null, "sequence": [1, 2], "str": "string"}, "dropped_attributes": 0, "timestamp": "1970-01-01T00:00:00.000000Z", "observed_timestamp": "1970-01-01T00:00:00.000000Z", "context": "", "trace_id": "", "span_id": "", "trace_flags": null, "resource": {"attributes": {"service.name": "foo"}, "schema_url": ""}}', ) def test_log_record_to_json_serializes_severity_number_as_int(self): From 4783932167f7beb259d6c945e9dafd67bee0ca6b Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Thu, 22 May 2025 12:36:04 -0700 Subject: [PATCH 02/10] Changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 001fbf28a5..c06a06167c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4593](https://github.com/open-telemetry/opentelemetry-python/pull/4593)) - opentelemetry-test-utils: assert explicit bucket boundaries in histogram metrics ([#4595](https://github.com/open-telemetry/opentelemetry-python/pull/4595)) +- Logging API accepts optional SpanContext + ([#4597](https://github.com/open-telemetry/opentelemetry-python/pull/4597)) ## Version 1.33.0/0.54b0 (2025-05-09) From 5308eee9ecd485c40b4cfd9a3a87db5dfcdc063b Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Thu, 22 May 2025 15:01:36 -0700 Subject: [PATCH 03/10] LoggingHandler translates to LogRecord with current Otel context --- .../sdk/_logs/_internal/__init__.py | 2 + opentelemetry-sdk/tests/logs/test_handler.py | 37 +++++++++++++------ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 9e5977f797..c6aef825bf 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -37,6 +37,7 @@ std_to_otel, ) from opentelemetry.attributes import _VALID_ANY_VALUE_TYPES, BoundedAttributes +from opentelemetry.context import get_current from opentelemetry.context.context import Context from opentelemetry.sdk.environment_variables import ( OTEL_ATTRIBUTE_COUNT_LIMIT, @@ -554,6 +555,7 @@ def _translate(self, record: logging.LogRecord) -> LogRecord: return LogRecord( timestamp=timestamp, observed_timestamp=observered_timestamp, + context=get_current(), trace_id=span_context.trace_id, span_id=span_context.span_id, trace_flags=span_context.trace_flags, diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index 3817c44025..a2e68677d5 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -20,6 +20,7 @@ from opentelemetry._logs import NoOpLoggerProvider, SeverityNumber from opentelemetry._logs import get_logger as APIGetLogger from opentelemetry.attributes import BoundedAttributes +from opentelemetry.context.context import Context from opentelemetry.sdk import trace from opentelemetry.sdk._logs import ( LogData, @@ -268,21 +269,33 @@ def __str__(self): def test_log_record_trace_correlation(self): processor, logger = set_up_test_logging(logging.WARNING) + mock_context = Context() tracer = trace.TracerProvider().get_tracer(__name__) with tracer.start_as_current_span("test") as span: - with self.assertLogs(level=logging.CRITICAL): - logger.critical("Critical message within span") - - log_record = processor.get_log_record(0) - - self.assertEqual(log_record.body, "Critical message within span") - self.assertEqual(log_record.severity_text, "CRITICAL") - self.assertEqual(log_record.severity_number, SeverityNumber.FATAL) - span_context = span.get_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) + with patch( + "opentelemetry.sdk._logs._internal.get_current", + return_value=mock_context, + ): + with self.assertLogs(level=logging.CRITICAL): + logger.critical("Critical message within span") + + log_record = processor.get_log_record(0) + + self.assertEqual( + log_record.body, "Critical message within span" + ) + self.assertEqual(log_record.severity_text, "CRITICAL") + self.assertEqual( + log_record.severity_number, SeverityNumber.FATAL + ) + self.assertEqual(log_record.context, mock_context) + span_context = span.get_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_warning_without_formatter(self): processor, logger = set_up_test_logging(logging.WARNING) From a45694c0b70fc7bdc337ca93600b09ada781ddaa Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Thu, 22 May 2025 15:48:57 -0700 Subject: [PATCH 04/10] Add LogRecord init priority for context's span over old span info --- .../sdk/_logs/_internal/__init__.py | 11 ++++ opentelemetry-sdk/tests/logs/test_handler.py | 65 +++++++++++++++---- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index c6aef825bf..b1fdfb5812 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -189,6 +189,17 @@ def __init__( attributes: _ExtendedAttributes | None = None, limits: LogLimits | None = _UnsetLogLimits, ): + # Prioritizes context over trace_id / span_id / trace_flags. + # If context provided and its current span valid, then uses that span info. + # Otherwise, uses provided trace_id etc for backwards compatibility. + if context is not None: + span = get_current_span(context) + span_context = span.get_span_context() + if span_context.is_valid: + trace_id = span_context.trace_id + span_id = span_context.span_id + trace_flags = span_context.trace_flags + super().__init__( **{ "timestamp": timestamp, diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index a2e68677d5..46f5af0eb3 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -30,7 +30,10 @@ ) from opentelemetry.semconv._incubating.attributes import code_attributes from opentelemetry.semconv.attributes import exception_attributes -from opentelemetry.trace import INVALID_SPAN_CONTEXT +from opentelemetry.trace import ( + INVALID_SPAN_CONTEXT, + set_span_in_context, +) class TestLoggingHandler(unittest.TestCase): @@ -92,19 +95,26 @@ def test_log_flush_noop(self): def test_log_record_no_span_context(self): processor, logger = set_up_test_logging(logging.WARNING) + mock_context = Context() - # Assert emit gets called for warning message - with self.assertLogs(level=logging.WARNING): - logger.warning("Warning message") + with patch( + "opentelemetry.sdk._logs._internal.get_current", + return_value=mock_context, + ): + # Assert emit gets called for warning message + with self.assertLogs(level=logging.WARNING): + logger.warning("Warning message") - log_record = processor.get_log_record(0) + log_record = processor.get_log_record(0) - self.assertIsNotNone(log_record) - self.assertEqual(log_record.trace_id, INVALID_SPAN_CONTEXT.trace_id) - self.assertEqual(log_record.span_id, INVALID_SPAN_CONTEXT.span_id) - self.assertEqual( - log_record.trace_flags, INVALID_SPAN_CONTEXT.trace_flags - ) + self.assertIsNotNone(log_record) + self.assertEqual( + log_record.trace_id, INVALID_SPAN_CONTEXT.trace_id + ) + self.assertEqual(log_record.span_id, INVALID_SPAN_CONTEXT.span_id) + self.assertEqual( + log_record.trace_flags, INVALID_SPAN_CONTEXT.trace_flags + ) def test_log_record_observed_timestamp(self): processor, logger = set_up_test_logging(logging.WARNING) @@ -269,7 +279,38 @@ def __str__(self): def test_log_record_trace_correlation(self): processor, logger = set_up_test_logging(logging.WARNING) - mock_context = Context() + + tracer = trace.TracerProvider().get_tracer(__name__) + with tracer.start_as_current_span("test") as span: + mock_context = set_span_in_context(span) + + with patch( + "opentelemetry.sdk._logs._internal.get_current", + return_value=mock_context, + ): + with self.assertLogs(level=logging.CRITICAL): + logger.critical("Critical message within span") + + log_record = processor.get_log_record(0) + + self.assertEqual( + log_record.body, "Critical message within span" + ) + self.assertEqual(log_record.severity_text, "CRITICAL") + self.assertEqual( + log_record.severity_number, SeverityNumber.FATAL + ) + self.assertEqual(log_record.context, mock_context) + span_context = span.get_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_trace_correlation_backwards_compatibility(self): + processor, logger = set_up_test_logging(logging.WARNING) + mock_context = Context() # no span in context tracer = trace.TracerProvider().get_tracer(__name__) with tracer.start_as_current_span("test") as span: From f8c56d3656d4457c1fa5ce89135ab533f968762f Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Fri, 23 May 2025 16:43:03 -0700 Subject: [PATCH 05/10] Add LogRecord serialized_context for to_json of arbitrary objects --- .../sdk/_logs/_internal/__init__.py | 17 ++- .../tests/logs/test_log_record.py | 139 +++++++++++++++++- 2 files changed, 151 insertions(+), 5 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index b1fdfb5812..856372afe7 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -235,6 +235,19 @@ def __eq__(self, other: object) -> bool: return NotImplemented return self.__dict__ == other.__dict__ + def serialized_context(self) -> dict: + """Returns JSON-serializable copy of stored Context""" + context_dict = {} + if self.context is not None: + for key, value in self.context.items(): + try: + json.dumps(value) + context_dict[key] = value + except TypeError: + # If not JSON-serializable, use string representation + context_dict[key] = str(value) + return context_dict + def to_json(self, indent: int | None = 4) -> str: return json.dumps( { @@ -249,9 +262,7 @@ def to_json(self, indent: int | None = 4) -> str: "dropped_attributes": self.dropped_attributes, "timestamp": ns_to_iso_str(self.timestamp), "observed_timestamp": ns_to_iso_str(self.observed_timestamp), - "context": ( - dict(self.context) if self.context is not None else "" - ), + "context": self.serialized_context(), "trace_id": ( f"0x{format_trace_id(self.trace_id)}" if self.trace_id is not None diff --git a/opentelemetry-sdk/tests/logs/test_log_record.py b/opentelemetry-sdk/tests/logs/test_log_record.py index a5413f282a..5a40371541 100644 --- a/opentelemetry-sdk/tests/logs/test_log_record.py +++ b/opentelemetry-sdk/tests/logs/test_log_record.py @@ -13,17 +13,30 @@ # limitations under the License. import json +import logging import unittest import warnings +from unittest.mock import patch from opentelemetry._logs.severity import SeverityNumber from opentelemetry.attributes import BoundedAttributes +from opentelemetry.sdk import trace from opentelemetry.sdk._logs import ( + LogData, LogDroppedAttributesWarning, + LoggerProvider, + LoggingHandler, LogLimits, LogRecord, + LogRecordProcessor, ) from opentelemetry.sdk.resources import Resource +from opentelemetry.trace import ( + INVALID_SPAN, + format_span_id, + format_trace_id, + set_span_in_context, +) class TestLogRecord(unittest.TestCase): @@ -42,7 +55,7 @@ def test_log_record_to_json(self): "dropped_attributes": 0, "timestamp": "1970-01-01T00:00:00.000000Z", "observed_timestamp": "1970-01-01T00:00:00.000000Z", - "context": "", + "context": None, "trace_id": "", "span_id": "", "trace_flags": None, @@ -69,9 +82,99 @@ def test_log_record_to_json(self): self.assertEqual(expected, actual.to_json(indent=4)) self.assertEqual( actual.to_json(indent=None), - '{"body": "a log line", "severity_number": null, "severity_text": null, "attributes": {"mapping": {"key": "value"}, "none": null, "sequence": [1, 2], "str": "string"}, "dropped_attributes": 0, "timestamp": "1970-01-01T00:00:00.000000Z", "observed_timestamp": "1970-01-01T00:00:00.000000Z", "context": "", "trace_id": "", "span_id": "", "trace_flags": null, "resource": {"attributes": {"service.name": "foo"}, "schema_url": ""}}', + '{"body": "a log line", "severity_number": null, "severity_text": null, "attributes": {"mapping": {"key": "value"}, "none": null, "sequence": [1, 2], "str": "string"}, "dropped_attributes": 0, "timestamp": "1970-01-01T00:00:00.000000Z", "observed_timestamp": "1970-01-01T00:00:00.000000Z", "context": null, "trace_id": "", "span_id": "", "trace_flags": null, "resource": {"attributes": {"service.name": "foo"}, "schema_url": ""}}', ) + @patch("opentelemetry.sdk._logs._internal.get_current_span") + @patch("opentelemetry.trace.propagation.set_value") + @patch("opentelemetry.sdk.trace.RandomIdGenerator.generate_span_id") + @patch("opentelemetry.sdk.trace.RandomIdGenerator.generate_trace_id") + def test_log_record_to_json_with_span_correlation( + self, + mock_generate_trace_id, + mock_generate_span_id, + mock_set_value, + mock_get_current_span, + ): + trace_id = 0x000000000000000000000000DEADBEEF + span_id = 0x00000000DEADBEF0 + fixed_key = "current-span-test" + + mock_generate_trace_id.return_value = trace_id + mock_generate_span_id.return_value = span_id + + def mock_set_value_impl(key, value, context=None): + if context is None: + context = {} + context[fixed_key] = value + return context + + mock_set_value.side_effect = mock_set_value_impl + + def mock_get_span_impl(context=None): + if context is None or fixed_key not in context: + return INVALID_SPAN + return context[fixed_key] + + mock_get_current_span.side_effect = mock_get_span_impl + + _, _ = set_up_test_logging(logging.WARNING) + tracer = trace.TracerProvider().get_tracer(__name__) + + with tracer.start_as_current_span("test") as span: + context = set_span_in_context(span) + span_context = span.get_span_context() + + expected = json.dumps( + { + "body": "a log line", + "severity_number": None, + "severity_text": None, + "attributes": { + "mapping": {"key": "value"}, + "none": None, + "sequence": [1, 2], + "str": "string", + }, + "dropped_attributes": 0, + "timestamp": "1970-01-01T00:00:00.000000Z", + "observed_timestamp": "1970-01-01T00:00:00.000000Z", + "context": { + fixed_key: f'_Span(name="test", context=SpanContext(trace_id=0x{format_trace_id(trace_id)}, ' + f"span_id=0x{format_span_id(span_id)}, " + f"trace_flags=0x01, trace_state=[], is_remote=False))" + }, + "trace_id": f"0x{format_trace_id(span_context.trace_id)}", + "span_id": f"0x{format_span_id(span_context.span_id)}", + "trace_flags": span_context.trace_flags, + "resource": { + "attributes": {"service.name": "foo"}, + "schema_url": "", + }, + }, + indent=4, + ) + + actual = LogRecord( + timestamp=0, + observed_timestamp=0, + context=context, + body="a log line", + resource=Resource({"service.name": "foo"}), + attributes={ + "mapping": {"key": "value"}, + "none": None, + "sequence": [1, 2], + "str": "string", + }, + ) + + self.assertEqual(expected, actual.to_json(indent=4)) + self.assertEqual( + '{"body": "a log line", "severity_number": null, "severity_text": null, "attributes": {"mapping": {"key": "value"}, "none": null, "sequence": [1, 2], "str": "string"}, "dropped_attributes": 0, "timestamp": "1970-01-01T00:00:00.000000Z", "observed_timestamp": "1970-01-01T00:00:00.000000Z", "context": {"current-span-test": "_Span(name=\\"test\\", context=SpanContext(trace_id=0x000000000000000000000000deadbeef, span_id=0x00000000deadbef0, trace_flags=0x01, trace_state=[], is_remote=False))"}, "trace_id": "0x000000000000000000000000deadbeef", "span_id": "0x00000000deadbef0", "trace_flags": 1, "resource": {"attributes": {"service.name": "foo"}, "schema_url": ""}}', + actual.to_json(indent=None), + ) + def test_log_record_to_json_serializes_severity_number_as_int(self): actual = LogRecord( timestamp=0, @@ -169,3 +272,35 @@ def test_log_record_dropped_attributes_unset_limits(self): ) self.assertTrue(result.dropped_attributes == 0) self.assertEqual(attr, result.attributes) + + +def set_up_test_logging(level, formatter=None, root_logger=False): + logger_provider = LoggerProvider() + processor = FakeProcessor() + logger_provider.add_log_record_processor(processor) + logger = logging.getLogger(None if root_logger else "foo") + handler = LoggingHandler(level=level, logger_provider=logger_provider) + if formatter: + handler.setFormatter(formatter) + logger.addHandler(handler) + return processor, logger + + +class FakeProcessor(LogRecordProcessor): + def __init__(self): + self.log_data_emitted = [] + + def emit(self, log_data: LogData): + self.log_data_emitted.append(log_data) + + def shutdown(self): + pass + + def force_flush(self, timeout_millis: int = 30000): + pass + + def emit_count(self): + return len(self.log_data_emitted) + + def get_log_record(self, i): + return self.log_data_emitted[i].log_record From fa5ec3905ad9c518cab060dc615234529cfe212e Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Fri, 23 May 2025 16:54:09 -0700 Subject: [PATCH 06/10] Add test coverage --- .../tests/logs/test_log_record.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/opentelemetry-sdk/tests/logs/test_log_record.py b/opentelemetry-sdk/tests/logs/test_log_record.py index 5a40371541..361d772254 100644 --- a/opentelemetry-sdk/tests/logs/test_log_record.py +++ b/opentelemetry-sdk/tests/logs/test_log_record.py @@ -40,6 +40,32 @@ class TestLogRecord(unittest.TestCase): + def test_serialized_context_none(self): + record = LogRecord(context=None) + self.assertEqual(None, record.serialized_context()) + + def test_serialized_context_serializable(self): + context = { + "test-string": "value", + "test-number": 42, + "test-list": [1, 2, 3], + "test-dict": {"key": "value"}, + "test-null": None, + "test-bool": True, + } + record = LogRecord(context=context) + self.assertEqual(context, record.serialized_context()) + + def test_serialized_context_non_serializable(self): + class MyTestObject: + def __str__(self): + return "foo-bar" + + context = {"test-string": "value", "test-object": MyTestObject()} + record = LogRecord(context=context) + expected = {"test-string": "value", "test-object": "foo-bar"} + self.assertEqual(expected, record.serialized_context()) + def test_log_record_to_json(self): expected = json.dumps( { From 56172fa1310e1681200dd8da4db8584a979bdd1a Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Fri, 23 May 2025 16:55:33 -0700 Subject: [PATCH 07/10] Changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c06a06167c..25ccf0abea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4593](https://github.com/open-telemetry/opentelemetry-python/pull/4593)) - opentelemetry-test-utils: assert explicit bucket boundaries in histogram metrics ([#4595](https://github.com/open-telemetry/opentelemetry-python/pull/4595)) -- Logging API accepts optional SpanContext +- Logging API accepts optional SpanContext that takes precedence over `trace_id`, + `span_id`, `trace_flags` if provided ([#4597](https://github.com/open-telemetry/opentelemetry-python/pull/4597)) ## Version 1.33.0/0.54b0 (2025-05-09) From 4b381185da695eec0d998cb69a9b33525fa46898 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Fri, 23 May 2025 16:58:42 -0700 Subject: [PATCH 08/10] lint --- .../src/opentelemetry/sdk/_logs/_internal/__init__.py | 2 +- opentelemetry-sdk/tests/logs/test_log_record.py | 1 + 2 files changed, 2 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 856372afe7..6efc3c8d77 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -246,7 +246,7 @@ def serialized_context(self) -> dict: except TypeError: # If not JSON-serializable, use string representation context_dict[key] = str(value) - return context_dict + return context_dict def to_json(self, indent: int | None = 4) -> str: return json.dumps( diff --git a/opentelemetry-sdk/tests/logs/test_log_record.py b/opentelemetry-sdk/tests/logs/test_log_record.py index 361d772254..5520be6077 100644 --- a/opentelemetry-sdk/tests/logs/test_log_record.py +++ b/opentelemetry-sdk/tests/logs/test_log_record.py @@ -111,6 +111,7 @@ def test_log_record_to_json(self): '{"body": "a log line", "severity_number": null, "severity_text": null, "attributes": {"mapping": {"key": "value"}, "none": null, "sequence": [1, 2], "str": "string"}, "dropped_attributes": 0, "timestamp": "1970-01-01T00:00:00.000000Z", "observed_timestamp": "1970-01-01T00:00:00.000000Z", "context": null, "trace_id": "", "span_id": "", "trace_flags": null, "resource": {"attributes": {"service.name": "foo"}, "schema_url": ""}}', ) + # pylint: disable=too-many-locals @patch("opentelemetry.sdk._logs._internal.get_current_span") @patch("opentelemetry.trace.propagation.set_value") @patch("opentelemetry.sdk.trace.RandomIdGenerator.generate_span_id") From ded31f6c52cae0614c79276abbdd92af084081b6 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Fri, 23 May 2025 17:00:59 -0700 Subject: [PATCH 09/10] Fix tests --- opentelemetry-sdk/tests/logs/test_log_record.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opentelemetry-sdk/tests/logs/test_log_record.py b/opentelemetry-sdk/tests/logs/test_log_record.py index 5520be6077..a0b635edb6 100644 --- a/opentelemetry-sdk/tests/logs/test_log_record.py +++ b/opentelemetry-sdk/tests/logs/test_log_record.py @@ -42,7 +42,7 @@ class TestLogRecord(unittest.TestCase): def test_serialized_context_none(self): record = LogRecord(context=None) - self.assertEqual(None, record.serialized_context()) + self.assertEqual({}, record.serialized_context()) def test_serialized_context_serializable(self): context = { @@ -81,7 +81,7 @@ def test_log_record_to_json(self): "dropped_attributes": 0, "timestamp": "1970-01-01T00:00:00.000000Z", "observed_timestamp": "1970-01-01T00:00:00.000000Z", - "context": None, + "context": {}, "trace_id": "", "span_id": "", "trace_flags": None, @@ -108,7 +108,7 @@ def test_log_record_to_json(self): self.assertEqual(expected, actual.to_json(indent=4)) self.assertEqual( actual.to_json(indent=None), - '{"body": "a log line", "severity_number": null, "severity_text": null, "attributes": {"mapping": {"key": "value"}, "none": null, "sequence": [1, 2], "str": "string"}, "dropped_attributes": 0, "timestamp": "1970-01-01T00:00:00.000000Z", "observed_timestamp": "1970-01-01T00:00:00.000000Z", "context": null, "trace_id": "", "span_id": "", "trace_flags": null, "resource": {"attributes": {"service.name": "foo"}, "schema_url": ""}}', + '{"body": "a log line", "severity_number": null, "severity_text": null, "attributes": {"mapping": {"key": "value"}, "none": null, "sequence": [1, 2], "str": "string"}, "dropped_attributes": 0, "timestamp": "1970-01-01T00:00:00.000000Z", "observed_timestamp": "1970-01-01T00:00:00.000000Z", "context": {}, "trace_id": "", "span_id": "", "trace_flags": null, "resource": {"attributes": {"service.name": "foo"}, "schema_url": ""}}', ) # pylint: disable=too-many-locals From bce5a7e20b0f54479a91d71aeb84d0a178726e73 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Fri, 23 May 2025 17:44:21 -0700 Subject: [PATCH 10/10] Changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25ccf0abea..1f7af824c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,8 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4593](https://github.com/open-telemetry/opentelemetry-python/pull/4593)) - opentelemetry-test-utils: assert explicit bucket boundaries in histogram metrics ([#4595](https://github.com/open-telemetry/opentelemetry-python/pull/4595)) -- Logging API accepts optional SpanContext that takes precedence over `trace_id`, - `span_id`, `trace_flags` if provided +- Logging API accepts optional Context with precedence over `trace_id`, `span_id`, + `trace_flags` if provided with valid span. LoggingHandler passes current Context. ([#4597](https://github.com/open-telemetry/opentelemetry-python/pull/4597)) ## Version 1.33.0/0.54b0 (2025-05-09)