Skip to content

Commit 7c381e8

Browse files
committed
feat: add error message to log file
1 parent 841e831 commit 7c381e8

File tree

10 files changed

+999
-32
lines changed

10 files changed

+999
-32
lines changed

fastdeploy/logger.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""
2+
日志模块:用于初始化和获取 FastDeploy 日志记录器。
3+
本模块提供 get_logger 方法,统一管理各子模块的日志记录行为。
4+
"""
5+
6+
import logging
7+
import os
8+
9+
from fastdeploy import envs
10+
from fastdeploy.util.formatters import ColoredFormatter
11+
from fastdeploy.util.handlers import DailyRotatingFileHandler
12+
from fastdeploy.util.setup_logging import setup_logging
13+
14+
# 初始化一次日志系统
15+
setup_logging()
16+
17+
18+
def get_logger(name, file_name=None, without_formater=False, print_to_console=False):
19+
"""
20+
获取日志记录器(兼容原有接口)
21+
22+
Args:
23+
name: 日志器名称
24+
file_name: 日志文件名(保持兼容性)
25+
without_formater: 是否不使用格式化器
26+
print_to_console: 是否打印到控制台
27+
"""
28+
# 如果只有一个参数,使用新的统一命名方式
29+
if file_name is None and not without_formater and not print_to_console:
30+
return _get_unified_logger(name)
31+
32+
# 兼容原有接口
33+
return _get_legacy_logger(name, file_name, without_formater, print_to_console)
34+
35+
36+
def _get_unified_logger(name):
37+
"""
38+
新的统一日志获取方式
39+
"""
40+
if name is None:
41+
return logging.getLogger("fastdeploy")
42+
43+
# 处理 __main__ 特殊情况
44+
if name == "__main__":
45+
return logging.getLogger("fastdeploy.main")
46+
47+
# 如果已经是fastdeploy命名空间,直接使用
48+
if name.startswith("fastdeploy.") or name == "fastdeploy":
49+
return logging.getLogger(name)
50+
else:
51+
# 其他情况添加fastdeploy前缀
52+
return logging.getLogger(f"fastdeploy.{name}")
53+
54+
55+
def _get_legacy_logger(name, file_name, without_formater=False, print_to_console=False):
56+
"""
57+
兼容原有接口的日志获取方式
58+
"""
59+
60+
log_dir = envs.FD_LOG_DIR
61+
if not os.path.exists(log_dir):
62+
os.makedirs(log_dir, exist_ok=True)
63+
64+
is_debug = int(envs.FD_DEBUG)
65+
# logger = logging.getLogger(name)
66+
legacy_name = f"legacy.{name}"
67+
logger = logging.getLogger(legacy_name)
68+
69+
# 设置日志级别
70+
if is_debug:
71+
logger.setLevel(level=logging.DEBUG)
72+
else:
73+
logger.setLevel(level=logging.INFO)
74+
75+
# 清除现有的handlers(保持原有逻辑)
76+
for handler in logger.handlers[:]:
77+
logger.removeHandler(handler)
78+
79+
# 创建主日志文件handler
80+
LOG_FILE = f"{log_dir}/{file_name}"
81+
backup_count = int(envs.FD_LOG_BACKUP_COUNT)
82+
handler = DailyRotatingFileHandler(LOG_FILE, backupCount=backup_count)
83+
84+
# 创建ERROR日志文件handler(新增功能)
85+
ERROR_LOG_FILE = f"{log_dir}/error_{file_name}"
86+
error_handler = DailyRotatingFileHandler(ERROR_LOG_FILE, backupCount=backup_count)
87+
error_handler.setLevel(logging.ERROR)
88+
89+
# 设置格式化器
90+
formatter = ColoredFormatter("%(levelname)-8s %(asctime)s %(process)-5s %(filename)s[line:%(lineno)d] %(message)s")
91+
92+
if not without_formater:
93+
handler.setFormatter(formatter)
94+
error_handler.setFormatter(formatter)
95+
96+
# 添加文件handlers
97+
logger.addHandler(handler)
98+
logger.addHandler(error_handler)
99+
100+
# 控制台handler(如果需要)
101+
if print_to_console:
102+
console_handler = logging.StreamHandler()
103+
if not without_formater:
104+
console_handler.setFormatter(formatter)
105+
logger.addHandler(console_handler)
106+
console_handler.propagate = False
107+
108+
# 设置propagate(保持原有逻辑)
109+
handler.propagate = False
110+
error_handler.propagate = False
111+
logger.propagate = False
112+
113+
return logger

fastdeploy/util/__init__.py

Whitespace-only changes.

fastdeploy/util/formatters.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""
2+
自定义日志格式化器模块
3+
4+
该模块定义了 ColoredFormatter 类,用于在控制台输出带颜色的日志信息,
5+
便于开发者在终端中快速识别不同级别的日志。
6+
"""
7+
8+
import logging
9+
10+
11+
class ColoredFormatter(logging.Formatter):
12+
"""
13+
自定义日志格式器,用于控制台输出带颜色的日志。
14+
15+
支持的颜色:
16+
- WARNING: 黄色
17+
- ERROR: 红色
18+
- CRITICAL: 红色
19+
- 其他等级: 默认终端颜色
20+
"""
21+
22+
COLOR_CODES = {
23+
logging.WARNING: 33, # 黄色
24+
logging.ERROR: 31, # 红色
25+
logging.CRITICAL: 31, # 红色
26+
}
27+
28+
def format(self, record):
29+
"""
30+
格式化日志记录,并根据日志等级添加 ANSI 颜色前缀和后缀。
31+
32+
Args:
33+
record (LogRecord): 日志记录对象。
34+
35+
Returns:
36+
str: 带有颜色的日志消息字符串。
37+
"""
38+
color_code = self.COLOR_CODES.get(record.levelno, 0)
39+
prefix = f"\033[{color_code}m"
40+
suffix = "\033[0m"
41+
message = super().format(record)
42+
if color_code:
43+
message = f"{prefix}{message}{suffix}"
44+
return message

