# SPDX-License-Identifier: MIT OR Apache-2.0
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the MIT License. See the LICENSE file in the root of this
# repository for complete details.
"""
Helpers that make development with *structlog* more pleasant.
See also the narrative documentation in `development`.
"""
from __future__ import annotations
import shutil
import sys
import warnings
from dataclasses import dataclass
from io import StringIO
from types import ModuleType
from typing import (
Any,
Callable,
Literal,
Protocol,
Sequence,
TextIO,
Type,
Union,
cast,
)
from ._frames import _format_exception
from .processors import _figure_out_exc_info
from .typing import EventDict, ExceptionRenderer, ExcInfo, WrappedLogger
try:
import colorama
except ImportError:
colorama = None
try:
import better_exceptions
except ImportError:
better_exceptions = None
try:
import rich
from rich.console import Console
from rich.traceback import Traceback
except ImportError:
rich = None # type: ignore[assignment]
__all__ = [
"ConsoleRenderer",
"plain_traceback",
"rich_traceback",
"better_traceback",
]
_IS_WINDOWS = sys.platform == "win32"
_MISSING = "{who} requires the {package} package installed. "
_EVENT_WIDTH = 30 # pad the event name to so many characters
def _pad(s: str, length: int) -> str:
"""
Pads *s* to length *length*.
"""
missing = length - len(s)
return s + " " * (missing if missing > 0 else 0)
if colorama is not None:
RESET_ALL = colorama.Style.RESET_ALL
BRIGHT = colorama.Style.BRIGHT
DIM = colorama.Style.DIM
RED = colorama.Fore.RED
BLUE = colorama.Fore.BLUE
CYAN = colorama.Fore.CYAN
MAGENTA = colorama.Fore.MAGENTA
YELLOW = colorama.Fore.YELLOW
GREEN = colorama.Fore.GREEN
RED_BACK = colorama.Back.RED
else:
# These are the same values as the Colorama color codes. Redefining them
# here allows users to specify that they want color without having to
# install Colorama, which is only supposed to be necessary in Windows.
RESET_ALL = "\033[0m"
BRIGHT = "\033[1m"
DIM = "\033[2m"
RED = "\033[31m"
BLUE = "\033[34m"
CYAN = "\033[36m"
MAGENTA = "\033[35m"
YELLOW = "\033[33m"
GREEN = "\033[32m"
RED_BACK = "\033[41m"
# On Windows, colors are only available if Colorama is installed.
_has_colors = not _IS_WINDOWS or colorama is not None
# Prevent breakage of packages that used the old name of the variable.
_use_colors = _has_colors
class _Styles(Protocol):
reset: str
bright: str
level_critical: str
level_exception: str
level_error: str
level_warn: str
level_info: str
level_debug: str
level_notset: str
timestamp: str
logger_name: str
kv_key: str
kv_value: str
Styles = Union[_Styles, Type[_Styles]]
class _ColorfulStyles:
reset = RESET_ALL
bright = BRIGHT
level_critical = RED
level_exception = RED
level_error = RED
level_warn = YELLOW
level_info = GREEN
level_debug = GREEN
level_notset = RED_BACK
timestamp = DIM
logger_name = BLUE
kv_key = CYAN
kv_value = MAGENTA
class _PlainStyles:
reset = ""
bright = ""
level_critical = ""
level_exception = ""
level_error = ""
level_warn = ""
level_info = ""
level_debug = ""
level_notset = ""
timestamp = ""
logger_name = ""
kv_key = ""
kv_value = ""
[docs]
@dataclass
class Column:
"""
A column defines the way a key-value pair is formatted, and, by it's
position to the *columns* argument of `ConsoleRenderer`, the order in which
it is rendered.
Args:
key:
The key for which this column is responsible. Leave empty to define
it as the default formatter.
formatter: The formatter for columns with *key*.
.. versionadded:: 23.3.0
"""
key: str
formatter: ColumnFormatter
_NOTHING = object()
[docs]
def plain_traceback(sio: TextIO, exc_info: ExcInfo) -> None:
"""
"Pretty"-print *exc_info* to *sio* using our own plain formatter.
To be passed into `ConsoleRenderer`'s ``exception_formatter`` argument.
Used by default if neither Rich nor *better-exceptions* are present.
.. versionadded:: 21.2.0
"""
sio.write("\n" + _format_exception(exc_info))
rich_traceback = RichTracebackFormatter()
"""
Pretty-print *exc_info* to *sio* using the Rich package.
To be passed into `ConsoleRenderer`'s ``exception_formatter`` argument.
This is a `RichTracebackFormatter` with default arguments and used by default
if Rich is installed.
.. versionadded:: 21.2.0
"""
[docs]
def better_traceback(sio: TextIO, exc_info: ExcInfo) -> None:
"""
Pretty-print *exc_info* to *sio* using the *better-exceptions* package.
To be passed into `ConsoleRenderer`'s ``exception_formatter`` argument.
Used by default if *better-exceptions* is installed and Rich is absent.
.. versionadded:: 21.2.0
"""
sio.write("\n" + "".join(better_exceptions.format_exception(*exc_info)))
if rich is not None:
default_exception_formatter = rich_traceback
elif better_exceptions is not None:
default_exception_formatter = better_traceback
else:
default_exception_formatter = plain_traceback
[docs]
class ConsoleRenderer:
"""
Render ``event_dict`` nicely aligned, possibly in colors, and ordered.
If ``event_dict`` contains a true-ish ``exc_info`` key, it will be rendered
*after* the log line. If Rich_ or better-exceptions_ are present, in colors
and with extra context.
Args:
columns:
A list of `Column` objects defining both the order and format of
the key-value pairs in the output. If passed, most other arguments
become meaningless.
**Must** contain a column with ``key=''`` that defines the default
formatter.
.. seealso:: `columns-config`
pad_event:
Pad the event to this many characters. Ignored if *columns* are
passed.
colors:
Use colors for a nicer output. `True` by default. On Windows only
if Colorama_ is installed. Ignored if *columns* are passed.
force_colors:
Force colors even for non-tty destinations. Use this option if your
logs are stored in a file that is meant to be streamed to the
console. Only meaningful on Windows. Ignored if *columns* are
passed.
repr_native_str:
When `True`, `repr` is also applied to ``str``s. The ``event`` key
is *never* `repr` -ed. Ignored if *columns* are passed.
level_styles:
When present, use these styles for colors. This must be a dict from
level names (strings) to Colorama styles. The default can be
obtained by calling `ConsoleRenderer.get_default_level_styles`.
Ignored when *columns* are passed.
exception_formatter:
A callable to render ``exc_infos``. If Rich_ or better-exceptions_
are installed, they are used for pretty-printing by default (rich_
taking precedence). You can also manually set it to
`plain_traceback`, `better_traceback`, an instance of
`RichTracebackFormatter` like `rich_traceback`, or implement your
own.
sort_keys:
Whether to sort keys when formatting. `True` by default. Ignored if
*columns* are passed.
event_key:
The key to look for the main log message. Needed when you rename it
e.g. using `structlog.processors.EventRenamer`. Ignored if
*columns* are passed.
timestamp_key:
The key to look for timestamp of the log message. Needed when you
rename it e.g. using `structlog.processors.EventRenamer`. Ignored
if *columns* are passed.
Requires the Colorama_ package if *colors* is `True` **on Windows**.
Raises:
ValueError: If there's not exactly one default column formatter.
.. _Colorama: https://pypi.org/project/colorama/
.. _better-exceptions: https://pypi.org/project/better-exceptions/
.. _Rich: https://pypi.org/project/rich/
.. versionadded:: 16.0.0
.. versionadded:: 16.1.0 *colors*
.. versionadded:: 17.1.0 *repr_native_str*
.. versionadded:: 18.1.0 *force_colors*
.. versionadded:: 18.1.0 *level_styles*
.. versionchanged:: 19.2.0
Colorama now initializes lazily to avoid unwanted initializations as
``ConsoleRenderer`` is used by default.
.. versionchanged:: 19.2.0 Can be pickled now.
.. versionchanged:: 20.1.0
Colorama does not initialize lazily on Windows anymore because it breaks
rendering.
.. versionchanged:: 21.1.0
It is additionally possible to set the logger name using the
``logger_name`` key in the ``event_dict``.
.. versionadded:: 21.2.0 *exception_formatter*
.. versionchanged:: 21.2.0
`ConsoleRenderer` now handles the ``exc_info`` event dict key itself. Do
**not** use the `structlog.processors.format_exc_info` processor
together with `ConsoleRenderer` anymore! It will keep working, but you
can't have customize exception formatting and a warning will be raised
if you ask for it.
.. versionchanged:: 21.2.0
The colors keyword now defaults to True on non-Windows systems, and
either True or False in Windows depending on whether Colorama is
installed.
.. versionadded:: 21.3.0 *sort_keys*
.. versionadded:: 22.1.0 *event_key*
.. versionadded:: 23.2.0 *timestamp_key*
.. versionadded:: 23.3.0 *columns*
"""
def __init__( # noqa: PLR0912
self,
pad_event: int = _EVENT_WIDTH,
colors: bool = _has_colors,
force_colors: bool = False,
repr_native_str: bool = False,
level_styles: Styles | None = None,
exception_formatter: ExceptionRenderer = default_exception_formatter,
sort_keys: bool = True,
event_key: str = "event",
timestamp_key: str = "timestamp",
columns: list[Column] | None = None,
):
self._exception_formatter = exception_formatter
self._sort_keys = sort_keys
if columns is not None:
to_warn = []
def add_meaningless_arg(arg: str) -> None:
to_warn.append(
f"The `{arg}` argument is ignored when passing `columns`.",
)
if pad_event != _EVENT_WIDTH:
add_meaningless_arg("pad_event")
if colors != _has_colors:
add_meaningless_arg("colors")
if force_colors is not False:
add_meaningless_arg("force_colors")
if repr_native_str is not False:
add_meaningless_arg("repr_native_str")
if level_styles is not None:
add_meaningless_arg("level_styles")
if event_key != "event":
add_meaningless_arg("event_key")
if timestamp_key != "timestamp":
add_meaningless_arg("timestamp_key")
for w in to_warn:
warnings.warn(w, stacklevel=2)
defaults = [col for col in columns if col.key == ""]
if not defaults:
raise ValueError(
"Must pass a default column formatter (a column with `key=''`)."
)
if len(defaults) > 1:
raise ValueError("Only one default column formatter allowed.")
self._default_column_formatter = defaults[0].formatter
self._columns = [col for col in columns if col.key]
return
# Create default columns configuration.
styles: Styles
if colors:
if _IS_WINDOWS: # pragma: no cover
# On Windows, we can't do colorful output without colorama.
if colorama is None:
classname = self.__class__.__name__
raise SystemError(
_MISSING.format(
who=classname + " with `colors=True`",
package="colorama",
)
)
# Colorama must be init'd on Windows, but must NOT be
# init'd on other OSes, because it can break colors.
if force_colors:
colorama.deinit()
colorama.init(strip=False)
else:
colorama.init()
styles = _ColorfulStyles
else:
styles = _PlainStyles
self._styles = styles
level_to_color = (
self.get_default_level_styles(colors)
if level_styles is None
else level_styles
)
for key in level_to_color:
level_to_color[key] += styles.bright
self._longest_level = len(
max(level_to_color.keys(), key=lambda e: len(e))
)
self._repr_native_str = repr_native_str
self._default_column_formatter = KeyValueColumnFormatter(
styles.kv_key,
styles.kv_value,
styles.reset,
value_repr=self._repr,
width=0,
)
logger_name_formatter = KeyValueColumnFormatter(
key_style=None,
value_style=styles.bright + styles.logger_name,
reset_style=styles.reset,
value_repr=str,
prefix="[",
postfix="]",
)
self._columns = [
Column(
timestamp_key,
KeyValueColumnFormatter(
key_style=None,
value_style=styles.timestamp,
reset_style=styles.reset,
value_repr=str,
),
),
Column(
"level",
LogLevelColumnFormatter(
level_to_color, reset_style=styles.reset
),
),
Column(
event_key,
KeyValueColumnFormatter(
key_style=None,
value_style=styles.bright,
reset_style=styles.reset,
value_repr=str,
width=pad_event,
),
),
Column("logger", logger_name_formatter),
Column("logger_name", logger_name_formatter),
]
def _repr(self, val: Any) -> str:
"""
Determine representation of *val* depending on its type &
self._repr_native_str.
"""
if self._repr_native_str is True:
return repr(val)
if isinstance(val, str):
return val
return repr(val)
def __call__(
self, logger: WrappedLogger, name: str, event_dict: EventDict
) -> str:
stack = event_dict.pop("stack", None)
exc = event_dict.pop("exception", None)
exc_info = event_dict.pop("exc_info", None)
kvs = [
col.formatter(col.key, val)
for col in self._columns
if (val := event_dict.pop(col.key, _NOTHING)) is not _NOTHING
] + [
self._default_column_formatter(key, event_dict[key])
for key in (sorted(event_dict) if self._sort_keys else event_dict)
]
sio = StringIO()
sio.write((" ".join(kv for kv in kvs if kv)).rstrip(" "))
if stack is not None:
sio.write("\n" + stack)
if exc_info or exc is not None:
sio.write("\n\n" + "=" * 79 + "\n")
if exc_info:
exc_info = _figure_out_exc_info(exc_info)
if exc_info != (None, None, None):
self._exception_formatter(sio, exc_info)
elif exc is not None:
if self._exception_formatter is not plain_traceback:
warnings.warn(
"Remove `format_exc_info` from your processor chain "
"if you want pretty exceptions.",
stacklevel=2,
)
sio.write("\n" + exc)
return sio.getvalue()
[docs]
@staticmethod
def get_default_level_styles(colors: bool = True) -> Any:
"""
Get the default styles for log levels
This is intended to be used with `ConsoleRenderer`'s ``level_styles``
parameter. For example, if you are adding custom levels in your
home-grown :func:`~structlog.stdlib.add_log_level` you could do::
my_styles = ConsoleRenderer.get_default_level_styles()
my_styles["EVERYTHING_IS_ON_FIRE"] = my_styles["critical"] renderer
= ConsoleRenderer(level_styles=my_styles)
Args:
colors:
Whether to use colorful styles. This must match the *colors*
parameter to `ConsoleRenderer`. Default: `True`.
"""
styles: Styles
styles = _ColorfulStyles if colors else _PlainStyles
return {
"critical": styles.level_critical,
"exception": styles.level_exception,
"error": styles.level_error,
"warn": styles.level_warn,
"warning": styles.level_warn,
"info": styles.level_info,
"debug": styles.level_debug,
"notset": styles.level_notset,
}
_SENTINEL = object()
[docs]
def set_exc_info(
logger: WrappedLogger, method_name: str, event_dict: EventDict
) -> EventDict:
"""
Set ``event_dict["exc_info"] = True`` if *method_name* is ``"exception"``.
Do nothing if the name is different or ``exc_info`` is already set.
"""
if (
method_name != "exception"
or event_dict.get("exc_info", _SENTINEL) is not _SENTINEL
):
return event_dict
event_dict["exc_info"] = True
return event_dict