Logging (Effect's Logger, LogLevel and the log references).
fx.log(...) and fx.log_info(...) / log_debug / ... write an
entry to every logger in CurrentLoggers, unless MinimumLogLevel is
above its level::
yield from fx.log_info("user signed in", user_id)
program.pipe(fx.provide(logger.layer([logger.console_json])))
program.pipe(fx.provide_service(fx.MinimumLogLevel, fx.LogLevel.DEBUG))eff.annotate_logs("request", rid), eff.with_log_span("db") and
fx.annotate_logs_scoped(...) add context to the entries;
logger.to_logging("app") hands them to the standard library's
logging.
Classes#
Functions#
console_pretty#
console_pretty(*, colors=None, format_date=default_date_format) -> Logger[None]A readable multi-line logger for terminals: a colored header with
the first message, then the cause, the other items and the
annotations, indented in a console group. colors defaults to
whether stdout is a terminal.
| Parameter | Type | Description |
|---|---|---|
colors | bool | None = None | |
format_date | Callable[[datetime], str] = default_date_format |
Returns Logger[None]
def console_pretty(
*, colors: bool | None = None, format_date: Callable[[datetime], str] = default_date_format
) -> Logger[None]:
"""A readable multi-line logger for terminals: a colored header with
the first message, then the cause, the other items and the
annotations, indented in a console group. ``colors`` defaults to
whether stdout is a terminal."""
show = sys.stdout.isatty() if colors is None else colors
color = _paint if show else _plain
def log(options: Options) -> None:
fiber = options.fiber
console = fiber.get_ref(CurrentConsole)
write = console.error if fiber.get_ref(LogToStderr) else console.log
level = options.log_level
items = list(options.message)
first_line = (
color(f"[{format_date(options.date)}]", "white")
+ f" {color(level.name, *_LEVEL_COLORS[level])}"
+ f" (#{fiber.id}){_spans(options)}:"
)
if items and isinstance(first := structured_message(items[0]), str):
first_line += " " + color(first, "bold", "cyan")
items.pop(0)
write(first_line)
console.group()
try:
if options.cause.reasons:
write(options.cause.pretty())
for item in items:
write(item)
for key, value in fiber.get_ref(CurrentLogAnnotations).items():
write(color(f"{key}:", "bold", "white"), value)
finally:
console.group_end()
return Logger(log)default_date_format#
default_date_format(when) -> strLocal wall-clock time, HH:MM:SS.mmm.
| Parameter | Type | Description |
|---|---|---|
when | datetime |
Returns str
def default_date_format(when: datetime) -> str:
"""Local wall-clock time, ``HH:MM:SS.mmm``."""
local = when.astimezone()
return local.strftime("%H:%M:%S.") + f"{local.microsecond // 1000:03d}"layer#
layer(loggers, *, merge_with_existing=False) -> Layer[Any, Any, Any]A layer that replaces the current loggers with loggers (or adds
them, with merge_with_existing). An item may be an effect making a
logger, such as Logger.batched; it runs when the layer builds, and
its resources are released with the layer.
| Parameter | Type | Description |
|---|---|---|
loggers | _Loggers[Any, Any] | |
merge_with_existing | bool = False |
Returns Layer[Any, Any, Any]
def layer(
loggers: _Loggers[Any, Any], *, merge_with_existing: bool = False
) -> Layer[Any, Any, Any]:
"""A layer that replaces the current loggers with ``loggers`` (or adds
them, with ``merge_with_existing``). An item may be an effect making a
logger, such as ``Logger.batched``; it runs when the layer builds, and
its resources are released with the layer."""
given = list(loggers)
def build():
current: list[Logger[Any]] = (
list((yield from CurrentLoggers())) if merge_with_existing else []
)
for item in given:
made = (yield from item) if isinstance(item, Effect) else item
if all(made is not existing for existing in current):
current.append(made)
return tuple(current)
return effect(CurrentLoggers, _Gen(build))annotate_logs_scoped#
annotate_logs_scoped(key_or_values, value=_MISSING) -> Effect[None, Never, Scope]Add log annotations for the rest of the current fiber's region, until the current scope closes. Closing it restores each key's earlier value (or removes the key), unless the key was set again since.
| Parameter | Type | Description |
|---|---|---|
key_or_values | str | Mapping[str, object] | |
value | object = _MISSING |
Returns Effect[None, Never, Scope]
def annotate_logs_scoped(
key_or_values: str | Mapping[str, object], value: object = _MISSING, /
) -> Effect[None, Never, Scope]:
"""Add log annotations for the rest of the current fiber's region,
until the current scope closes. Closing it restores each key's earlier
value (or removes the key), unless the key was set again since."""
added = {key_or_values: value} if isinstance(key_or_values, str) else dict(key_or_values)
def add(annotations: Mapping[str, object]) -> Mapping[str, object]:
return {**annotations, **added}
def restore(
before: Mapping[str, object], _updated: Mapping[str, object], now: Mapping[str, object]
) -> Mapping[str, object]:
restored = dict(now)
for k, v in added.items():
if not _same(now.get(k, _MISSING), v):
continue
if k in before:
restored[k] = before[k]
else:
del restored[k]
return restored
return update_service_scoped(CurrentLogAnnotations, add, reset=restore)log#
log(*message) -> Effect[None]Log at the CurrentLogLevel (INFO unless provided).
| Parameter | Type | Description |
|---|---|---|
message | object = () |
Returns Effect[None]
def log(*message: object) -> Effect[None]:
"""Log at the ``CurrentLogLevel`` (``INFO`` unless provided)."""
return log_with_level(None, *message)log_debug#
log_debug(*message) -> Effect[None]| Parameter | Type | Description |
|---|---|---|
message | object = () |
Returns Effect[None]
def log_debug(*message: object) -> Effect[None]:
return log_with_level(LogLevel.DEBUG, *message)log_error#
log_error(*message) -> Effect[None]| Parameter | Type | Description |
|---|---|---|
message | object = () |
Returns Effect[None]
def log_error(*message: object) -> Effect[None]:
return log_with_level(LogLevel.ERROR, *message)log_fatal#
log_fatal(*message) -> Effect[None]| Parameter | Type | Description |
|---|---|---|
message | object = () |
Returns Effect[None]
def log_fatal(*message: object) -> Effect[None]:
return log_with_level(LogLevel.FATAL, *message)log_info#
log_info(*message) -> Effect[None]| Parameter | Type | Description |
|---|---|---|
message | object = () |
Returns Effect[None]
def log_info(*message: object) -> Effect[None]:
return log_with_level(LogLevel.INFO, *message)log_trace#
log_trace(*message) -> Effect[None]| Parameter | Type | Description |
|---|---|---|
message | object = () |
Returns Effect[None]
def log_trace(*message: object) -> Effect[None]:
return log_with_level(LogLevel.TRACE, *message)log_warning#
log_warning(*message) -> Effect[None]| Parameter | Type | Description |
|---|---|---|
message | object = () |
Returns Effect[None]
def log_warning(*message: object) -> Effect[None]:
return log_with_level(LogLevel.WARN, *message)log_with_level#
log_with_level(level, *message) -> Effect[None]Log message at level (None: the CurrentLogLevel) to
every logger in CurrentLoggers, unless MinimumLogLevel is above
it. Cause items are taken out of the message and logged as its
cause.
| Parameter | Type | Description |
|---|---|---|
level | Severity | None | |
message | object = () |
Returns Effect[None]
def log_with_level(level: Severity | None, *message: object) -> Effect[None]:
"""Log ``message`` at ``level`` (``None``: the ``CurrentLogLevel``) to
every logger in ``CurrentLoggers``, unless ``MinimumLogLevel`` is above
it. ``Cause`` items are taken out of the message and logged as its
cause."""
reasons: list[Reason[object]] = []
items: list[object] = []
for item in message:
if isinstance(item, Cause):
reasons.extend(item.reasons) # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
else:
items.append(item)
cause: Cause[object] = Cause(tuple(reasons))
logged = tuple(items)
def body(fiber: Fiber[Any, Any]) -> Effect[None]:
at = fiber.get_ref(CurrentLogLevel) if level is None else level
if fiber.get_ref(MinimumLogLevel) > at:
return _VOID
loggers = fiber.get_ref(CurrentLoggers)
if loggers:
nanos = fiber.get_ref(Clock).unsafe_now_nanos()
date = _EPOCH + timedelta(microseconds=nanos // 1000)
options = Options(logged, at, cause, fiber, date)
for logger in loggers:
logger.log(options)
return _VOID
return _WithFiber(body)to_logging#
to_logging(target='pyfect') -> Logger[None]A logger writing each entry to target (a logging.Logger or
the name of one) as a LogRecord, at the matching standard level
(TRACE is 5, named "TRACE"). The record's message is the logged
items joined by spaces; its time is the entry's (the fiber's
Clock), and a logged cause becomes its exc_info. It also
carries fiber_id ("#12"), log_annotations (a dict),
log_spans (label to elapsed milliseconds) and cause. The
target's own level and handlers then apply as usual.
| Parameter | Type | Description |
|---|---|---|
target | str | logging.Logger = 'pyfect' |
Returns Logger[None]
def to_logging(target: str | logging.Logger = "pyfect") -> Logger[None]:
"""A logger writing each entry to ``target`` (a ``logging.Logger`` or
the name of one) as a ``LogRecord``, at the matching standard level
(``TRACE`` is 5, named ``"TRACE"``). The record's message is the logged
items joined by spaces; its time is the entry's (the fiber's
``Clock``), and a logged cause becomes its ``exc_info``. It also
carries ``fiber_id`` (``"#12"``), ``log_annotations`` (a dict),
``log_spans`` (label to elapsed milliseconds) and ``cause``. The
target's own level and handlers then apply as usual."""
std = target if isinstance(target, logging.Logger) else logging.getLogger(target)
if logging.getLevelName(LogLevel.TRACE) == f"Level {int(LogLevel.TRACE)}":
logging.addLevelName(LogLevel.TRACE, "TRACE")
def emit(options: Options) -> None:
level = int(options.log_level)
if not std.isEnabledFor(level):
return
fiber = options.fiber
cause = options.cause
error = cause.squash() if cause.reasons else None
now = options.millis
record = std.makeRecord(
std.name,
level,
"(pyfect)",
0,
" ".join(["%s"] * len(options.message)),
options.message,
None if error is None else (type(error), error, error.__traceback__),
extra={
"fiber_id": f"#{fiber.id}",
"log_annotations": dict(fiber.get_ref(CurrentLogAnnotations)),
"log_spans": {s.label: now - s.start for s in fiber.get_ref(CurrentLogSpans)},
"cause": cause,
},
)
record.created = options.date.timestamp()
record.msecs = options.date.microsecond // 1000
std.handle(record)
return Logger(emit)