Skip to content

feat:add error message to log file #3325

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions fastdeploy/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""
日志模块:用于初始化和获取 FastDeploy 日志记录器。
本模块提供 get_logger 方法,统一管理各子模块的日志记录行为。
"""

import logging
import os

from fastdeploy import envs
from fastdeploy.util.formatters import ColoredFormatter
from fastdeploy.util.handlers import DailyRotatingFileHandler
from fastdeploy.util.setup_logging import setup_logging

# 初始化一次日志系统
setup_logging()


def get_logger(name, file_name=None, without_formater=False, print_to_console=False):
"""
获取日志记录器(兼容原有接口)

Args:
name: 日志器名称
file_name: 日志文件名(保持兼容性)
without_formater: 是否不使用格式化器
print_to_console: 是否打印到控制台
"""
# 如果只有一个参数,使用新的统一命名方式
if file_name is None and not without_formater and not print_to_console:
return _get_unified_logger(name)

# 兼容原有接口
return _get_legacy_logger(name, file_name, without_formater, print_to_console)


def _get_unified_logger(name):
"""
新的统一日志获取方式
"""
if name is None:
return logging.getLogger("fastdeploy")

# 处理 __main__ 特殊情况
if name == "__main__":
return logging.getLogger("fastdeploy.main")

# 如果已经是fastdeploy命名空间,直接使用
if name.startswith("fastdeploy.") or name == "fastdeploy":
return logging.getLogger(name)
else:
# 其他情况添加fastdeploy前缀
return logging.getLogger(f"fastdeploy.{name}")


def _get_legacy_logger(name, file_name, without_formater=False, print_to_console=False):
"""
兼容原有接口的日志获取方式
"""

log_dir = envs.FD_LOG_DIR
if not os.path.exists(log_dir):
os.makedirs(log_dir, exist_ok=True)

is_debug = int(envs.FD_DEBUG)
# logger = logging.getLogger(name)
legacy_name = f"legacy.{name}"
logger = logging.getLogger(legacy_name)

# 设置日志级别
if is_debug:
logger.setLevel(level=logging.DEBUG)
else:
logger.setLevel(level=logging.INFO)

# 清除现有的handlers(保持原有逻辑)
for handler in logger.handlers[:]:
logger.removeHandler(handler)

# 创建主日志文件handler
LOG_FILE = f"{log_dir}/{file_name}"
backup_count = int(envs.FD_LOG_BACKUP_COUNT)
handler = DailyRotatingFileHandler(LOG_FILE, backupCount=backup_count)

# 创建ERROR日志文件handler(新增功能)
ERROR_LOG_FILE = f"{log_dir}/error_{file_name}"
error_handler = DailyRotatingFileHandler(ERROR_LOG_FILE, backupCount=backup_count)
error_handler.setLevel(logging.ERROR)

# 设置格式化器
formatter = ColoredFormatter("%(levelname)-8s %(asctime)s %(process)-5s %(filename)s[line:%(lineno)d] %(message)s")

if not without_formater:
handler.setFormatter(formatter)
error_handler.setFormatter(formatter)

# 添加文件handlers
logger.addHandler(handler)
logger.addHandler(error_handler)

# 控制台handler(如果需要)
if print_to_console:
console_handler = logging.StreamHandler()
if not without_formater:
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
console_handler.propagate = False

# 设置propagate(保持原有逻辑)
handler.propagate = False
error_handler.propagate = False
logger.propagate = False

return logger
Empty file added fastdeploy/util/__init__.py
Empty file.
44 changes: 44 additions & 0 deletions fastdeploy/util/formatters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""
自定义日志格式化器模块

该模块定义了 ColoredFormatter 类,用于在控制台输出带颜色的日志信息,
便于开发者在终端中快速识别不同级别的日志。
"""

import logging


class ColoredFormatter(logging.Formatter):
"""
自定义日志格式器,用于控制台输出带颜色的日志。

支持的颜色:
- WARNING: 黄色
- ERROR: 红色
- CRITICAL: 红色
- 其他等级: 默认终端颜色
"""

COLOR_CODES = {
logging.WARNING: 33, # 黄色
logging.ERROR: 31, # 红色
logging.CRITICAL: 31, # 红色
}

def format(self, record):
"""
格式化日志记录,并根据日志等级添加 ANSI 颜色前缀和后缀。

Args:
record (LogRecord): 日志记录对象。

Returns:
str: 带有颜色的日志消息字符串。
"""
color_code = self.COLOR_CODES.get(record.levelno, 0)
prefix = f"\033[{color_code}m"
suffix = "\033[0m"
message = super().format(record)
if color_code:
message = f"{prefix}{message}{suffix}"
return message
177 changes: 177 additions & 0 deletions fastdeploy/util/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import codecs
import os
import re
import time
from datetime import datetime
from logging.handlers import BaseRotatingHandler, TimedRotatingFileHandler
from pathlib import Path

