-
Notifications
You must be signed in to change notification settings - Fork 429
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: JSON logging for Gunicorn #3672
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import os | ||
|
||
accesslog = os.getenv("ACCESS_LOG_LOCATION", "-") | ||
access_log_format = os.getenv("ACCESS_LOG_FORMAT") or ( | ||
r'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" ' | ||
r'"%(a)s" %({origin}i)s %({access-control-allow-origin}o)s' | ||
) | ||
keep_alive = os.getenv("GUNICORN_KEEP_ALIVE", 2) | ||
logger_class = "util.logging.GunicornJsonCapableLogger" | ||
threads = os.getenv("GUNICORN_THREADS", 2) | ||
timeout = os.getenv("GUNICORN_TIMEOUT", 30) | ||
workers = os.getenv("GUNICORN_WORKERS", 3) | ||
|
||
if _statsd_host := os.getenv("STATSD_HOST"): | ||
statsd_host = f'{_statsd_host}:{os.getenv("STATSD_PORT")}' | ||
statsd_prefix = os.getenv("STATSD_PREFIX") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,68 @@ | ||
import json | ||
import logging | ||
import sys | ||
from datetime import datetime | ||
from typing import Any | ||
|
||
from django.conf import settings | ||
from gunicorn.config import Config | ||
from gunicorn.glogging import Logger as GunicornLogger | ||
|
||
|
||
class JsonFormatter(logging.Formatter): | ||
"""Custom formatter for json logs.""" | ||
|
||
def format(self, record): | ||
""" | ||
%s is replaced with {} because legacy string formatting | ||
conventions in django-axes module prevent correct | ||
interpolation of arguments when using this formatter. | ||
""" | ||
def get_json_record(self, record: logging.LogRecord) -> dict[str, Any]: | ||
formatted_message = record.getMessage() | ||
return { | ||
"levelname": record.levelname, | ||
"message": formatted_message, | ||
"timestamp": self.formatTime(record, self.datefmt), | ||
"logger_name": record.name, | ||
"process_id": record.process, | ||
"thread_name": record.threadName, | ||
} | ||
|
||
def format(self, record: logging.LogRecord) -> str: | ||
try: | ||
log_message = record.msg.replace("%s", "{}") | ||
formatted_message = log_message.format(*record.args) | ||
log_record = { | ||
"levelname": record.levelname, | ||
"message": formatted_message, | ||
"timestamp": self.formatTime(record, self.datefmt), | ||
"logger_name": record.name, | ||
"process_id": record.process, | ||
"thread_name": record.threadName, | ||
} | ||
return json.dumps(log_record) | ||
return json.dumps(self.get_json_record(record)) | ||
except (ValueError, TypeError) as e: | ||
return f"Error formatting log record: {str(e)}" | ||
return json.dumps({"message": f"{e} when dumping log"}) | ||
|
||
|
||
class GunicornAccessLogJsonFormatter(JsonFormatter): | ||
def get_json_record(self, record: logging.LogRecord) -> dict[str, Any]: | ||
response_time = datetime.strptime(record.args["t"], "[%d/%b/%Y:%H:%M:%S %z]") | ||
url = record.args["U"] | ||
if record.args["q"]: | ||
url += f"?{record.args['q']}" | ||
|
||
return { | ||
**super().get_json_record(record), | ||
"time": response_time.isoformat(), | ||
"path": url, | ||
"remote_ip": record.args["h"], | ||
"method": record.args["m"], | ||
"status": str(record.args["s"]), | ||
"user_agent": record.args["a"], | ||
"referer": record.args["f"], | ||
"duration_in_ms": record.args["M"], | ||
"pid": record.args["p"], | ||
} | ||
|
||
|
||
class GunicornJsonCapableLogger(GunicornLogger): | ||
def setup(self, cfg: Config) -> None: | ||
super().setup(cfg) | ||
if settings.LOG_FORMAT == "json": | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it's not set to JSON then the logger won't really be a JSON logger, right? It's weird to let it operate as a normal logger with the name of the class There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Renamed to |
||
self._set_handler( | ||
self.error_log, | ||
cfg.errorlog, | ||
JsonFormatter(), | ||
) | ||
self._set_handler( | ||
self.access_log, | ||
cfg.accesslog, | ||
GunicornAccessLogJsonFormatter(), | ||
stream=sys.stdout, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docstring of this function is now out of date.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, removed!