From 8b9284f414137e0afe66b6c80cc002bc33056fa5 Mon Sep 17 00:00:00 2001 From: Antti Kettunen Date: Tue, 21 Apr 2026 18:00:29 +0300 Subject: [PATCH] Tune Gunicorn log output - keep access logs on stdout/stderr - filter static, Socket.IO, and boot noise - align Gunicorn rows with app log format --- gunicorn.conf.py | 4 ++- gunicorn.dev.conf.py | 5 ++- utils/gunicorn_logger.py | 74 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 utils/gunicorn_logger.py diff --git a/gunicorn.conf.py b/gunicorn.conf.py index 0500e468..f33ead35 100644 --- a/gunicorn.conf.py +++ b/gunicorn.conf.py @@ -11,7 +11,9 @@ timeout = 120 # Keep shutdowns under Docker's stop window so container restarts stay graceful. graceful_timeout = 8 -# Logging goes to stdout/stderr so Docker can collect it. +# Logging goes to stdout/stderr and is filtered by the custom logger class. accesslog = "-" errorlog = "-" +access_log_format = '%(h)s - - "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' loglevel = "info" +logger_class = "utils.gunicorn_logger.FilteredGunicornLogger" diff --git a/gunicorn.dev.conf.py b/gunicorn.dev.conf.py index e62ce120..5bf2132e 100644 --- a/gunicorn.dev.conf.py +++ b/gunicorn.dev.conf.py @@ -13,7 +13,10 @@ timeout = 120 # Don't let local reloads wait too long for shutdown. graceful_timeout = 1 -# Logging goes to stdout/stderr so the shell launcher can collect it. +# Logging goes to stdout/stderr and is filtered by the custom logger class. accesslog = "-" errorlog = "-" +# Mimic process log format +access_log_format = '%(h)s - - "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' loglevel = "info" +logger_class = "utils.gunicorn_logger.FilteredGunicornLogger" diff --git a/utils/gunicorn_logger.py b/utils/gunicorn_logger.py new file mode 100644 index 00000000..9926886d --- /dev/null +++ b/utils/gunicorn_logger.py @@ -0,0 +1,74 @@ +"""Gunicorn logger tweaks for SoulSync.""" + +from __future__ import annotations + +import logging + +from gunicorn.glogging import Logger as GunicornLogger +from utils.logging_config import ColoredFormatter + + +class FilteredGunicornLogger(GunicornLogger): + """Gunicorn logger that skips noisy static and Socket.IO access logs.""" + + _STATIC_PREFIXES = ( + "/static/", + "/assets/", + "/socket.io", + "/favicon.ico", + "/robots.txt", + ) + + _STATIC_SUFFIXES = ( + ".css", + ".js", + ".map", + ".png", + ".jpg", + ".jpeg", + ".gif", + ".svg", + ".ico", + ".woff", + ".woff2", + ".ttf", + ".eot", + ) + + def _should_skip_access_log(self, environ) -> bool: + path = environ.get("PATH_INFO") or "" + if not path: + return False + + normalized = path if path.startswith("/") else f"/{path}" + lower_path = normalized.lower() + + if any( + lower_path == prefix.rstrip("/") or lower_path.startswith(prefix) + for prefix in self._STATIC_PREFIXES + ): + return True + + return any(lower_path.endswith(suffix) for suffix in self._STATIC_SUFFIXES) + + def access(self, resp, req, environ, request_time): + if self._should_skip_access_log(environ): + return + super().access(resp, req, environ, request_time) + + def setup(self, cfg): + super().setup(cfg) + + app_like_formatter = ColoredFormatter( + fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + error_level = getattr(logging, cfg.loglevel.upper(), logging.INFO) + + for handler in self.access_log.handlers: + handler.setFormatter(app_like_formatter) + handler.setLevel(logging.INFO) + + for handler in self.error_log.handlers: + handler.setFormatter(app_like_formatter) + handler.setLevel(error_level)