diff --git a/CHANGELOG.md b/CHANGELOG.md index b23d71a7..409abb48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,11 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/ ## [Unreleased](https://github.com/hynek/structlog/compare/26.1.0...HEAD) +### Added + +- `structlog.processors.CallsiteParameterAdder` now also accepts a mapping of `{event_dict_key: CallsiteParameter}` for *parameters*, to use custom event dictionary keys instead of the default `CallsiteParameter` values (e.g. to conform to a third-party log ingest pipeline's expected field names). + [#553](https://github.com/hynek/structlog/issues/553) + ## [26.1.0](https://github.com/hynek/structlog/compare/25.5.0...26.1.0) - 2026-06-06 diff --git a/src/structlog/processors.py b/src/structlog/processors.py index 8eefaecc..c1ad7645 100644 --- a/src/structlog/processors.py +++ b/src/structlog/processors.py @@ -19,7 +19,7 @@ import threading import time -from collections.abc import Callable, Collection, Sequence +from collections.abc import Callable, Collection, Mapping, Sequence from types import FrameType, TracebackType from typing import ( Any, @@ -844,13 +844,17 @@ class CallsiteParameterAdder: from the `logging` module, and stack frames from modules with names that start with values in ``additional_ignores``, if it is specified. - The keys used for callsite parameters in the event dictionary are the - string values of `CallsiteParameter` enum members. + The keys used for callsite parameters in the event dictionary are, by + default, the string values of `CallsiteParameter` enum members. Pass a + mapping of ``{key: CallsiteParameter}`` instead of a plain collection to + use custom keys -- for example, to conform to a third-party log ingest + pipeline's expected field names. Args: parameters: A collection of `CallsiteParameter` values that should be added to - the event dictionary. + the event dictionary, or a mapping of the event dictionary key to + use for each `CallsiteParameter` value. additional_ignores: Additional names with which a stack frame's module name must not @@ -867,6 +871,9 @@ class CallsiteParameterAdder: `structlog.stdlib.ProcessorFormatter`. .. versionadded:: 21.5.0 + .. versionadded:: 26.2.0 + *parameters* also accepts a mapping of custom event dictionary keys to + `CallsiteParameter` values. """ _handlers: ClassVar[ @@ -907,7 +914,8 @@ class _RecordMapping(NamedTuple): def __init__( self, - parameters: Collection[CallsiteParameter] = _all_parameters, + parameters: Collection[CallsiteParameter] + | Mapping[str, CallsiteParameter] = _all_parameters, additional_ignores: list[str] | None = None, ) -> None: if additional_ignores is None: @@ -917,19 +925,25 @@ def __init__( # module should not be logging using structlog. self._additional_ignores = ["logging", *additional_ignores] self._active_handlers: list[ - tuple[CallsiteParameter, Callable[[str, FrameType], Any]] + tuple[str, Callable[[str, FrameType], Any]] ] = [] self._record_mappings: list[CallsiteParameterAdder._RecordMapping] = [] - for parameter in parameters: - self._active_handlers.append( - (parameter, self._handlers[parameter]) - ) + + if isinstance(parameters, Mapping): + items = list(parameters.items()) + else: + # Default: each parameter is keyed by its own enum value, exactly + # as before -- fully backwards-compatible with a plain collection. + items = [(parameter.value, parameter) for parameter in parameters] + + for key, parameter in items: + self._active_handlers.append((key, self._handlers[parameter])) if ( record_attr := self._record_attribute_map.get(parameter) ) is not None: self._record_mappings.append( self._RecordMapping( - parameter.value, + key, record_attr, ) ) @@ -953,8 +967,8 @@ def __call__( frame, module = _find_first_app_frame_and_name( additional_ignores=self._additional_ignores ) - for parameter, handler in self._active_handlers: - event_dict[parameter.value] = handler(module, frame) + for key, handler in self._active_handlers: + event_dict[key] = handler(module, frame) return event_dict diff --git a/tests/processors/test_processors.py b/tests/processors/test_processors.py index 52044cad..bfae047a 100644 --- a/tests/processors/test_processors.py +++ b/tests/processors/test_processors.py @@ -326,6 +326,57 @@ def test_all_parameters(self) -> None: } assert self.parameter_strings == self.get_callsite_parameters().keys() + def test_custom_keys_structlog_originated(self) -> None: + """ + Passing a mapping instead of a plain collection uses the mapping's + keys as the event dict keys, for structlog-originated events. + """ + processor = CallsiteParameterAdder( + parameters={ + "ln": CallsiteParameter.LINENO, + "fn": CallsiteParameter.FUNC_NAME, + } + ) + + event_dict = processor(None, None, {}) + + assert event_dict.keys() == {"ln", "fn"} + assert event_dict["fn"] == "test_custom_keys_structlog_originated" + + def test_custom_keys_foreign_log_record(self) -> None: + """ + Passing a mapping also uses its keys as the event dict keys when the + callsite parameters come from a foreign (stdlib logging) LogRecord. + """ + processor = CallsiteParameterAdder( + parameters={ + "ln": CallsiteParameter.LINENO, + "fn": CallsiteParameter.FUNC_NAME, + } + ) + record = logging.LogRecord( + "test", logging.INFO, "/path/foo.py", 42, "msg", None, None + ) + record.funcName = "a_function" + + event_dict = processor( + None, + None, + {"_record": record, "_from_structlog": False}, + ) + + assert event_dict["ln"] == 42 + assert event_dict["fn"] == "a_function" + + def test_custom_keys_pickleable(self) -> None: + """ + A ``CallsiteParameterAdder`` configured with a mapping of custom keys + can still be pickled. + """ + pickle.dumps( + CallsiteParameterAdder(parameters={"ln": CallsiteParameter.LINENO}) + ) + @pytest.mark.skipif( sys.version_info < (3, 11), reason="QUAL_NAME requires Python 3.11+" )