-
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 4 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.GunicornJsonLogger" | ||
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,73 @@ | ||
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): | ||
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: | ||
""" | ||
%s is replaced with {} because legacy string formatting | ||
conventions in django-axes module prevent correct | ||
interpolation of arguments when using this formatter. | ||
""" | ||
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)}" | ||
|
||
|
||
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 GunicornJsonLogger(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, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -150,7 +150,11 @@ the below variables will be ignored. | |
define a shared DJANGO_SECRET_KEY. | ||
- `LOG_LEVEL`: DJANGO logging level. Can be one of `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` | ||
- `LOG_FORMAT`: Can be `generic` (plain-text) or `json`. Defaults to `generic`. | ||
- `ACCESS_LOG_LOCATION`: The location to store web logs generated by gunicorn if running as a Docker container. If not | ||
- `GUNICORN_CMD_ARGS`: Gunicorn command line arguments. Overrides Flagsmith's defaults. See | ||
[Gunicorn documentation](https://docs.gunicorn.org/en/stable/settings.html) for reference. | ||
- `ACCESS_LOG_FORMAT`: Message format for Gunicorn's access log. See | ||
[variable details](https://docs.gunicorn.org/en/stable/settings.html#access-log-format) to define your own format. | ||
- `ACCESS_LOG_LOCATION`: The location to store web logs generated by Gunicorn if running as a Docker container. If not | ||
set, no logs will be stored. If set to `-` the logs will be sent to `stdout`. | ||
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.
I think if no logs will be stored then 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. Thanks! Updated the docs. |
||
- `DJANGO_SETTINGS_MODULE`: python path to settings file for the given environment, e.g. "app.settings.develop" | ||
- `ALLOW_ADMIN_INITIATION_VIA_CLI`: Enables the `bootstrap` management command which creates default admin user, | ||
|
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!