feat(backend): PRWLR-4030 Implement Gunicorn as Django WSGI (#9)

* feat: PRWLR-4030 implement Gunicorn server and logging

* build: PRWLR-4030 update docker files

* docs: PRWLR-4030 update README

* chore: PRWLR-4030 change binding address to 0.0.0.0 by default
This commit is contained in:
Víctor Fernández Poyatos authored and GitHub committed 2024-07-10 16:32:56 +02:00
1 parent 457801f752
commit f0a276773e
8 files changed
+270 -119

No files matched your search

+79 -8
View File
@@ -6,6 +6,9 @@ from django_guid.log_filters import CorrelationId
class BackendLogger(StrEnum):
GUNICORN = "gunicorn"
GUNICORN_ACCESS = "gunicorn.access"
GUNICORN_ERROR = "gunicorn.error"
DJANGO = "django"
SECURITY = "django.security"
DB = "django.db"
@@ -36,7 +39,7 @@ class NDJSONFormatter(logging.Formatter):
"thread": record.thread,
"transaction_id": record.transaction_id
if hasattr(record, "transaction_id")
else "N/A",
else None,
}
# Add REST API extra fields
@@ -57,6 +60,44 @@ class NDJSONFormatter(logging.Formatter):
return json.dumps(log_record)
class HumanReadableFormatter(logging.Formatter):
"""Human-readable custom formatter for logging messages.
If available, it will include all kinds of API request metadata.
"""
def format(self, record):
log_components = [
f"{self.formatTime(record, self.datefmt)}",
f"[{record.name}]",
f"{record.levelname}:",
f"({record.module})",
f"[module={record.module}",
f"path={record.pathname}",
f"line={record.lineno}",
f"function={record.funcName}",
f"process={record.process}",
f"thread={record.thread}",
f"transaction-id={record.transaction_id if hasattr(record, "transaction_id") else None}]",
f"{record.getMessage()}",
]
# Add REST API extra fields
if hasattr(record, "method"):
log_components.append(f'"{record.method} {record.path}"')
if hasattr(record, "query_params"):
log_components.append(f"with parameters {record.query_params}")
if hasattr(record, "duration"):
log_components.append(f"done in {record.duration}s:")
if hasattr(record, "status_code"):
log_components.append(f"{record.status_code}")
if record.exc_info:
log_components.append(self.formatException(record.exc_info))
return " ".join(log_components)
# Filters
@@ -83,6 +124,10 @@ LOGGING = {
"()": NDJSONFormatter,
"datefmt": "%Y-%m-%d %H:%M:%S",
},
"human_readable": {
"()": HumanReadableFormatter,
"datefmt": "%Y-%m-%d %H:%M:%S",
},
"django": {
"format": "{asctime} [{name}] {levelname}: ({module}) {message}",
"style": "{",
@@ -106,46 +151,67 @@ LOGGING = {
},
},
"handlers": {
"gunicorn_console": {
"level": "INFO",
"class": "logging.StreamHandler",
"formatter": "human_readable",
"filters": ["transaction_id"],
},
"django_console": {
"level": "INFO",
"class": "logging.StreamHandler",
"formatter": "ndjson",
"formatter": "human_readable",
"filters": ["transaction_id"],
},
"api_console": {
"level": "INFO",
"class": "logging.StreamHandler",
"formatter": "ndjson",
"formatter": "human_readable",
"filters": ["transaction_id"],
},
"db_console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "ndjson",
"formatter": "human_readable",
"filters": ["transaction_id"],
},
"security_console": {
"level": "INFO",
"class": "logging.StreamHandler",
"formatter": "ndjson",
"formatter": "human_readable",
"filters": ["transaction_id"],
},
"tasks_console": {
"level": "INFO",
"class": "logging.StreamHandler",
"formatter": "ndjson",
"formatter": "human_readable",
"filters": ["transaction_id"],
},
},
"loggers": {
BackendLogger.GUNICORN: {
"handlers": ["gunicorn_console"],
"level": "INFO",
"propagate": False,
},
BackendLogger.GUNICORN_ACCESS: {
"handlers": ["gunicorn_console"],
"level": "CRITICAL",
"propagate": False,
},
BackendLogger.GUNICORN_ERROR: {
"handlers": ["gunicorn_console"],
"level": "INFO",
"propagate": False,
},
BackendLogger.DJANGO: {
"handlers": ["django_console"],
"level": "INFO",
"level": "WARNING",
"propagate": True,
},
BackendLogger.DB: {
"handlers": ["db_console"],
"level": "DEBUG",
"level": "INFO",
"propagate": False,
},
BackendLogger.SECURITY: {
@@ -164,4 +230,9 @@ LOGGING = {
"propagate": False,
},
},
# Gunicorn required configuration
"root": {
"level": "ERROR",
"handlers": ["gunicorn_console"],
},
}
+38
View File
@@ -0,0 +1,38 @@
import logging
import multiprocessing
import os
# Ensure the environment variable for Django settings is set
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
# Import Django and set it up before accessing settings
import django # noqa: E402
django.setup()
from backend.settings import LOGGING as DJANGO_LOGGERS # noqa: E402
from backend.custom_logging import BackendLogger # noqa: E402
# Server settings
bind = ["0.0.0.0:8000"]
# Calculate the number of workers based on CPU count. Refactor when adding config from env vars
workers = multiprocessing.cpu_count() * 2 + 1
reload = True
# Logging
logconfig_dict = DJANGO_LOGGERS
gunicorn_logger = logging.getLogger(BackendLogger.GUNICORN)
# Hooks
def on_starting(_):
gunicorn_logger.info(f"Starting gunicorn server with {workers} workers")
if reload:
gunicorn_logger.warning("Reload settings enabled (dev mode)")
def on_reload(_):
gunicorn_logger.warning("Gunicorn server has reloaded")
def when_ready(_):
gunicorn_logger.info("Gunicorn server is ready")
+1 -1
View File
@@ -15,7 +15,7 @@ SECRET_KEY = "django-insecure-9ut_dim+ma+r=ys094hoiwns#g1t@0*_&-*d!g+h)y%lye$0_^
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
ALLOWED_HOSTS = ["*"]
# Application definition