1# SPDX-License-Identifier: MIT OR Apache-2.0
2# This file is dual licensed under the terms of the Apache License, Version
3# 2.0, and the MIT License. See the LICENSE file in the root of this
4# repository for complete details.
5
6"""
7Processors useful regardless of the logging framework.
8"""
9
10from __future__ import annotations
11
12import datetime
13import enum
14import json
15import logging
16import operator
17import os
18import sys
19import threading
20import time
21
22from types import FrameType, TracebackType
23from typing import (
24 Any,
25 Callable,
26 ClassVar,
27 Collection,
28 NamedTuple,
29 Sequence,
30 TextIO,
31 cast,
32)
33
34from ._frames import (
35 _find_first_app_frame_and_name,
36 _format_exception,
37 _format_stack,
38)
39from ._log_levels import NAME_TO_LEVEL, add_log_level
40from ._utils import get_processname
41from .tracebacks import ExceptionDictTransformer
42from .typing import (
43 EventDict,
44 ExceptionTransformer,
45 ExcInfo,
46 WrappedLogger,
47)
48
49
50__all__ = [
51 "NAME_TO_LEVEL", # some people rely on it being here
52 "CallsiteParameter",
53 "CallsiteParameterAdder",
54 "EventRenamer",
55 "ExceptionPrettyPrinter",
56 "JSONRenderer",
57 "KeyValueRenderer",
58 "LogfmtRenderer",
59 "StackInfoRenderer",
60 "TimeStamper",
61 "UnicodeDecoder",
62 "UnicodeEncoder",
63 "add_log_level",
64 "dict_tracebacks",
65 "format_exc_info",
66]
67
68
69class KeyValueRenderer:
70 """
71 Render ``event_dict`` as a list of ``Key=repr(Value)`` pairs.
72
73 Args:
74 sort_keys: Whether to sort keys when formatting.
75
76 key_order:
77 List of keys that should be rendered in this exact order. Missing
78 keys will be rendered as ``None``, extra keys depending on
79 *sort_keys* and the dict class.
80
81 drop_missing:
82 When ``True``, extra keys in *key_order* will be dropped rather
83 than rendered as ``None``.
84
85 repr_native_str:
86 When ``True``, :func:`repr()` is also applied to native strings.
87
88 .. versionadded:: 0.2.0 *key_order*
89 .. versionadded:: 16.1.0 *drop_missing*
90 .. versionadded:: 17.1.0 *repr_native_str*
91 """
92
93 def __init__(
94 self,
95 sort_keys: bool = False,
96 key_order: Sequence[str] | None = None,
97 drop_missing: bool = False,
98 repr_native_str: bool = True,
99 ):
100 self._ordered_items = _items_sorter(sort_keys, key_order, drop_missing)
101
102 if repr_native_str is True:
103 self._repr = repr
104 else:
105
106 def _repr(inst: Any) -> str:
107 if isinstance(inst, str):
108 return inst
109
110 return repr(inst)
111
112 self._repr = _repr
113
114 def __call__(
115 self, _: WrappedLogger, __: str, event_dict: EventDict
116 ) -> str:
117 return " ".join(
118 k + "=" + self._repr(v) for k, v in self._ordered_items(event_dict)
119 )
120
121
122class LogfmtRenderer:
123 """
124 Render ``event_dict`` using the logfmt_ format.
125
126 .. _logfmt: https://brandur.org/logfmt
127
128 Args:
129 sort_keys: Whether to sort keys when formatting.
130
131 key_order:
132 List of keys that should be rendered in this exact order. Missing
133 keys are rendered with empty values, extra keys depending on
134 *sort_keys* and the dict class.
135
136 drop_missing:
137 When ``True``, extra keys in *key_order* will be dropped rather
138 than rendered with empty values.
139
140 bool_as_flag:
141 When ``True``, render ``{"flag": True}`` as ``flag``, instead of
142 ``flag=true``. ``{"flag": False}`` is always rendered as
143 ``flag=false``.
144
145 Raises:
146 ValueError: If a key contains non-printable or whitespace characters.
147
148 .. versionadded:: 21.5.0
149 """
150
151 def __init__(
152 self,
153 sort_keys: bool = False,
154 key_order: Sequence[str] | None = None,
155 drop_missing: bool = False,
156 bool_as_flag: bool = True,
157 ):
158 self._ordered_items = _items_sorter(sort_keys, key_order, drop_missing)
159 self.bool_as_flag = bool_as_flag
160
161 def __call__(
162 self, _: WrappedLogger, __: str, event_dict: EventDict
163 ) -> str:
164 elements: list[str] = []
165 for key, value in self._ordered_items(event_dict):
166 if any(c <= " " for c in key):
167 msg = f'Invalid key: "{key}"'
168 raise ValueError(msg)
169
170 if value is None:
171 elements.append(f"{key}=")
172 continue
173
174 if isinstance(value, bool):
175 if self.bool_as_flag and value:
176 elements.append(f"{key}")
177 continue
178 value = "true" if value else "false"
179
180 value = str(value)
181 backslashes_need_escaping = (
182 " " in value or "=" in value or '"' in value
183 )
184 if backslashes_need_escaping and "\\" in value:
185 value = value.replace("\\", "\\\\")
186
187 value = value.replace('"', '\\"').replace("\n", "\\n")
188
189 if backslashes_need_escaping:
190 value = f'"{value}"'
191
192 elements.append(f"{key}={value}")
193
194 return " ".join(elements)
195
196
197def _items_sorter(
198 sort_keys: bool,
199 key_order: Sequence[str] | None,
200 drop_missing: bool,
201) -> Callable[[EventDict], list[tuple[str, object]]]:
202 """
203 Return a function to sort items from an ``event_dict``.
204
205 See `KeyValueRenderer` for an explanation of the parameters.
206 """
207 # Use an optimized version for each case.
208 if key_order and sort_keys:
209
210 def ordered_items(event_dict: EventDict) -> list[tuple[str, Any]]:
211 items = []
212 for key in key_order:
213 value = event_dict.pop(key, None)
214 if value is not None or not drop_missing:
215 items.append((key, value))
216
217 items += sorted(event_dict.items())
218
219 return items
220
221 elif key_order:
222
223 def ordered_items(event_dict: EventDict) -> list[tuple[str, Any]]:
224 items = []
225 for key in key_order:
226 value = event_dict.pop(key, None)
227 if value is not None or not drop_missing:
228 items.append((key, value))
229
230 items += event_dict.items()
231
232 return items
233
234 elif sort_keys:
235
236 def ordered_items(event_dict: EventDict) -> list[tuple[str, Any]]:
237 return sorted(event_dict.items())
238
239 else:
240 ordered_items = operator.methodcaller( # type: ignore[assignment]
241 "items"
242 )
243
244 return ordered_items
245
246
247class UnicodeEncoder:
248 """
249 Encode unicode values in ``event_dict``.
250
251 Args:
252 encoding: Encoding to encode to (default: ``"utf-8"``).
253
254 errors:
255 How to cope with encoding errors (default ``"backslashreplace"``).
256
257 Just put it in the processor chain before the renderer.
258
259 .. note:: Not very useful in a Python 3-only world.
260 """
261
262 _encoding: str
263 _errors: str
264
265 def __init__(
266 self, encoding: str = "utf-8", errors: str = "backslashreplace"
267 ) -> None:
268 self._encoding = encoding
269 self._errors = errors
270
271 def __call__(
272 self, logger: WrappedLogger, name: str, event_dict: EventDict
273 ) -> EventDict:
274 for key, value in event_dict.items():
275 if isinstance(value, str):
276 event_dict[key] = value.encode(self._encoding, self._errors)
277
278 return event_dict
279
280
281class UnicodeDecoder:
282 """
283 Decode byte string values in ``event_dict``.
284
285 Args:
286 encoding: Encoding to decode from (default: ``"utf-8"``).
287
288 errors: How to cope with encoding errors (default: ``"replace"``).
289
290 Useful to prevent ``b"abc"`` being rendered as as ``'b"abc"'``.
291
292 Just put it in the processor chain before the renderer.
293
294 .. versionadded:: 15.4.0
295 """
296
297 _encoding: str
298 _errors: str
299
300 def __init__(
301 self, encoding: str = "utf-8", errors: str = "replace"
302 ) -> None:
303 self._encoding = encoding
304 self._errors = errors
305
306 def __call__(
307 self, logger: WrappedLogger, name: str, event_dict: EventDict
308 ) -> EventDict:
309 for key, value in event_dict.items():
310 if isinstance(value, bytes):
311 event_dict[key] = value.decode(self._encoding, self._errors)
312
313 return event_dict
314
315
316class JSONRenderer:
317 """
318 Render the ``event_dict`` using ``serializer(event_dict, **dumps_kw)``.
319
320 Args:
321 dumps_kw:
322 Are passed unmodified to *serializer*. If *default* is passed, it
323 will disable support for ``__structlog__``-based serialization.
324
325 serializer:
326 A :func:`json.dumps`-compatible callable that will be used to
327 format the string. This can be used to use alternative JSON
328 encoders (default: :func:`json.dumps`).
329
330 .. seealso:: :doc:`performance` for examples.
331
332 .. versionadded:: 0.2.0 Support for ``__structlog__`` serialization method.
333 .. versionadded:: 15.4.0 *serializer* parameter.
334 .. versionadded:: 18.2.0
335 Serializer's *default* parameter can be overwritten now.
336 """
337
338 def __init__(
339 self,
340 serializer: Callable[..., str | bytes] = json.dumps,
341 **dumps_kw: Any,
342 ) -> None:
343 dumps_kw.setdefault("default", _json_fallback_handler)
344 self._dumps_kw = dumps_kw
345 self._dumps = serializer
346
347 def __call__(
348 self, logger: WrappedLogger, name: str, event_dict: EventDict
349 ) -> str | bytes:
350 """
351 The return type of this depends on the return type of self._dumps.
352 """
353 return self._dumps(event_dict, **self._dumps_kw)
354
355
356def _json_fallback_handler(obj: Any) -> Any:
357 """
358 Serialize custom datatypes and pass the rest to __structlog__ & repr().
359 """
360 # circular imports :(
361 from structlog.threadlocal import _ThreadLocalDictWrapper
362
363 if isinstance(obj, _ThreadLocalDictWrapper):
364 return obj._dict
365
366 try:
367 return obj.__structlog__()
368 except AttributeError:
369 return repr(obj)
370
371
372class ExceptionRenderer:
373 """
374 Replace an ``exc_info`` field with an ``exception`` field which is rendered
375 by *exception_formatter*.
376
377 The contents of the ``exception`` field depends on the return value of the
378 *exception_formatter* that is passed:
379
380 - The default produces a formatted string via Python's built-in traceback
381 formatting (this is :obj:`.format_exc_info`).
382 - If you pass a :class:`~structlog.tracebacks.ExceptionDictTransformer`, it
383 becomes a list of stack dicts that can be serialized to JSON.
384
385 If *event_dict* contains the key ``exc_info``, there are three possible
386 behaviors:
387
388 1. If the value is a tuple, render it into the key ``exception``.
389 2. If the value is an Exception render it into the key ``exception``.
390 3. If the value true but no tuple, obtain exc_info ourselves and render
391 that.
392
393 If there is no ``exc_info`` key, the *event_dict* is not touched. This
394 behavior is analog to the one of the stdlib's logging.
395
396 Args:
397 exception_formatter:
398 A callable that is used to format the exception from the
399 ``exc_info`` field into the ``exception`` field.
400
401 .. seealso::
402 :doc:`exceptions` for a broader explanation of *structlog*'s exception
403 features.
404
405 .. versionadded:: 22.1.0
406 """
407
408 def __init__(
409 self,
410 exception_formatter: ExceptionTransformer = _format_exception,
411 ) -> None:
412 self.format_exception = exception_formatter
413
414 def __call__(
415 self, logger: WrappedLogger, name: str, event_dict: EventDict
416 ) -> EventDict:
417 exc_info = _figure_out_exc_info(event_dict.pop("exc_info", None))
418 if exc_info:
419 event_dict["exception"] = self.format_exception(exc_info)
420
421 return event_dict
422
423
424format_exc_info = ExceptionRenderer()
425"""
426Replace an ``exc_info`` field with an ``exception`` string field using Python's
427built-in traceback formatting.
428
429If *event_dict* contains the key ``exc_info``, there are three possible
430behaviors:
431
4321. If the value is a tuple, render it into the key ``exception``.
4332. If the value is an Exception render it into the key ``exception``.
4343. If the value is true but no tuple, obtain exc_info ourselves and render
435 that.
436
437If there is no ``exc_info`` key, the *event_dict* is not touched. This behavior
438is analog to the one of the stdlib's logging.
439
440.. seealso::
441 :doc:`exceptions` for a broader explanation of *structlog*'s exception
442 features.
443"""
444
445dict_tracebacks = ExceptionRenderer(ExceptionDictTransformer())
446"""
447Replace an ``exc_info`` field with an ``exception`` field containing structured
448tracebacks suitable for, e.g., JSON output.
449
450It is a shortcut for :class:`ExceptionRenderer` with a
451:class:`~structlog.tracebacks.ExceptionDictTransformer`.
452
453The treatment of the ``exc_info`` key is identical to `format_exc_info`.
454
455.. versionadded:: 22.1.0
456
457.. seealso::
458 :doc:`exceptions` for a broader explanation of *structlog*'s exception
459 features.
460"""
461
462
463class TimeStamper:
464 """
465 Add a timestamp to ``event_dict``.
466
467 Args:
468 fmt:
469 strftime format string, or ``"iso"`` for `ISO 8601
470 <https://en.wikipedia.org/wiki/ISO_8601>`_, or `None` for a `UNIX
471 timestamp <https://en.wikipedia.org/wiki/Unix_time>`_.
472
473 utc: Whether timestamp should be in UTC or local time.
474
475 key: Target key in *event_dict* for added timestamps.
476
477 .. versionchanged:: 19.2.0 Can be pickled now.
478 """
479
480 __slots__ = ("_stamper", "fmt", "key", "utc")
481
482 def __init__(
483 self,
484 fmt: str | None = None,
485 utc: bool = True,
486 key: str = "timestamp",
487 ) -> None:
488 self.fmt, self.utc, self.key = fmt, utc, key
489
490 self._stamper = _make_stamper(fmt, utc, key)
491
492 def __call__(
493 self, logger: WrappedLogger, name: str, event_dict: EventDict
494 ) -> EventDict:
495 return self._stamper(event_dict)
496
497 def __getstate__(self) -> dict[str, Any]:
498 return {"fmt": self.fmt, "utc": self.utc, "key": self.key}
499
500 def __setstate__(self, state: dict[str, Any]) -> None:
501 self.fmt = state["fmt"]
502 self.utc = state["utc"]
503 self.key = state["key"]
504
505 self._stamper = _make_stamper(**state)
506
507
508def _make_stamper(
509 fmt: str | None, utc: bool, key: str
510) -> Callable[[EventDict], EventDict]:
511 """
512 Create a stamper function.
513 """
514 if fmt is None and not utc:
515 msg = "UNIX timestamps are always UTC."
516 raise ValueError(msg)
517
518 now: Callable[[], datetime.datetime]
519
520 if utc:
521
522 def now() -> datetime.datetime:
523 return datetime.datetime.now(tz=datetime.timezone.utc)
524
525 else:
526
527 def now() -> datetime.datetime:
528 # We don't need the TZ for our own formatting. We add it only for
529 # user-defined formats later.
530 return datetime.datetime.now() # noqa: DTZ005
531
532 if fmt is None:
533
534 def stamper_unix(event_dict: EventDict) -> EventDict:
535 event_dict[key] = time.time()
536
537 return event_dict
538
539 return stamper_unix
540
541 if fmt.upper() == "ISO":
542
543 def stamper_iso_local(event_dict: EventDict) -> EventDict:
544 event_dict[key] = now().isoformat()
545 return event_dict
546
547 def stamper_iso_utc(event_dict: EventDict) -> EventDict:
548 event_dict[key] = now().isoformat().replace("+00:00", "Z")
549 return event_dict
550
551 if utc:
552 return stamper_iso_utc
553
554 return stamper_iso_local
555
556 def stamper_fmt_local(event_dict: EventDict) -> EventDict:
557 event_dict[key] = now().astimezone().strftime(fmt)
558 return event_dict
559
560 def stamper_fmt_utc(event_dict: EventDict) -> EventDict:
561 event_dict[key] = now().strftime(fmt)
562 return event_dict
563
564 if utc:
565 return stamper_fmt_utc
566
567 return stamper_fmt_local
568
569
570class MaybeTimeStamper:
571 """
572 A timestamper that only adds a timestamp if there is none.
573
574 This allows you to overwrite the ``timestamp`` key in the event dict for
575 example when the event is coming from another system.
576
577 It takes the same arguments as `TimeStamper`.
578
579 .. versionadded:: 23.2.0
580 """
581
582 __slots__ = ("stamper",)
583
584 def __init__(
585 self,
586 fmt: str | None = None,
587 utc: bool = True,
588 key: str = "timestamp",
589 ):
590 self.stamper = TimeStamper(fmt=fmt, utc=utc, key=key)
591
592 def __call__(
593 self, logger: WrappedLogger, name: str, event_dict: EventDict
594 ) -> EventDict:
595 if "timestamp" not in event_dict:
596 return self.stamper(logger, name, event_dict)
597
598 return event_dict
599
600
601def _figure_out_exc_info(v: Any) -> ExcInfo | None:
602 """
603 Try to convert *v* into an ``exc_info`` tuple.
604
605 Return ``None`` if *v* does not represent an exception or if there is no
606 current exception.
607 """
608 if isinstance(v, BaseException):
609 return (v.__class__, v, v.__traceback__)
610
611 if isinstance(v, tuple) and len(v) == 3:
612 has_type = isinstance(v[0], type) and issubclass(v[0], BaseException)
613 has_exc = isinstance(v[1], BaseException)
614 has_tb = v[2] is None or isinstance(v[2], TracebackType)
615 if has_type and has_exc and has_tb:
616 return v
617
618 if v:
619 result = sys.exc_info()
620 if result == (None, None, None):
621 return None
622 return cast(ExcInfo, result)
623
624 return None
625
626
627class ExceptionPrettyPrinter:
628 """
629 Pretty print exceptions rendered by *exception_formatter* and remove them
630 from the ``event_dict``.
631
632 Args:
633 file: Target file for output (default: ``sys.stdout``).
634 exception_formatter:
635 A callable that is used to format the exception from the
636 ``exc_info`` field into the ``exception`` field.
637
638 This processor is mostly for development and testing so you can read
639 exceptions properly formatted.
640
641 It behaves like `format_exc_info`, except that it removes the exception data
642 from the event dictionary after printing it using the passed
643 *exception_formatter*, which defaults to Python's built-in traceback formatting.
644
645 It's tolerant to having `format_exc_info` in front of itself in the
646 processor chain but doesn't require it. In other words, it handles both
647 ``exception`` as well as ``exc_info`` keys.
648
649 .. versionadded:: 0.4.0
650
651 .. versionchanged:: 16.0.0
652 Added support for passing exceptions as ``exc_info`` on Python 3.
653
654 .. versionchanged:: 25.4.0
655 Fixed *exception_formatter* so that it overrides the default if set.
656 """
657
658 def __init__(
659 self,
660 file: TextIO | None = None,
661 exception_formatter: ExceptionTransformer = _format_exception,
662 ) -> None:
663 self.format_exception = exception_formatter
664 if file is not None:
665 self._file = file
666 else:
667 self._file = sys.stdout
668
669 def __call__(
670 self, logger: WrappedLogger, name: str, event_dict: EventDict
671 ) -> EventDict:
672 exc = event_dict.pop("exception", None)
673 if exc is None:
674 exc_info = _figure_out_exc_info(event_dict.pop("exc_info", None))
675 if exc_info:
676 exc = self.format_exception(exc_info)
677
678 if exc:
679 print(exc, file=self._file)
680
681 return event_dict
682
683
684class StackInfoRenderer:
685 """
686 Add stack information with key ``stack`` if ``stack_info`` is `True`.
687
688 Useful when you want to attach a stack dump to a log entry without
689 involving an exception and works analogously to the *stack_info* argument
690 of the Python standard library logging.
691
692 Args:
693 additional_ignores:
694 By default, stack frames coming from *structlog* are ignored. With
695 this argument you can add additional names that are ignored, before
696 the stack starts being rendered. They are matched using
697 ``startswith()``, so they don't have to match exactly. The names
698 are used to find the first relevant name, therefore once a frame is
699 found that doesn't start with *structlog* or one of
700 *additional_ignores*, **no filtering** is applied to subsequent
701 frames.
702
703 .. versionadded:: 0.4.0
704 .. versionadded:: 22.1.0 *additional_ignores*
705 """
706
707 __slots__ = ("_additional_ignores",)
708
709 def __init__(self, additional_ignores: list[str] | None = None) -> None:
710 self._additional_ignores = additional_ignores
711
712 def __call__(
713 self, logger: WrappedLogger, name: str, event_dict: EventDict
714 ) -> EventDict:
715 if event_dict.pop("stack_info", None):
716 event_dict["stack"] = _format_stack(
717 _find_first_app_frame_and_name(self._additional_ignores)[0]
718 )
719
720 return event_dict
721
722
723class CallsiteParameter(enum.Enum):
724 """
725 Callsite parameters that can be added to an event dictionary with the
726 `structlog.processors.CallsiteParameterAdder` processor class.
727
728 The string values of the members of this enum will be used as the keys for
729 the callsite parameters in the event dictionary.
730
731 .. versionadded:: 21.5.0
732 """
733
734 #: The full path to the python source file of the callsite.
735 PATHNAME = "pathname"
736 #: The basename part of the full path to the python source file of the
737 #: callsite.
738 FILENAME = "filename"
739 #: The python module the callsite was in. This mimics the module attribute
740 #: of `logging.LogRecord` objects and will be the basename, without
741 #: extension, of the full path to the python source file of the callsite.
742 MODULE = "module"
743 #: The name of the function that the callsite was in.
744 FUNC_NAME = "func_name"
745 #: The line number of the callsite.
746 LINENO = "lineno"
747 #: The ID of the thread the callsite was executed in.
748 THREAD = "thread"
749 #: The name of the thread the callsite was executed in.
750 THREAD_NAME = "thread_name"
751 #: The ID of the process the callsite was executed in.
752 PROCESS = "process"
753 #: The name of the process the callsite was executed in.
754 PROCESS_NAME = "process_name"
755
756
757def _get_callsite_pathname(module: str, frame: FrameType) -> Any:
758 return frame.f_code.co_filename
759
760
761def _get_callsite_filename(module: str, frame: FrameType) -> Any:
762 return os.path.basename(frame.f_code.co_filename)
763
764
765def _get_callsite_module(module: str, frame: FrameType) -> Any:
766 return os.path.splitext(os.path.basename(frame.f_code.co_filename))[0]
767
768
769def _get_callsite_func_name(module: str, frame: FrameType) -> Any:
770 return frame.f_code.co_name
771
772
773def _get_callsite_lineno(module: str, frame: FrameType) -> Any:
774 return frame.f_lineno
775
776
777def _get_callsite_thread(module: str, frame: FrameType) -> Any:
778 return threading.get_ident()
779
780
781def _get_callsite_thread_name(module: str, frame: FrameType) -> Any:
782 return threading.current_thread().name
783
784
785def _get_callsite_process(module: str, frame: FrameType) -> Any:
786 return os.getpid()
787
788
789def _get_callsite_process_name(module: str, frame: FrameType) -> Any:
790 return get_processname()
791
792
793class CallsiteParameterAdder:
794 """
795 Adds parameters of the callsite that an event dictionary originated from to
796 the event dictionary. This processor can be used to enrich events
797 dictionaries with information such as the function name, line number and
798 filename that an event dictionary originated from.
799
800 If the event dictionary has an embedded `logging.LogRecord` object and did
801 not originate from *structlog* then the callsite information will be
802 determined from the `logging.LogRecord` object. For event dictionaries
803 without an embedded `logging.LogRecord` object the callsite will be
804 determined from the stack trace, ignoring all intra-structlog calls, calls
805 from the `logging` module, and stack frames from modules with names that
806 start with values in ``additional_ignores``, if it is specified.
807
808 The keys used for callsite parameters in the event dictionary are the
809 string values of `CallsiteParameter` enum members.
810
811 Args:
812 parameters:
813 A collection of `CallsiteParameter` values that should be added to
814 the event dictionary.
815
816 additional_ignores:
817 Additional names with which a stack frame's module name must not
818 start for it to be considered when determening the callsite.
819
820 .. note::
821
822 When used with `structlog.stdlib.ProcessorFormatter` the most efficient
823 configuration is to either use this processor in ``foreign_pre_chain``
824 of `structlog.stdlib.ProcessorFormatter` and in ``processors`` of
825 `structlog.configure`, or to use it in ``processors`` of
826 `structlog.stdlib.ProcessorFormatter` without using it in
827 ``processors`` of `structlog.configure` and ``foreign_pre_chain`` of
828 `structlog.stdlib.ProcessorFormatter`.
829
830 .. versionadded:: 21.5.0
831 """
832
833 _handlers: ClassVar[
834 dict[CallsiteParameter, Callable[[str, FrameType], Any]]
835 ] = {
836 CallsiteParameter.PATHNAME: _get_callsite_pathname,
837 CallsiteParameter.FILENAME: _get_callsite_filename,
838 CallsiteParameter.MODULE: _get_callsite_module,
839 CallsiteParameter.FUNC_NAME: _get_callsite_func_name,
840 CallsiteParameter.LINENO: _get_callsite_lineno,
841 CallsiteParameter.THREAD: _get_callsite_thread,
842 CallsiteParameter.THREAD_NAME: _get_callsite_thread_name,
843 CallsiteParameter.PROCESS: _get_callsite_process,
844 CallsiteParameter.PROCESS_NAME: _get_callsite_process_name,
845 }
846 _record_attribute_map: ClassVar[dict[CallsiteParameter, str]] = {
847 CallsiteParameter.PATHNAME: "pathname",
848 CallsiteParameter.FILENAME: "filename",
849 CallsiteParameter.MODULE: "module",
850 CallsiteParameter.FUNC_NAME: "funcName",
851 CallsiteParameter.LINENO: "lineno",
852 CallsiteParameter.THREAD: "thread",
853 CallsiteParameter.THREAD_NAME: "threadName",
854 CallsiteParameter.PROCESS: "process",
855 CallsiteParameter.PROCESS_NAME: "processName",
856 }
857
858 _all_parameters: ClassVar[set[CallsiteParameter]] = set(CallsiteParameter)
859
860 class _RecordMapping(NamedTuple):
861 event_dict_key: str
862 record_attribute: str
863
864 __slots__ = ("_active_handlers", "_additional_ignores", "_record_mappings")
865
866 def __init__(
867 self,
868 parameters: Collection[CallsiteParameter] = _all_parameters,
869 additional_ignores: list[str] | None = None,
870 ) -> None:
871 if additional_ignores is None:
872 additional_ignores = []
873 # Ignore stack frames from the logging module. They will occur if this
874 # processor is used in ProcessorFormatter, and additionally the logging
875 # module should not be logging using structlog.
876 self._additional_ignores = ["logging", *additional_ignores]
877 self._active_handlers: list[
878 tuple[CallsiteParameter, Callable[[str, FrameType], Any]]
879 ] = []
880 self._record_mappings: list[CallsiteParameterAdder._RecordMapping] = []
881 for parameter in parameters:
882 self._active_handlers.append(
883 (parameter, self._handlers[parameter])
884 )
885 self._record_mappings.append(
886 self._RecordMapping(
887 parameter.value,
888 self._record_attribute_map[parameter],
889 )
890 )
891
892 def __call__(
893 self, logger: logging.Logger, name: str, event_dict: EventDict
894 ) -> EventDict:
895 record: logging.LogRecord | None = event_dict.get("_record")
896 from_structlog: bool | None = event_dict.get("_from_structlog")
897 # If the event dictionary has a record, but it comes from structlog,
898 # then the callsite parameters of the record will not be correct.
899 if record is not None and not from_structlog:
900 for mapping in self._record_mappings:
901 event_dict[mapping.event_dict_key] = record.__dict__[
902 mapping.record_attribute
903 ]
904 else:
905 frame, module = _find_first_app_frame_and_name(
906 additional_ignores=self._additional_ignores
907 )
908 for parameter, handler in self._active_handlers:
909 event_dict[parameter.value] = handler(module, frame)
910 return event_dict
911
912
913class EventRenamer:
914 r"""
915 Rename the ``event`` key in event dicts.
916
917 This is useful if you want to use consistent log message keys across
918 platforms and/or use the ``event`` key for something custom.
919
920 .. warning::
921
922 It's recommended to put this processor right before the renderer, since
923 some processors may rely on the presence and meaning of the ``event``
924 key.
925
926 Args:
927 to: Rename ``event_dict["event"]`` to ``event_dict[to]``
928
929 replace_by:
930 Rename ``event_dict[replace_by]`` to ``event_dict["event"]``.
931 *replace_by* missing from ``event_dict`` is handled gracefully.
932
933 .. versionadded:: 22.1.0
934
935 See also the :ref:`rename-event` recipe.
936 """
937
938 def __init__(self, to: str, replace_by: str | None = None):
939 self.to = to
940 self.replace_by = replace_by
941
942 def __call__(
943 self, logger: logging.Logger, name: str, event_dict: EventDict
944 ) -> EventDict:
945 event = event_dict.pop("event")
946 event_dict[self.to] = event
947
948 if self.replace_by is not None:
949 replace_by = event_dict.pop(self.replace_by, None)
950 if replace_by is not None:
951 event_dict["event"] = replace_by
952
953 return event_dict