Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
40 changes: 27 additions & 13 deletions src/structlog/processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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[
Expand Down Expand Up @@ -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:
Expand All @@ -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,
)
)
Expand All @@ -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

Expand Down
51 changes: 51 additions & 0 deletions tests/processors/test_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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+"
)
Expand Down