import logging import logging.config import time from logging.handlers import RotatingFileHandler from pathlib import Path from pythonjsonlogger import jsonlogger BASE_DIR = Path(__file__).resolve().parents[2] LOG_DIR = BASE_DIR / "logs" LOG_DIR.mkdir(exist_ok=True) # Custom formatter for console output (ERROR logs) class CustomConsoleFormatter(logging.Formatter): def format(self, record): if isinstance(record.msg, dict): msg_dict = record.msg return ( f"{record.levelname} {self.formatTime(record, self.datefmt)} " f"{msg_dict.get('request_method', '')} " f"{msg_dict.get('request_url', '')} " f"{msg_dict.get('status_code', '')} " f"{msg_dict.get('remote_addr', '')} " f"{msg_dict.get('duration_ms', 0)}ms " f"{msg_dict.get('message', record.getMessage())}" ) return super().format(record) # Custom formatter for INFO console output (simpler format) class CustomConsoleInfoFormatter(logging.Formatter): def format(self, record): if isinstance(record.msg, dict): msg_dict = record.msg return ( f"{record.levelname} {self.formatTime(record, self.datefmt)} " f"{msg_dict.get('request_method', '')} " f"{msg_dict.get('request_url', '')} " f"{msg_dict.get('status_code', '')} " f"{msg_dict.get('remote_addr', '')} " f"{msg_dict.get('duration_ms', 0)}ms" ) return super().format(record) LOGGING = { "version": 1, "disable_existing_loggers": True, "formatters": { "verbose": { "format": "{levelname} {asctime} {name} {module} {funcName} {lineno} {message}", "style": "{", }, "json": { "()": "pythonjsonlogger.jsonlogger.JsonFormatter", "format": "%(levelname)s %(asctime)s %(name)s %(module)s %(funcName)s %(lineno)d %(message)s %(pathname)s", }, "request_formatter": { "()": "pythonjsonlogger.jsonlogger.JsonFormatter", "format": "%(levelname)s %(asctime)s %(name)s %(message)s %(pathname)s %(lineno)d %(request_method)s %(request_url)s %(status_code)d %(remote_addr)s %(user_agent)s %(duration_ms)d", # noqa: E501 }, # Custom formatters for console "error_console": { "()": CustomConsoleFormatter, "format": "%(levelname)s %(asctime)s %(name)s %(message)s", "datefmt": "%Y-%m-%d %H:%M:%S", }, "info_console": { "()": CustomConsoleInfoFormatter, "format": "%(levelname)s %(asctime)s %(name)s %(message)s", "datefmt": "%Y-%m-%d %H:%M:%S", }, }, "handlers": { "console": { "level": "INFO", "class": "logging.StreamHandler", "formatter": "verbose", }, "error_console": { "level": "ERROR", "class": "logging.StreamHandler", "formatter": "error_console", }, "info_console": { "level": "INFO", "class": "logging.StreamHandler", "formatter": "info_console", }, "file": { "level": "INFO", "class": "logging.handlers.RotatingFileHandler", "filename": str(LOG_DIR / "django.log"), "maxBytes": 1024 * 1024 * 15, # 15MB "backupCount": 10, "formatter": "json", }, "error_file": { "level": "ERROR", "class": "logging.handlers.RotatingFileHandler", "filename": str(LOG_DIR / "errors.log"), "maxBytes": 1024 * 1024 * 15, # 15MB "backupCount": 5, "formatter": "json", }, "debug_file": { "level": "DEBUG", "class": "logging.handlers.RotatingFileHandler", "filename": str(LOG_DIR / "debug.log"), "maxBytes": 1024 * 1024 * 15, # 15MB "backupCount": 5, "formatter": "verbose", }, "request_info_file": { "level": "INFO", "class": "logging.handlers.RotatingFileHandler", "filename": str(LOG_DIR / "info.log"), "maxBytes": 1024 * 1024 * 15, # 15MB "backupCount": 10, "formatter": "request_formatter", }, "request_error_file": { "level": "ERROR", "class": "logging.handlers.RotatingFileHandler", "filename": str(LOG_DIR / "errors.log"), "maxBytes": 1024 * 1024 * 15, # 15MB "backupCount": 10, "formatter": "request_formatter", }, }, "loggers": { "django.request": { "handlers": ["error_console", "error_file"], "level": "ERROR", "propagate": False, }, "django.server": { "handlers": ["error_console"], "level": "ERROR", "propagate": False, }, "myapp": { "handlers": ["console", "file", "debug_file", "error_file"], "level": "DEBUG", "propagate": False, }, "project.requests.info": { "handlers": [ "request_info_file", "info_console", ], # Use info_console for INFO "level": "INFO", "propagate": False, }, "project.requests.error": { "handlers": [ "request_error_file", "error_console", ], # Use error_console for ERROR "level": "ERROR", "propagate": False, }, "common.exceptions": { "handlers": ["error_console", "error_file", "debug_file"], "level": "ERROR", "propagate": False, }, "common.middleware": { "handlers": ["error_console", "error_file", "debug_file"], "level": "ERROR", "propagate": False, }, }, } class RequestLoggingMiddleware: def __init__(self, get_response): self.get_response = get_response self.info_logger = logging.getLogger("project.requests.info") self.error_logger = logging.getLogger("project.requests.error") def __call__(self, request): start = time.perf_counter() try: response = self.get_response(request) except Exception: duration_ms = int((time.perf_counter() - start) * 1000) message = f"HTTP {request.method} request to {request.get_full_path()}" log_data = { "request_method": request.method, "request_url": request.get_full_path(), "status_code": 500, "remote_addr": request.META.get("REMOTE_ADDR"), "user_agent": request.META.get("HTTP_USER_AGENT", ""), "duration_ms": duration_ms, "message": message, } self.error_logger.exception(log_data) raise duration_ms = int((time.perf_counter() - start) * 1000) message = f"HTTP {request.method} request to {request.get_full_path()}" log_data = { "request_method": request.method, "request_url": request.get_full_path(), "status_code": response.status_code, "remote_addr": request.META.get("REMOTE_ADDR"), "user_agent": request.META.get("HTTP_USER_AGENT", ""), "duration_ms": duration_ms, "message": message, } if response.status_code == 404: log_data["message"] = f"Not Found: {request.get_full_path()}" elif response.status_code == 401: log_data["message"] = f"Unauthorized: {request.get_full_path()}" if response.status_code >= 400: self.error_logger.error(log_data) else: self.info_logger.info(log_data) return response def get_custom_logger(name): logger = logging.getLogger(name) logger.setLevel(logging.DEBUG) custom_handler = RotatingFileHandler( filename=str(LOG_DIR / f"{name}.log"), maxBytes=1024 * 1024 * 15, # 15MB backupCount=10, ) custom_handler.setFormatter(jsonlogger.JsonFormatter()) if not logger.handlers: logger.addHandler(custom_handler) return logger logging.config.dictConfig(LOGGING)