summaryrefslogtreecommitdiff
path: root/venv/lib/python3.11/site-packages/litestar/logging/config.py
diff options
context:
space:
mode:
Diffstat (limited to 'venv/lib/python3.11/site-packages/litestar/logging/config.py')
-rw-r--r--venv/lib/python3.11/site-packages/litestar/logging/config.py509
1 files changed, 509 insertions, 0 deletions
diff --git a/venv/lib/python3.11/site-packages/litestar/logging/config.py b/venv/lib/python3.11/site-packages/litestar/logging/config.py
new file mode 100644
index 0000000..a4a3713
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/litestar/logging/config.py
@@ -0,0 +1,509 @@
+from __future__ import annotations
+
+import sys
+from abc import ABC, abstractmethod
+from dataclasses import asdict, dataclass, field, fields
+from importlib.util import find_spec
+from logging import INFO
+from typing import TYPE_CHECKING, Any, Callable, Literal, cast
+
+from litestar.exceptions import ImproperlyConfiguredException, MissingDependencyException
+from litestar.serialization.msgspec_hooks import _msgspec_json_encoder
+from litestar.utils.deprecation import deprecated
+
+__all__ = ("BaseLoggingConfig", "LoggingConfig", "StructLoggingConfig")
+
+
+if TYPE_CHECKING:
+ from collections.abc import Iterable
+ from typing import NoReturn
+
+ # these imports are duplicated on purpose so sphinx autodoc can find and link them
+ from structlog.types import BindableLogger, Processor, WrappedLogger
+ from structlog.typing import EventDict
+
+ from litestar.types import Logger, Scope
+ from litestar.types.callable_types import ExceptionLoggingHandler, GetLogger
+
+
+try:
+ from structlog.types import BindableLogger, Processor, WrappedLogger
+except ImportError:
+ BindableLogger = Any # type: ignore[assignment, misc]
+ Processor = Any # type: ignore[misc]
+ WrappedLogger = Any # type: ignore[misc]
+
+
+default_handlers: dict[str, dict[str, Any]] = {
+ "console": {
+ "class": "logging.StreamHandler",
+ "level": "DEBUG",
+ "formatter": "standard",
+ },
+ "queue_listener": {
+ "class": "litestar.logging.standard.QueueListenerHandler",
+ "level": "DEBUG",
+ "formatter": "standard",
+ },
+}
+
+if sys.version_info >= (3, 12, 0):
+ default_handlers["queue_listener"].update(
+ {
+ "class": "logging.handlers.QueueHandler",
+ "queue": {
+ "()": "queue.Queue",
+ "maxsize": -1,
+ },
+ "listener": "litestar.logging.standard.LoggingQueueListener",
+ "handlers": ["console"],
+ }
+ )
+
+ # do not format twice, the console handler will do the job
+ del default_handlers["queue_listener"]["formatter"]
+
+
+default_picologging_handlers: dict[str, dict[str, Any]] = {
+ "console": {
+ "class": "picologging.StreamHandler",
+ "level": "DEBUG",
+ "formatter": "standard",
+ },
+ "queue_listener": {
+ "class": "litestar.logging.picologging.QueueListenerHandler",
+ "level": "DEBUG",
+ "formatter": "standard",
+ },
+}
+
+
+def get_logger_placeholder(_: str | None = None) -> NoReturn:
+ """Raise: An :class:`ImproperlyConfiguredException <.exceptions.ImproperlyConfiguredException>`"""
+ raise ImproperlyConfiguredException(
+ "cannot call '.get_logger' without passing 'logging_config' to the Litestar constructor first"
+ )
+
+
+def _get_default_handlers() -> dict[str, dict[str, Any]]:
+ """Return the default logging handlers for the config.
+
+ Returns:
+ A dictionary of logging handlers
+ """
+ if find_spec("picologging"):
+ return default_picologging_handlers
+ return default_handlers
+
+
+def _default_exception_logging_handler_factory(
+ is_struct_logger: bool, traceback_line_limit: int
+) -> ExceptionLoggingHandler:
+ """Create an exception logging handler function.
+
+ Args:
+ is_struct_logger: Whether the logger is a structlog instance.
+ traceback_line_limit: Maximal number of lines to log from the
+ traceback.
+
+ Returns:
+ An exception logging handler.
+ """
+
+ def _default_exception_logging_handler(logger: Logger, scope: Scope, tb: list[str]) -> None:
+ # we limit the length of the stack trace to 20 lines.
+ first_line = tb.pop(0)
+
+ if is_struct_logger:
+ logger.exception(
+ "Uncaught Exception",
+ connection_type=scope["type"],
+ path=scope["path"],
+ traceback="".join(tb[-traceback_line_limit:]),
+ )
+ else:
+ stack_trace = first_line + "".join(tb[-traceback_line_limit:])
+ logger.exception(
+ "exception raised on %s connection to route %s\n\n%s", scope["type"], scope["path"], stack_trace
+ )
+
+ return _default_exception_logging_handler
+
+
+class BaseLoggingConfig(ABC):
+ """Abstract class that should be extended by logging configs."""
+
+ __slots__ = ("log_exceptions", "traceback_line_limit", "exception_logging_handler")
+
+ log_exceptions: Literal["always", "debug", "never"]
+ """Should exceptions be logged, defaults to log exceptions when ``app.debug == True``'"""
+ traceback_line_limit: int
+ """Max number of lines to print for exception traceback"""
+ exception_logging_handler: ExceptionLoggingHandler | None
+ """Handler function for logging exceptions."""
+
+ @abstractmethod
+ def configure(self) -> GetLogger:
+ """Return logger with the given configuration.
+
+ Returns:
+ A 'logging.getLogger' like function.
+ """
+ raise NotImplementedError("abstract method")
+
+ @staticmethod
+ def set_level(logger: Any, level: int) -> None:
+ """Provides a consistent interface to call `setLevel` for all loggers."""
+ raise NotImplementedError("abstract method")
+
+
+@dataclass
+class LoggingConfig(BaseLoggingConfig):
+ """Configuration class for standard logging.
+
+ Notes:
+ - If 'picologging' is installed it will be used by default.
+ """
+
+ version: Literal[1] = field(default=1)
+ """The only valid value at present is 1."""
+ incremental: bool = field(default=False)
+ """Whether the configuration is to be interpreted as incremental to the existing configuration.
+
+ Notes:
+ - This option is ignored for 'picologging'
+ """
+ disable_existing_loggers: bool = field(default=False)
+ """Whether any existing non-root loggers are to be disabled."""
+ filters: dict[str, dict[str, Any]] | None = field(default=None)
+ """A dict in which each key is a filter id and each value is a dict describing how to configure the corresponding
+ Filter instance.
+ """
+ propagate: bool = field(default=True)
+ """If messages must propagate to handlers higher up the logger hierarchy from this logger."""
+ formatters: dict[str, dict[str, Any]] = field(
+ default_factory=lambda: {
+ "standard": {"format": "%(levelname)s - %(asctime)s - %(name)s - %(module)s - %(message)s"}
+ }
+ )
+ handlers: dict[str, dict[str, Any]] = field(default_factory=_get_default_handlers)
+ """A dict in which each key is a handler id and each value is a dict describing how to configure the corresponding
+ Handler instance.
+ """
+ loggers: dict[str, dict[str, Any]] = field(
+ default_factory=lambda: {
+ "litestar": {"level": "INFO", "handlers": ["queue_listener"], "propagate": False},
+ }
+ )
+ """A dict in which each key is a logger name and each value is a dict describing how to configure the corresponding
+ Logger instance.
+ """
+ root: dict[str, dict[str, Any] | list[Any] | str] = field(
+ default_factory=lambda: {
+ "handlers": ["queue_listener"],
+ "level": "INFO",
+ }
+ )
+ """This will be the configuration for the root logger.
+
+ Processing of the configuration will be as for any logger, except that the propagate setting will not be applicable.
+ """
+ configure_root_logger: bool = field(default=True)
+ """Should the root logger be configured, defaults to True for ease of configuration."""
+ log_exceptions: Literal["always", "debug", "never"] = field(default="debug")
+ """Should exceptions be logged, defaults to log exceptions when 'app.debug == True'"""
+ traceback_line_limit: int = field(default=20)
+ """Max number of lines to print for exception traceback"""
+ exception_logging_handler: ExceptionLoggingHandler | None = field(default=None)
+ """Handler function for logging exceptions."""
+
+ def __post_init__(self) -> None:
+ if "queue_listener" not in self.handlers:
+ self.handlers["queue_listener"] = _get_default_handlers()["queue_listener"]
+
+ if "litestar" not in self.loggers:
+ self.loggers["litestar"] = {
+ "level": "INFO",
+ "handlers": ["queue_listener"],
+ "propagate": False,
+ }
+
+ if self.log_exceptions != "never" and self.exception_logging_handler is None:
+ self.exception_logging_handler = _default_exception_logging_handler_factory(
+ is_struct_logger=False, traceback_line_limit=self.traceback_line_limit
+ )
+
+ def configure(self) -> GetLogger:
+ """Return logger with the given configuration.
+
+ Returns:
+ A 'logging.getLogger' like function.
+ """
+
+ excluded_fields: tuple[str, ...]
+ if "picologging" in " ".join([handler["class"] for handler in self.handlers.values()]):
+ try:
+ from picologging import config, getLogger
+ except ImportError as e:
+ raise MissingDependencyException("picologging") from e
+
+ excluded_fields = ("incremental", "configure_root_logger")
+ else:
+ from logging import config, getLogger # type: ignore[no-redef, assignment]
+
+ excluded_fields = ("configure_root_logger",)
+
+ values = {
+ _field.name: getattr(self, _field.name)
+ for _field in fields(self)
+ if getattr(self, _field.name) is not None and _field.name not in excluded_fields
+ }
+
+ if not self.configure_root_logger:
+ values.pop("root")
+ config.dictConfig(values)
+ return cast("Callable[[str], Logger]", getLogger)
+
+ @staticmethod
+ def set_level(logger: Logger, level: int) -> None:
+ """Provides a consistent interface to call `setLevel` for all loggers."""
+ logger.setLevel(level)
+
+
+class StructlogEventFilter:
+ """Remove keys from the log event.
+
+ Add an instance to the processor chain.
+
+ .. code-block:: python
+ :caption: Examples
+
+ structlog.configure(
+ ...,
+ processors=[
+ ...,
+ EventFilter(["color_message"]),
+ ...,
+ ],
+ )
+
+ """
+
+ def __init__(self, filter_keys: Iterable[str]) -> None:
+ """Initialize the EventFilter.
+
+ Args:
+ filter_keys: Iterable of string keys to be excluded from the log event.
+ """
+ self.filter_keys = filter_keys
+
+ def __call__(self, _: WrappedLogger, __: str, event_dict: EventDict) -> EventDict:
+ """Receive the log event, and filter keys.
+
+ Args:
+ _ ():
+ __ ():
+ event_dict (): The data to be logged.
+
+ Returns:
+ The log event with any key in `self.filter_keys` removed.
+ """
+ for key in self.filter_keys:
+ event_dict.pop(key, None)
+ return event_dict
+
+
+def default_json_serializer(value: EventDict, **_: Any) -> bytes:
+ return _msgspec_json_encoder.encode(value)
+
+
+def stdlib_json_serializer(value: EventDict, **_: Any) -> str: # pragma: no cover
+ return _msgspec_json_encoder.encode(value).decode("utf-8")
+
+
+def default_structlog_processors(as_json: bool = True) -> list[Processor]: # pyright: ignore
+ """Set the default processors for structlog.
+
+ Returns:
+ An optional list of processors.
+ """
+ try:
+ import structlog
+ from structlog.dev import RichTracebackFormatter
+
+ if as_json:
+ return [
+ structlog.contextvars.merge_contextvars,
+ structlog.processors.add_log_level,
+ structlog.processors.format_exc_info,
+ structlog.processors.TimeStamper(fmt="iso"),
+ structlog.processors.JSONRenderer(serializer=default_json_serializer),
+ ]
+ return [
+ structlog.contextvars.merge_contextvars,
+ structlog.processors.add_log_level,
+ structlog.processors.TimeStamper(fmt="iso"),
+ structlog.dev.ConsoleRenderer(
+ colors=True, exception_formatter=RichTracebackFormatter(max_frames=1, show_locals=False, width=80)
+ ),
+ ]
+
+ except ImportError:
+ return []
+
+
+def default_structlog_standard_lib_processors(as_json: bool = True) -> list[Processor]: # pyright: ignore
+ """Set the default processors for structlog stdlib.
+
+ Returns:
+ An optional list of processors.
+ """
+ try:
+ import structlog
+ from structlog.dev import RichTracebackFormatter
+
+ if as_json:
+ return [
+ structlog.processors.TimeStamper(fmt="iso"),
+ structlog.stdlib.add_log_level,
+ structlog.stdlib.ExtraAdder(),
+ StructlogEventFilter(["color_message"]),
+ structlog.stdlib.ProcessorFormatter.remove_processors_meta,
+ structlog.processors.JSONRenderer(serializer=stdlib_json_serializer),
+ ]
+ return [
+ structlog.processors.TimeStamper(fmt="iso"),
+ structlog.stdlib.add_log_level,
+ structlog.stdlib.ExtraAdder(),
+ StructlogEventFilter(["color_message"]),
+ structlog.stdlib.ProcessorFormatter.remove_processors_meta,
+ structlog.dev.ConsoleRenderer(
+ colors=True, exception_formatter=RichTracebackFormatter(max_frames=1, show_locals=False, width=80)
+ ),
+ ]
+ except ImportError:
+ return []
+
+
+def default_logger_factory(as_json: bool = True) -> Callable[..., WrappedLogger] | None:
+ """Set the default logger factory for structlog.
+
+ Returns:
+ An optional logger factory.
+ """
+ try:
+ import structlog
+
+ if as_json:
+ return structlog.BytesLoggerFactory()
+ return structlog.WriteLoggerFactory()
+ except ImportError:
+ return None
+
+
+@dataclass
+class StructLoggingConfig(BaseLoggingConfig):
+ """Configuration class for structlog.
+
+ Notes:
+ - requires ``structlog`` to be installed.
+ """
+
+ processors: list[Processor] | None = field(default=None) # pyright: ignore
+ """Iterable of structlog logging processors."""
+ standard_lib_logging_config: LoggingConfig | None = field(default=None) # pyright: ignore
+ """Optional customized standard logging configuration.
+
+ Use this when you need to modify the standard library outside of the Structlog pre-configured implementation.
+ """
+ wrapper_class: type[BindableLogger] | None = field(default=None) # pyright: ignore
+ """Structlog bindable logger."""
+ context_class: dict[str, Any] | None = None
+ """Context class (a 'contextvar' context) for the logger."""
+ logger_factory: Callable[..., WrappedLogger] | None = field(default=None) # pyright: ignore
+ """Logger factory to use."""
+ cache_logger_on_first_use: bool = field(default=True)
+ """Whether to cache the logger configuration and reuse."""
+ log_exceptions: Literal["always", "debug", "never"] = field(default="debug")
+ """Should exceptions be logged, defaults to log exceptions when 'app.debug == True'"""
+ traceback_line_limit: int = field(default=20)
+ """Max number of lines to print for exception traceback"""
+ exception_logging_handler: ExceptionLoggingHandler | None = field(default=None)
+ """Handler function for logging exceptions."""
+ pretty_print_tty: bool = field(default=True)
+ """Pretty print log output when run from an interactive terminal."""
+
+ def __post_init__(self) -> None:
+ if self.processors is None:
+ self.processors = default_structlog_processors(not sys.stderr.isatty() and self.pretty_print_tty)
+ if self.logger_factory is None:
+ self.logger_factory = default_logger_factory(not sys.stderr.isatty() and self.pretty_print_tty)
+ if self.log_exceptions != "never" and self.exception_logging_handler is None:
+ self.exception_logging_handler = _default_exception_logging_handler_factory(
+ is_struct_logger=True, traceback_line_limit=self.traceback_line_limit
+ )
+ try:
+ import structlog
+
+ if self.standard_lib_logging_config is None:
+ self.standard_lib_logging_config = LoggingConfig(
+ formatters={
+ "standard": {
+ "()": structlog.stdlib.ProcessorFormatter,
+ "processors": default_structlog_standard_lib_processors(
+ as_json=not sys.stderr.isatty() and self.pretty_print_tty
+ ),
+ }
+ }
+ )
+ except ImportError:
+ self.standard_lib_logging_config = LoggingConfig()
+
+ def configure(self) -> GetLogger:
+ """Return logger with the given configuration.
+
+ Returns:
+ A 'logging.getLogger' like function.
+ """
+ try:
+ import structlog
+ except ImportError as e:
+ raise MissingDependencyException("structlog") from e
+
+ structlog.configure(
+ **{
+ k: v
+ for k, v in asdict(self).items()
+ if k
+ not in (
+ "standard_lib_logging_config",
+ "log_exceptions",
+ "traceback_line_limit",
+ "exception_logging_handler",
+ "pretty_print_tty",
+ )
+ }
+ )
+ return structlog.get_logger
+
+ @staticmethod
+ def set_level(logger: Logger, level: int) -> None:
+ """Provides a consistent interface to call `setLevel` for all loggers."""
+
+ try:
+ import structlog
+
+ structlog.configure(wrapper_class=structlog.make_filtering_bound_logger(level))
+ except ImportError:
+ """"""
+ return
+
+
+@deprecated(version="2.6.0", removal_in="3.0.0", alternative="`StructLoggingConfig.set_level`")
+def default_wrapper_class(log_level: int = INFO) -> type[BindableLogger] | None: # pragma: no cover # pyright: ignore
+ try: # pragma: no cover
+ import structlog
+
+ return structlog.make_filtering_bound_logger(log_level)
+ except ImportError:
+ return None