diff --git a/src/structlog/stdlib.py b/src/structlog/stdlib.py index 2906598d..827137b4 100644 --- a/src/structlog/stdlib.py +++ b/src/structlog/stdlib.py @@ -12,6 +12,7 @@ from __future__ import annotations import asyncio +import contextlib import contextvars import functools import logging @@ -802,7 +803,8 @@ def __call__( if len(args) == 1 and isinstance(args[0], dict) and args[0]: args = args[0] - event_dict["event"] %= args + with contextlib.suppress(TypeError, ValueError): + event_dict["event"] %= args if self.remove_positional_args and args is not None: del event_dict["positional_args"] diff --git a/tests/test_stdlib.py b/tests/test_stdlib.py index 655cadb4..6bd054fa 100644 --- a/tests/test_stdlib.py +++ b/tests/test_stdlib.py @@ -518,6 +518,27 @@ def test_args_removed_if_empty(self): assert {} == formatter(None, None, {"positional_args": ()}) + def test_graceful_failure_on_formatting_error(self): + """ + If positional arguments do not match the formatting placeholders + in the event string, PositionalArgumentsFormatter should not crash + with a TypeError or ValueError. It should leave the event string + unformatted. + + Regression test for https://github.com/hynek/structlog/issues/258. + """ + formatter = PositionalArgumentsFormatter() + + # Mismatched placeholders (event has no %s but args are passed) + event_dict = formatter( + None, + None, + {"event": "Info message", "positional_args": ("x",)}, + ) + + assert "Info message" == event_dict["event"] + assert "positional_args" not in event_dict + class TestAddLogLevelNumber: @pytest.mark.parametrize(("level", "number"), NAME_TO_LEVEL.items())