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..1b62cc6c788 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,40 @@ 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 CustomException(Exception): + def __str__(self): + return "CustomException stringified" + + try: + raise CustomException("CustomException message") + 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.assertEqual( + log_record.attributes[SpanAttributes.EXCEPTION_MESSAGE], + "CustomException 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(__file__ in stack_trace) + def test_log_record_trace_correlation(self): processor, logger = set_up_test_logging(logging.WARNING)