Skip to content

Commit c87e487

Browse files
authored
Always use LogRecord.getMessage to get the log body (open-telemetry#4372)
* Always use `LogRecord.getMessage` to get the log body * SQUASHME: Fix lint issue, move changelog entry to breaking changes section * SQUASHME: update changelog text
1 parent 915165b commit c87e487

3 files changed

Lines changed: 43 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
106106

107107
- `opentelemetry-instrumentation-boto`: Remove instrumentation
108108
([#4303](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4303))
109+
- `opentelemetry-instrumentation-logging`: Use `LogRecord.getMessage()` to format and extract each log record's body text to more closely match the expected usage of the logging system. As a result, all OTel log record bodies are now always strings.
110+
Previously, if `LogRecord.msg` (which contains the format string) was set to a non-string object (e.g. `logger.warning(some_dict)`), the object was exported as-is to the OTLP body field. Now, `LogRecord.getMessage()` will convert it to to a string.
111+
If you are passing in non-strings as the format string argument and your backend is expecting them as-is, you will need to update accordingly.
112+
([#4372](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4372))
109113

110114
## Version 1.40.0/0.61b0 (2026-03-04)
111115

instrumentation/opentelemetry-instrumentation-logging/src/opentelemetry/instrumentation/logging/handler.py

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
get_logger,
2020
get_logger_provider,
2121
)
22-
from opentelemetry.attributes import _VALID_ANY_VALUE_TYPES
2322
from opentelemetry.context import get_current
2423
from opentelemetry.semconv._incubating.attributes import code_attributes
2524
from opentelemetry.semconv.attributes import exception_attributes
@@ -169,25 +168,7 @@ def _translate(self, record: logging.LogRecord) -> LogRecord:
169168
if self.formatter:
170169
body = self.format(record)
171170
else:
172-
# `record.getMessage()` uses `record.msg` as a template to format
173-
# `record.args` into. There is a special case in `record.getMessage()`
174-
# where it will only attempt formatting if args are provided,
175-
# otherwise, it just stringifies `record.msg`.
176-
#
177-
# Since the OTLP body field has a type of 'any' and the logging module
178-
# is sometimes used in such a way that objects incorrectly end up
179-
# set as record.msg, in those cases we would like to bypass
180-
# `record.getMessage()` completely and set the body to the object
181-
# itself instead of its string representation.
182-
# For more background, see: https://github.com/open-telemetry/opentelemetry-python/pull/4216
183-
if not record.args and not isinstance(record.msg, str):
184-
# if record.msg is not a value we can export, cast it to string
185-
if not isinstance(record.msg, _VALID_ANY_VALUE_TYPES):
186-
body = str(record.msg)
187-
else:
188-
body = record.msg
189-
else:
190-
body = record.getMessage()
171+
body = record.getMessage()
191172

192173
# Map Python log level names to OTel severity text as defined in
193174
# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/logs/data-model.md#displaying-severity

instrumentation/opentelemetry-instrumentation-logging/tests/test_handler.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,44 @@ def test_log_body_is_always_string_with_formatter(self):
441441

442442
logger.removeHandler(handler)
443443

444+
def test_simple_log_record_processor_custom_single_obj(self):
445+
"""
446+
Tests that logging a single non-string object uses getMessage
447+
"""
448+
processor, logger, handler = set_up_test_logging(logging.WARNING)
449+
450+
# NOTE: the behaviour of `record.getMessage` is detailed in the
451+
# `logging.Logger.debug` documentation:
452+
# > The msg is the message format string, and the args are the arguments
453+
# > which are merged into msg using the string formatting operator. [...]
454+
# > No % formatting operation is performed on msg when no args are supplied.
455+
456+
# This test uses the presence of '%s' in the first arg to determine if
457+
# formatting was applied
458+
459+
# string msg with no args - getMessage bypasses formatting and sets the string directly
460+
logger.warning("a string with a percent-s: %s") # pylint: disable=logging-too-few-args
461+
462+
# string msg with args - getMessage formats args into the msg
463+
logger.warning("a string with a percent-s: %s", "and arg")
464+
# non-string msg with args - getMessage stringifies msg and formats args into it
465+
logger.warning(["a non-string with a percent-s", "%s"], "and arg")
466+
# non-string msg with no args - getMessage stringifies the object and bypasses formatting
467+
logger.warning(["a non-string with a percent-s", "%s"])
468+
469+
logger.removeHandler(handler)
470+
471+
assert processor.emit_count() == 4
472+
expected = [
473+
("a string with a percent-s: %s"),
474+
("a string with a percent-s: and arg"),
475+
("['a non-string with a percent-s', 'and arg']"),
476+
("['a non-string with a percent-s', '%s']"),
477+
]
478+
for index, msg in enumerate(expected):
479+
record = processor.get_log_record(index)
480+
self.assertEqual(record.log_record.body, msg)
481+
444482
@patch.dict(os.environ, {"OTEL_SDK_DISABLED": "true"})
445483
def test_handler_root_logger_with_disabled_sdk_does_not_go_into_recursion_error(
446484
self,

0 commit comments

Comments
 (0)