butterbot-python 3.1.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- butterbot/__init__.py +15 -0
- butterbot/app/__init__.py +41 -0
- butterbot/app/bot_app.py +374 -0
- butterbot/app/config.py +156 -0
- butterbot/app/source_manager.py +367 -0
- butterbot/core/__init__.py +46 -0
- butterbot/core/api/__init__.py +9 -0
- butterbot/core/api/base_api.py +32 -0
- butterbot/core/context/README.md +1 -0
- butterbot/core/context/__init__.py +9 -0
- butterbot/core/context/api_registry.py +109 -0
- butterbot/core/context/app_context.py +51 -0
- butterbot/core/context/config_provider.py +14 -0
- butterbot/core/data/__init__.py +15 -0
- butterbot/core/data/base_data.py +29 -0
- butterbot/core/data/base_model.py +197 -0
- butterbot/core/event/README.md +1 -0
- butterbot/core/event/__init__.py +10 -0
- butterbot/core/event/event.py +27 -0
- butterbot/core/event/event_bus.py +276 -0
- butterbot/core/event/subscriber.py +130 -0
- butterbot/core/exceptions.py +90 -0
- butterbot/core/filter/__init__.py +11 -0
- butterbot/core/filter/base_filter.py +74 -0
- butterbot/core/source/README.md +1 -0
- butterbot/core/source/__init__.py +9 -0
- butterbot/core/source/base_source.py +134 -0
- butterbot/core/types/__init__.py +9 -0
- butterbot/core/types/base_type.py +74 -0
- butterbot/sources/__init__.py +1 -0
- butterbot/sources/bilibili/README.md +10 -0
- butterbot/sources/bilibili/__init__.py +20 -0
- butterbot/sources/bilibili/api/__init__.py +5 -0
- butterbot/sources/bilibili/api/bili_api.py +123 -0
- butterbot/sources/bilibili/data/__init__.py +52 -0
- butterbot/sources/bilibili/data/danmaku_gift_data.py +135 -0
- butterbot/sources/bilibili/data/danmaku_guard_data.py +54 -0
- butterbot/sources/bilibili/data/danmaku_msg_data.py +71 -0
- butterbot/sources/bilibili/data/dto/__init__.py +59 -0
- butterbot/sources/bilibili/data/dto/danmaku_gift_dto.py +193 -0
- butterbot/sources/bilibili/data/dto/danmaku_guard_buy_dto.py +54 -0
- butterbot/sources/bilibili/data/dto/danmaku_msg_dto.py +123 -0
- butterbot/sources/bilibili/data/dto/dynamic_dto.py +276 -0
- butterbot/sources/bilibili/data/dto/live_room_dto.py +169 -0
- butterbot/sources/bilibili/data/dto/video_part_dto.py +18 -0
- butterbot/sources/bilibili/data/dynamic_data.py +362 -0
- butterbot/sources/bilibili/data/live_room_data.py +162 -0
- butterbot/sources/bilibili/data/video_part.py +46 -0
- butterbot/sources/bilibili/source/__init__.py +15 -0
- butterbot/sources/bilibili/source/base_polling_source.py +130 -0
- butterbot/sources/bilibili/source/bili_danmaku_source.py +230 -0
- butterbot/sources/bilibili/source/bili_dynamic_source.py +135 -0
- butterbot/sources/bilibili/source/bili_live_source.py +137 -0
- butterbot/sources/bilibili/types/__init__.py +11 -0
- butterbot/sources/bilibili/types/bili_type.py +32 -0
- butterbot/sources/napcat/README.md +10 -0
- butterbot/sources/napcat/__init__.py +20 -0
- butterbot/sources/napcat/api/__init__.py +3 -0
- butterbot/sources/napcat/api/napcat_api.py +316 -0
- butterbot/sources/napcat/data/__init__.py +179 -0
- butterbot/sources/napcat/data/event_data.py +441 -0
- butterbot/sources/napcat/data/segment_data.py +432 -0
- butterbot/sources/napcat/events.py +91 -0
- butterbot/sources/napcat/filters/__init__.py +19 -0
- butterbot/sources/napcat/filters/filters.py +202 -0
- butterbot/sources/napcat/source/__init__.py +5 -0
- butterbot/sources/napcat/source/napcat_source.py +58 -0
- butterbot/sources/napcat/types/__init__.py +5 -0
- butterbot/sources/napcat/types/napcat_type.py +61 -0
- butterbot/utils/README.md +15 -0
- butterbot/utils/__init__.py +11 -0
- butterbot/utils/data_pair.py +27 -0
- butterbot/utils/logging_config.py +521 -0
- butterbot/utils/terminal.py +308 -0
- butterbot/utils/websocket.py +1270 -0
- butterbot_python-3.1.0.dev1.dist-info/METADATA +769 -0
- butterbot_python-3.1.0.dev1.dist-info/RECORD +79 -0
- butterbot_python-3.1.0.dev1.dist-info/WHEEL +4 -0
- butterbot_python-3.1.0.dev1.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
# -------------------------
|
|
2
|
+
# @Author : Fish-LP fish.zh@outlook.com
|
|
3
|
+
# @Date : 2025-02-12 13:41:02
|
|
4
|
+
# @LastEditors : Fish-LP fish.zh@outlook.com
|
|
5
|
+
# @LastEditTime : 2025-06-22 21:51:42
|
|
6
|
+
# @Description : 日志格式化
|
|
7
|
+
# @Copyright (c) 2025 by Fish-LP, MIT 使用许可协议
|
|
8
|
+
# -------------------------
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import warnings
|
|
13
|
+
from logging.handlers import TimedRotatingFileHandler
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from .terminal import Color, set_console_mode
|
|
17
|
+
|
|
18
|
+
# 尝试从 tqdm 导入进度条类,不强制依赖
|
|
19
|
+
try:
|
|
20
|
+
from tqdm import tqdm as tqdm_original # type: ignore[import-untyped]
|
|
21
|
+
except ImportError:
|
|
22
|
+
tqdm_original = None
|
|
23
|
+
|
|
24
|
+
__author__ = "Fish-LP <Fish.zh@outlook.com>"
|
|
25
|
+
__status__ = "dev"
|
|
26
|
+
__version__ = "2.1.1-dev"
|
|
27
|
+
|
|
28
|
+
# NOTE: 这里保存的是针对不同目标(console/file)和不同日志级别的消息格式模板
|
|
29
|
+
LOG_MESSAGE_FORMATS = {
|
|
30
|
+
"console": {
|
|
31
|
+
"DEBUG": (
|
|
32
|
+
Color.CYAN
|
|
33
|
+
+ "[%(asctime)s.%(msecs)s]"
|
|
34
|
+
+ Color.RESET
|
|
35
|
+
+ " "
|
|
36
|
+
+ Color.BLUE
|
|
37
|
+
+ "%(colored_levelname)-8s"
|
|
38
|
+
+ Color.RESET
|
|
39
|
+
+ " "
|
|
40
|
+
+ Color.GRAY
|
|
41
|
+
+ "[%(threadName)s|%(processName)s]"
|
|
42
|
+
+ Color.RESET
|
|
43
|
+
+ " "
|
|
44
|
+
+ Color.MAGENTA
|
|
45
|
+
+ "%(name)s"
|
|
46
|
+
+ Color.RESET
|
|
47
|
+
+ " "
|
|
48
|
+
+ Color.YELLOW
|
|
49
|
+
+ "%(filename)s:%(lineno)d %(funcName)s"
|
|
50
|
+
+ Color.RESET
|
|
51
|
+
+ " "
|
|
52
|
+
+ Color.RESET
|
|
53
|
+
+ "| %(message)s"
|
|
54
|
+
+ Color.RESET
|
|
55
|
+
),
|
|
56
|
+
"INFO": (
|
|
57
|
+
Color.CYAN
|
|
58
|
+
+ "[%(asctime)s]"
|
|
59
|
+
+ Color.RESET
|
|
60
|
+
+ " "
|
|
61
|
+
+ Color.GREEN
|
|
62
|
+
+ "%(colored_levelname)-8s"
|
|
63
|
+
+ Color.RESET
|
|
64
|
+
+ " "
|
|
65
|
+
+ Color.MAGENTA
|
|
66
|
+
+ "%(name)s"
|
|
67
|
+
+ Color.RESET
|
|
68
|
+
+ " ➜ "
|
|
69
|
+
+ Color.RESET
|
|
70
|
+
+ "%(message)s"
|
|
71
|
+
+ Color.RESET
|
|
72
|
+
),
|
|
73
|
+
"WARNING": (
|
|
74
|
+
Color.CYAN
|
|
75
|
+
+ "[%(asctime)s]"
|
|
76
|
+
+ Color.RESET
|
|
77
|
+
+ " "
|
|
78
|
+
+ Color.YELLOW
|
|
79
|
+
+ "%(colored_levelname)-8s"
|
|
80
|
+
+ Color.RESET
|
|
81
|
+
+ " "
|
|
82
|
+
+ Color.MAGENTA
|
|
83
|
+
+ "%(name)s"
|
|
84
|
+
+ Color.RESET
|
|
85
|
+
+ " "
|
|
86
|
+
+ Color.YELLOW
|
|
87
|
+
+ "➜"
|
|
88
|
+
+ Color.RESET
|
|
89
|
+
+ " "
|
|
90
|
+
+ Color.RESET
|
|
91
|
+
+ "%(message)s"
|
|
92
|
+
+ Color.RESET
|
|
93
|
+
),
|
|
94
|
+
"ERROR": (
|
|
95
|
+
Color.CYAN
|
|
96
|
+
+ "[%(asctime)s]"
|
|
97
|
+
+ Color.RESET
|
|
98
|
+
+ " "
|
|
99
|
+
+ Color.RED
|
|
100
|
+
+ "%(colored_levelname)-8s"
|
|
101
|
+
+ Color.RESET
|
|
102
|
+
+ " "
|
|
103
|
+
+ Color.GRAY
|
|
104
|
+
+ "[%(filename)s]"
|
|
105
|
+
+ Color.RESET
|
|
106
|
+
+ Color.MAGENTA
|
|
107
|
+
+ "%(name)s:%(lineno)d"
|
|
108
|
+
+ Color.RESET
|
|
109
|
+
+ " "
|
|
110
|
+
+ Color.RED
|
|
111
|
+
+ "➜"
|
|
112
|
+
+ Color.RESET
|
|
113
|
+
+ " "
|
|
114
|
+
+ Color.RESET
|
|
115
|
+
+ "%(message)s"
|
|
116
|
+
+ Color.RESET
|
|
117
|
+
),
|
|
118
|
+
"CRITICAL": (
|
|
119
|
+
Color.CYAN
|
|
120
|
+
+ "[%(asctime)s]"
|
|
121
|
+
+ Color.RESET
|
|
122
|
+
+ " "
|
|
123
|
+
+ Color.RED
|
|
124
|
+
+ Color.BOLD
|
|
125
|
+
+ "%(colored_levelname)-8s"
|
|
126
|
+
+ Color.RESET
|
|
127
|
+
+ " "
|
|
128
|
+
+ Color.GRAY
|
|
129
|
+
+ "{%(module)s}"
|
|
130
|
+
+ Color.RESET
|
|
131
|
+
+ Color.MAGENTA
|
|
132
|
+
+ "[%(filename)s]"
|
|
133
|
+
+ Color.RESET
|
|
134
|
+
+ Color.MAGENTA
|
|
135
|
+
+ "%(name)s:%(lineno)d"
|
|
136
|
+
+ Color.RESET
|
|
137
|
+
+ " "
|
|
138
|
+
+ Color.RED
|
|
139
|
+
+ "➜"
|
|
140
|
+
+ Color.RESET
|
|
141
|
+
+ " "
|
|
142
|
+
+ Color.RESET
|
|
143
|
+
+ "%(message)s"
|
|
144
|
+
+ Color.RESET
|
|
145
|
+
),
|
|
146
|
+
},
|
|
147
|
+
"file": {
|
|
148
|
+
"DEBUG": "[%(asctime)s] %(levelname)-8s [%(threadName)s|%(processName)s] %(name)s (%(filename)s:%(funcName)s:%(lineno)d) | %(message)s",
|
|
149
|
+
"INFO": "[%(asctime)s] %(levelname)-8s %(name)s ➜ %(message)s",
|
|
150
|
+
"WARNING": "[%(asctime)s] %(levelname)-8s %(name)s ➜ %(message)s",
|
|
151
|
+
"ERROR": "[%(asctime)s] %(levelname)-8s [%(filename)s]%(name)s:%(lineno)d ➜ %(message)s",
|
|
152
|
+
"CRITICAL": "[%(asctime)s] %(levelname)-8s {%(module)s}[%(filename)s]%(name)s:%(lineno)d ➜ %(message)s",
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if tqdm_original is not None:
|
|
157
|
+
|
|
158
|
+
class tqdm(tqdm_original):
|
|
159
|
+
"""
|
|
160
|
+
自定义 tqdm 类的初始化方法
|
|
161
|
+
通过设置默认参数,确保每次创建 tqdm 进度条时都能应用统一的风格
|
|
162
|
+
|
|
163
|
+
参数说明:
|
|
164
|
+
:param args: 原生 tqdm 支持的非关键字参数(如可迭代对象等)
|
|
165
|
+
:param kwargs: 原生 tqdm 支持的关键字参数,用于自定义进度条的行为和外观
|
|
166
|
+
- bar_format (str): 进度条的格式化字符串
|
|
167
|
+
- ncols (int): 进度条的宽度(以字符为单位)
|
|
168
|
+
- colour (str): 进度条的颜色
|
|
169
|
+
- desc (str): 进度条的描述信息
|
|
170
|
+
- unit (str): 进度条的单位
|
|
171
|
+
- leave (bool): 进度条完成后是否保留显示
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
_STYLE_MAP = {
|
|
175
|
+
"BLACK": Color.BLACK,
|
|
176
|
+
"RED": Color.RED,
|
|
177
|
+
"GREEN": Color.GREEN,
|
|
178
|
+
"YELLOW": Color.YELLOW,
|
|
179
|
+
"BLUE": Color.BLUE,
|
|
180
|
+
"MAGENTA": Color.MAGENTA,
|
|
181
|
+
"CYAN": Color.CYAN,
|
|
182
|
+
"WHITE": Color.WHITE,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
def __init__(self, *args, **kwargs):
|
|
186
|
+
# 保存颜色参数以便后续处理
|
|
187
|
+
self._custom_colour = kwargs.get("colour", "GREEN")
|
|
188
|
+
|
|
189
|
+
# 设置默认进度条格式
|
|
190
|
+
kwargs.setdefault(
|
|
191
|
+
"bar_format",
|
|
192
|
+
(
|
|
193
|
+
Color.CYAN
|
|
194
|
+
+ "{desc}"
|
|
195
|
+
+ Color.RESET
|
|
196
|
+
+ " "
|
|
197
|
+
+ Color.WHITE
|
|
198
|
+
+ "{percentage:3.0f}%"
|
|
199
|
+
+ Color.RESET
|
|
200
|
+
+ " "
|
|
201
|
+
+ Color.GRAY
|
|
202
|
+
+ "[{n_fmt}]"
|
|
203
|
+
+ Color.RESET
|
|
204
|
+
+ Color.WHITE
|
|
205
|
+
+ "|{bar:20}|"
|
|
206
|
+
+ Color.RESET
|
|
207
|
+
+ Color.BLUE
|
|
208
|
+
+ "[{elapsed}]"
|
|
209
|
+
+ Color.RESET
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
kwargs.setdefault("ncols", 80)
|
|
213
|
+
kwargs.setdefault("colour", None) # 避免基类处理颜色
|
|
214
|
+
|
|
215
|
+
super().__init__(*args, **kwargs)
|
|
216
|
+
|
|
217
|
+
# 在初始化完成后应用颜色
|
|
218
|
+
self.colour = self._custom_colour
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def colour(self):
|
|
222
|
+
return self._colour
|
|
223
|
+
|
|
224
|
+
@colour.setter
|
|
225
|
+
def colour(self, color):
|
|
226
|
+
# 确保颜色值有效
|
|
227
|
+
if not color:
|
|
228
|
+
color = "GREEN"
|
|
229
|
+
|
|
230
|
+
color_upper = color.upper()
|
|
231
|
+
valid_color = self._STYLE_MAP.get(color_upper, "GREEN")
|
|
232
|
+
|
|
233
|
+
# 保存颜色值
|
|
234
|
+
self._colour = color_upper
|
|
235
|
+
|
|
236
|
+
# 更新描述信息颜色
|
|
237
|
+
if hasattr(self, "GREEN") and self.desc:
|
|
238
|
+
self.desc = "%s%s%s" % (
|
|
239
|
+
getattr(Color, valid_color),
|
|
240
|
+
self.desc,
|
|
241
|
+
Color.RESET,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# 日志级别颜色映射
|
|
246
|
+
LOG_LEVEL_TO_COLOR = {
|
|
247
|
+
"DEBUG": Color.CYAN,
|
|
248
|
+
"INFO": Color.GREEN,
|
|
249
|
+
"WARNING": Color.YELLOW,
|
|
250
|
+
"ERROR": Color.RED,
|
|
251
|
+
"CRITICAL": Color.MAGENTA,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
_managed_handlers: list[tuple[logging.Logger, logging.Handler]] = []
|
|
255
|
+
_redirect_logger_state: dict[logging.Logger, tuple[int, bool]] = {}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# 定义动态格式化器,根据日志级别选择不同的格式
|
|
259
|
+
class DynamicFormatter(logging.Formatter):
|
|
260
|
+
"""根据日志记录级别动态选择格式的格式化器"""
|
|
261
|
+
|
|
262
|
+
def __init__(
|
|
263
|
+
self, fmt_dict: dict, datefmt: str | None = None, use_color: bool = True
|
|
264
|
+
):
|
|
265
|
+
"""
|
|
266
|
+
初始化动态格式化器
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
fmt_dict: 包含不同日志级别格式字符串的字典,键为级别名称(如"DEBUG")
|
|
270
|
+
datefmt: 日期时间格式字符串
|
|
271
|
+
use_color: 是否使用颜色
|
|
272
|
+
"""
|
|
273
|
+
super().__init__(datefmt=datefmt)
|
|
274
|
+
self.fmt_dict = fmt_dict
|
|
275
|
+
self.use_color = use_color
|
|
276
|
+
|
|
277
|
+
# 为每个级别预创建Formatter实例,提高性能
|
|
278
|
+
self._formatters = {}
|
|
279
|
+
for level_name, fmt in fmt_dict.items():
|
|
280
|
+
self._formatters[level_name] = logging.Formatter(fmt, datefmt=datefmt)
|
|
281
|
+
|
|
282
|
+
# 默认格式(使用第一个可用的格式)
|
|
283
|
+
self._default_formatter = list(self._formatters.values())[0]
|
|
284
|
+
|
|
285
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
286
|
+
"""格式化日志记录,根据记录级别选择对应的格式"""
|
|
287
|
+
# 动态颜色处理
|
|
288
|
+
if self.use_color:
|
|
289
|
+
record.colored_levelname = "%s%8s%s" % (
|
|
290
|
+
LOG_LEVEL_TO_COLOR.get(record.levelname, Color.RESET),
|
|
291
|
+
record.levelname,
|
|
292
|
+
Color.RESET,
|
|
293
|
+
)
|
|
294
|
+
# 添加统一颜色字段
|
|
295
|
+
record.colored_name = "%s%s%s" % (Color.MAGENTA, record.name, Color.RESET)
|
|
296
|
+
# 添加毫秒信息用于ERROR级别格式
|
|
297
|
+
else:
|
|
298
|
+
record.colored_levelname = record.levelname
|
|
299
|
+
record.colored_name = record.name
|
|
300
|
+
|
|
301
|
+
# 根据记录级别选择格式
|
|
302
|
+
level_name = record.levelname
|
|
303
|
+
formatter = self._formatters.get(level_name, self._default_formatter)
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
return formatter.format(record)
|
|
307
|
+
except Exception as e:
|
|
308
|
+
warnings.warn("日志格式化错误: %s" % e)
|
|
309
|
+
# 使用默认格式作为备选
|
|
310
|
+
return self._default_formatter.format(record)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _get_valid_log_level(level_name: str, default: str) -> int:
|
|
314
|
+
"""验证并获取有效的日志级别"""
|
|
315
|
+
level = getattr(logging, level_name.upper(), None)
|
|
316
|
+
if not isinstance(level, int):
|
|
317
|
+
warnings.warn(
|
|
318
|
+
"Invalid log level: %s, using %s instead." % (level_name, default)
|
|
319
|
+
)
|
|
320
|
+
return getattr(logging, default)
|
|
321
|
+
return level
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _resolve_log_file(log_dir: Path, file_name: str) -> Path:
|
|
325
|
+
"""解析日志文件路径,并阻止文件逃逸出配置目录."""
|
|
326
|
+
if not file_name or Path(file_name).is_absolute():
|
|
327
|
+
raise ValueError("日志文件名必须是 LOG_FILE_PATH 下的非空相对路径")
|
|
328
|
+
|
|
329
|
+
resolved_dir = log_dir.resolve()
|
|
330
|
+
resolved_file = (resolved_dir / file_name).resolve()
|
|
331
|
+
try:
|
|
332
|
+
resolved_file.relative_to(resolved_dir)
|
|
333
|
+
except ValueError as exc:
|
|
334
|
+
raise ValueError("日志文件必须位于 LOG_FILE_PATH 配置目录内") from exc
|
|
335
|
+
if resolved_file == resolved_dir:
|
|
336
|
+
raise ValueError("日志文件名不能指向 LOG_FILE_PATH 目录本身")
|
|
337
|
+
return resolved_file
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _load_redirect_rules() -> dict[str, str]:
|
|
341
|
+
"""读取并校验日志重定向规则."""
|
|
342
|
+
redirect_rules_json = os.getenv("LOG_REDIRECT_RULES", "{}")
|
|
343
|
+
try:
|
|
344
|
+
raw_rules = json.loads(redirect_rules_json)
|
|
345
|
+
except json.JSONDecodeError:
|
|
346
|
+
warnings.warn("LOG_REDIRECT_RULES 不是有效的 JSON,将忽略重定向规则")
|
|
347
|
+
return {}
|
|
348
|
+
|
|
349
|
+
if not isinstance(raw_rules, dict):
|
|
350
|
+
warnings.warn("LOG_REDIRECT_RULES 必须是记录器名称到文件名的对象")
|
|
351
|
+
return {}
|
|
352
|
+
|
|
353
|
+
redirect_rules: dict[str, str] = {}
|
|
354
|
+
for logger_name, file_name in raw_rules.items():
|
|
355
|
+
if not isinstance(logger_name, str) or not logger_name:
|
|
356
|
+
raise ValueError("日志重定向规则中的记录器名称必须是非空字符串")
|
|
357
|
+
if not isinstance(file_name, str):
|
|
358
|
+
raise ValueError("日志重定向规则中的日志文件名必须是字符串")
|
|
359
|
+
redirect_rules[logger_name] = file_name
|
|
360
|
+
return redirect_rules
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _clear_managed_handlers() -> None:
|
|
364
|
+
"""移除并关闭上一次初始化创建的 handler."""
|
|
365
|
+
for logger, handler in _managed_handlers:
|
|
366
|
+
logger.removeHandler(handler)
|
|
367
|
+
handler.close()
|
|
368
|
+
_managed_handlers.clear()
|
|
369
|
+
|
|
370
|
+
for logger, (level, propagate) in _redirect_logger_state.items():
|
|
371
|
+
logger.setLevel(level)
|
|
372
|
+
logger.propagate = propagate
|
|
373
|
+
_redirect_logger_state.clear()
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def setup_logging(console_level: str | None = None) -> None:
|
|
377
|
+
"""显式设置日志系统,支持根据记录器名称重定向到不同文件."""
|
|
378
|
+
# 环境变量读取
|
|
379
|
+
console_level = console_level or os.getenv("LOG_LEVEL", "INFO").upper()
|
|
380
|
+
file_level = os.getenv("FILE_LOG_LEVEL", "DEBUG").upper()
|
|
381
|
+
|
|
382
|
+
# 验证并转换日志级别
|
|
383
|
+
console_log_level = _get_valid_log_level(console_level, "INFO")
|
|
384
|
+
file_log_level = _get_valid_log_level(file_level, "DEBUG")
|
|
385
|
+
|
|
386
|
+
# 文件路径配置 - 使用固定名称,不使用日期
|
|
387
|
+
log_dir = os.getenv("LOG_FILE_PATH", "./logs")
|
|
388
|
+
file_name = os.getenv("LOG_FILE_NAME", "bot.log") # 改为固定名称
|
|
389
|
+
|
|
390
|
+
# 备份数量验证
|
|
391
|
+
try:
|
|
392
|
+
backup_count = int(os.getenv("BACKUP_COUNT", "7"))
|
|
393
|
+
if backup_count < 0:
|
|
394
|
+
raise ValueError
|
|
395
|
+
except ValueError:
|
|
396
|
+
backup_count = 7
|
|
397
|
+
warnings.warn("BACKUP_COUNT 必须是非负整数,将使用默认值 7")
|
|
398
|
+
|
|
399
|
+
# 在修改现有日志配置前完成全部路径校验。
|
|
400
|
+
log_dir_path = Path(log_dir)
|
|
401
|
+
root_file_path = _resolve_log_file(log_dir_path, file_name)
|
|
402
|
+
redirect_rules = _load_redirect_rules()
|
|
403
|
+
redirect_paths = {
|
|
404
|
+
logger_name: _resolve_log_file(log_dir_path, redirect_file_name)
|
|
405
|
+
for logger_name, redirect_file_name in redirect_rules.items()
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
# ===== 1. 配置根记录器 =====
|
|
409
|
+
root_logger = logging.getLogger()
|
|
410
|
+
|
|
411
|
+
# 先创建完整的新 handler 集合;失败时保留当前日志配置。
|
|
412
|
+
file_formatter = DynamicFormatter(
|
|
413
|
+
fmt_dict=LOG_MESSAGE_FORMATS["file"],
|
|
414
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
415
|
+
use_color=False,
|
|
416
|
+
)
|
|
417
|
+
console_handler: logging.StreamHandler | None = None
|
|
418
|
+
root_file_handler: TimedRotatingFileHandler | None = None
|
|
419
|
+
redirect_handlers: dict[str, TimedRotatingFileHandler] = {}
|
|
420
|
+
try:
|
|
421
|
+
for path in {root_file_path, *redirect_paths.values()}:
|
|
422
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
423
|
+
|
|
424
|
+
set_console_mode()
|
|
425
|
+
console_handler = logging.StreamHandler()
|
|
426
|
+
console_handler.setLevel(console_log_level)
|
|
427
|
+
console_handler.setFormatter(
|
|
428
|
+
DynamicFormatter(
|
|
429
|
+
fmt_dict=LOG_MESSAGE_FORMATS["console"],
|
|
430
|
+
datefmt="%H:%M:%S",
|
|
431
|
+
use_color=True,
|
|
432
|
+
)
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
root_file_handler = TimedRotatingFileHandler(
|
|
436
|
+
filename=root_file_path,
|
|
437
|
+
when="midnight",
|
|
438
|
+
interval=1,
|
|
439
|
+
backupCount=backup_count,
|
|
440
|
+
encoding="utf-8",
|
|
441
|
+
utc=True,
|
|
442
|
+
)
|
|
443
|
+
root_file_handler.setLevel(file_log_level)
|
|
444
|
+
root_file_handler.setFormatter(file_formatter)
|
|
445
|
+
|
|
446
|
+
for logger_name, redirect_file_path in redirect_paths.items():
|
|
447
|
+
file_handler = TimedRotatingFileHandler(
|
|
448
|
+
filename=redirect_file_path,
|
|
449
|
+
when="midnight",
|
|
450
|
+
interval=1,
|
|
451
|
+
backupCount=backup_count,
|
|
452
|
+
encoding="utf-8",
|
|
453
|
+
utc=True,
|
|
454
|
+
)
|
|
455
|
+
file_handler.setLevel(file_log_level)
|
|
456
|
+
file_handler.setFormatter(file_formatter)
|
|
457
|
+
redirect_handlers[logger_name] = file_handler
|
|
458
|
+
except Exception:
|
|
459
|
+
if console_handler is not None:
|
|
460
|
+
console_handler.close()
|
|
461
|
+
if root_file_handler is not None:
|
|
462
|
+
root_file_handler.close()
|
|
463
|
+
for file_handler in redirect_handlers.values():
|
|
464
|
+
file_handler.close()
|
|
465
|
+
raise
|
|
466
|
+
|
|
467
|
+
assert console_handler is not None
|
|
468
|
+
assert root_file_handler is not None
|
|
469
|
+
|
|
470
|
+
# 所有新 handler 创建成功后再替换上一次由本模块管理的配置。
|
|
471
|
+
_clear_managed_handlers()
|
|
472
|
+
root_logger.setLevel(logging.DEBUG)
|
|
473
|
+
root_logger.handlers = [console_handler, root_file_handler]
|
|
474
|
+
_managed_handlers.extend(
|
|
475
|
+
[(root_logger, console_handler), (root_logger, root_file_handler)]
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
for logger_name, file_handler in redirect_handlers.items():
|
|
479
|
+
logger = logging.getLogger(logger_name)
|
|
480
|
+
_redirect_logger_state[logger] = (logger.level, logger.propagate)
|
|
481
|
+
logger.setLevel(file_log_level)
|
|
482
|
+
logger.addHandler(file_handler)
|
|
483
|
+
logger.propagate = False
|
|
484
|
+
_managed_handlers.append((logger, file_handler))
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def get_log(name="Logger"):
|
|
488
|
+
"""
|
|
489
|
+
获取日志记录器
|
|
490
|
+
"""
|
|
491
|
+
warnings.warn(
|
|
492
|
+
"The 'get_log' method is deprecated, use 'logging.getLogger' instead",
|
|
493
|
+
DeprecationWarning,
|
|
494
|
+
2,
|
|
495
|
+
)
|
|
496
|
+
return logging.getLogger(name)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
# 示例用法
|
|
500
|
+
if __name__ == "__main__":
|
|
501
|
+
# 获取不同记录器的日志
|
|
502
|
+
root_logger = logging.getLogger()
|
|
503
|
+
db_logger = logging.getLogger("database")
|
|
504
|
+
net_logger = logging.getLogger("network")
|
|
505
|
+
sec_logger = logging.getLogger("security")
|
|
506
|
+
setup_logging("DEBUG")
|
|
507
|
+
|
|
508
|
+
print("测试不同级别的日志输出(使用动态格式):")
|
|
509
|
+
root_logger.debug("根记录器调试信息")
|
|
510
|
+
root_logger.info("根记录器普通信息")
|
|
511
|
+
root_logger.warning("根记录器警告信息")
|
|
512
|
+
net_logger.error("网络错误: 连接超时")
|
|
513
|
+
sec_logger.critical("安全警报: 检测到异常登录尝试")
|
|
514
|
+
|
|
515
|
+
# 测试不同格式的差异
|
|
516
|
+
print("\n测试不同日志级别的格式差异:")
|
|
517
|
+
root_logger.debug("调试信息 - 包含文件名、行号和函数名")
|
|
518
|
+
root_logger.info("普通信息 - 简洁格式")
|
|
519
|
+
root_logger.warning("警告信息 - 带警告符号")
|
|
520
|
+
root_logger.error("错误信息 - 包含文件名和行号")
|
|
521
|
+
root_logger.critical("严重错误 - 包含模块名和文件名")
|