From b32c9efb8a9a5702c4b7bf2d2962b2c68225e8b0 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 15 Feb 2026 04:16:45 +0100 Subject: [PATCH] feat: add structured JSON logging New JsonFormatter emits one JSON object per log line (JSONL). Activated via format = "json" in [logging] config. Default remains "text". --- src/derp/cli.py | 13 ++++++++++--- src/derp/log.py | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 src/derp/log.py diff --git a/src/derp/cli.py b/src/derp/cli.py index f2fd3da..2474e4e 100644 --- a/src/derp/cli.py +++ b/src/derp/cli.py @@ -10,6 +10,7 @@ import sys from derp import __version__ from derp.bot import Bot from derp.config import resolve_config +from derp.log import JsonFormatter from derp.plugin import PluginRegistry LOG_FORMAT = "%(asctime)s %(levelname)-5s %(name)s %(message)s" @@ -60,11 +61,17 @@ def main(argv: list[str] | None = None) -> int: parser = build_parser() args = parser.parse_args(argv) - level = logging.DEBUG if args.verbose else logging.INFO - logging.basicConfig(format=LOG_FORMAT, datefmt=LOG_DATE, level=level) - config = resolve_config(args.config) + level = logging.DEBUG if args.verbose else logging.INFO + log_fmt = config.get("logging", {}).get("format", "text") + if log_fmt == "json": + handler = logging.StreamHandler() + handler.setFormatter(JsonFormatter()) + logging.basicConfig(handlers=[handler], level=level) + else: + logging.basicConfig(format=LOG_FORMAT, datefmt=LOG_DATE, level=level) + log = logging.getLogger("derp") log.info("derp %s starting", __version__) diff --git a/src/derp/log.py b/src/derp/log.py new file mode 100644 index 0000000..cb134fd --- /dev/null +++ b/src/derp/log.py @@ -0,0 +1,21 @@ +"""Structured logging formatters.""" + +from __future__ import annotations + +import json +import logging + + +class JsonFormatter(logging.Formatter): + """Emit log records as single-line JSON objects.""" + + def format(self, record: logging.LogRecord) -> str: + entry: dict[str, str] = { + "ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S"), + "level": record.levelname.lower(), + "logger": record.name, + "msg": record.getMessage(), + } + if record.exc_info and not isinstance(record.exc_info, bool) and record.exc_info[0]: + entry["exc"] = self.formatException(record.exc_info) + return json.dumps(entry, ensure_ascii=False, separators=(",", ":"))