You've already forked docker-ara
88 lines
3.3 KiB
Python
88 lines
3.3 KiB
Python
"""
|
|
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)
|