Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/structlog/stdlib.py: 40%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is dual licensed under the terms of the Apache License, Version
2# 2.0, and the MIT License. See the LICENSE file in the root of this
3# repository for complete details.
5"""
6Processors and helpers specific to the :mod:`logging` module from the `Python
7standard library <https://docs.python.org/>`_.
9See also :doc:`structlog's standard library support <standard-library>`.
10"""
12from __future__ import annotations
14import asyncio
15import contextvars
16import functools
17import logging
18import sys
19import threading
20import warnings
22from collections.abc import Callable, Collection, Iterable, Sequence
23from functools import partial
24from typing import Any, cast
27if sys.version_info >= (3, 11):
28 from typing import Self
29else:
30 from typing_extensions import Self
33from . import _config
34from ._base import BoundLoggerBase
35from ._frames import _find_first_app_frame_and_name, _format_stack
36from ._log_levels import LEVEL_TO_NAME, NAME_TO_LEVEL, add_log_level
37from .contextvars import (
38 _ASYNC_CALLING_STACK,
39 _ASYNC_CALLING_THREAD,
40 merge_contextvars,
41)
42from .exceptions import DropEvent
43from .processors import StackInfoRenderer
44from .typing import (
45 Context,
46 EventDict,
47 ExcInfo,
48 Processor,
49 ProcessorReturnValue,
50 WrappedLogger,
51)
54__all__ = [
55 "BoundLogger",
56 "ExtraAdder",
57 "LoggerFactory",
58 "PositionalArgumentsFormatter",
59 "ProcessorFormatter",
60 "add_log_level",
61 "add_log_level_number",
62 "add_logger_name",
63 "filter_by_level",
64 "get_logger",
65 "recreate_defaults",
66 "render_to_log_args_and_kwargs",
67 "render_to_log_kwargs",
68]
71def recreate_defaults(*, log_level: int | None = logging.NOTSET) -> None:
72 """
73 Recreate defaults on top of standard library's logging.
75 The output looks the same, but goes through `logging`.
77 As with vanilla defaults, the backwards-compatibility guarantees don't
78 apply to the settings applied here.
80 Args:
81 log_level:
82 If `None`, don't configure standard library logging **at all**.
84 Otherwise configure it to log to `sys.stdout` at *log_level*
85 (``logging.NOTSET`` being the default).
87 If you need more control over `logging`, pass `None` here and
88 configure it yourself.
90 .. versionadded:: 22.1.0
91 .. versionchanged:: 23.3.0 Added `add_logger_name`.
92 .. versionchanged:: 25.1.0 Added `PositionalArgumentsFormatter`.
93 """
94 if log_level is not None:
95 kw = {"force": True}
97 logging.basicConfig(
98 format="%(message)s",
99 stream=sys.stdout,
100 level=log_level,
101 **kw, # type: ignore[call-overload]
102 )
104 _config.reset_defaults()
105 _config.configure(
106 processors=[
107 PositionalArgumentsFormatter(), # handled by native loggers
108 merge_contextvars,
109 add_log_level,
110 add_logger_name,
111 StackInfoRenderer(),
112 _config._BUILTIN_DEFAULT_PROCESSORS[-2], # TimeStamper
113 _config._BUILTIN_DEFAULT_PROCESSORS[-1], # ConsoleRenderer
114 ],
115 wrapper_class=BoundLogger,
116 logger_factory=LoggerFactory(),
117 )
120_SENTINEL = object()
123class _FixedFindCallerLogger(logging.Logger):
124 """
125 Change the behavior of `logging.Logger.findCaller` to cope with
126 *structlog*'s extra frames.
127 """
129 def findCaller(
130 self, stack_info: bool = False, stacklevel: int = 1
131 ) -> tuple[str, int, str, str | None]:
132 """
133 Finds the first caller frame outside of structlog so that the caller
134 info is populated for wrapping stdlib.
136 This logger gets set as the default one when using LoggerFactory.
137 """
138 sinfo: str | None
139 # stdlib logging passes stacklevel=1 from log methods like .warning(),
140 # but we've already skipped those frames by ignoring "logging", so we
141 # need to adjust stacklevel down by 1. We need to manually drop
142 # logging frames, because there's cases where we call logging methods
143 # from within structlog and the stacklevel offsets don't work anymore.
144 adjusted_stacklevel = max(0, stacklevel - 1) if stacklevel else None
145 f, _name = _find_first_app_frame_and_name(
146 ["logging"], stacklevel=adjusted_stacklevel
147 )
148 sinfo = _format_stack(f) if stack_info else None
150 return f.f_code.co_filename, f.f_lineno, f.f_code.co_name, sinfo
153class BoundLogger(BoundLoggerBase):
154 """
155 Python Standard Library version of `structlog.BoundLogger`.
157 Works exactly like the generic one except that it takes advantage of
158 knowing the logging methods in advance.
160 Use it like::
162 structlog.configure(
163 wrapper_class=structlog.stdlib.BoundLogger,
164 )
166 It also contains a bunch of properties that pass-through to the wrapped
167 `logging.Logger` which should make it work as a drop-in replacement.
169 .. versionadded:: 23.1.0
170 Async variants `alog()`, `adebug()`, `ainfo()`, and so forth.
172 .. versionchanged:: 24.2.0
173 Callsite parameters are now also collected by
174 `structlog.processors.CallsiteParameterAdder` for async log methods.
175 """
177 _logger: logging.Logger
179 def bind(self, **new_values: Any) -> Self:
180 """
181 Return a new logger with *new_values* added to the existing ones.
182 """
183 return super().bind(**new_values)
185 def unbind(self, *keys: str) -> Self:
186 """
187 Return a new logger with *keys* removed from the context.
189 Raises:
190 KeyError: If the key is not part of the context.
191 """
192 return super().unbind(*keys)
194 def try_unbind(self, *keys: str) -> Self:
195 """
196 Like :meth:`unbind`, but best effort: missing keys are ignored.
198 .. versionadded:: 18.2.0
199 """
200 return super().try_unbind(*keys)
202 def new(self, **new_values: Any) -> Self:
203 """
204 Clear context and binds *initial_values* using `bind`.
206 Only necessary with dict implementations that keep global state like
207 those wrapped by `structlog.threadlocal.wrap_dict` when threads
208 are reused.
209 """
210 return super().new(**new_values)
212 def debug(self, event: str | None = None, *args: Any, **kw: Any) -> Any:
213 """
214 Process event and call `logging.Logger.debug` with the result.
215 """
216 return self._proxy_to_logger("debug", event, *args, **kw)
218 def info(self, event: str | None = None, *args: Any, **kw: Any) -> Any:
219 """
220 Process event and call `logging.Logger.info` with the result.
221 """
222 return self._proxy_to_logger("info", event, *args, **kw)
224 def warning(self, event: str | None = None, *args: Any, **kw: Any) -> Any:
225 """
226 Process event and call `logging.Logger.warning` with the result.
227 """
228 return self._proxy_to_logger("warning", event, *args, **kw)
230 warn = warning
232 def error(self, event: str | None = None, *args: Any, **kw: Any) -> Any:
233 """
234 Process event and call `logging.Logger.error` with the result.
235 """
236 return self._proxy_to_logger("error", event, *args, **kw)
238 def critical(self, event: str | None = None, *args: Any, **kw: Any) -> Any:
239 """
240 Process event and call `logging.Logger.critical` with the result.
241 """
242 return self._proxy_to_logger("critical", event, *args, **kw)
244 def fatal(self, event: str | None = None, *args: Any, **kw: Any) -> Any:
245 """
246 Process event and call `logging.Logger.critical` with the result.
247 """
248 return self._proxy_to_logger("critical", event, *args, **kw)
250 def exception(
251 self, event: str | None = None, *args: Any, **kw: Any
252 ) -> Any:
253 """
254 Process event and call `logging.Logger.exception` with the result,
255 after setting ``exc_info`` to `True` if it's not already set.
256 """
257 kw.setdefault("exc_info", True)
258 return self._proxy_to_logger("exception", event, *args, **kw)
260 def log(
261 self, level: int, event: str | None = None, *args: Any, **kw: Any
262 ) -> Any:
263 """
264 Process *event* and call the appropriate logging method depending on
265 *level*.
266 """
267 return self._proxy_to_logger(LEVEL_TO_NAME[level], event, *args, **kw)
269 def _proxy_to_logger(
270 self,
271 method_name: str,
272 event: str | None = None,
273 *event_args: str,
274 **event_kw: Any,
275 ) -> Any:
276 """
277 Propagate a method call to the wrapped logger.
279 This is the same as the superclass implementation, except that
280 it also preserves positional arguments in the ``event_dict`` so
281 that the stdlib's support for format strings can be used.
282 """
283 if event_args:
284 event_kw["positional_args"] = event_args
286 return super()._proxy_to_logger(method_name, event=event, **event_kw)
288 # Pass-through attributes and methods to mimic the stdlib's logger
289 # interface.
291 @property
292 def name(self) -> str:
293 """
294 Returns :attr:`logging.Logger.name`
295 """
296 return self._logger.name
298 @property
299 def level(self) -> int:
300 """
301 Returns :attr:`logging.Logger.level`
302 """
303 return self._logger.level
305 @property
306 def parent(self) -> Any:
307 """
308 Returns :attr:`logging.Logger.parent`
309 """
310 return self._logger.parent
312 @property
313 def propagate(self) -> bool:
314 """
315 Returns :attr:`logging.Logger.propagate`
316 """
317 return self._logger.propagate
319 @property
320 def handlers(self) -> Any:
321 """
322 Returns :attr:`logging.Logger.handlers`
323 """
324 return self._logger.handlers
326 @property
327 def disabled(self) -> int:
328 """
329 Returns :attr:`logging.Logger.disabled`
330 """
331 return self._logger.disabled
333 def setLevel(self, level: int) -> None:
334 """
335 Calls :meth:`logging.Logger.setLevel` with unmodified arguments.
336 """
337 self._logger.setLevel(level)
339 def findCaller(
340 self, stack_info: bool = False, stacklevel: int = 1
341 ) -> tuple[str, int, str, str | None]:
342 """
343 Calls :meth:`logging.Logger.findCaller` with unmodified arguments.
344 """
345 # No need for stacklevel-adjustments since we're within structlog and
346 # our frames are ignored unconditionally.
347 return self._logger.findCaller(
348 stack_info=stack_info, stacklevel=stacklevel
349 )
351 def makeRecord(
352 self,
353 name: str,
354 level: int,
355 fn: str,
356 lno: int,
357 msg: str,
358 args: tuple[Any, ...],
359 exc_info: ExcInfo,
360 func: str | None = None,
361 extra: Any = None,
362 ) -> logging.LogRecord:
363 """
364 Calls :meth:`logging.Logger.makeRecord` with unmodified arguments.
365 """
366 return self._logger.makeRecord(
367 name, level, fn, lno, msg, args, exc_info, func=func, extra=extra
368 )
370 def handle(self, record: logging.LogRecord) -> None:
371 """
372 Calls :meth:`logging.Logger.handle` with unmodified arguments.
373 """
374 self._logger.handle(record)
376 def addHandler(self, hdlr: logging.Handler) -> None:
377 """
378 Calls :meth:`logging.Logger.addHandler` with unmodified arguments.
379 """
380 self._logger.addHandler(hdlr)
382 def removeHandler(self, hdlr: logging.Handler) -> None:
383 """
384 Calls :meth:`logging.Logger.removeHandler` with unmodified arguments.
385 """
386 self._logger.removeHandler(hdlr)
388 def hasHandlers(self) -> bool:
389 """
390 Calls :meth:`logging.Logger.hasHandlers` with unmodified arguments.
392 Exists only in Python 3.
393 """
394 return self._logger.hasHandlers()
396 def callHandlers(self, record: logging.LogRecord) -> None:
397 """
398 Calls :meth:`logging.Logger.callHandlers` with unmodified arguments.
399 """
400 self._logger.callHandlers(record)
402 def getEffectiveLevel(self) -> int:
403 """
404 Calls :meth:`logging.Logger.getEffectiveLevel` with unmodified
405 arguments.
406 """
407 return self._logger.getEffectiveLevel()
409 def isEnabledFor(self, level: int) -> bool:
410 """
411 Calls :meth:`logging.Logger.isEnabledFor` with unmodified arguments.
412 """
413 return self._logger.isEnabledFor(level)
415 def is_enabled_for(self, level: int) -> bool:
416 """
417 A snake_case alias of `isEnabledFor` for compatibility with
418 `structlog.typing.FilteringBoundLogger`.
420 .. note::
422 This method is more complex than the native `is_enabled_for` since
423 it supports standard library-only features like
424 :attr:`logging.Logger.disabled` while the native one only compares
425 log levels.
427 .. versionadded:: 26.1.0
428 """
429 return self._logger.isEnabledFor(level)
431 def get_effective_level(self) -> int:
432 """
433 A snake_case alias of `getEffectiveLevel` for compatibility with
434 `structlog.typing.FilteringBoundLogger`.
436 .. versionadded:: 26.1.0
437 """
438 return self._logger.getEffectiveLevel()
440 def getChild(self, suffix: str) -> logging.Logger:
441 """
442 Calls :meth:`logging.Logger.getChild` with unmodified arguments.
443 """
444 return self._logger.getChild(suffix)
446 # Non-Standard Async
447 async def _dispatch_to_sync(
448 self,
449 meth: Callable[..., Any],
450 event: str,
451 args: tuple[Any, ...],
452 kw: dict[str, Any],
453 ) -> None:
454 """
455 Merge contextvars and log using the sync logger in a thread pool.
456 """
457 # Capture thread-specific info before handing off to the executor.
458 thread_token = _ASYNC_CALLING_THREAD.set(
459 (threading.get_ident(), threading.current_thread().name)
460 )
461 scs_token = _ASYNC_CALLING_STACK.set(sys._getframe().f_back.f_back) # type: ignore[union-attr, arg-type, unused-ignore]
462 ctx = contextvars.copy_context()
464 try:
465 await asyncio.get_running_loop().run_in_executor(
466 None,
467 lambda: ctx.run(lambda: meth(event, *args, **kw)),
468 )
469 finally:
470 _ASYNC_CALLING_STACK.reset(scs_token)
471 _ASYNC_CALLING_THREAD.reset(thread_token)
473 async def adebug(self, event: str, *args: Any, **kw: Any) -> None:
474 """
475 Log using `debug()`, but asynchronously in a separate thread.
477 .. versionadded:: 23.1.0
478 """
479 await self._dispatch_to_sync(self.debug, event, args, kw)
481 async def ainfo(self, event: str, *args: Any, **kw: Any) -> None:
482 """
483 Log using `info()`, but asynchronously in a separate thread.
485 .. versionadded:: 23.1.0
486 """
487 await self._dispatch_to_sync(self.info, event, args, kw)
489 async def awarning(self, event: str, *args: Any, **kw: Any) -> None:
490 """
491 Log using `warning()`, but asynchronously in a separate thread.
493 .. versionadded:: 23.1.0
494 """
495 await self._dispatch_to_sync(self.warning, event, args, kw)
497 async def aerror(self, event: str, *args: Any, **kw: Any) -> None:
498 """
499 Log using `error()`, but asynchronously in a separate thread.
501 .. versionadded:: 23.1.0
502 """
503 await self._dispatch_to_sync(self.error, event, args, kw)
505 async def acritical(self, event: str, *args: Any, **kw: Any) -> None:
506 """
507 Log using `critical()`, but asynchronously in a separate thread.
509 .. versionadded:: 23.1.0
510 """
511 await self._dispatch_to_sync(self.critical, event, args, kw)
513 async def afatal(self, event: str, *args: Any, **kw: Any) -> None:
514 """
515 Log using `critical()`, but asynchronously in a separate thread.
517 .. versionadded:: 23.1.0
518 """
519 await self._dispatch_to_sync(self.critical, event, args, kw)
521 async def aexception(self, event: str, *args: Any, **kw: Any) -> None:
522 """
523 Log using `exception()`, but asynchronously in a separate thread.
525 .. versionadded:: 23.1.0
526 """
527 # To make `log.exception("foo") work, we have to check if the user
528 # passed an explicit exc_info and if not, supply our own.
529 if kw.get("exc_info", True) is True and kw.get("exception") is None:
530 kw["exc_info"] = sys.exc_info()
532 await self._dispatch_to_sync(self.exception, event, args, kw)
534 async def alog(
535 self, level: Any, event: str, *args: Any, **kw: Any
536 ) -> None:
537 """
538 Log using `log()`, but asynchronously in a separate thread.
540 .. versionadded:: 23.1.0
541 """
542 await self._dispatch_to_sync(partial(self.log, level), event, args, kw)
545def get_logger(*args: Any, **initial_values: Any) -> BoundLogger:
546 """
547 Only calls `structlog.get_logger`, but has the correct type hints.
549 .. warning::
551 Does **not** check whether -- or ensure that -- you've configured
552 *structlog* for standard library :mod:`logging`!
554 See :doc:`standard-library` for details.
556 .. versionadded:: 20.2.0
557 """
558 return _config.get_logger(*args, **initial_values)
561class AsyncBoundLogger:
562 """
563 Wraps a `BoundLogger` & exposes its logging methods as ``async`` versions.
565 This approach has turned out to be a mistake and the class has been
566 deprecated in 23.1.0. Use the regular `BoundLogger` with its a-prefixed
567 methods instead.
569 .. versionadded:: 20.2.0
570 .. versionchanged:: 20.2.0 fix _dispatch_to_sync contextvars usage
571 .. deprecated:: 23.1.0
572 Use the regular `BoundLogger` with its a-prefixed methods instead.
573 .. versionchanged:: 23.3.0
574 Callsite parameters are now also collected for async log methods.
575 """
577 __slots__ = ("_loop", "sync_bl")
579 #: The wrapped synchronous logger. It is useful to be able to log
580 #: synchronously occasionally.
581 sync_bl: BoundLogger
583 _executor = None
584 _bound_logger_factory = BoundLogger
586 def __init__(
587 self,
588 logger: logging.Logger,
589 processors: Iterable[Processor],
590 context: Context,
591 *,
592 # Only as an optimization for binding!
593 _sync_bl: Any = None, # *vroom vroom* over purity.
594 _loop: Any = None,
595 ):
596 if _sync_bl:
597 self.sync_bl = _sync_bl
598 self._loop = _loop
600 return
602 self.sync_bl = self._bound_logger_factory(
603 logger=logger, processors=processors, context=context
604 )
605 self._loop = asyncio.get_running_loop()
607 # Instances would've been correctly recognized as such, however the class
608 # not and we need the class in `structlog.configure()`.
609 @property
610 def _context(self) -> Context:
611 return self.sync_bl._context
613 def bind(self, **new_values: Any) -> Self:
614 return self.__class__(
615 # logger, processors and context are within sync_bl. These
616 # arguments are ignored if _sync_bl is passed. *vroom vroom* over
617 # purity.
618 logger=None, # type: ignore[arg-type]
619 processors=(),
620 context={},
621 _sync_bl=self.sync_bl.bind(**new_values),
622 _loop=self._loop,
623 )
625 def new(self, **new_values: Any) -> Self:
626 return self.__class__(
627 # c.f. comment in bind
628 logger=None, # type: ignore[arg-type]
629 processors=(),
630 context={},
631 _sync_bl=self.sync_bl.new(**new_values),
632 _loop=self._loop,
633 )
635 def unbind(self, *keys: str) -> Self:
636 return self.__class__(
637 # c.f. comment in bind
638 logger=None, # type: ignore[arg-type]
639 processors=(),
640 context={},
641 _sync_bl=self.sync_bl.unbind(*keys),
642 _loop=self._loop,
643 )
645 def try_unbind(self, *keys: str) -> Self:
646 return self.__class__(
647 # c.f. comment in bind
648 logger=None, # type: ignore[arg-type]
649 processors=(),
650 context={},
651 _sync_bl=self.sync_bl.try_unbind(*keys),
652 _loop=self._loop,
653 )
655 async def _dispatch_to_sync(
656 self,
657 meth: Callable[..., Any],
658 event: str,
659 args: tuple[Any, ...],
660 kw: dict[str, Any],
661 ) -> None:
662 """
663 Merge contextvars and log using the sync logger in a thread pool.
664 """
665 # Capture thread-specific info before handing off to the executor.
666 thread_token = _ASYNC_CALLING_THREAD.set(
667 (threading.get_ident(), threading.current_thread().name)
668 )
669 scs_token = _ASYNC_CALLING_STACK.set(sys._getframe().f_back.f_back) # type: ignore[union-attr, arg-type, unused-ignore]
670 ctx = contextvars.copy_context()
672 try:
673 await asyncio.get_running_loop().run_in_executor(
674 self._executor,
675 lambda: ctx.run(lambda: meth(event, *args, **kw)),
676 )
677 finally:
678 _ASYNC_CALLING_STACK.reset(scs_token)
679 _ASYNC_CALLING_THREAD.reset(thread_token)
681 async def debug(self, event: str, *args: Any, **kw: Any) -> None:
682 await self._dispatch_to_sync(self.sync_bl.debug, event, args, kw)
684 async def info(self, event: str, *args: Any, **kw: Any) -> None:
685 await self._dispatch_to_sync(self.sync_bl.info, event, args, kw)
687 async def warning(self, event: str, *args: Any, **kw: Any) -> None:
688 await self._dispatch_to_sync(self.sync_bl.warning, event, args, kw)
690 async def warn(self, event: str, *args: Any, **kw: Any) -> None:
691 await self._dispatch_to_sync(self.sync_bl.warning, event, args, kw)
693 async def error(self, event: str, *args: Any, **kw: Any) -> None:
694 await self._dispatch_to_sync(self.sync_bl.error, event, args, kw)
696 async def critical(self, event: str, *args: Any, **kw: Any) -> None:
697 await self._dispatch_to_sync(self.sync_bl.critical, event, args, kw)
699 async def fatal(self, event: str, *args: Any, **kw: Any) -> None:
700 await self._dispatch_to_sync(self.sync_bl.critical, event, args, kw)
702 async def exception(self, event: str, *args: Any, **kw: Any) -> None:
703 # To make `log.exception("foo") work, we have to check if the user
704 # passed an explicit exc_info and if not, supply our own.
705 ei = kw.pop("exc_info", None)
706 if ei is None and kw.get("exception") is None:
707 ei = sys.exc_info()
709 kw["exc_info"] = ei
711 await self._dispatch_to_sync(self.sync_bl.exception, event, args, kw)
713 async def log(self, level: Any, event: str, *args: Any, **kw: Any) -> None:
714 await self._dispatch_to_sync(
715 partial(self.sync_bl.log, level), event, args, kw
716 )
719class LoggerFactory:
720 """
721 Build a standard library logger when an *instance* is called.
723 Sets a custom logger using :func:`logging.setLoggerClass` so variables in
724 log format are expanded properly.
726 >>> from structlog import configure
727 >>> from structlog.stdlib import LoggerFactory
728 >>> configure(logger_factory=LoggerFactory())
730 Args:
731 ignore_frame_names:
732 When guessing the name of a logger, skip frames whose names *start*
733 with one of these. For example, in pyramid applications you'll
734 want to set it to ``["venusian", "pyramid.config"]``. This argument
735 is called *additional_ignores* in other APIs throughout
736 *structlog*.
737 """
739 def __init__(self, ignore_frame_names: list[str] | None = None):
740 self._ignore = ignore_frame_names
741 logging.setLoggerClass(_FixedFindCallerLogger)
743 def __call__(self, *args: Any) -> logging.Logger:
744 """
745 Deduce the caller's module name and create a stdlib logger.
747 If an optional argument is passed, it will be used as the logger name
748 instead of guesswork. This optional argument would be passed from the
749 :func:`structlog.get_logger` call. For example
750 ``structlog.get_logger("foo")`` would cause this method to be called
751 with ``"foo"`` as its first positional argument.
753 .. versionchanged:: 0.4.0
754 Added support for optional positional arguments. Using the first
755 one for naming the constructed logger.
756 """
757 if args:
758 return logging.getLogger(args[0])
760 # We skip all frames that originate from within structlog or one of the
761 # configured names.
762 _, name = _find_first_app_frame_and_name(self._ignore)
764 return logging.getLogger(name)
767class PositionalArgumentsFormatter:
768 """
769 Apply stdlib-like string formatting to the ``event`` key.
771 If the ``positional_args`` key in the event dict is set, it must
772 contain a tuple that is used for formatting (using the ``%s`` string
773 formatting operator) of the value from the ``event`` key. This works
774 in the same way as the stdlib handles arguments to the various log
775 methods: if the tuple contains only a single `dict` argument it is
776 used for keyword placeholders in the ``event`` string, otherwise it
777 will be used for positional placeholders.
779 ``positional_args`` is populated by `structlog.stdlib.BoundLogger` or
780 can be set manually.
782 The *remove_positional_args* flag can be set to `False` to keep the
783 ``positional_args`` key in the event dict; by default it will be
784 removed from the event dict after formatting a message.
785 """
787 def __init__(self, remove_positional_args: bool = True) -> None:
788 self.remove_positional_args = remove_positional_args
790 def __call__(
791 self, _: WrappedLogger, __: str, event_dict: EventDict
792 ) -> EventDict:
793 args = event_dict.get("positional_args")
795 # Mimic the formatting behaviour of the stdlib's logging module, which
796 # accepts both positional arguments and a single dict argument. The
797 # "single dict" check is the same one as the stdlib's logging module
798 # performs in LogRecord.__init__().
799 if args:
800 if len(args) == 1 and isinstance(args[0], dict) and args[0]:
801 args = args[0]
803 event_dict["event"] %= args
805 if self.remove_positional_args and args is not None:
806 del event_dict["positional_args"]
808 return event_dict
811def filter_by_level(
812 logger: logging.Logger, method_name: str, event_dict: EventDict
813) -> EventDict:
814 """
815 Check whether logging is configured to accept messages from this log level.
817 Should be the first processor if stdlib's filtering by level is used so
818 possibly expensive processors like exception formatters are avoided in the
819 first place.
821 >>> import logging
822 >>> from structlog.stdlib import filter_by_level
823 >>> logging.basicConfig(level=logging.WARN)
824 >>> logger = logging.getLogger()
825 >>> filter_by_level(logger, 'warn', {})
826 {}
827 >>> filter_by_level(logger, 'debug', {})
828 Traceback (most recent call last):
829 ...
830 DropEvent
831 """
832 if (
833 # We can't use logger.isEnabledFor() because it's always disabled when
834 # a log entry is in flight on Python 3.14 and later,
835 not logger.disabled
836 and NAME_TO_LEVEL[method_name] >= logger.getEffectiveLevel()
837 ):
838 return event_dict
840 raise DropEvent
843def add_log_level_number(
844 logger: logging.Logger, method_name: str, event_dict: EventDict
845) -> EventDict:
846 """
847 Add the log level number to the event dict.
849 Log level numbers map to the log level names. The Python stdlib uses them
850 for filtering logic. This adds the same numbers so users can leverage
851 similar filtering. Compare::
853 level in ("warning", "error", "critical")
854 level_number >= 30
856 The mapping of names to numbers is in
857 ``structlog.stdlib._log_levels._NAME_TO_LEVEL``.
859 .. versionadded:: 18.2.0
860 """
861 event_dict["level_number"] = NAME_TO_LEVEL[method_name]
863 return event_dict
866def add_logger_name(
867 logger: logging.Logger, method_name: str, event_dict: EventDict
868) -> EventDict:
869 """
870 Add the logger name to the event dict.
871 """
872 record = event_dict.get("_record")
873 if record is None:
874 event_dict["logger"] = logger.name
875 else:
876 event_dict["logger"] = record.name
877 return event_dict
880_LOG_RECORD_KEYS = logging.LogRecord(
881 "name", 0, "pathname", 0, "msg", (), None
882).__dict__.keys()
885class ExtraAdder:
886 """
887 Add extra attributes of `logging.LogRecord` objects to the event
888 dictionary.
890 This processor can be used for adding data passed in the ``extra``
891 parameter of the `logging` module's log methods to the event dictionary.
893 Args:
894 allow:
895 An optional collection of attributes that, if present in
896 `logging.LogRecord` objects, will be copied to event dictionaries.
898 If ``allow`` is None all attributes of `logging.LogRecord` objects
899 that do not exist on a standard `logging.LogRecord` object will be
900 copied to event dictionaries.
902 .. versionadded:: 21.5.0
903 """
905 __slots__ = ("_copier",)
907 def __init__(self, allow: Collection[str] | None = None) -> None:
908 self._copier: Callable[[EventDict, logging.LogRecord], None]
909 if allow is not None:
910 # The contents of allow is copied to a new list so that changes to
911 # the list passed into the constructor does not change the
912 # behaviour of this processor.
913 self._copier = functools.partial(self._copy_allowed, [*allow])
914 else:
915 self._copier = self._copy_all
917 def __call__(
918 self, logger: logging.Logger, name: str, event_dict: EventDict
919 ) -> EventDict:
920 record: logging.LogRecord | None = event_dict.get("_record")
921 if record is not None:
922 self._copier(event_dict, record)
923 return event_dict
925 @classmethod
926 def _copy_all(
927 cls, event_dict: EventDict, record: logging.LogRecord
928 ) -> None:
929 for key, value in record.__dict__.items():
930 if key not in _LOG_RECORD_KEYS:
931 event_dict[key] = value
933 @classmethod
934 def _copy_allowed(
935 cls,
936 allow: Collection[str],
937 event_dict: EventDict,
938 record: logging.LogRecord,
939 ) -> None:
940 for key in allow:
941 if key in record.__dict__:
942 event_dict[key] = record.__dict__[key]
945LOG_KWARG_NAMES = ("exc_info", "stack_info", "stacklevel")
948def render_to_log_args_and_kwargs(
949 _: logging.Logger, __: str, event_dict: EventDict
950) -> tuple[tuple[Any, ...], dict[str, Any]]:
951 """
952 Render ``event_dict`` into positional and keyword arguments for
953 `logging.Logger` logging methods.
954 See `logging.Logger.debug` method for keyword arguments reference.
956 The ``event`` field is passed in the first positional argument, positional
957 arguments from ``positional_args`` field are passed in subsequent positional
958 arguments, keyword arguments are extracted from the *event_dict* and the
959 rest of the *event_dict* is added as ``extra``.
961 This allows you to defer formatting to `logging`.
963 .. versionadded:: 25.1.0
964 """
965 args = (event_dict.pop("event"), *event_dict.pop("positional_args", ()))
967 kwargs = {
968 kwarg_name: event_dict.pop(kwarg_name)
969 for kwarg_name in LOG_KWARG_NAMES
970 if kwarg_name in event_dict
971 }
972 if event_dict:
973 kwargs["extra"] = event_dict
975 return args, kwargs
978def render_to_log_kwargs(
979 _: logging.Logger, __: str, event_dict: EventDict
980) -> EventDict:
981 """
982 Render ``event_dict`` into keyword arguments for `logging.Logger` logging
983 methods.
984 See `logging.Logger.debug` method for keyword arguments reference.
986 The ``event`` field is translated into ``msg``, keyword arguments are
987 extracted from the *event_dict* and the rest of the *event_dict* is added as
988 ``extra``.
990 This allows you to defer formatting to `logging`.
992 .. versionadded:: 17.1.0
993 .. versionchanged:: 22.1.0
994 ``exc_info``, ``stack_info``, and ``stacklevel`` are passed as proper
995 kwargs and not put into ``extra``.
996 .. versionchanged:: 24.2.0
997 ``stackLevel`` corrected to ``stacklevel``.
998 """
999 return {
1000 "msg": event_dict.pop("event"),
1001 "extra": event_dict,
1002 **{
1003 kw: event_dict.pop(kw)
1004 for kw in LOG_KWARG_NAMES
1005 if kw in event_dict
1006 },
1007 }
1010class ProcessorFormatter(logging.Formatter):
1011 r"""
1012 Call *structlog* processors on `logging.LogRecord`\s.
1014 This is an implementation of a `logging.Formatter` that can be used to
1015 format log entries from both *structlog* and `logging`.
1017 Its static method `wrap_for_formatter` must be the final processor in
1018 *structlog*'s processor chain.
1020 Please refer to :ref:`processor-formatter` for examples.
1022 Args:
1023 foreign_pre_chain:
1024 If not `None`, it is used as a processor chain that is applied to
1025 **non**-*structlog* log entries before the event dictionary is
1026 passed to *processors*. (default: `None`)
1028 processors:
1029 A chain of *structlog* processors that is used to process **all**
1030 log entries. The last one must render to a `str` which then gets
1031 passed on to `logging` for output.
1033 Compared to *structlog*'s regular processor chains, there's a few
1034 differences:
1036 - The event dictionary contains two additional keys:
1038 #. ``_record``: a `logging.LogRecord` that either was created
1039 using `logging` APIs, **or** is a wrapped *structlog* log
1040 entry created by `wrap_for_formatter`.
1042 #. ``_from_structlog``: a `bool` that indicates whether or not
1043 ``_record`` was created by a *structlog* logger.
1045 Since you most likely don't want ``_record`` and
1046 ``_from_structlog`` in your log files, we've added the static
1047 method `remove_processors_meta` to ``ProcessorFormatter`` that
1048 you can add just before your renderer.
1050 - Since this is a `logging` *formatter*, raising
1051 `structlog.DropEvent` will crash your application.
1053 keep_exc_info:
1054 ``exc_info`` on `logging.LogRecord`\ s is added to the
1055 ``event_dict`` and removed afterwards. Set this to ``True`` to keep
1056 it on the `logging.LogRecord`. (default: False)
1058 keep_stack_info:
1059 Same as *keep_exc_info* except for ``stack_info``. (default: False)
1061 logger:
1062 Logger which we want to push through the *structlog* processor
1063 chain. This parameter is necessary for some of the processors like
1064 `filter_by_level`. (default: None)
1066 pass_foreign_args:
1067 If True, pass a foreign log record's ``args`` attribute to the
1068 ``event_dict`` under ``positional_args`` key. (default: False)
1070 processor:
1071 A single *structlog* processor used for rendering the event
1072 dictionary before passing it off to `logging`. Must return a `str`.
1073 The event dictionary does **not** contain ``_record`` and
1074 ``_from_structlog``.
1076 This parameter exists for historic reasons. Please use *processors*
1077 instead.
1079 use_get_message:
1080 If True, use ``record.getMessage`` to get a fully rendered log
1081 message, otherwise use ``str(record.msg)``. (default: True)
1083 Raises:
1084 TypeError: If both or neither *processor* and *processors* are passed.
1086 .. versionadded:: 17.1.0
1087 .. versionadded:: 17.2.0 *keep_exc_info* and *keep_stack_info*
1088 .. versionadded:: 19.2.0 *logger*
1089 .. versionadded:: 19.2.0 *pass_foreign_args*
1090 .. versionadded:: 21.3.0 *processors*
1091 .. deprecated:: 21.3.0
1092 *processor* (singular) in favor of *processors* (plural). Removal is not
1093 planned.
1094 .. versionadded:: 23.3.0 *use_get_message*
1095 """
1097 def __init__(
1098 self,
1099 processor: Processor | None = None,
1100 processors: Sequence[Processor] | None = (),
1101 foreign_pre_chain: Sequence[Processor] | None = None,
1102 keep_exc_info: bool = False,
1103 keep_stack_info: bool = False,
1104 logger: logging.Logger | None = None,
1105 pass_foreign_args: bool = False,
1106 use_get_message: bool = True,
1107 *args: Any,
1108 **kwargs: Any,
1109 ) -> None:
1110 fmt = kwargs.pop("fmt", "%(message)s")
1111 super().__init__(*args, fmt=fmt, **kwargs) # type: ignore[misc]
1113 if processor and processors:
1114 msg = (
1115 "The `processor` and `processors` arguments are mutually"
1116 " exclusive."
1117 )
1118 raise TypeError(msg)
1120 self.processors: Sequence[Processor]
1121 if processor is not None:
1122 self.processors = (self.remove_processors_meta, processor)
1123 elif processors:
1124 self.processors = processors
1125 else:
1126 msg = "Either `processor` or `processors` must be passed."
1127 raise TypeError(msg)
1129 self.foreign_pre_chain = foreign_pre_chain
1130 self.keep_exc_info = keep_exc_info
1131 self.keep_stack_info = keep_stack_info
1132 self.logger = logger
1133 self.pass_foreign_args = pass_foreign_args
1134 self.use_get_message = use_get_message
1136 def format(self, record: logging.LogRecord) -> str:
1137 """
1138 Extract *structlog*'s `event_dict` from ``record.msg`` and format it.
1140 *record* has been patched by `wrap_for_formatter` first though, so the
1141 type isn't quite right.
1142 """
1143 # Make a shallow copy of the record to let other handlers/formatters
1144 # process the original one
1145 record = logging.makeLogRecord(record.__dict__)
1147 logger = getattr(record, "_logger", _SENTINEL)
1148 meth_name = getattr(record, "_name", "__structlog_sentinel__")
1150 ed: ProcessorReturnValue
1151 if logger is not _SENTINEL and meth_name != "__structlog_sentinel__":
1152 # Both attached by wrap_for_formatter
1153 if self.logger is not None:
1154 logger = self.logger
1155 meth_name = cast(str, record._name) # type:ignore[attr-defined]
1157 # We need to copy because it's possible that the same record gets
1158 # processed by multiple logging formatters. LogRecord.getMessage
1159 # would transform our dict into a str.
1160 ed = cast(dict[str, Any], record.msg).copy()
1161 ed["_record"] = record
1162 ed["_from_structlog"] = True
1163 else:
1164 logger = self.logger
1165 meth_name = record.levelname.lower()
1166 ed = {
1167 "event": (
1168 record.getMessage()
1169 if self.use_get_message
1170 else str(record.msg)
1171 ),
1172 "_record": record,
1173 "_from_structlog": False,
1174 }
1176 if self.pass_foreign_args:
1177 ed["positional_args"] = record.args
1179 record.args = ()
1181 # Add stack-related attributes to the event dict
1182 if record.exc_info:
1183 ed["exc_info"] = record.exc_info
1184 if record.stack_info:
1185 ed["stack_info"] = record.stack_info
1187 # Non-structlog allows to run through a chain to prepare it for the
1188 # final processor (e.g. adding timestamps and log levels).
1189 for proc in self.foreign_pre_chain or ():
1190 ed = cast(EventDict, proc(logger, meth_name, ed))
1192 # If required, unset stack-related attributes on the record copy so
1193 # that the base implementation doesn't append stacktraces to the
1194 # output.
1195 if not self.keep_exc_info:
1196 record.exc_text = None
1197 record.exc_info = None
1198 if not self.keep_stack_info:
1199 record.stack_info = None
1201 for p in self.processors:
1202 ed = p(logger, meth_name, ed) # type: ignore[arg-type]
1204 if not isinstance(ed, str):
1205 warnings.warn(
1206 "The last processor in ProcessorFormatter.processors must "
1207 f"return a string, but {self.processors[-1]} returned a "
1208 f"{type(ed)} instead.",
1209 category=RuntimeWarning,
1210 stacklevel=1,
1211 )
1212 ed = cast(str, ed)
1214 record.msg = ed
1216 return super().format(record)
1218 @staticmethod
1219 def wrap_for_formatter(
1220 logger: logging.Logger, name: str, event_dict: EventDict
1221 ) -> tuple[tuple[EventDict], dict[str, dict[str, Any]]]:
1222 """
1223 Wrap *logger*, *name*, and *event_dict*.
1225 The result is later unpacked by `ProcessorFormatter` when formatting
1226 log entries.
1228 Use this static method as the renderer (in other words, final
1229 processor) if you want to use `ProcessorFormatter` in your `logging`
1230 configuration.
1231 """
1232 return (event_dict,), {"extra": {"_logger": logger, "_name": name}}
1234 @staticmethod
1235 def remove_processors_meta(
1236 _: WrappedLogger, __: str, event_dict: EventDict
1237 ) -> EventDict:
1238 """
1239 Remove ``_record`` and ``_from_structlog`` from *event_dict*.
1241 These keys are added to the event dictionary, before
1242 `ProcessorFormatter`'s *processors* are run.
1244 .. versionadded:: 21.3.0
1245 """
1246 del event_dict["_record"]
1247 del event_dict["_from_structlog"]
1249 return event_dict