"""自定义日志处理器模块:
该模块包含FastDeploy项目中使用的自定义日志处理器实现,
用于处理和控制日志输出格式、级别和目标等。
"""


class DailyFolderTimedRotatingFileHandler(TimedRotatingFileHandler):
"""
自定义处理器:每天一个目录,每小时一个文件
文件结构:
logs/
└── 2025-08-05/
├── fastdeploy_error_10.log
└── fastdeploy_debug_10.log
"""

def __init__(self, filename, when="H", interval=1, backupCount=48, encoding=None, utc=False, **kwargs):
# 支持从dictConfig中通过filename传入 base_log_dir/base_filename
base_log_dir, base_name = os.path.split(filename)
base_filename = os.path.splitext(base_name)[0]

self.base_log_dir = base_log_dir
self.base_filename = base_filename
self.current_day = datetime.now().strftime("%Y-%m-%d")
self._update_baseFilename()

super().__init__(
filename=self.baseFilename,
when=when,
interval=interval,
backupCount=backupCount,
encoding=encoding,
utc=utc,
)

def _update_baseFilename(self):
dated_dir = os.path.join(self.base_log_dir, self.current_day)
os.makedirs(dated_dir, exist_ok=True)
self.baseFilename = os.path.abspath(
os.path.join(dated_dir, f"{self.base_filename}_{datetime.now().strftime('%H')}.log")
)

def shouldRollover(self, record):
new_day = datetime.now().strftime("%Y-%m-%d")
if new_day != self.current_day:
self.current_day = new_day
return 1
return super().shouldRollover(record)

def doRollover(self):
self.stream.close()
self._update_baseFilename()
self.stream = self._open()


class DailyRotatingFileHandler(BaseRotatingHandler):
"""
like `logging.TimedRotatingFileHandler`, but this class support multi-process
"""

def __init__(
self,
filename,
backupCount=0,
encoding="utf-8",
delay=False,
utc=False,
**kwargs,
):
"""
初始化 RotatingFileHandler 对象。

Args:
filename (str): 日志文件的路径,可以是相对路径或绝对路径。
backupCount (int, optional, default=0): 保存的备份文件数量,默认为 0,表示不保存备份文件。
encoding (str, optional, default='utf-8'): 编码格式,默认为 'utf-8'。
delay (bool, optional, default=False): 是否延迟写入,默认为 False,表示立即写入。
utc (bool, optional, default=False): 是否使用 UTC 时区,默认为 False,表示不使用 UTC 时区。
kwargs (dict, optional): 其他参数将被传递给 BaseRotatingHandler 类的 init 方法。

Raises:
TypeError: 如果 filename 不是 str 类型。
ValueError: 如果 backupCount 小于等于 0。
"""
self.backup_count = backupCount
self.utc = utc
self.suffix = "%Y-%m-%d"
self.base_log_path = Path(filename)
self.base_filename = self.base_log_path.name
self.current_filename = self._compute_fn()
self.current_log_path = self.base_log_path.with_name(self.current_filename)
BaseRotatingHandler.__init__(self, filename, "a", encoding, delay)

def shouldRollover(self, record):
"""
check scroll through the log
"""
if self.current_filename != self._compute_fn():
return True
return False

def doRollover(self):
"""
scroll log
"""
if self.stream:
self.stream.close()
self.stream = None

self.current_filename = self._compute_fn()
self.current_log_path = self.base_log_path.with_name(self.current_filename)

if not self.delay:
self.stream = self._open()

self.delete_expired_files()

def _compute_fn(self):
"""
Calculate the log file name corresponding current time
"""
return self.base_filename + "." + time.strftime(self.suffix, time.localtime())

def _open(self):
"""
open new log file
"""
if self.encoding is None:
stream = open(str(self.current_log_path), self.mode)
else:
stream = codecs.open(str(self.current_log_path), self.mode, self.encoding)

if self.base_log_path.exists():
try:
if not self.base_log_path.is_symlink() or os.readlink(self.base_log_path) != self.current_filename:
os.remove(self.base_log_path)
except OSError:
pass

try:
os.symlink(self.current_filename, str(self.base_log_path))
except OSError:
pass
return stream

def delete_expired_files(self):
"""
delete expired log files
"""
if self.backup_count <= 0:
return

file_names = os.listdir(str(self.base_log_path.parent))
result = []
prefix = self.base_filename + "."
plen = len(prefix)
for file_name in file_names:
if file_name[:plen] == prefix:
suffix = file_name[plen:]
if re.match(r"^\d{4}-\d{2}-\d{2}(\.\w+)?$", suffix):
result.append(file_name)
if len(result) < self.backup_count:
result = []
else:
result.sort()
result = result[: len(result) - self.backup_count]

for file_name in result:
os.remove(str(self.base_log_path.with_name(file_name)))
Loading
Loading