Formatting and filtering

Updated Jul 05, 2026
  • Logger-level formatting is fixed ("{logger} [LEVEL] message"). To customize output wrap handlers with formatter callables using the builder API:
def json_formatter(record: dict[str, object]) -> str:
    import json

    return json.dumps(
        {
            "logger": record["logger"],
            "level": record["level"],
            "message": record["message"],
        },
        separators=(",", ":"),
    )


stream = StreamHandlerBuilder.stdout().with_formatter(json_formatter).build()
  • Formatter identifiers are limited to "default" for now. Future releases will connect FormatterBuilder and handler formatter IDs, but this is not yet implemented.
  • Filters are available through both ConfigBuilder and dictConfig. fileConfig does not yet support filter declarations. Handler-level filters and level attributes remain unsupported, as these require Rust infrastructure changes outside the current scope.
  • Python callback filters receive a mutable logging.LogRecord view on the producer thread. When they accept the record, any new supported attributes they add are copied into metadata.key_values before the record enters the async queue, making them visible to formatter callables and StdlibHandlerAdapter.
import contextvars
import logging

from femtologging import PythonCallbackFilterBuilder

request_id = contextvars.ContextVar("request_id", default="")


def enrich_request(record: logging.LogRecord) -> bool:
    record.request_id = request_id.get()
    return record.levelname == "INFO"


callback_filter = PythonCallbackFilterBuilder(enrich_request)