logging_config.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import logging
  2. import logging.config
  3. import re
  4. import sys
  5. from pathlib import Path
  6. class HTTPStatusFilter(logging.Filter):
  7. """
  8. This filter inspects uvicorn.access log records. It uses record.getMessage() to retrieve
  9. the fully formatted log message. Then it searches for HTTP status codes and adjusts the
  10. record's log level based on that status:
  11. - 4xx: WARNING
  12. - 5xx: ERROR
  13. All other logs remain unchanged.
  14. """
  15. # A broad pattern to find any 3-digit number in the message.
  16. # This should capture the HTTP status code from a line like:
  17. # '127.0.0.1:54946 - "GET /v2/relationships HTTP/1.1" 404'
  18. STATUS_CODE_PATTERN = re.compile(r"\b(\d{3})\b")
  19. LEVEL_TO_ANSI = {
  20. logging.INFO: "\033[32m", # green
  21. logging.WARNING: "\033[33m", # yellow
  22. logging.ERROR: "\033[31m", # red
  23. }
  24. RESET = "\033[0m"
  25. def filter(self, record: logging.LogRecord) -> bool:
  26. if record.name != "uvicorn.access":
  27. return True
  28. message = record.getMessage()
  29. if codes := self.STATUS_CODE_PATTERN.findall(message):
  30. status_code = int(codes[-1])
  31. if 200 <= status_code < 300:
  32. record.levelno = logging.INFO
  33. record.levelname = "INFO"
  34. color = self.LEVEL_TO_ANSI[logging.INFO]
  35. elif 400 <= status_code < 500:
  36. record.levelno = logging.WARNING
  37. record.levelname = "WARNING"
  38. color = self.LEVEL_TO_ANSI[logging.WARNING]
  39. elif 500 <= status_code < 600:
  40. record.levelno = logging.ERROR
  41. record.levelname = "ERROR"
  42. color = self.LEVEL_TO_ANSI[logging.ERROR]
  43. else:
  44. return True
  45. # Wrap the status code in ANSI codes
  46. colored_code = f"{color}{status_code}{self.RESET}"
  47. # Replace the status code in the message
  48. new_msg = message.replace(str(status_code), colored_code)
  49. # Update record.msg and clear args to avoid formatting issues
  50. record.msg = new_msg
  51. record.args = ()
  52. return True
  53. def configure_logging():
  54. log_dir = Path.cwd() / "logs"
  55. log_dir.mkdir(exist_ok=True)
  56. log_config = {
  57. "version": 1,
  58. "disable_existing_loggers": False,
  59. "filters": {"http_status_filter": {"()": HTTPStatusFilter}},
  60. "formatters": {
  61. "default": {
  62. "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
  63. "datefmt": "%Y-%m-%d %H:%M:%S",
  64. },
  65. "colored": {
  66. "()": "colorlog.ColoredFormatter",
  67. "format": "%(asctime)s - %(log_color)s%(levelname)s%(reset)s - %(message)s",
  68. "datefmt": "%Y-%m-%d %H:%M:%S",
  69. "log_colors": {
  70. "DEBUG": "white",
  71. "INFO": "green",
  72. "WARNING": "yellow",
  73. "ERROR": "red",
  74. "CRITICAL": "bold_red",
  75. },
  76. },
  77. },
  78. "handlers": {
  79. "file": {
  80. "class": "logging.handlers.RotatingFileHandler",
  81. "formatter": "colored",
  82. "filename": str(log_dir / "app.log"),
  83. "maxBytes": 10485760, # 10MB
  84. "backupCount": 5,
  85. "filters": ["http_status_filter"],
  86. },
  87. "console": {
  88. "class": "logging.StreamHandler",
  89. "formatter": "colored",
  90. "stream": sys.stdout,
  91. "filters": ["http_status_filter"],
  92. },
  93. },
  94. "loggers": {
  95. "": { # Root logger
  96. "handlers": ["console", "file"],
  97. "level": "INFO",
  98. },
  99. "uvicorn": {
  100. "handlers": ["console", "file"],
  101. "level": "INFO",
  102. "propagate": False,
  103. },
  104. "uvicorn.error": {
  105. "handlers": ["console", "file"],
  106. "level": "INFO",
  107. "propagate": False,
  108. },
  109. "uvicorn.access": {
  110. "handlers": ["console", "file"],
  111. "level": "INFO",
  112. "propagate": False,
  113. },
  114. },
  115. }
  116. logging.config.dictConfig(log_config)
  117. logger = logging.getLogger()
  118. return logger, Path(log_config["handlers"]["file"]["filename"])