Source code for fluxlit.logging.json_formatter

"""Optional JSON :class:`logging.Formatter` for one-line structured logs (stdlib only)."""

from __future__ import annotations

import json
import logging
import numbers
from datetime import date, datetime
from typing import Any

_LOG_RECORD_RESERVED: frozenset[str] = frozenset(
    {
        "name",
        "msg",
        "args",
        "levelname",
        "levelno",
        "pathname",
        "filename",
        "module",
        "exc_info",
        "exc_text",
        "stack_info",
        "lineno",
        "funcName",
        "created",
        "msecs",
        "relativeCreated",
        "thread",
        "threadName",
        "processName",
        "process",
        "message",
        "taskName",
    }
)


def _json_safe(value: Any) -> Any:
    if value is None or isinstance(value, (str, bool, numbers.Number)):
        return value
    if isinstance(value, (datetime, date)):
        return value.isoformat()
    if isinstance(value, (bytes, bytearray)):
        return value.decode("utf-8", errors="replace")
    if isinstance(value, BaseException):
        return repr(value)
    return repr(value)


[docs] class JsonLogFormatter(logging.Formatter): """Emit one JSON object per log line, including merged ``extra`` fields."""
[docs] def format(self, record: logging.LogRecord) -> str: message = record.getMessage() payload: dict[str, Any] = { "time": self.formatTime(record, self.datefmt), "level": record.levelname, "logger": record.name, "message": message, } for key, raw in record.__dict__.items(): if key in _LOG_RECORD_RESERVED or key.startswith("_"): continue payload[key] = _json_safe(raw) if record.exc_info: payload["exception"] = self.formatException(record.exc_info) return json.dumps(payload, ensure_ascii=False)