123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
- import logging
- import logging.config
- import re
- import sys
- from pathlib import Path
- class HTTPStatusFilter(logging.Filter):
- """
- This filter inspects uvicorn.access log records. It uses record.getMessage() to retrieve
- the fully formatted log message. Then it searches for HTTP status codes and adjusts the
- record's log level based on that status:
- - 4xx: WARNING
- - 5xx: ERROR
- All other logs remain unchanged.
- """
- # A broad pattern to find any 3-digit number in the message.
- # This should capture the HTTP status code from a line like:
- # '127.0.0.1:54946 - "GET /v2/relationships HTTP/1.1" 404'
- STATUS_CODE_PATTERN = re.compile(r"\b(\d{3})\b")
- LEVEL_TO_ANSI = {
- logging.INFO: "\033[32m", # green
- logging.WARNING: "\033[33m", # yellow
- logging.ERROR: "\033[31m", # red
- }
- RESET = "\033[0m"
- def filter(self, record: logging.LogRecord) -> bool:
- if record.name != "uvicorn.access":
- return True
- message = record.getMessage()
- if codes := self.STATUS_CODE_PATTERN.findall(message):
- status_code = int(codes[-1])
- if 200 <= status_code < 300:
- record.levelno = logging.INFO
- record.levelname = "INFO"
- color = self.LEVEL_TO_ANSI[logging.INFO]
- elif 400 <= status_code < 500:
- record.levelno = logging.WARNING
- record.levelname = "WARNING"
- color = self.LEVEL_TO_ANSI[logging.WARNING]
- elif 500 <= status_code < 600:
- record.levelno = logging.ERROR
- record.levelname = "ERROR"
- color = self.LEVEL_TO_ANSI[logging.ERROR]
- else:
- return True
- # Wrap the status code in ANSI codes
- colored_code = f"{color}{status_code}{self.RESET}"
- # Replace the status code in the message
- new_msg = message.replace(str(status_code), colored_code)
- # Update record.msg and clear args to avoid formatting issues
- record.msg = new_msg
- record.args = ()
- return True
- def configure_logging():
- log_dir = Path.cwd() / "logs"
- log_dir.mkdir(exist_ok=True)
- log_config = {
- "version": 1,
- "disable_existing_loggers": False,
- "filters": {"http_status_filter": {"()": HTTPStatusFilter}},
- "formatters": {
- "default": {
- "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
- "datefmt": "%Y-%m-%d %H:%M:%S",
- },
- "colored": {
- "()": "colorlog.ColoredFormatter",
- "format": "%(asctime)s - %(log_color)s%(levelname)s%(reset)s - %(message)s",
- "datefmt": "%Y-%m-%d %H:%M:%S",
- "log_colors": {
- "DEBUG": "white",
- "INFO": "green",
- "WARNING": "yellow",
- "ERROR": "red",
- "CRITICAL": "bold_red",
- },
- },
- },
- "handlers": {
- "file": {
- "class": "logging.handlers.RotatingFileHandler",
- "formatter": "colored",
- "filename": str(log_dir / "app.log"),
- "maxBytes": 10485760, # 10MB
- "backupCount": 5,
- "filters": ["http_status_filter"],
- },
- "console": {
- "class": "logging.StreamHandler",
- "formatter": "colored",
- "stream": sys.stdout,
- "filters": ["http_status_filter"],
- },
- },
- "loggers": {
- "": { # Root logger
- "handlers": ["console", "file"],
- "level": "INFO",
- },
- "uvicorn": {
- "handlers": ["console", "file"],
- "level": "INFO",
- "propagate": False,
- },
- "uvicorn.error": {
- "handlers": ["console", "file"],
- "level": "INFO",
- "propagate": False,
- },
- "uvicorn.access": {
- "handlers": ["console", "file"],
- "level": "INFO",
- "propagate": False,
- },
- },
- }
- logging.config.dictConfig(log_config)
- logger = logging.getLogger()
- return logger, Path(log_config["handlers"]["file"]["filename"])
|