Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

opentelemetry-sdk: fix serialization of objects in log handler #4528

Merged
merged 2 commits into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
13 changes: 12 additions & 1 deletion opentelemetry-api/src/opentelemetry/attributes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()

Expand Down
42 changes: 42 additions & 0 deletions opentelemetry-sdk/tests/logs/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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(
"<tests.logs.test_handler.TestLoggingHandler.test_log_record_exception_with_object_payload.<locals>.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)

Expand Down
Loading