From 2f9417aa4cd7f348895665bc878dcd090931b8d2 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 4 Apr 2025 18:10:22 +0200 Subject: [PATCH 1/2] opentelemetry-sdk: fix serialization of objects in log handler We should convert to string objects that are not AnyValues because otherwise exporter will fail later in the pipeline. While the export of all AnyValue types is not correct yet, exporter tests expects to being able to handle them and so they are already used in the handler. --- CHANGELOG.md | 4 +- .../src/opentelemetry/attributes/__init__.py | 13 +++++- .../sdk/_logs/_internal/__init__.py | 9 ++-- opentelemetry-sdk/tests/logs/test_handler.py | 42 +++++++++++++++++++ 4 files changed, 63 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4eccf8ae69..51ab4a4186c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Patch logging.basicConfig so OTel logs don't cause console logs to disappear ([#4436](https://github.com/open-telemetry/opentelemetry-python/pull/4436)) - Fix ExplicitBucketHistogramAggregation to handle multiple explicit bucket boundaries advisories - ([#4521](https://github.com/open-telemetry/opentelemetry-python/pull/4521)) + ([#4521](https://github.com/open-telemetry/opentelemetry-python/pull/4521)) +- opentelemetry-sdk: Fix serialization of objects in log handler + ([#4528](https://github.com/open-telemetry/opentelemetry-python/pull/4528)) ## Version 1.31.0/0.52b0 (2025-03-12) diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index 497952984db..71121f84697 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -16,13 +16,24 @@ import threading from collections import OrderedDict from collections.abc import MutableMapping -from typing import Optional, Sequence, Tuple, Union +from typing import Mapping, Optional, Sequence, Tuple, Union from opentelemetry.util import types # bytes are accepted as a user supplied value for attributes but # decoded to strings internally. _VALID_ATTR_VALUE_TYPES = (bool, str, bytes, int, float) +# AnyValue possible values +_VALID_ANY_VALUE_TYPES = ( + type(None), + bool, + bytes, + int, + float, + str, + Sequence, + Mapping, +) _logger = logging.getLogger(__name__) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 302ca1ed4d2..5d17c39f332 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -36,7 +36,7 @@ get_logger_provider, std_to_otel, ) -from opentelemetry.attributes import BoundedAttributes +from opentelemetry.attributes import _VALID_ANY_VALUE_TYPES, BoundedAttributes from opentelemetry.sdk.environment_variables import ( OTEL_ATTRIBUTE_COUNT_LIMIT, OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, @@ -523,8 +523,11 @@ def _translate(self, record: logging.LogRecord) -> LogRecord: # itself instead of its string representation. # For more background, see: https://github.com/open-telemetry/opentelemetry-python/pull/4216 if not record.args and not isinstance(record.msg, str): - # no args are provided so it's *mostly* safe to use the message template as the body - body = record.msg + # if record.msg is not a value we can export, cast it to string + if not isinstance(record.msg, _VALID_ANY_VALUE_TYPES): + body = str(record.msg) + else: + body = record.msg else: body = record.getMessage() diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index 7f8763bb008..dcc434a2e99 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -153,6 +153,7 @@ def test_log_record_exception(self): log_record = processor.get_log_record(0) self.assertIsNotNone(log_record) + self.assertTrue(isinstance(log_record.body, str)) self.assertEqual(log_record.body, "Zero Division Error") self.assertEqual( log_record.attributes[SpanAttributes.EXCEPTION_TYPE], @@ -226,6 +227,47 @@ def test_log_exc_info_false(self): SpanAttributes.EXCEPTION_STACKTRACE, log_record.attributes ) + def test_log_record_exception_with_object_payload(self): + processor, logger = set_up_test_logging(logging.ERROR) + + class CustomObject: + pass + + class CustomException(Exception): + def __init__(self, data): + self.data = data + + def __str__(self): + return "CustomException stringified" + + try: + raise CustomException(CustomObject()) + except CustomException as exception: + with self.assertLogs(level=logging.ERROR): + logger.exception(exception) + + log_record = processor.get_log_record(0) + + self.assertIsNotNone(log_record) + self.assertTrue(isinstance(log_record.body, str)) + self.assertEqual(log_record.body, "CustomException stringified") + self.assertEqual( + log_record.attributes[SpanAttributes.EXCEPTION_TYPE], + CustomException.__name__, + ) + self.assertTrue( + ".CustomObject" + in log_record.attributes[SpanAttributes.EXCEPTION_MESSAGE] + ) + stack_trace = log_record.attributes[ + SpanAttributes.EXCEPTION_STACKTRACE + ] + self.assertIsInstance(stack_trace, str) + self.assertTrue("Traceback" in stack_trace) + self.assertTrue("CustomException" in stack_trace) + self.assertTrue("CustomObject" in stack_trace) + self.assertTrue(__file__ in stack_trace) + def test_log_record_trace_correlation(self): processor, logger = set_up_test_logging(logging.WARNING) From 93d69ebcef018ae6440b0cfb8d11f368b374daf2 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 7 Apr 2025 10:28:42 +0200 Subject: [PATCH 2/2] Simplify test --- opentelemetry-sdk/tests/logs/test_handler.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index dcc434a2e99..1b62cc6c788 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -230,18 +230,12 @@ def test_log_exc_info_false(self): def test_log_record_exception_with_object_payload(self): processor, logger = set_up_test_logging(logging.ERROR) - class CustomObject: - pass - class CustomException(Exception): - def __init__(self, data): - self.data = data - def __str__(self): return "CustomException stringified" try: - raise CustomException(CustomObject()) + raise CustomException("CustomException message") except CustomException as exception: with self.assertLogs(level=logging.ERROR): logger.exception(exception) @@ -255,9 +249,9 @@ def __str__(self): log_record.attributes[SpanAttributes.EXCEPTION_TYPE], CustomException.__name__, ) - self.assertTrue( - ".CustomObject" - in log_record.attributes[SpanAttributes.EXCEPTION_MESSAGE] + self.assertEqual( + log_record.attributes[SpanAttributes.EXCEPTION_MESSAGE], + "CustomException message", ) stack_trace = log_record.attributes[ SpanAttributes.EXCEPTION_STACKTRACE @@ -265,7 +259,6 @@ def __str__(self): self.assertIsInstance(stack_trace, str) self.assertTrue("Traceback" in stack_trace) self.assertTrue("CustomException" in stack_trace) - self.assertTrue("CustomObject" in stack_trace) self.assertTrue(__file__ in stack_trace) def test_log_record_trace_correlation(self):