""" Minimal JSON logger for gunicorn. Replaces gunicorn's default text logger with structured JSON output, one JSON object per line. Both access and error logs are covered. Usage: gunicorn --logger-class json_logger.JsonLogger ... """ import json import logging import time from gunicorn.glogging import Logger class JsonLogger(Logger): """Gunicorn logger that emits one JSON object per log line.""" # ------------------------------------------------------------------ # # Error / application log records # # ------------------------------------------------------------------ # def setup(self, cfg): super().setup(cfg) # Replace every handler's formatter on both error and access loggers for logger_name in ("error_log", "access_log"): lgr = getattr(self, logger_name) for handler in lgr.handlers: handler.setFormatter(_JsonFormatter()) # ------------------------------------------------------------------ # # Access log records # # ------------------------------------------------------------------ # def access(self, resp, req, environ, request_time): """Emit a structured JSON access log record.""" if not self.access_log.handlers or not self.cfg.accesslog: return status = resp.status if isinstance(status, str): status_code = int(status.split(None, 1)[0]) else: status_code = status record = { "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%z"), "level": "INFO", "logger": "gunicorn.access", "method": environ.get("REQUEST_METHOD", "-"), "path": environ.get("PATH_INFO", "-"), "query": environ.get("QUERY_STRING", "") or None, "status": status_code, "response_bytes": getattr(resp, "sent", None), "duration_ms": round(request_time.seconds * 1000 + request_time.microseconds / 1000, 2), "remote_addr": environ.get("REMOTE_ADDR", "-"), "x_forwarded_for": environ.get("HTTP_X_FORWARDED_FOR") or None, "user_agent": environ.get("HTTP_USER_AGENT", "-"), "referer": environ.get("HTTP_REFERER") or None, "http_version": environ.get("SERVER_PROTOCOL", "-"), } # Drop None values for cleaner output record = {k: v for k, v in record.items() if v is not None} # Write directly to handler stream to avoid double-formatting line = json.dumps(record) for handler in self.access_log.handlers: stream = getattr(handler, "stream", None) if stream: stream.write(line + "\n") stream.flush() class _JsonFormatter(logging.Formatter): """Formatter that converts a LogRecord to a single JSON line.""" def format(self, record: logging.LogRecord) -> str: obj = { "timestamp": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"), "level": record.levelname, "logger": record.name, "message": record.getMessage(), } if record.exc_info: obj["exception"] = self.formatException(record.exc_info) return json.dumps(obj)