Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/structlog/_native.py: 60%
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# 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.
6"""
7structlog's native high-performance loggers.
8"""
10from __future__ import annotations
12import asyncio
13import collections
14import contextvars
15import sys
17from typing import Any, Callable
19from ._base import BoundLoggerBase
20from ._log_levels import (
21 CRITICAL,
22 DEBUG,
23 ERROR,
24 INFO,
25 LEVEL_TO_NAME,
26 NAME_TO_LEVEL,
27 NOTSET,
28 WARNING,
29)
30from .contextvars import _ASYNC_CALLING_STACK
31from .typing import FilteringBoundLogger
34def _nop(self: Any, event: str, *args: Any, **kw: Any) -> Any:
35 return None
38async def _anop(self: Any, event: str, *args: Any, **kw: Any) -> Any:
39 return None
42def exception(
43 self: FilteringBoundLogger, event: str, *args: Any, **kw: Any
44) -> Any:
45 kw.setdefault("exc_info", True)
47 return self.error(event, *args, **kw)
50async def aexception(
51 self: FilteringBoundLogger, event: str, *args: Any, **kw: Any
52) -> Any:
53 """
54 .. versionchanged:: 23.3.0
55 Callsite parameters are now also collected under asyncio.
56 """
57 # Exception info has to be extracted this early, because it is no longer
58 # available once control is passed to the executor.
59 if kw.get("exc_info", True) is True:
60 kw["exc_info"] = sys.exc_info()
62 scs_token = _ASYNC_CALLING_STACK.set(sys._getframe().f_back) # type: ignore[arg-type]
63 ctx = contextvars.copy_context()
64 try:
65 runner = await asyncio.get_running_loop().run_in_executor(
66 None,
67 lambda: ctx.run(lambda: self.error(event, *args, **kw)),
68 )
69 finally:
70 _ASYNC_CALLING_STACK.reset(scs_token)
72 return runner
75def make_filtering_bound_logger(
76 min_level: int | str,
77) -> type[FilteringBoundLogger]:
78 """
79 Create a new `FilteringBoundLogger` that only logs *min_level* or higher.
81 The logger is optimized such that log levels below *min_level* only consist
82 of a ``return None``.
84 All familiar log methods are present, with async variants of each that are
85 prefixed by an ``a``. Therefore, the async version of ``log.info("hello")``
86 is ``await log.ainfo("hello")``.
88 Additionally it has a ``log(self, level: int, **kw: Any)`` method to mirror
89 `logging.Logger.log` and `structlog.stdlib.BoundLogger.log`.
91 Compared to using *structlog*'s standard library integration and the
92 `structlog.stdlib.filter_by_level` processor:
94 - It's faster because once the logger is built at program start; it's a
95 static class.
96 - For the same reason you can't change the log level once configured. Use
97 the dynamic approach of `standard-library` instead, if you need this
98 feature.
99 - You *can* have (much) more fine-grained filtering by :ref:`writing a
100 simple processor <finer-filtering>`.
102 Args:
103 min_level:
104 The log level as an integer. You can use the constants from
105 `logging` like ``logging.INFO`` or pass the values directly. See
106 `this table from the logging docs
107 <https://docs.python.org/3/library/logging.html#levels>`_ for
108 possible values.
110 If you pass a string, it must be one of: ``critical``, ``error``,
111 ``warning``, ``info``, ``debug``, ``notset`` (upper/lower case
112 doesn't matter).
114 .. versionadded:: 20.2.0
115 .. versionchanged:: 21.1.0 The returned loggers are now pickleable.
116 .. versionadded:: 20.1.0 The ``log()`` method.
117 .. versionadded:: 22.2.0
118 Async variants ``alog()``, ``adebug()``, ``ainfo()``, and so forth.
119 .. versionchanged:: 25.1.0 *min_level* can now be a string.
120 """
121 if isinstance(min_level, str):
122 min_level = NAME_TO_LEVEL[min_level.lower()]
124 return LEVEL_TO_FILTERING_LOGGER[min_level]
127def _maybe_interpolate(event: str, args: tuple[Any, ...]) -> str:
128 """
129 Interpolate the event string with the given arguments.
131 If there's exactly one argument and it's a mapping, use it for dict-based
132 interpolation. Otherwise, use the arguments for positional interpolation.
133 """
134 if not args:
135 return event
137 if (
138 len(args) == 1
139 and isinstance(args[0], collections.abc.Mapping)
140 and args[0]
141 ):
142 return event % args[0]
144 return event % args
147def _make_filtering_bound_logger(min_level: int) -> type[FilteringBoundLogger]:
148 """
149 Create a new `FilteringBoundLogger` that only logs *min_level* or higher.
151 The logger is optimized such that log levels below *min_level* only consist
152 of a ``return None``.
153 """
155 def make_method(
156 level: int,
157 ) -> tuple[Callable[..., Any], Callable[..., Any]]:
158 if level < min_level:
159 return _nop, _anop
161 name = LEVEL_TO_NAME[level]
163 def meth(self: Any, event: str, *args: Any, **kw: Any) -> Any:
164 return self._proxy_to_logger(
165 name, _maybe_interpolate(event, args), **kw
166 )
168 async def ameth(self: Any, event: str, *args: Any, **kw: Any) -> Any:
169 """
170 .. versionchanged:: 23.3.0
171 Callsite parameters are now also collected under asyncio.
172 """
173 event = _maybe_interpolate(event, args)
175 scs_token = _ASYNC_CALLING_STACK.set(sys._getframe().f_back) # type: ignore[arg-type]
176 ctx = contextvars.copy_context()
177 try:
178 await asyncio.get_running_loop().run_in_executor(
179 None,
180 lambda: ctx.run(
181 lambda: self._proxy_to_logger(name, event, **kw)
182 ),
183 )
184 finally:
185 _ASYNC_CALLING_STACK.reset(scs_token)
187 meth.__name__ = name
188 ameth.__name__ = f"a{name}"
190 return meth, ameth
192 def log(self: Any, level: int, event: str, *args: Any, **kw: Any) -> Any:
193 if level < min_level:
194 return None
195 name = LEVEL_TO_NAME[level]
197 return self._proxy_to_logger(
198 name, _maybe_interpolate(event, args), **kw
199 )
201 async def alog(
202 self: Any, level: int, event: str, *args: Any, **kw: Any
203 ) -> Any:
204 """
205 .. versionchanged:: 23.3.0
206 Callsite parameters are now also collected under asyncio.
207 """
208 if level < min_level:
209 return None
210 name = LEVEL_TO_NAME[level]
211 event = _maybe_interpolate(event, args)
213 scs_token = _ASYNC_CALLING_STACK.set(sys._getframe().f_back) # type: ignore[arg-type]
214 ctx = contextvars.copy_context()
215 try:
216 runner = await asyncio.get_running_loop().run_in_executor(
217 None,
218 lambda: ctx.run(
219 lambda: self._proxy_to_logger(name, event, **kw)
220 ),
221 )
222 finally:
223 _ASYNC_CALLING_STACK.reset(scs_token)
224 return runner
226 meths: dict[str, Callable[..., Any]] = {"log": log, "alog": alog}
227 for lvl, name in LEVEL_TO_NAME.items():
228 meths[name], meths[f"a{name}"] = make_method(lvl)
230 meths["exception"] = exception
231 meths["aexception"] = aexception
232 meths["fatal"] = meths["critical"]
233 meths["afatal"] = meths["acritical"]
234 meths["warn"] = meths["warning"]
235 meths["awarn"] = meths["awarning"]
236 meths["msg"] = meths["info"]
237 meths["amsg"] = meths["ainfo"]
239 # Introspection
240 meths["is_enabled_for"] = lambda self, level: level >= min_level
241 meths["get_effective_level"] = lambda self: min_level
243 return type(
244 f"BoundLoggerFilteringAt{LEVEL_TO_NAME.get(min_level, 'Notset').capitalize()}",
245 (BoundLoggerBase,),
246 meths,
247 )
250# Pre-create all possible filters to make them pickleable.
251BoundLoggerFilteringAtNotset = _make_filtering_bound_logger(NOTSET)
252BoundLoggerFilteringAtDebug = _make_filtering_bound_logger(DEBUG)
253BoundLoggerFilteringAtInfo = _make_filtering_bound_logger(INFO)
254BoundLoggerFilteringAtWarning = _make_filtering_bound_logger(WARNING)
255BoundLoggerFilteringAtError = _make_filtering_bound_logger(ERROR)
256BoundLoggerFilteringAtCritical = _make_filtering_bound_logger(CRITICAL)
258LEVEL_TO_FILTERING_LOGGER = {
259 CRITICAL: BoundLoggerFilteringAtCritical,
260 ERROR: BoundLoggerFilteringAtError,
261 WARNING: BoundLoggerFilteringAtWarning,
262 INFO: BoundLoggerFilteringAtInfo,
263 DEBUG: BoundLoggerFilteringAtDebug,
264 NOTSET: BoundLoggerFilteringAtNotset,
265}