Skip to content

Commit add44e8

Browse files
committed
fix: Avoid using a Gunicorn config file
1 parent 87a6901 commit add44e8

File tree

6 files changed

+81
-67
lines changed

6 files changed

+81
-67
lines changed

api/Makefile

+4-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ django-collect-static:
8383

8484
.PHONY: serve
8585
serve:
86-
poetry run gunicorn --bind 0.0.0.0:8000 app.wsgi --reload
86+
poetry run gunicorn --bind 0.0.0.0:8000 \
87+
--logger-class ${GUNICORN_LOGGER_CLASS:-'util.logging.GunicornJsonCapableLogger'} \
88+
--reload \
89+
app.wsgi
8790

8891
.PHONY: generate-ld-client-types
8992
generate-ld-client-types:

api/scripts/run-docker.sh

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ function serve() {
2222
--workers ${GUNICORN_WORKERS:-3} \
2323
--threads ${GUNICORN_THREADS:-2} \
2424
--access-logfile $ACCESS_LOG_LOCATION \
25-
--access-logformat '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %({origin}i)s %({access-control-allow-origin}o)s' \
25+
--logger-class ${GUNICORN_LOGGER_CLASS:-'util.logging.GunicornJsonCapableLogger'} \
26+
--access-logformat ${ACCESS_LOG_FORMAT:-'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %({origin}i)s %({access-control-allow-origin}o)s'} \
2627
--keep-alive ${GUNICORN_KEEP_ALIVE:-2} \
2728
${STATSD_HOST:+--statsd-host $STATSD_HOST:$STATSD_PORT} \
2829
${STATSD_HOST:+--statsd-prefix $STATSD_PREFIX} \

api/tests/unit/util/test_logging.py

+4-44
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44
from util.logging import JsonFormatter
55

66

7-
def test_json_formatter():
7+
def test_json_formatter__outputs_expected():
88
json_formatter = JsonFormatter()
99

1010
log_record = logging.LogRecord(
1111
name="test_logger",
1212
level=logging.INFO,
1313
pathname="test.py",
1414
lineno=42,
15-
msg="This is a test.",
16-
args=(),
15+
msg="This is a test message with args: %s and %s",
16+
args=("arg1", "arg2"),
1717
exc_info=None,
1818
)
1919
formatted_message = json_formatter.format(log_record)
@@ -25,44 +25,4 @@ def test_json_formatter():
2525
assert "logger_name" in json_message
2626
assert "process_id" in json_message
2727
assert "thread_name" in json_message
28-
29-
30-
def test_json_formatter_with_old_style_placeholders():
31-
json_formatter = JsonFormatter()
32-
33-
log_record = logging.LogRecord(
34-
name="test_logger",
35-
level=logging.INFO,
36-
pathname="example.py",
37-
lineno=42,
38-
msg="This is a test with old-style placeholders: %s and %s",
39-
args=("arg1", "arg2"),
40-
exc_info=None,
41-
)
42-
43-
formatted_message = json_formatter.format(log_record)
44-
parsed_json = json.loads(formatted_message)
45-
assert (
46-
parsed_json["message"]
47-
== "This is a test with old-style placeholders: arg1 and arg2"
48-
)
49-
50-
51-
def test_json_formatter_arguments_with_new_style_placeholders():
52-
json_formatter = JsonFormatter()
53-
log_record = logging.LogRecord(
54-
name="test_logger",
55-
level=logging.INFO,
56-
pathname="example.py",
57-
lineno=42,
58-
msg="This is a test with new-style placeholders: {} and {}",
59-
args=("arg1", "arg2"),
60-
exc_info=None,
61-
)
62-
63-
formatted_message = json_formatter.format(log_record)
64-
parsed_json = json.loads(formatted_message)
65-
assert (
66-
parsed_json["message"]
67-
== "This is a test with new-style placeholders: arg1 and arg2"
68-
)
28+
assert json_message["message"] == "This is a test message with args: arg1 and arg2"

api/util/logging.py

+59-18
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,68 @@
11
import json
22
import logging
3+
import sys
4+
from datetime import datetime
5+
from typing import Any
6+
7+
from django.conf import settings
8+
from gunicorn.config import Config
9+
from gunicorn.glogging import Logger as GunicornLogger
310

411

512
class JsonFormatter(logging.Formatter):
613
"""Custom formatter for json logs."""
714

8-
def format(self, record):
9-
"""
10-
%s is replaced with {} because legacy string formatting
11-
conventions in django-axes module prevent correct
12-
interpolation of arguments when using this formatter.
13-
"""
15+
def get_json_record(self, record: logging.LogRecord) -> dict[str, Any]:
16+
formatted_message = record.getMessage()
17+
return {
18+
"levelname": record.levelname,
19+
"message": formatted_message,
20+
"timestamp": self.formatTime(record, self.datefmt),
21+
"logger_name": record.name,
22+
"process_id": record.process,
23+
"thread_name": record.threadName,
24+
}
25+
26+
def format(self, record: logging.LogRecord) -> str:
1427
try:
15-
log_message = record.msg.replace("%s", "{}")
16-
formatted_message = log_message.format(*record.args)
17-
log_record = {
18-
"levelname": record.levelname,
19-
"message": formatted_message,
20-
"timestamp": self.formatTime(record, self.datefmt),
21-
"logger_name": record.name,
22-
"process_id": record.process,
23-
"thread_name": record.threadName,
24-
}
25-
return json.dumps(log_record)
28+
return json.dumps(self.get_json_record(record))
2629
except (ValueError, TypeError) as e:
27-
return f"Error formatting log record: {str(e)}"
30+
return json.dumps({"message": f"{e} when dumping log"})
31+
32+
33+
class GunicornAccessLogJsonFormatter(JsonFormatter):
34+
def get_json_record(self, record: logging.LogRecord) -> dict[str, Any]:
35+
response_time = datetime.strptime(record.args["t"], "[%d/%b/%Y:%H:%M:%S %z]")
36+
url = record.args["U"]
37+
if record.args["q"]:
38+
url += f"?{record.args['q']}"
39+
40+
return {
41+
**super().get_json_record(record),
42+
"time": response_time.isoformat(),
43+
"path": url,
44+
"remote_ip": record.args["h"],
45+
"method": record.args["m"],
46+
"status": str(record.args["s"]),
47+
"user_agent": record.args["a"],
48+
"referer": record.args["f"],
49+
"duration_in_ms": record.args["M"],
50+
"pid": record.args["p"],
51+
}
52+
53+
54+
class GunicornJsonCapableLogger(GunicornLogger):
55+
def setup(self, cfg: Config) -> None:
56+
super().setup(cfg)
57+
if settings.LOG_FORMAT == "json":
58+
self._set_handler(
59+
self.error_log,
60+
cfg.errorlog,
61+
JsonFormatter(),
62+
)
63+
self._set_handler(
64+
self.access_log,
65+
cfg.accesslog,
66+
GunicornAccessLogJsonFormatter(),
67+
stream=sys.stdout,
68+
)

docs/docs/deployment/hosting/docker.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,18 @@ new account at [http://localhost:8000/signup](http://localhost:8000/signup)
2020
As well as the Environment Variables specified in the [API](/deployment/hosting/locally-api#environment-variables) and
2121
[Front End](/deployment/hosting/locally-frontend#environment-variables) you can also specify the following:
2222

23+
- `GUNICORN_CMD_ARGS`: Gunicorn command line arguments. Overrides Flagsmith's defaults. See
24+
[Gunicorn documentation](https://docs.gunicorn.org/en/stable/settings.html) for reference.
2325
- `GUNICORN_WORKERS`: The number of [Gunicorn Workers](https://docs.gunicorn.org/en/stable/settings.html#workers) that
2426
are created
2527
- `GUNICORN_THREADS`: The number of
2628
[Gunicorn Threads per Worker](https://docs.gunicorn.org/en/stable/settings.html#threads)
2729
- `GUNICORN_TIMEOUT`: The number of seconds before the
2830
[Gunicorn times out](https://docs.gunicorn.org/en/stable/settings.html#timeout)
29-
- `ACCESS_LOG_LOCATION`: The location to write access logs to
31+
- `ACCESS_LOG_FORMAT`: Message format for Gunicorn's access log. See
32+
[variable details](https://docs.gunicorn.org/en/stable/settings.html#access-log-format) to define your own format.
33+
- `ACCESS_LOG_LOCATION`: The location to write access logs to. If not set, or set to `-`, the logs will be sent to
34+
`stdout`
3035

3136
## Platform Architectures
3237

docs/docs/deployment/hosting/locally-api.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,12 @@ the below variables will be ignored.
150150
define a shared DJANGO_SECRET_KEY.
151151
- `LOG_LEVEL`: DJANGO logging level. Can be one of `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`
152152
- `LOG_FORMAT`: Can be `generic` (plain-text) or `json`. Defaults to `generic`.
153-
- `ACCESS_LOG_LOCATION`: The location to store web logs generated by gunicorn if running as a Docker container. If not
154-
set, no logs will be stored. If set to `-` the logs will be sent to `stdout`.
153+
- `GUNICORN_CMD_ARGS`: Gunicorn command line arguments. Overrides Flagsmith's defaults. See
154+
[Gunicorn documentation](https://docs.gunicorn.org/en/stable/settings.html) for reference.
155+
- `ACCESS_LOG_FORMAT`: Message format for Gunicorn's access log. See
156+
[variable details](https://docs.gunicorn.org/en/stable/settings.html#access-log-format) to define your own format.
157+
- `ACCESS_LOG_LOCATION`: The location to store web logs generated by Gunicorn if running as a Docker container. If not
158+
set, no logs will be stored. If set to `-`, the logs will be sent to `stdout`.
155159
- `DJANGO_SETTINGS_MODULE`: python path to settings file for the given environment, e.g. "app.settings.develop"
156160
- `ALLOW_ADMIN_INITIATION_VIA_CLI`: Enables the `bootstrap` management command which creates default admin user,
157161
organisation, and project.

0 commit comments

Comments
 (0)