Files
docker-ara/docker/json_logger.py

88 lines
3.3 KiB
Python
Raw Normal View History

"""
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)