GitHub

API reference for logger.

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.

ParameterTypeDescription
colorsbool | None = None
format_dateCallable[[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) -> str

Local wall-clock time, HH:MM:SS.mmm.

ParameterTypeDescription
whendatetime

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.

ParameterTypeDescription
loggers_Loggers[Any, Any]
merge_with_existingbool = 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.

ParameterTypeDescription
key_or_valuesstr | Mapping[str, object]
valueobject = _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).

ParameterTypeDescription
messageobject = ()

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]
ParameterTypeDescription
messageobject = ()

Returns Effect[None]

def log_debug(*message: object) -> Effect[None]:
    return log_with_level(LogLevel.DEBUG, *message)

log_error

log_error(*message) -> Effect[None]
ParameterTypeDescription
messageobject = ()

Returns Effect[None]

def log_error(*message: object) -> Effect[None]:
    return log_with_level(LogLevel.ERROR, *message)

log_fatal

log_fatal(*message) -> Effect[None]
ParameterTypeDescription
messageobject = ()

Returns Effect[None]

def log_fatal(*message: object) -> Effect[None]:
    return log_with_level(LogLevel.FATAL, *message)

log_info

log_info(*message) -> Effect[None]
ParameterTypeDescription
messageobject = ()

Returns Effect[None]

def log_info(*message: object) -> Effect[None]:
    return log_with_level(LogLevel.INFO, *message)

log_trace

log_trace(*message) -> Effect[None]
ParameterTypeDescription
messageobject = ()

Returns Effect[None]

def log_trace(*message: object) -> Effect[None]:
    return log_with_level(LogLevel.TRACE, *message)

log_warning

log_warning(*message) -> Effect[None]
ParameterTypeDescription
messageobject = ()

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.

ParameterTypeDescription
levelSeverity | None
messageobject = ()

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.

ParameterTypeDescription
targetstr | 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)