logger.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import json
  2. import logging
  3. import sys
  4. from typing import TYPE_CHECKING
  5. from loguru import logger
  6. from open_webui.env import (
  7. AUDIT_LOG_FILE_ROTATION_SIZE,
  8. AUDIT_LOG_LEVEL,
  9. AUDIT_LOGS_FILE_PATH,
  10. GLOBAL_LOG_LEVEL,
  11. )
  12. if TYPE_CHECKING:
  13. from loguru import Record
  14. def stdout_format(record: "Record") -> str:
  15. """
  16. Generates a formatted string for log records that are output to the console. This format includes a timestamp, log level, source location (module, function, and line), the log message, and any extra data (serialized as JSON).
  17. Parameters:
  18. record (Record): A Loguru record that contains logging details including time, level, name, function, line, message, and any extra context.
  19. Returns:
  20. str: A formatted log string intended for stdout.
  21. """
  22. record["extra"]["extra_json"] = json.dumps(record["extra"])
  23. return (
  24. "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
  25. "<level>{level: <8}</level> | "
  26. "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
  27. "<level>{message}</level> - {extra[extra_json]}"
  28. "\n{exception}"
  29. )
  30. class InterceptHandler(logging.Handler):
  31. """
  32. Intercepts log records from Python's standard logging module
  33. and redirects them to Loguru's logger.
  34. """
  35. def emit(self, record):
  36. """
  37. Called by the standard logging module for each log event.
  38. It transforms the standard `LogRecord` into a format compatible with Loguru
  39. and passes it to Loguru's logger.
  40. """
  41. try:
  42. level = logger.level(record.levelname).name
  43. except ValueError:
  44. level = record.levelno
  45. frame, depth = sys._getframe(6), 6
  46. while frame and frame.f_code.co_filename == logging.__file__:
  47. frame = frame.f_back
  48. depth += 1
  49. logger.opt(depth=depth, exception=record.exc_info).log(
  50. level, record.getMessage()
  51. )
  52. def file_format(record: "Record"):
  53. """
  54. Formats audit log records into a structured JSON string for file output.
  55. Parameters:
  56. record (Record): A Loguru record containing extra audit data.
  57. Returns:
  58. str: A JSON-formatted string representing the audit data.
  59. """
  60. audit_data = {
  61. "id": record["extra"].get("id", ""),
  62. "timestamp": int(record["time"].timestamp()),
  63. "user": record["extra"].get("user", dict()),
  64. "audit_level": record["extra"].get("audit_level", ""),
  65. "verb": record["extra"].get("verb", ""),
  66. "request_uri": record["extra"].get("request_uri", ""),
  67. "response_status_code": record["extra"].get("response_status_code", 0),
  68. "source_ip": record["extra"].get("source_ip", ""),
  69. "user_agent": record["extra"].get("user_agent", ""),
  70. "request_object": record["extra"].get("request_object", b""),
  71. "response_object": record["extra"].get("response_object", b""),
  72. "extra": record["extra"].get("extra", {}),
  73. }
  74. record["extra"]["file_extra"] = json.dumps(audit_data, default=str)
  75. return "{extra[file_extra]}\n"
  76. def start_logger():
  77. """
  78. Initializes and configures Loguru's logger with distinct handlers:
  79. A console (stdout) handler for general log messages (excluding those marked as auditable).
  80. An optional file handler for audit logs if audit logging is enabled.
  81. Additionally, this function reconfigures Python’s standard logging to route through Loguru and adjusts logging levels for Uvicorn.
  82. Parameters:
  83. enable_audit_logging (bool): Determines whether audit-specific log entries should be recorded to file.
  84. """
  85. logger.remove()
  86. logger.add(
  87. sys.stdout,
  88. level=GLOBAL_LOG_LEVEL,
  89. format=stdout_format,
  90. filter=lambda record: "auditable" not in record["extra"],
  91. )
  92. if AUDIT_LOG_LEVEL != "NONE":
  93. try:
  94. logger.add(
  95. AUDIT_LOGS_FILE_PATH,
  96. level="INFO",
  97. rotation=AUDIT_LOG_FILE_ROTATION_SIZE,
  98. compression="zip",
  99. format=file_format,
  100. filter=lambda record: record["extra"].get("auditable") is True,
  101. )
  102. except Exception as e:
  103. logger.error(f"Failed to initialize audit log file handler: {str(e)}")
  104. logging.basicConfig(
  105. handlers=[InterceptHandler()], level=GLOBAL_LOG_LEVEL, force=True
  106. )
  107. for uvicorn_logger_name in ["uvicorn", "uvicorn.error"]:
  108. uvicorn_logger = logging.getLogger(uvicorn_logger_name)
  109. uvicorn_logger.setLevel(GLOBAL_LOG_LEVEL)
  110. uvicorn_logger.handlers = []
  111. for uvicorn_logger_name in ["uvicorn.access"]:
  112. uvicorn_logger = logging.getLogger(uvicorn_logger_name)
  113. uvicorn_logger.setLevel(GLOBAL_LOG_LEVEL)
  114. uvicorn_logger.handlers = [InterceptHandler()]
  115. logger.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")