fastdeploy/util/handlers.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import codecs
2+
import os
3+
import re
4+
import time
5+
from datetime import datetime
6+
from logging.handlers import BaseRotatingHandler, TimedRotatingFileHandler
7+
from pathlib import Path
8+
9+
"""自定义日志处理器模块:
10+
该模块包含FastDeploy项目中使用的自定义日志处理器实现,
11+
用于处理和控制日志输出格式、级别和目标等。
12+
"""
13+
14+
15+
class DailyFolderTimedRotatingFileHandler(TimedRotatingFileHandler):
16+
"""
17+
自定义处理器:每天一个目录,每小时一个文件
18+
文件结构:
19+
logs/
20+
└── 2025-08-05/
21+
├── fastdeploy_error_10.log
22+
└── fastdeploy_debug_10.log
23+
"""
24+
25+
def __init__(self, filename, when="H", interval=1, backupCount=48, encoding=None, utc=False, **kwargs):
26+
# 支持从dictConfig中通过filename传入 base_log_dir/base_filename
27+
base_log_dir, base_name = os.path.split(filename)
28+
base_filename = os.path.splitext(base_name)[0]
29+
30+
self.base_log_dir = base_log_dir
31+
self.base_filename = base_filename
32+
self.current_day = datetime.now().strftime("%Y-%m-%d")
33+
self._update_baseFilename()
34+
35+
super().__init__(
36+
filename=self.baseFilename,
37+
when=when,
38+
interval=interval,
39+
backupCount=backupCount,
40+
encoding=encoding,
41+
utc=utc,
42+
)
43+
44+
def _update_baseFilename(self):
45+
dated_dir = os.path.join(self.base_log_dir, self.current_day)
46+
os.makedirs(dated_dir, exist_ok=True)
47+
self.baseFilename = os.path.abspath(
48+
os.path.join(dated_dir, f"{self.base_filename}_{datetime.now().strftime('%H')}.log")
49+
)
50+
51+
def shouldRollover(self, record):
52+
new_day = datetime.now().strftime("%Y-%m-%d")
53+
if new_day != self.current_day:
54+
self.current_day = new_day
55+
return 1
56+
return super().shouldRollover(record)
57+
58+
def doRollover(self):
59+
self.stream.close()
60+
self._update_baseFilename()
61+
self.stream = self._open()
62+
63+
64+
class DailyRotatingFileHandler(BaseRotatingHandler):
65+
"""
66+
like `logging.TimedRotatingFileHandler`, but this class support multi-process
67+
"""
68+
69+
def __init__(
70+
self,
71+
filename,
72+
backupCount=0,
73+
encoding="utf-8",
74+
delay=False,
75+
utc=False,
76+
**kwargs,
77+
):
78+
"""
79+
初始化 RotatingFileHandler 对象。
80+
81+
Args:
82+
filename (str): 日志文件的路径,可以是相对路径或绝对路径。
83+
backupCount (int, optional, default=0): 保存的备份文件数量,默认为 0,表示不保存备份文件。
84+
encoding (str, optional, default='utf-8'): 编码格式,默认为 'utf-8'。
85+
delay (bool, optional, default=False): 是否延迟写入,默认为 False,表示立即写入。
86+
utc (bool, optional, default=False): 是否使用 UTC 时区,默认为 False,表示不使用 UTC 时区。
87+
kwargs (dict, optional): 其他参数将被传递给 BaseRotatingHandler 类的 init 方法。
88+
89+
Raises:
90+
TypeError: 如果 filename 不是 str 类型。
91+
ValueError: 如果 backupCount 小于等于 0。
92+
"""
93+
self.backup_count = backupCount
94+
self.utc = utc
95+
self.suffix = "%Y-%m-%d"
96+
self.base_log_path = Path(filename)
97+
self.base_filename = self.base_log_path.name
98+
self.current_filename = self._compute_fn()
99+
self.current_log_path = self.base_log_path.with_name(self.current_filename)
100+
BaseRotatingHandler.__init__(self, filename, "a", encoding, delay)
101+
102+
def shouldRollover(self, record):
103+
"""
104+
check scroll through the log
105+
"""
106+
if self.current_filename != self._compute_fn():
107+
return True
108+
return False
109+
110+
def doRollover(self):
111+
"""
112+
scroll log
113+
"""
114+
if self.stream:
115+
self.stream.close()
116+
self.stream = None
117+
118+
self.current_filename = self._compute_fn()
119+
self.current_log_path = self.base_log_path.with_name(self.current_filename)
120+
121+
if not self.delay:
122+
self.stream = self._open()
123+
124+
self.delete_expired_files()
125+
126+
def _compute_fn(self):
127+
"""
128+
Calculate the log file name corresponding current time
129+
"""
130+
return self.base_filename + "." + time.strftime(self.suffix, time.localtime())
131+
132+
def _open(self):
133+
"""
134+
open new log file
135+
"""
136+
if self.encoding is None:
137+
stream = open(str(self.current_log_path), self.mode)
138+
else:
139+
stream = codecs.open(str(self.current_log_path), self.mode, self.encoding)
140+
141+
if self.base_log_path.exists():
142+
try:
143+
if not self.base_log_path.is_symlink() or os.readlink(self.base_log_path) != self.current_filename:
144+
os.remove(self.base_log_path)
145+
except OSError:
146+
pass
147+
148+
try:
149+
os.symlink(self.current_filename, str(self.base_log_path))
150+
except OSError:
151+
pass
152+
return stream
153+
154+
def delete_expired_files(self):
155+
"""
156+
delete expired log files
157+
"""
158+
if self.backup_count <= 0:
159+
return
160+
161+
file_names = os.listdir(str(self.base_log_path.parent))
162+
result = []
163+
prefix = self.base_filename + "."
164+
plen = len(prefix)
165+
for file_name in file_names:
166+
if file_name[:plen] == prefix:
167+
suffix = file_name[plen:]
168+
if re.match(r"^\d{4}-\d{2}-\d{2}(\.\w+)?$", suffix):
169+
result.append(file_name)
170+
if len(result) < self.backup_count:
171+
result = []
172+
else:
173+
result.sort()
174+
result = result[: len(result) - self.backup_count]
175+
176+
for file_name in result:
177+
os.remove(str(self.base_log_path.with_name(file_name)))

0 commit comments

Comments
 (0)