log_util.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import logging
  2. from pathlib import Path
  3. import json
  4. from datetime import datetime, timedelta
  5. import glob
  6. import os
  7. LOG_DIR = Path(__file__).resolve().parent / "logs"
  8. LOG_DIR.mkdir(parents=True, exist_ok=True)
  9. class DailyRotatingFileHandler(logging.Handler):
  10. """A simple daily rotating file handler that names files as {name}_YYYYMMDD.log
  11. It will open a new file when the date changes and optionally remove old log
  12. files older than backup_count days.
  13. """
  14. def __init__(self, name: str, backup_count: int = 7, encoding: str = "utf-8"):
  15. super().__init__()
  16. self.name = name
  17. self.backup_count = backup_count
  18. self.encoding = encoding
  19. self.current_date = datetime.now().strftime("%Y%m%d")
  20. self._fh = None
  21. def _open(self):
  22. filename = LOG_DIR / f"{self.name}_{self.current_date}.log"
  23. # Ensure parent exists (LOG_DIR already exists)
  24. self._fh = logging.FileHandler(filename, encoding=self.encoding)
  25. if self.formatter:
  26. self._fh.setFormatter(self.formatter)
  27. def _cleanup_old(self):
  28. # Remove files older than backup_count days matching pattern name_*.log
  29. pattern = str(LOG_DIR / f"{self.name}_*.log")
  30. files = glob.glob(pattern)
  31. cutoff = datetime.now() - timedelta(days=self.backup_count)
  32. for f in files:
  33. basename = os.path.basename(f)
  34. # Expect basename like name_YYYYMMDD.log
  35. try:
  36. parts = basename.rsplit("_", 1)
  37. if len(parts) != 2:
  38. continue
  39. date_part = parts[1].split(".")[0]
  40. file_date = datetime.strptime(date_part, "%Y%m%d")
  41. if file_date < cutoff:
  42. try:
  43. os.remove(f)
  44. except Exception:
  45. pass
  46. except Exception:
  47. # ignore parsing errors
  48. continue
  49. def emit(self, record: logging.LogRecord) -> None:
  50. now_date = datetime.now().strftime("%Y%m%d")
  51. if self._fh is None:
  52. self._open()
  53. self._cleanup_old()
  54. elif now_date != self.current_date:
  55. # Date changed: rotate
  56. try:
  57. self._fh.close()
  58. except Exception:
  59. pass
  60. self.current_date = now_date
  61. self._open()
  62. self._cleanup_old()
  63. # Delegate emission to underlying FileHandler
  64. try:
  65. if self._fh.formatter is None and self.formatter:
  66. self._fh.setFormatter(self.formatter)
  67. self._fh.emit(record)
  68. except Exception:
  69. self.handleError(record)
  70. def setFormatter(self, fmt: logging.Formatter) -> None:
  71. super().setFormatter(fmt)
  72. if self._fh:
  73. self._fh.setFormatter(fmt)
  74. def close(self) -> None:
  75. try:
  76. if self._fh:
  77. self._fh.close()
  78. finally:
  79. super().close()
  80. def _make_file_handler(name: str):
  81. handler = DailyRotatingFileHandler(name, backup_count=7, encoding="utf-8")
  82. formatter = logging.Formatter(
  83. "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
  84. datefmt="%Y-%m-%d %H:%M:%S",
  85. )
  86. handler.setFormatter(formatter)
  87. return handler
  88. def get_logger(name: str = "app") -> logging.Logger:
  89. logger = logging.getLogger(name)
  90. if logger.handlers:
  91. return logger
  92. logger.setLevel(logging.DEBUG)
  93. fh = _make_file_handler(name)
  94. logger.addHandler(fh)
  95. sh = logging.StreamHandler()
  96. sh.setFormatter(
  97. logging.Formatter("%(asctime)s | %(levelname)-7s | %(name)s | %(message)s", datefmt="%H:%M:%S")
  98. )
  99. logger.addHandler(sh)
  100. return logger
  101. def log_request(logger: logging.Logger, func_name: str, url: str, request_body, response):
  102. try:
  103. req_text = json.dumps(request_body, ensure_ascii=False)
  104. except Exception:
  105. req_text = str(request_body)
  106. # Try to capture response text or JSON
  107. resp_text = None
  108. try:
  109. resp_text = response.text
  110. except Exception:
  111. resp_text = str(response)
  112. # Use Chinese labels where appropriate, keep technical terms like URL and status code
  113. logger.info(
  114. f"调用: {func_name} | URL: {url} | 请求: {req_text} | 响应状态: {getattr(response, 'status_code', 'N/A')} | 响应: {resp_text}"
  115. )