Skip to content

Commit a3038e6

Browse files
committed
Always use LogRecord.getMessage to get the log body
1 parent a428585 commit a3038e6

3 files changed

Lines changed: 40 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3636
([#4302](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4302))
3737
- `opentelemetry-instrumentation-grpc`: Fix bidirectional streaming RPCs raising `AttributeError: 'generator' object has no attribute 'add_done_callback'`
3838
([#4259](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4259))
39+
- `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
40+
([#4372](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4372))
3941

4042
### Breaking changes
4143

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

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

191172
# related to https://github.com/open-telemetry/opentelemetry-python/issues/3548
192173
# Severity Text = WARN as defined in 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: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,43 @@ def test_log_body_is_always_string_with_formatter(self):
451451

452452
logger.removeHandler(handler)
453453

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

0 commit comments

Comments
 (0)