API refactor
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-10-07 16:25:52 +09:00
parent 76d0d86211
commit 91c7e04474
1171 changed files with 81940 additions and 44117 deletions

View File

@@ -11,7 +11,6 @@ from __future__ import annotations
import datetime
import enum
import inspect
import json
import logging
import operator
@@ -20,6 +19,7 @@ import sys
import threading
import time
from types import FrameType, TracebackType
from typing import (
Any,
Callable,
@@ -28,6 +28,7 @@ from typing import (
NamedTuple,
Sequence,
TextIO,
cast,
)
from ._frames import (
@@ -35,27 +36,33 @@ from ._frames import (
_format_exception,
_format_stack,
)
from ._log_levels import _NAME_TO_LEVEL, add_log_level
from ._log_levels import NAME_TO_LEVEL, add_log_level
from ._utils import get_processname
from .tracebacks import ExceptionDictTransformer
from .typing import EventDict, ExceptionTransformer, ExcInfo, WrappedLogger
from .typing import (
EventDict,
ExceptionTransformer,
ExcInfo,
WrappedLogger,
)
__all__ = [
"_NAME_TO_LEVEL", # some people rely on it being here
"add_log_level",
"NAME_TO_LEVEL", # some people rely on it being here
"CallsiteParameter",
"CallsiteParameterAdder",
"dict_tracebacks",
"EventRenamer",
"ExceptionPrettyPrinter",
"format_exc_info",
"JSONRenderer",
"KeyValueRenderer",
"LogfmtRenderer",
"StackInfoRenderer",
"TimeStamper",
"UnicodeDecoder",
"UnicodeEncoder",
"add_log_level",
"dict_tracebacks",
"format_exc_info",
]
@@ -63,8 +70,7 @@ class KeyValueRenderer:
"""
Render ``event_dict`` as a list of ``Key=repr(Value)`` pairs.
Arguments:
Args:
sort_keys: Whether to sort keys when formatting.
key_order:
@@ -119,8 +125,7 @@ class LogfmtRenderer:
.. _logfmt: https://brandur.org/logfmt
Arguments:
Args:
sort_keys: Whether to sort keys when formatting.
key_order:
@@ -138,8 +143,7 @@ class LogfmtRenderer:
``flag=false``.
Raises:
ValueError: If a key contains non printable or space characters.
ValueError: If a key contains non-printable or whitespace characters.
.. versionadded:: 21.5.0
"""
@@ -173,9 +177,16 @@ class LogfmtRenderer:
continue
value = "true" if value else "false"
value = f"{value}".replace('"', '\\"')
value = str(value)
backslashes_need_escaping = (
" " in value or "=" in value or '"' in value
)
if backslashes_need_escaping and "\\" in value:
value = value.replace("\\", "\\\\")
if " " in value or "=" in value:
value = value.replace('"', '\\"').replace("\n", "\\n")
if backslashes_need_escaping:
value = f'"{value}"'
elements.append(f"{key}={value}")
@@ -237,8 +248,7 @@ class UnicodeEncoder:
"""
Encode unicode values in ``event_dict``.
Arguments:
Args:
encoding: Encoding to encode to (default: ``"utf-8"``).
errors:
@@ -272,8 +282,7 @@ class UnicodeDecoder:
"""
Decode byte string values in ``event_dict``.
Arguments:
Args:
encoding: Encoding to decode from (default: ``"utf-8"``).
errors: How to cope with encoding errors (default: ``"replace"``).
@@ -308,8 +317,7 @@ class JSONRenderer:
"""
Render the ``event_dict`` using ``serializer(event_dict, **dumps_kw)``.
Arguments:
Args:
dumps_kw:
Are passed unmodified to *serializer*. If *default* is passed, it
will disable support for ``__structlog__``-based serialization.
@@ -317,9 +325,9 @@ class JSONRenderer:
serializer:
A :func:`json.dumps`-compatible callable that will be used to
format the string. This can be used to use alternative JSON
encoders like `orjson <https://pypi.org/project/orjson/>`__ or
`RapidJSON <https://pypi.org/project/python-rapidjson/>`_
(default: :func:`json.dumps`).
encoders (default: :func:`json.dumps`).
.. seealso:: :doc:`performance` for examples.
.. versionadded:: 0.2.0 Support for ``__structlog__`` serialization method.
.. versionadded:: 15.4.0 *serializer* parameter.
@@ -385,8 +393,7 @@ class ExceptionRenderer:
If there is no ``exc_info`` key, the *event_dict* is not touched. This
behavior is analog to the one of the stdlib's logging.
Arguments:
Args:
exception_formatter:
A callable that is used to format the exception from the
``exc_info`` field into the ``exception`` field.
@@ -407,11 +414,9 @@ class ExceptionRenderer:
def __call__(
self, logger: WrappedLogger, name: str, event_dict: EventDict
) -> EventDict:
exc_info = event_dict.pop("exc_info", None)
exc_info = _figure_out_exc_info(event_dict.pop("exc_info", None))
if exc_info:
event_dict["exception"] = self.format_exception(
_figure_out_exc_info(exc_info)
)
event_dict["exception"] = self.format_exception(exc_info)
return event_dict
@@ -459,8 +464,7 @@ class TimeStamper:
"""
Add a timestamp to ``event_dict``.
Arguments:
Args:
fmt:
strftime format string, or ``"iso"`` for `ISO 8601
<https://en.wikipedia.org/wiki/ISO_8601>`_, or `None` for a `UNIX
@@ -473,7 +477,7 @@ class TimeStamper:
.. versionchanged:: 19.2.0 Can be pickled now.
"""
__slots__ = ("_stamper", "fmt", "utc", "key")
__slots__ = ("_stamper", "fmt", "key", "utc")
def __init__(
self,
@@ -521,7 +525,8 @@ def _make_stamper(
else:
def now() -> datetime.datetime:
# A naive local datetime is fine here, because we only format it.
# We don't need the TZ for our own formatting. We add it only for
# user-defined formats later.
return datetime.datetime.now() # noqa: DTZ005
if fmt is None:
@@ -548,12 +553,18 @@ def _make_stamper(
return stamper_iso_local
def stamper_fmt(event_dict: EventDict) -> EventDict:
event_dict[key] = now().strftime(fmt)
def stamper_fmt_local(event_dict: EventDict) -> EventDict:
event_dict[key] = now().astimezone().strftime(fmt)
return event_dict
return stamper_fmt
def stamper_fmt_utc(event_dict: EventDict) -> EventDict:
event_dict[key] = now().strftime(fmt)
return event_dict
if utc:
return stamper_fmt_utc
return stamper_fmt_local
class MaybeTimeStamper:
@@ -587,36 +598,49 @@ class MaybeTimeStamper:
return event_dict
def _figure_out_exc_info(v: Any) -> ExcInfo:
def _figure_out_exc_info(v: Any) -> ExcInfo | None:
"""
Depending on the Python version will try to do the smartest thing possible
to transform *v* into an ``exc_info`` tuple.
Try to convert *v* into an ``exc_info`` tuple.
Return ``None`` if *v* does not represent an exception or if there is no
current exception.
"""
if isinstance(v, BaseException):
return (v.__class__, v, v.__traceback__)
if isinstance(v, tuple):
return v # type: ignore[return-value]
if isinstance(v, tuple) and len(v) == 3:
has_type = isinstance(v[0], type) and issubclass(v[0], BaseException)
has_exc = isinstance(v[1], BaseException)
has_tb = v[2] is None or isinstance(v[2], TracebackType)
if has_type and has_exc and has_tb:
return v
if v:
return sys.exc_info() # type: ignore[return-value]
result = sys.exc_info()
if result == (None, None, None):
return None
return cast(ExcInfo, result)
return v
return None
class ExceptionPrettyPrinter:
"""
Pretty print exceptions and remove them from the ``event_dict``.
Arguments:
Pretty print exceptions rendered by *exception_formatter* and remove them
from the ``event_dict``.
Args:
file: Target file for output (default: ``sys.stdout``).
exception_formatter:
A callable that is used to format the exception from the
``exc_info`` field into the ``exception`` field.
This processor is mostly for development and testing so you can read
exceptions properly formatted.
It behaves like `format_exc_info` except it removes the exception data from
the event dictionary after printing it.
It behaves like `format_exc_info`, except that it removes the exception data
from the event dictionary after printing it using the passed
*exception_formatter*, which defaults to Python's built-in traceback formatting.
It's tolerant to having `format_exc_info` in front of itself in the
processor chain but doesn't require it. In other words, it handles both
@@ -626,6 +650,9 @@ class ExceptionPrettyPrinter:
.. versionchanged:: 16.0.0
Added support for passing exceptions as ``exc_info`` on Python 3.
.. versionchanged:: 25.4.0
Fixed *exception_formatter* so that it overrides the default if set.
"""
def __init__(
@@ -633,6 +660,7 @@ class ExceptionPrettyPrinter:
file: TextIO | None = None,
exception_formatter: ExceptionTransformer = _format_exception,
) -> None:
self.format_exception = exception_formatter
if file is not None:
self._file = file
else:
@@ -645,7 +673,7 @@ class ExceptionPrettyPrinter:
if exc is None:
exc_info = _figure_out_exc_info(event_dict.pop("exc_info", None))
if exc_info:
exc = _format_exception(exc_info)
exc = self.format_exception(exc_info)
if exc:
print(exc, file=self._file)
@@ -661,8 +689,7 @@ class StackInfoRenderer:
involving an exception and works analogously to the *stack_info* argument
of the Python standard library logging.
Arguments:
Args:
additional_ignores:
By default, stack frames coming from *structlog* are ignored. With
this argument you can add additional names that are ignored, before
@@ -727,6 +754,42 @@ class CallsiteParameter(enum.Enum):
PROCESS_NAME = "process_name"
def _get_callsite_pathname(module: str, frame: FrameType) -> Any:
return frame.f_code.co_filename
def _get_callsite_filename(module: str, frame: FrameType) -> Any:
return os.path.basename(frame.f_code.co_filename)
def _get_callsite_module(module: str, frame: FrameType) -> Any:
return os.path.splitext(os.path.basename(frame.f_code.co_filename))[0]
def _get_callsite_func_name(module: str, frame: FrameType) -> Any:
return frame.f_code.co_name
def _get_callsite_lineno(module: str, frame: FrameType) -> Any:
return frame.f_lineno
def _get_callsite_thread(module: str, frame: FrameType) -> Any:
return threading.get_ident()
def _get_callsite_thread_name(module: str, frame: FrameType) -> Any:
return threading.current_thread().name
def _get_callsite_process(module: str, frame: FrameType) -> Any:
return os.getpid()
def _get_callsite_process_name(module: str, frame: FrameType) -> Any:
return get_processname()
class CallsiteParameterAdder:
"""
Adds parameters of the callsite that an event dictionary originated from to
@@ -734,10 +797,6 @@ class CallsiteParameterAdder:
dictionaries with information such as the function name, line number and
filename that an event dictionary originated from.
.. warning::
This processor cannot detect the correct callsite for invocation of
async functions.
If the event dictionary has an embedded `logging.LogRecord` object and did
not originate from *structlog* then the callsite information will be
determined from the `logging.LogRecord` object. For event dictionaries
@@ -749,8 +808,7 @@ class CallsiteParameterAdder:
The keys used for callsite parameters in the event dictionary are the
string values of `CallsiteParameter` enum members.
Arguments:
Args:
parameters:
A collection of `CallsiteParameter` values that should be added to
the event dictionary.
@@ -773,35 +831,17 @@ class CallsiteParameterAdder:
"""
_handlers: ClassVar[
dict[CallsiteParameter, Callable[[str, inspect.Traceback], Any]]
dict[CallsiteParameter, Callable[[str, FrameType], Any]]
] = {
CallsiteParameter.PATHNAME: (
lambda module, frame_info: frame_info.filename
),
CallsiteParameter.FILENAME: (
lambda module, frame_info: os.path.basename(frame_info.filename)
),
CallsiteParameter.MODULE: (
lambda module, frame_info: os.path.splitext(
os.path.basename(frame_info.filename)
)[0]
),
CallsiteParameter.FUNC_NAME: (
lambda module, frame_info: frame_info.function
),
CallsiteParameter.LINENO: (
lambda module, frame_info: frame_info.lineno
),
CallsiteParameter.THREAD: (
lambda module, frame_info: threading.get_ident()
),
CallsiteParameter.THREAD_NAME: (
lambda module, frame_info: threading.current_thread().name
),
CallsiteParameter.PROCESS: (lambda module, frame_info: os.getpid()),
CallsiteParameter.PROCESS_NAME: (
lambda module, frame_info: get_processname()
),
CallsiteParameter.PATHNAME: _get_callsite_pathname,
CallsiteParameter.FILENAME: _get_callsite_filename,
CallsiteParameter.MODULE: _get_callsite_module,
CallsiteParameter.FUNC_NAME: _get_callsite_func_name,
CallsiteParameter.LINENO: _get_callsite_lineno,
CallsiteParameter.THREAD: _get_callsite_thread,
CallsiteParameter.THREAD_NAME: _get_callsite_thread_name,
CallsiteParameter.PROCESS: _get_callsite_process,
CallsiteParameter.PROCESS_NAME: _get_callsite_process_name,
}
_record_attribute_map: ClassVar[dict[CallsiteParameter, str]] = {
CallsiteParameter.PATHNAME: "pathname",
@@ -835,7 +875,7 @@ class CallsiteParameterAdder:
# module should not be logging using structlog.
self._additional_ignores = ["logging", *additional_ignores]
self._active_handlers: list[
tuple[CallsiteParameter, Callable[[str, inspect.Traceback], Any]]
tuple[CallsiteParameter, Callable[[str, FrameType], Any]]
] = []
self._record_mappings: list[CallsiteParameterAdder._RecordMapping] = []
for parameter in parameters:
@@ -865,9 +905,8 @@ class CallsiteParameterAdder:
frame, module = _find_first_app_frame_and_name(
additional_ignores=self._additional_ignores
)
frame_info = inspect.getframeinfo(frame)
for parameter, handler in self._active_handlers:
event_dict[parameter.value] = handler(module, frame_info)
event_dict[parameter.value] = handler(module, frame)
return event_dict
@@ -884,8 +923,7 @@ class EventRenamer:
some processors may rely on the presence and meaning of the ``event``
key.
Arguments:
Args:
to: Rename ``event_dict["event"]`` to ``event_dict[to]``
replace_by: