logging_config.py 5.3 KB

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