xmi-logger 0.0.1__py3-none-any.whl → 0.0.2__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.
- xmi_logger/xmi_logger.py +463 -176
- xmi_logger-0.0.2.dist-info/METADATA +211 -0
- xmi_logger-0.0.2.dist-info/RECORD +6 -0
- {xmi_logger-0.0.1.dist-info → xmi_logger-0.0.2.dist-info}/WHEEL +1 -1
- xmi_logger-0.0.1.dist-info/METADATA +0 -221
- xmi_logger-0.0.1.dist-info/RECORD +0 -6
- {xmi_logger-0.0.1.dist-info → xmi_logger-0.0.2.dist-info}/top_level.txt +0 -0
xmi_logger/xmi_logger.py
CHANGED
|
@@ -10,13 +10,18 @@ import os
|
|
|
10
10
|
import sys
|
|
11
11
|
import inspect
|
|
12
12
|
import requests
|
|
13
|
+
import asyncio
|
|
14
|
+
import aiohttp
|
|
13
15
|
|
|
14
|
-
from typing import Optional
|
|
16
|
+
from typing import Optional, Dict, Any, Union, List, Tuple
|
|
15
17
|
|
|
16
|
-
from functools import wraps
|
|
18
|
+
from functools import wraps, lru_cache
|
|
17
19
|
from time import perf_counter
|
|
18
20
|
from contextvars import ContextVar
|
|
19
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
21
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
22
|
+
from collections import defaultdict
|
|
23
|
+
from datetime import datetime, timedelta
|
|
24
|
+
import threading
|
|
20
25
|
|
|
21
26
|
from loguru import logger
|
|
22
27
|
|
|
@@ -55,8 +60,8 @@ class XmiLogger:
|
|
|
55
60
|
'END_ASYNC_FUNCTION_CALL': "结束异步函数调用",
|
|
56
61
|
'CALLING_FUNCTION': "调用函数: {func},参数: {args},关键字参数: {kwargs}",
|
|
57
62
|
'CALLING_ASYNC_FUNCTION': "调用异步函数: {func},参数: {args},关键字参数: {kwargs}",
|
|
58
|
-
'FUNCTION_RETURNED': "函数 {func} 返回结果: {result},耗时: {duration
|
|
59
|
-
'ASYNC_FUNCTION_RETURNED': "异步函数 {func} 返回结果: {result},耗时: {duration
|
|
63
|
+
'FUNCTION_RETURNED': "函数 {func} 返回结果: {result},耗时: {duration}秒",
|
|
64
|
+
'ASYNC_FUNCTION_RETURNED': "异步函数 {func} 返回结果: {result},耗时: {duration}秒",
|
|
60
65
|
},
|
|
61
66
|
'en': {
|
|
62
67
|
'LOG_STATS': "Log statistics: Total {total}, Errors {error}, Warnings {warning}, Info {info}",
|
|
@@ -70,12 +75,15 @@ class XmiLogger:
|
|
|
70
75
|
'END_ASYNC_FUNCTION_CALL': "Ending async function call",
|
|
71
76
|
'CALLING_FUNCTION': "Calling function: {func}, args: {args}, kwargs: {kwargs}",
|
|
72
77
|
'CALLING_ASYNC_FUNCTION': "Calling async function: {func}, args: {args}, kwargs: {kwargs}",
|
|
73
|
-
'FUNCTION_RETURNED': "Function {func} returned: {result}, duration: {duration
|
|
74
|
-
'ASYNC_FUNCTION_RETURNED': "Async function {func} returned: {result}, duration: {duration
|
|
78
|
+
'FUNCTION_RETURNED': "Function {func} returned: {result}, duration: {duration}s",
|
|
79
|
+
'ASYNC_FUNCTION_RETURNED': "Async function {func} returned: {result}, duration: {duration}s",
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
#
|
|
83
|
+
# 添加类级别的缓存
|
|
84
|
+
_format_cache: Dict[str, str] = {}
|
|
85
|
+
_message_cache: Dict[str, str] = {}
|
|
86
|
+
|
|
79
87
|
def __init__(
|
|
80
88
|
self,
|
|
81
89
|
file_name: str,
|
|
@@ -90,9 +98,9 @@ class XmiLogger:
|
|
|
90
98
|
custom_format: Optional[str] = None, # 新增:自定义日志格式
|
|
91
99
|
filter_level: str = "DEBUG", # 新增:日志过滤级别
|
|
92
100
|
compression: str = "zip", # 新增:压缩格式,支持 zip, gz, tar
|
|
93
|
-
file_pattern: str = "{time:YYYY-MM-DD}", # 新增:文件命名模式
|
|
94
101
|
enable_stats: bool = False, # 新增:是否启用日志统计
|
|
95
102
|
categories: Optional[list] = None, # 新增:日志分类列表
|
|
103
|
+
cache_size: int = 128, # 新增:缓存大小配置
|
|
96
104
|
) -> None:
|
|
97
105
|
"""
|
|
98
106
|
初始化日志记录器。
|
|
@@ -118,9 +126,13 @@ class XmiLogger:
|
|
|
118
126
|
self.custom_format = custom_format
|
|
119
127
|
self.filter_level = filter_level
|
|
120
128
|
self.compression = compression
|
|
121
|
-
self.file_pattern = file_pattern
|
|
122
129
|
self.enable_stats = enable_stats
|
|
123
130
|
self.categories = categories or []
|
|
131
|
+
self._cache_size = cache_size
|
|
132
|
+
self._async_queue = asyncio.Queue() if remote_log_url else None
|
|
133
|
+
self._remote_task = None
|
|
134
|
+
if self._async_queue:
|
|
135
|
+
self._start_remote_worker()
|
|
124
136
|
|
|
125
137
|
# 语言选项
|
|
126
138
|
self.language = language if language in ('zh', 'en') else 'zh'
|
|
@@ -149,115 +161,172 @@ class XmiLogger:
|
|
|
149
161
|
# 初始化 Logger 配置
|
|
150
162
|
self.configure_logger()
|
|
151
163
|
|
|
164
|
+
self._stats_lock = threading.Lock()
|
|
165
|
+
self._stats = {
|
|
166
|
+
'total': 0,
|
|
167
|
+
'error': 0,
|
|
168
|
+
'warning': 0,
|
|
169
|
+
'info': 0,
|
|
170
|
+
'debug': 0,
|
|
171
|
+
'by_category': defaultdict(int),
|
|
172
|
+
'by_hour': defaultdict(int),
|
|
173
|
+
'errors': [],
|
|
174
|
+
'last_error_time': None,
|
|
175
|
+
'error_rate': 0.0
|
|
176
|
+
}
|
|
177
|
+
self._stats_start_time = datetime.now()
|
|
178
|
+
|
|
152
179
|
def _msg(self, key: str, **kwargs) -> str:
|
|
153
|
-
"""
|
|
154
|
-
根据当前语言,从 _LANG_MAP 中获取对应文本。
|
|
155
|
-
可使用 kwargs 替换字符串中的占位符。
|
|
156
|
-
|
|
157
|
-
Args:
|
|
158
|
-
key: Message key from _LANG_MAP
|
|
159
|
-
**kwargs: Format arguments for the message
|
|
160
|
-
|
|
161
|
-
Returns:
|
|
162
|
-
Formatted message string
|
|
163
|
-
"""
|
|
164
|
-
# 安全获取文本,如果键不存在则返回键名
|
|
165
|
-
text = self._LANG_MAP.get(self.language, {}).get(key, key)
|
|
180
|
+
"""消息格式化处理"""
|
|
166
181
|
try:
|
|
167
|
-
|
|
182
|
+
# 获取消息模板
|
|
183
|
+
text = self._LANG_MAP.get(self.language, {}).get(key, key)
|
|
184
|
+
|
|
185
|
+
# 将所有参数转换为字符串
|
|
186
|
+
str_kwargs = {}
|
|
187
|
+
for k, v in kwargs.items():
|
|
188
|
+
try:
|
|
189
|
+
if isinstance(v, (list, tuple)):
|
|
190
|
+
str_kwargs[k] = [str(item) for item in v]
|
|
191
|
+
elif isinstance(v, dict):
|
|
192
|
+
str_kwargs[k] = {str(kk): str(vv) for kk, vv in v.items()}
|
|
193
|
+
else:
|
|
194
|
+
str_kwargs[k] = str(v)
|
|
195
|
+
except Exception:
|
|
196
|
+
str_kwargs[k] = f"<{type(v).__name__}>"
|
|
197
|
+
|
|
198
|
+
# 格式化消息
|
|
199
|
+
return text.format(**str_kwargs)
|
|
200
|
+
|
|
168
201
|
except KeyError as e:
|
|
169
|
-
# 如果格式化失败,返回原始文本并记录警告
|
|
170
202
|
return f"{text} (格式化错误: 缺少参数 {e})"
|
|
203
|
+
except Exception as e:
|
|
204
|
+
return f"{text} (格式化错误: {str(e)})"
|
|
171
205
|
|
|
172
206
|
def configure_logger(self) -> None:
|
|
173
|
-
"""
|
|
207
|
+
"""配置日志记录器,添加错误处理和安全性检查"""
|
|
208
|
+
try:
|
|
209
|
+
# 移除所有现有的处理器
|
|
210
|
+
self.logger.remove()
|
|
211
|
+
|
|
212
|
+
# 验证配置参数
|
|
213
|
+
self._validate_config()
|
|
214
|
+
|
|
215
|
+
# 确保日志目录存在且可写
|
|
216
|
+
self._ensure_log_directory()
|
|
217
|
+
|
|
218
|
+
# 配置日志格式
|
|
219
|
+
log_format = self._get_log_format()
|
|
220
|
+
|
|
221
|
+
# 添加控制台处理器
|
|
222
|
+
self._add_console_handler(log_format)
|
|
223
|
+
|
|
224
|
+
# 添加文件处理器
|
|
225
|
+
self._add_file_handlers(log_format)
|
|
226
|
+
|
|
227
|
+
# 配置远程日志(如果启用)
|
|
228
|
+
if self.remote_log_url:
|
|
229
|
+
self._configure_remote_logging()
|
|
230
|
+
|
|
231
|
+
# 设置异常处理器
|
|
232
|
+
self.setup_exception_handler()
|
|
233
|
+
|
|
234
|
+
except Exception as e:
|
|
235
|
+
# 如果配置失败,使用基本配置
|
|
236
|
+
self._fallback_configuration()
|
|
237
|
+
raise RuntimeError(f"日志配置失败: {str(e)}")
|
|
238
|
+
|
|
239
|
+
def _validate_config(self) -> None:
|
|
240
|
+
"""验证配置参数"""
|
|
241
|
+
if not isinstance(self.max_size, int) or self.max_size <= 0:
|
|
242
|
+
raise ValueError("max_size 必须是正整数")
|
|
174
243
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
"""
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
|
|
244
|
+
if not isinstance(self.retention, str):
|
|
245
|
+
raise ValueError("retention 必须是字符串")
|
|
246
|
+
|
|
247
|
+
if self.remote_log_url and not self.remote_log_url.startswith(('http://', 'https://')):
|
|
248
|
+
raise ValueError("remote_log_url 必须是有效的 HTTP(S) URL")
|
|
249
|
+
|
|
250
|
+
if self.language not in ('zh', 'en'):
|
|
251
|
+
raise ValueError("language 必须是 'zh' 或 'en'")
|
|
252
|
+
|
|
253
|
+
if self.compression not in ('zip', 'gz', 'tar'):
|
|
254
|
+
raise ValueError("compression 必须是 'zip', 'gz' 或 'tar'")
|
|
255
|
+
|
|
256
|
+
def _ensure_log_directory(self) -> None:
|
|
257
|
+
"""确保日志目录存在且可写"""
|
|
258
|
+
try:
|
|
259
|
+
os.makedirs(self.log_dir, exist_ok=True)
|
|
260
|
+
# 测试目录是否可写
|
|
261
|
+
test_file = os.path.join(self.log_dir, '.write_test')
|
|
262
|
+
with open(test_file, 'w') as f:
|
|
263
|
+
f.write('test')
|
|
264
|
+
os.remove(test_file)
|
|
265
|
+
except (OSError, IOError) as e:
|
|
266
|
+
raise RuntimeError(f"无法创建或写入日志目录: {str(e)}")
|
|
267
|
+
|
|
268
|
+
def _get_log_format(self) -> str:
|
|
269
|
+
"""获取日志格式"""
|
|
270
|
+
if self.custom_format:
|
|
271
|
+
return self.custom_format
|
|
272
|
+
|
|
273
|
+
return (
|
|
199
274
|
"<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
|
|
200
275
|
"<level>{level: <8}</level> | "
|
|
201
276
|
"ReqID:{extra[request_id]} | "
|
|
202
277
|
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
|
|
203
278
|
"<level>{message}</level>"
|
|
204
279
|
)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
# 添加控制台处理器
|
|
280
|
+
|
|
281
|
+
def _add_console_handler(self, log_format: str) -> None:
|
|
282
|
+
"""添加控制台处理器"""
|
|
210
283
|
self.logger.add(
|
|
211
284
|
sys.stdout,
|
|
212
285
|
format=log_format,
|
|
213
286
|
level=self.filter_level,
|
|
214
|
-
enqueue=
|
|
287
|
+
enqueue=self.enqueue,
|
|
288
|
+
diagnose=self.diagnose,
|
|
289
|
+
backtrace=self.backtrace,
|
|
215
290
|
)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
except OSError as e:
|
|
221
|
-
self.logger.error(f"Failed to create log directory: {e}")
|
|
222
|
-
raise
|
|
223
|
-
|
|
224
|
-
# 设置日志轮转策略
|
|
225
|
-
rotation = self.rotation_time or f"{self.max_size} MB"
|
|
226
|
-
|
|
227
|
-
# 添加主日志文件
|
|
291
|
+
|
|
292
|
+
def _add_file_handlers(self, log_format: str) -> None:
|
|
293
|
+
"""添加文件处理器"""
|
|
294
|
+
# 主日志文件
|
|
228
295
|
self.logger.add(
|
|
229
|
-
os.path.join(self.log_dir, f"{self.file_name}
|
|
296
|
+
os.path.join(self.log_dir, f"{self.file_name}.log"),
|
|
230
297
|
format=log_format,
|
|
231
298
|
level=self.filter_level,
|
|
232
|
-
rotation=
|
|
299
|
+
rotation=self.rotation_time or f"{self.max_size} MB",
|
|
233
300
|
retention=self.retention,
|
|
234
301
|
compression=self.compression,
|
|
235
302
|
encoding='utf-8',
|
|
236
|
-
enqueue=
|
|
237
|
-
diagnose=
|
|
238
|
-
backtrace=
|
|
303
|
+
enqueue=self.enqueue,
|
|
304
|
+
diagnose=self.diagnose,
|
|
305
|
+
backtrace=self.backtrace,
|
|
239
306
|
)
|
|
240
|
-
|
|
241
|
-
#
|
|
307
|
+
|
|
308
|
+
# 错误日志文件
|
|
242
309
|
self.logger.add(
|
|
243
310
|
self._get_level_log_path("error"),
|
|
244
|
-
format=
|
|
311
|
+
format=log_format,
|
|
245
312
|
level="ERROR",
|
|
246
313
|
rotation=f"{self.max_size} MB",
|
|
247
314
|
retention=self.retention,
|
|
248
|
-
compression=
|
|
315
|
+
compression=self.compression,
|
|
249
316
|
encoding='utf-8',
|
|
250
317
|
enqueue=self.enqueue,
|
|
251
318
|
diagnose=self.diagnose,
|
|
252
319
|
backtrace=self.backtrace,
|
|
253
320
|
)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
321
|
+
|
|
322
|
+
def _fallback_configuration(self) -> None:
|
|
323
|
+
"""配置失败时的后备方案"""
|
|
324
|
+
self.logger.remove()
|
|
325
|
+
self.logger.add(
|
|
326
|
+
sys.stderr,
|
|
327
|
+
format="<red>{time:YYYY-MM-DD HH:mm:ss}</red> | <level>{level: <8}</level> | <level>{message}</level>",
|
|
328
|
+
level="ERROR"
|
|
329
|
+
)
|
|
261
330
|
|
|
262
331
|
def _configure_remote_logging(self):
|
|
263
332
|
"""
|
|
@@ -305,12 +374,22 @@ class XmiLogger:
|
|
|
305
374
|
# 允许程序被 Ctrl+C 中断
|
|
306
375
|
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
|
307
376
|
return
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
# 安全地格式化异常信息
|
|
380
|
+
error_msg = self._msg('UNHANDLED_EXCEPTION') if 'UNHANDLED_EXCEPTION' in self._LANG_MAP[self.language] else "未处理的异常"
|
|
308
381
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
382
|
+
# 安全地格式化异常值
|
|
383
|
+
exc_value_str = str(exc_value) if exc_value is not None else "None"
|
|
384
|
+
|
|
385
|
+
# 组合错误消息
|
|
386
|
+
full_error_msg = f"{error_msg}: {exc_type.__name__}: {exc_value_str}"
|
|
387
|
+
|
|
388
|
+
# 记录错误
|
|
389
|
+
self.logger.opt(exception=True).error(full_error_msg)
|
|
390
|
+
except Exception as e:
|
|
391
|
+
# 如果格式化失败,使用最基本的错误记录
|
|
392
|
+
self.logger.opt(exception=True).error(f"未处理的异常: {exc_type.__name__}")
|
|
314
393
|
|
|
315
394
|
sys.excepthook = exception_handler
|
|
316
395
|
|
|
@@ -328,11 +407,56 @@ class XmiLogger:
|
|
|
328
407
|
log_file = f"{log_level}.log"
|
|
329
408
|
return os.path.join(self.log_dir, log_file)
|
|
330
409
|
|
|
410
|
+
def _start_remote_worker(self):
|
|
411
|
+
"""启动异步远程日志工作器"""
|
|
412
|
+
async def remote_worker():
|
|
413
|
+
while True:
|
|
414
|
+
try:
|
|
415
|
+
message = await self._async_queue.get()
|
|
416
|
+
await self._send_to_remote_async(message)
|
|
417
|
+
self._async_queue.task_done()
|
|
418
|
+
except Exception as e:
|
|
419
|
+
self.logger.error(f"远程日志工作器错误: {e}")
|
|
420
|
+
await asyncio.sleep(0.1)
|
|
421
|
+
|
|
422
|
+
self._remote_task = asyncio.create_task(remote_worker())
|
|
423
|
+
|
|
424
|
+
async def _send_to_remote_async(self, message: Any) -> None:
|
|
425
|
+
"""异步发送日志到远程服务器"""
|
|
426
|
+
log_entry = message.record
|
|
427
|
+
payload = {
|
|
428
|
+
"time": log_entry["time"].strftime("%Y-%m-%d %H:%M:%S"),
|
|
429
|
+
"level": log_entry["level"].name,
|
|
430
|
+
"message": log_entry["message"],
|
|
431
|
+
"file": os.path.basename(log_entry["file"].path) if log_entry["file"] else "",
|
|
432
|
+
"line": log_entry["line"],
|
|
433
|
+
"function": log_entry["function"],
|
|
434
|
+
"request_id": log_entry["extra"].get("request_id", "no-request-id")
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async with aiohttp.ClientSession() as session:
|
|
438
|
+
for attempt in range(3):
|
|
439
|
+
try:
|
|
440
|
+
async with session.post(
|
|
441
|
+
self.remote_log_url,
|
|
442
|
+
json=payload,
|
|
443
|
+
timeout=5
|
|
444
|
+
) as response:
|
|
445
|
+
response.raise_for_status()
|
|
446
|
+
return
|
|
447
|
+
except Exception as e:
|
|
448
|
+
if attempt == 2:
|
|
449
|
+
self.logger.warning(
|
|
450
|
+
self._msg('FAILED_REMOTE', error=f"最终尝试失败: {e}")
|
|
451
|
+
)
|
|
452
|
+
await asyncio.sleep(1 * (attempt + 1))
|
|
453
|
+
|
|
331
454
|
def remote_sink(self, message):
|
|
332
|
-
"""
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
455
|
+
"""优化的远程日志处理器"""
|
|
456
|
+
if self._async_queue:
|
|
457
|
+
asyncio.create_task(self._async_queue.put(message))
|
|
458
|
+
else:
|
|
459
|
+
self._executor.submit(self._send_to_remote, message)
|
|
336
460
|
|
|
337
461
|
def _send_to_remote(self, message) -> None:
|
|
338
462
|
"""Send log message to remote server with retry logic.
|
|
@@ -455,84 +579,198 @@ class XmiLogger:
|
|
|
455
579
|
def _log_exception(self, func_name: str, error: Exception, msg_key: str,
|
|
456
580
|
level: str, trace: bool, is_async: bool):
|
|
457
581
|
"""统一的异常记录处理"""
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
582
|
+
try:
|
|
583
|
+
log_method = getattr(self.logger, level.lower(), self.logger.error)
|
|
584
|
+
|
|
585
|
+
# 安全地获取消息
|
|
586
|
+
error_msg = self._msg(msg_key) if msg_key in self._LANG_MAP[self.language] else f"发生异常: {msg_key}"
|
|
587
|
+
|
|
588
|
+
# 安全地格式化错误信息
|
|
589
|
+
error_type = type(error).__name__
|
|
590
|
+
error_value = str(error) if error is not None else "None"
|
|
591
|
+
|
|
592
|
+
# 组合错误消息
|
|
593
|
+
full_error_msg = f"{error_msg} [{error_type}]: {error_value}"
|
|
594
|
+
|
|
595
|
+
if trace:
|
|
596
|
+
# 记录错误消息
|
|
597
|
+
log_method(full_error_msg)
|
|
598
|
+
# 单独记录异常堆栈
|
|
599
|
+
self.logger.opt(exception=True).error("异常堆栈:")
|
|
600
|
+
else:
|
|
601
|
+
log_method(full_error_msg)
|
|
471
602
|
|
|
472
|
-
|
|
473
|
-
self.
|
|
474
|
-
|
|
475
|
-
|
|
603
|
+
# 记录函数调用结束
|
|
604
|
+
end_msg = self._msg('END_ASYNC_FUNCTION_CALL' if is_async else 'END_FUNCTION_CALL')
|
|
605
|
+
self.logger.info(end_msg)
|
|
606
|
+
|
|
607
|
+
except Exception as e:
|
|
608
|
+
# 如果格式化失败,使用最基本的错误记录
|
|
609
|
+
self.logger.error(f"记录异常时发生错误: {str(e)}")
|
|
610
|
+
if trace:
|
|
611
|
+
self.logger.opt(exception=True).error("原始异常堆栈:")
|
|
476
612
|
|
|
477
613
|
def _log_start(self, func_name, args, kwargs, is_async=False):
|
|
478
614
|
"""
|
|
479
615
|
记录函数调用开始的公共逻辑。
|
|
480
616
|
"""
|
|
617
|
+
def format_arg(arg):
|
|
618
|
+
try:
|
|
619
|
+
return str(arg)
|
|
620
|
+
except Exception:
|
|
621
|
+
return f"<{type(arg).__name__}>"
|
|
622
|
+
|
|
623
|
+
# 安全地格式化参数
|
|
624
|
+
args_str = [format_arg(arg) for arg in args]
|
|
625
|
+
kwargs_str = {k: format_arg(v) for k, v in kwargs.items()}
|
|
626
|
+
|
|
481
627
|
if is_async:
|
|
482
628
|
self.logger.info(self._msg('START_ASYNC_FUNCTION_CALL'))
|
|
483
629
|
self.logger.info(
|
|
484
|
-
self._msg('CALLING_ASYNC_FUNCTION',
|
|
630
|
+
self._msg('CALLING_ASYNC_FUNCTION',
|
|
631
|
+
func=func_name,
|
|
632
|
+
args=args_str,
|
|
633
|
+
kwargs=kwargs_str)
|
|
485
634
|
)
|
|
486
635
|
else:
|
|
487
636
|
self.logger.info(self._msg('START_FUNCTION_CALL'))
|
|
488
637
|
self.logger.info(
|
|
489
|
-
self._msg('CALLING_FUNCTION',
|
|
638
|
+
self._msg('CALLING_FUNCTION',
|
|
639
|
+
func=func_name,
|
|
640
|
+
args=args_str,
|
|
641
|
+
kwargs=kwargs_str)
|
|
490
642
|
)
|
|
491
643
|
|
|
492
644
|
def _log_end(self, func_name, result, duration, is_async=False):
|
|
493
645
|
"""
|
|
494
646
|
记录函数调用结束的公共逻辑。
|
|
495
647
|
"""
|
|
648
|
+
def format_result(res):
|
|
649
|
+
try:
|
|
650
|
+
return str(res)
|
|
651
|
+
except Exception:
|
|
652
|
+
return f"<{type(res).__name__}>"
|
|
653
|
+
|
|
654
|
+
# 安全地格式化结果和持续时间
|
|
655
|
+
result_str = format_result(result)
|
|
656
|
+
duration_str = f"{duration:.6f}" # 格式化持续时间为6位小数
|
|
657
|
+
|
|
496
658
|
if is_async:
|
|
497
659
|
self.logger.info(
|
|
498
|
-
self._msg('ASYNC_FUNCTION_RETURNED',
|
|
660
|
+
self._msg('ASYNC_FUNCTION_RETURNED',
|
|
661
|
+
func=func_name,
|
|
662
|
+
result=result_str,
|
|
663
|
+
duration=duration_str)
|
|
499
664
|
)
|
|
500
665
|
self.logger.info(self._msg('END_ASYNC_FUNCTION_CALL'))
|
|
501
666
|
else:
|
|
502
667
|
self.logger.info(
|
|
503
|
-
self._msg('FUNCTION_RETURNED',
|
|
668
|
+
self._msg('FUNCTION_RETURNED',
|
|
669
|
+
func=func_name,
|
|
670
|
+
result=result_str,
|
|
671
|
+
duration=duration_str)
|
|
504
672
|
)
|
|
505
673
|
self.logger.info(self._msg('END_FUNCTION_CALL'))
|
|
506
674
|
|
|
507
|
-
def
|
|
508
|
-
"""
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
'
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
675
|
+
def _update_stats(self, level: str, category: Optional[str] = None) -> None:
|
|
676
|
+
"""更新日志统计信息"""
|
|
677
|
+
if not self.enable_stats:
|
|
678
|
+
return
|
|
679
|
+
|
|
680
|
+
with self._stats_lock:
|
|
681
|
+
self._stats['total'] += 1
|
|
682
|
+
self._stats[level.lower()] += 1
|
|
683
|
+
|
|
684
|
+
if category:
|
|
685
|
+
self._stats['by_category'][category] += 1
|
|
686
|
+
|
|
687
|
+
current_hour = datetime.now().strftime('%Y-%m-%d %H:00')
|
|
688
|
+
self._stats['by_hour'][current_hour] += 1
|
|
689
|
+
|
|
690
|
+
if level.upper() == 'ERROR':
|
|
691
|
+
self._stats['errors'].append({
|
|
692
|
+
'time': datetime.now(),
|
|
693
|
+
'message': f"Error occurred at {current_hour}"
|
|
694
|
+
})
|
|
695
|
+
self._stats['last_error_time'] = datetime.now()
|
|
696
|
+
|
|
697
|
+
# 计算错误率
|
|
698
|
+
total_time = (datetime.now() - self._stats_start_time).total_seconds()
|
|
699
|
+
if total_time > 0:
|
|
700
|
+
self._stats['error_rate'] = self._stats['error'] / total_time
|
|
701
|
+
|
|
702
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
703
|
+
"""获取详细的日志统计信息"""
|
|
704
|
+
with self._stats_lock:
|
|
705
|
+
stats = {
|
|
706
|
+
'total': self._stats['total'],
|
|
707
|
+
'error': self._stats['error'],
|
|
708
|
+
'warning': self._stats['warning'],
|
|
709
|
+
'info': self._stats['info'],
|
|
710
|
+
'debug': self._stats['debug'],
|
|
711
|
+
'duration': str(datetime.now() - self._stats_start_time),
|
|
712
|
+
'by_category': dict(self._stats['by_category']),
|
|
713
|
+
'by_hour': dict(self._stats['by_hour']),
|
|
714
|
+
'error_rate': float(self._stats['error_rate']),
|
|
715
|
+
'time_since_last_error': str(datetime.now() - self._stats['last_error_time']) if self._stats['last_error_time'] else None
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
# 计算每小时的平均日志数
|
|
719
|
+
if stats['by_hour']:
|
|
720
|
+
stats['avg_logs_per_hour'] = sum(stats['by_hour'].values()) / len(stats['by_hour'])
|
|
721
|
+
|
|
722
|
+
# 获取最近的错误
|
|
723
|
+
if self._stats['errors']:
|
|
724
|
+
stats['recent_errors'] = [
|
|
725
|
+
{
|
|
726
|
+
'time': str(error['time']),
|
|
727
|
+
'message': str(error['message'])
|
|
728
|
+
}
|
|
729
|
+
for error in self._stats['errors'][-10:]
|
|
730
|
+
]
|
|
731
|
+
|
|
732
|
+
return stats
|
|
733
|
+
|
|
734
|
+
def get_stats_summary(self) -> str:
|
|
735
|
+
"""获取统计信息的摘要"""
|
|
736
|
+
stats = self.get_stats()
|
|
737
|
+
return self._msg('LOG_STATS',
|
|
738
|
+
total=stats['total'],
|
|
739
|
+
error=stats['error'],
|
|
740
|
+
warning=stats['warning'],
|
|
741
|
+
info=stats['info']
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
def get_error_trend(self) -> List[Tuple[str, int]]:
|
|
745
|
+
"""获取错误趋势数据"""
|
|
746
|
+
with self._stats_lock:
|
|
747
|
+
return sorted(
|
|
748
|
+
[(hour, count) for hour, count in self._stats['by_hour'].items()],
|
|
749
|
+
key=lambda x: x[0]
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
def get_category_distribution(self) -> Dict[str, int]:
|
|
753
|
+
"""获取日志分类分布"""
|
|
754
|
+
with self._stats_lock:
|
|
755
|
+
return dict(self._stats['by_category'])
|
|
756
|
+
|
|
757
|
+
def reset_stats(self) -> None:
|
|
758
|
+
"""重置统计信息"""
|
|
759
|
+
with self._stats_lock:
|
|
760
|
+
self._stats = {
|
|
761
|
+
'total': 0,
|
|
762
|
+
'error': 0,
|
|
763
|
+
'warning': 0,
|
|
764
|
+
'info': 0,
|
|
765
|
+
'debug': 0,
|
|
766
|
+
'by_category': defaultdict(int),
|
|
767
|
+
'by_hour': defaultdict(int),
|
|
768
|
+
'errors': [],
|
|
769
|
+
'last_error_time': None,
|
|
770
|
+
'error_rate': 0.0
|
|
771
|
+
}
|
|
772
|
+
self._stats_start_time = datetime.now()
|
|
773
|
+
|
|
536
774
|
|
|
537
775
|
|
|
538
776
|
"""
|
|
@@ -543,70 +781,119 @@ if __name__ == '__main__':
|
|
|
543
781
|
import time
|
|
544
782
|
import json
|
|
545
783
|
import asyncio
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
#
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
784
|
+
import random
|
|
785
|
+
|
|
786
|
+
# 自定义日志格式
|
|
787
|
+
custom_format = (
|
|
788
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
|
|
789
|
+
"<level>{level: <8}</level> | "
|
|
790
|
+
"ReqID:{extra[request_id]} | "
|
|
791
|
+
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
|
|
792
|
+
"<magenta>{process}</magenta> - "
|
|
793
|
+
"<level>{message}</level>"
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
# 初始化日志记录器,使用新功能
|
|
797
|
+
log = XmiLogger(
|
|
798
|
+
"test_log",
|
|
799
|
+
rotation_time="1 day", # 每天轮转
|
|
800
|
+
custom_format=custom_format, # 自定义格式
|
|
801
|
+
filter_level="DEBUG", # 日志级别
|
|
802
|
+
compression="zip", # 压缩格式
|
|
803
|
+
enable_stats=True, # 启用统计
|
|
804
|
+
categories=["api", "db", "ui"] # 日志分类
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
# 添加自定义日志级别
|
|
808
|
+
log.add_custom_level("IMPORTANT", no=25, color="<yellow>", icon="⚠️")
|
|
809
|
+
|
|
810
|
+
@log.log_decorator("除零错误", level="ERROR")
|
|
554
811
|
def test_zero_division_error(a, b):
|
|
555
812
|
return a / b
|
|
556
813
|
|
|
557
|
-
@log.log_decorator("
|
|
814
|
+
@log.log_decorator("JSON解析错误", level="WARNING")
|
|
558
815
|
def test_error():
|
|
559
|
-
json.loads("
|
|
816
|
+
json.loads("invalid_json")
|
|
560
817
|
|
|
561
|
-
@log.log_decorator("
|
|
818
|
+
@log.log_decorator("耗时操作", level="INFO", trace=False)
|
|
562
819
|
def compute_something_sync():
|
|
563
820
|
time.sleep(1)
|
|
564
|
-
return "
|
|
821
|
+
return "同步计算完成"
|
|
565
822
|
|
|
566
|
-
@log.log_decorator("
|
|
823
|
+
@log.log_decorator("异步耗时操作")
|
|
567
824
|
async def compute_something_async():
|
|
568
825
|
await asyncio.sleep(1)
|
|
569
|
-
return "
|
|
826
|
+
return "异步计算完成"
|
|
827
|
+
|
|
828
|
+
@log.log_decorator("生成随机数", level="INFO", trace=False)
|
|
829
|
+
def generate_random_number(min_val=1, max_val=100):
|
|
830
|
+
return random.randint(min_val, max_val)
|
|
570
831
|
|
|
571
|
-
#
|
|
572
|
-
token = log.request_id_var.set("
|
|
832
|
+
# 设置请求ID
|
|
833
|
+
token = log.request_id_var.set("🦉")
|
|
573
834
|
|
|
574
835
|
try:
|
|
575
|
-
#
|
|
576
|
-
|
|
577
|
-
log.
|
|
578
|
-
log.
|
|
579
|
-
log.
|
|
580
|
-
log.
|
|
581
|
-
log.
|
|
836
|
+
# 基本日志测试
|
|
837
|
+
xxx = "X"
|
|
838
|
+
log.info(f'这是一条信息日志{xxx}')
|
|
839
|
+
log.debug(f'这是一条调试日志{xxx}')
|
|
840
|
+
log.warning(f'这是一条警告日志{xxx}')
|
|
841
|
+
log.error(f'这是一条错误日志{xxx}')
|
|
842
|
+
log.critical(f'这是一条严重错误日志{xxx}')
|
|
843
|
+
|
|
844
|
+
# 使用自定义日志级别
|
|
845
|
+
log.log("IMPORTANT", "这是一条重要日志消息")
|
|
846
|
+
|
|
847
|
+
# 使用标签功能
|
|
848
|
+
log.log_with_tag("INFO", "这是带标签的日志", "FEATURE")
|
|
849
|
+
log.log_with_tag("WARNING", "这是带标签的警告", "DEPRECATED")
|
|
850
|
+
|
|
851
|
+
# 使用分类功能
|
|
852
|
+
log.log_with_category("INFO", "数据库连接成功", "db")
|
|
853
|
+
log.log_with_category("ERROR", "API请求失败", "api")
|
|
854
|
+
log.log_with_category("DEBUG", "UI组件渲染", "ui")
|
|
582
855
|
|
|
583
|
-
#
|
|
856
|
+
# 测试异常处理
|
|
584
857
|
try:
|
|
585
858
|
result = test_zero_division_error(1, 0)
|
|
586
|
-
log.info(f"test_zero_division_error result: {result}")
|
|
587
859
|
except ZeroDivisionError:
|
|
588
|
-
log.exception("
|
|
589
|
-
result = test_zero_division_error(1, 1)
|
|
860
|
+
log.exception("捕获到除零错误")
|
|
590
861
|
|
|
591
|
-
# 测试另一个示例函数
|
|
592
862
|
try:
|
|
593
863
|
result = test_error()
|
|
594
864
|
except json.JSONDecodeError:
|
|
595
|
-
log.exception("
|
|
865
|
+
log.exception("捕获到JSON解析错误")
|
|
596
866
|
|
|
597
867
|
# 测试同步函数
|
|
598
868
|
result = compute_something_sync()
|
|
599
|
-
log.info(
|
|
869
|
+
log.info('同步计算结果: {}'.format(result))
|
|
870
|
+
|
|
871
|
+
# 测试随机数生成
|
|
872
|
+
for _ in range(3):
|
|
873
|
+
num = generate_random_number(1, 1000)
|
|
874
|
+
log.info('生成的随机数: {}'.format(num))
|
|
600
875
|
|
|
601
876
|
# 测试异步函数
|
|
602
877
|
async def main():
|
|
878
|
+
# 单个异步任务
|
|
603
879
|
result = await compute_something_async()
|
|
604
|
-
log.info(
|
|
880
|
+
log.info('异步计算结果: {}'.format(result))
|
|
881
|
+
|
|
882
|
+
# 多个并发异步任务
|
|
883
|
+
tasks = [compute_something_async() for _ in range(3)]
|
|
884
|
+
results = await asyncio.gather(*tasks)
|
|
885
|
+
log.info('多任务异步结果: {}'.format(results))
|
|
605
886
|
|
|
606
887
|
asyncio.run(main())
|
|
888
|
+
|
|
889
|
+
# 输出日志统计
|
|
890
|
+
print("\n日志统计信息:")
|
|
891
|
+
print(json.dumps(log.get_stats(), indent=2, ensure_ascii=False))
|
|
607
892
|
|
|
608
893
|
finally:
|
|
609
|
-
#
|
|
894
|
+
# 重置请求ID
|
|
610
895
|
log.request_id_var.reset(token)
|
|
611
|
-
log.info("
|
|
896
|
+
log.info("测试完成")
|
|
897
|
+
|
|
612
898
|
"""
|
|
899
|
+
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xmi_logger
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: An enhanced logger based on Loguru
|
|
5
|
+
Home-page: https://github.com/wang-zhibo/xmi_logger
|
|
6
|
+
Author: gm.zhibo.wang
|
|
7
|
+
Author-email: gm.zhibo.wang@gmail.com
|
|
8
|
+
Project-URL: Bug Reports, https://github.com/wang-zhibo/xmi_logger/issues
|
|
9
|
+
Project-URL: Source, https://github.com/wang-zhibo/xmi_logger
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.6
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: loguru==0.7.3
|
|
15
|
+
Requires-Dist: requests
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: classifier
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: home-page
|
|
22
|
+
Dynamic: project-url
|
|
23
|
+
Dynamic: requires-dist
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
Dynamic: summary
|
|
26
|
+
|
|
27
|
+
# XmiLogger
|
|
28
|
+
|
|
29
|
+
基于 Loguru 的增强日志记录器,支持多语言、异步操作和高级统计功能。
|
|
30
|
+
|
|
31
|
+
## 特性
|
|
32
|
+
|
|
33
|
+
- 🚀 高性能:使用 LRU 缓存和异步处理
|
|
34
|
+
- 🌐 多语言支持:支持中文和英文日志输出
|
|
35
|
+
- 📊 高级统计:支持日志分类统计和趋势分析
|
|
36
|
+
- 🔄 异步支持:支持异步函数日志记录
|
|
37
|
+
- 📝 自定义格式:支持自定义日志格式
|
|
38
|
+
- 🔒 安全性:内置错误处理和配置验证
|
|
39
|
+
- 📦 日志轮转:支持按大小和时间轮转
|
|
40
|
+
- 🌍 远程日志:支持异步远程日志收集
|
|
41
|
+
|
|
42
|
+
## 安装
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install xmi-logger
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 快速开始
|
|
49
|
+
|
|
50
|
+
### 基本使用
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from xmi_logger import XmiLogger
|
|
54
|
+
|
|
55
|
+
# 创建日志记录器实例
|
|
56
|
+
logger = XmiLogger(
|
|
57
|
+
file_name="app",
|
|
58
|
+
log_dir="logs",
|
|
59
|
+
language="zh" # 使用中文输出
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# 记录不同级别的日志
|
|
63
|
+
logger.info("这是一条信息日志")
|
|
64
|
+
logger.warning("这是一条警告日志")
|
|
65
|
+
logger.error("这是一条错误日志")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 异步函数支持
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import asyncio
|
|
72
|
+
|
|
73
|
+
@logger.log_decorator()
|
|
74
|
+
async def async_function():
|
|
75
|
+
await asyncio.sleep(1)
|
|
76
|
+
return "异步操作完成"
|
|
77
|
+
|
|
78
|
+
# 使用异步函数
|
|
79
|
+
async def main():
|
|
80
|
+
result = await async_function()
|
|
81
|
+
logger.info(f"异步函数结果: {result}")
|
|
82
|
+
|
|
83
|
+
asyncio.run(main())
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 远程日志收集
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
logger = XmiLogger(
|
|
90
|
+
file_name="app",
|
|
91
|
+
remote_log_url="https://your-log-server.com/logs",
|
|
92
|
+
max_workers=3
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 日志统计功能
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
# 启用统计功能
|
|
100
|
+
logger = XmiLogger(
|
|
101
|
+
file_name="app",
|
|
102
|
+
enable_stats=True
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# 获取统计信息
|
|
106
|
+
stats = logger.get_stats()
|
|
107
|
+
print(logger.get_stats_summary())
|
|
108
|
+
|
|
109
|
+
# 获取错误趋势
|
|
110
|
+
error_trend = logger.get_error_trend()
|
|
111
|
+
|
|
112
|
+
# 获取分类分布
|
|
113
|
+
category_dist = logger.get_category_distribution()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 高级配置
|
|
117
|
+
|
|
118
|
+
### 完整配置示例
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
logger = XmiLogger(
|
|
122
|
+
file_name="app", # 日志文件名
|
|
123
|
+
log_dir="logs", # 日志目录
|
|
124
|
+
max_size=14, # 单个日志文件最大大小(MB)
|
|
125
|
+
retention="7 days", # 日志保留时间
|
|
126
|
+
remote_log_url=None, # 远程日志服务器URL
|
|
127
|
+
max_workers=3, # 远程日志发送线程数
|
|
128
|
+
work_type=False, # 工作模式(False为测试环境)
|
|
129
|
+
language="zh", # 日志语言(zh/en)
|
|
130
|
+
rotation_time="1 day", # 日志轮转时间
|
|
131
|
+
custom_format=None, # 自定义日志格式
|
|
132
|
+
filter_level="DEBUG", # 日志过滤级别
|
|
133
|
+
compression="zip", # 日志压缩格式
|
|
134
|
+
enable_stats=False, # 是否启用统计
|
|
135
|
+
categories=None, # 日志分类列表
|
|
136
|
+
cache_size=128 # 缓存大小
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 自定义日志格式
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
custom_format = (
|
|
144
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
|
|
145
|
+
"<level>{level: <8}</level> | "
|
|
146
|
+
"ReqID:{extra[request_id]} | "
|
|
147
|
+
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
|
|
148
|
+
"<level>{message}</level>"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
logger = XmiLogger(
|
|
152
|
+
file_name="app",
|
|
153
|
+
custom_format=custom_format
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## 主要功能
|
|
158
|
+
|
|
159
|
+
### 1. 日志记录
|
|
160
|
+
- 支持所有标准日志级别(DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
161
|
+
- 支持自定义日志级别
|
|
162
|
+
- 支持带标签和分类的日志记录
|
|
163
|
+
|
|
164
|
+
### 2. 日志管理
|
|
165
|
+
- 自动日志轮转
|
|
166
|
+
- 日志压缩
|
|
167
|
+
- 日志保留策略
|
|
168
|
+
- 多文件输出(按级别分文件)
|
|
169
|
+
|
|
170
|
+
### 3. 统计功能
|
|
171
|
+
- 日志总数统计
|
|
172
|
+
- 错误率统计
|
|
173
|
+
- 按类别统计
|
|
174
|
+
- 按时间统计
|
|
175
|
+
- 错误趋势分析
|
|
176
|
+
|
|
177
|
+
### 4. 远程日志
|
|
178
|
+
- 异步远程日志发送
|
|
179
|
+
- 自动重试机制
|
|
180
|
+
- 线程池管理
|
|
181
|
+
- 错误处理
|
|
182
|
+
|
|
183
|
+
### 5. 装饰器支持
|
|
184
|
+
- 函数执行时间记录
|
|
185
|
+
- 异常捕获和记录
|
|
186
|
+
- 支持同步和异步函数
|
|
187
|
+
|
|
188
|
+
## 错误处理
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
try:
|
|
192
|
+
logger = XmiLogger("app", log_dir="/path/to/logs")
|
|
193
|
+
except RuntimeError as e:
|
|
194
|
+
print(f"日志配置失败: {e}")
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## 注意事项
|
|
198
|
+
|
|
199
|
+
1. 确保日志目录具有写入权限
|
|
200
|
+
2. 远程日志URL必须是有效的HTTP(S)地址
|
|
201
|
+
3. 建议在生产环境中启用统计功能
|
|
202
|
+
4. 异步操作时注意正确处理异常
|
|
203
|
+
|
|
204
|
+
## 贡献
|
|
205
|
+
|
|
206
|
+
欢迎提交 Issue 和 Pull Request!
|
|
207
|
+
|
|
208
|
+
## 许可证
|
|
209
|
+
|
|
210
|
+
MIT License
|
|
211
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
xmi_logger/__init__.py,sha256=VOhz14eFSphmKeNv-r57mAvkITz5H5U1C3EaMSDjSjc,151
|
|
2
|
+
xmi_logger/xmi_logger.py,sha256=5zRWsIf08fuEtUDM8wdxSg_f9mPbh6v6h4g5wMgj2Y4,34233
|
|
3
|
+
xmi_logger-0.0.2.dist-info/METADATA,sha256=6QPYgJiVO9WHYFDZBhNxLejmOnP2tVSZuuW84j6aaqc,5030
|
|
4
|
+
xmi_logger-0.0.2.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
|
|
5
|
+
xmi_logger-0.0.2.dist-info/top_level.txt,sha256=utvT64x2C9UI6eB5aJTFpFRldWEvCk2OcZtB8NIMIeU,11
|
|
6
|
+
xmi_logger-0.0.2.dist-info/RECORD,,
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: xmi_logger
|
|
3
|
-
Version: 0.0.1
|
|
4
|
-
Summary: An enhanced logger based on Loguru
|
|
5
|
-
Home-page: https://github.com/wang-zhibo/xmi_logger
|
|
6
|
-
Author: gm.zhibo.wang
|
|
7
|
-
Author-email: gm.zhibo.wang@gmail.com
|
|
8
|
-
Project-URL: Bug Reports, https://github.com/wang-zhibo/xmi_logger/issues
|
|
9
|
-
Project-URL: Source, https://github.com/wang-zhibo/xmi_logger
|
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Operating System :: OS Independent
|
|
12
|
-
Requires-Python: >=3.6
|
|
13
|
-
Description-Content-Type: text/markdown
|
|
14
|
-
Requires-Dist: loguru==0.7.3
|
|
15
|
-
Requires-Dist: requests
|
|
16
|
-
Dynamic: author
|
|
17
|
-
Dynamic: author-email
|
|
18
|
-
Dynamic: classifier
|
|
19
|
-
Dynamic: description
|
|
20
|
-
Dynamic: description-content-type
|
|
21
|
-
Dynamic: home-page
|
|
22
|
-
Dynamic: project-url
|
|
23
|
-
Dynamic: requires-dist
|
|
24
|
-
Dynamic: requires-python
|
|
25
|
-
Dynamic: summary
|
|
26
|
-
|
|
27
|
-
## Enhanced Logger
|
|
28
|
-
|
|
29
|
-
这是一个基于 [Loguru](https://github.com/Delgan/loguru) 的扩展日志记录器,提供了一系列增强特性,包括:
|
|
30
|
-
|
|
31
|
-
- 自定义日志格式
|
|
32
|
-
- 日志轮转和保留策略
|
|
33
|
-
- 上下文信息管理(如 `request_id`)
|
|
34
|
-
- 远程日志收集(使用线程池防止阻塞)
|
|
35
|
-
- 装饰器用于记录函数调用和执行时间,支持同步/异步函数
|
|
36
|
-
- 自定义日志级别(避免与 Loguru 预定义的冲突)
|
|
37
|
-
- 统一异常处理
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
### 功能概述
|
|
42
|
-
|
|
43
|
-
1. **自定义日志格式**
|
|
44
|
-
可自由配置字段,如时间、进程/线程 ID、日志级别、请求 ID、所在文件、函数、行号等。
|
|
45
|
-
|
|
46
|
-
2. **日志轮转与保留**
|
|
47
|
-
- 支持按照文件大小、时间或文件数量进行滚动,并可自动删除过期日志。
|
|
48
|
-
- 默认使用大小轮转:单个文件超过 `max_size` MB 时自动滚动。
|
|
49
|
-
- 默认保留策略 `retention='9 days'`,可根据需要自定义。
|
|
50
|
-
|
|
51
|
-
3. **上下文管理**
|
|
52
|
-
- 使用 `ContextVar` 储存 `request_id`,可在异步环境中区分不同请求来源的日志。
|
|
53
|
-
|
|
54
|
-
4. **远程日志收集**
|
|
55
|
-
- 通过自定义处理器,使用线程池的方式将日志上报到远程服务,避免主线程阻塞。
|
|
56
|
-
- 默认仅收集 `ERROR` 及以上等级的日志。可在 `_configure_remote_logging()` 方法中自行配置。
|
|
57
|
-
|
|
58
|
-
5. **装饰器**
|
|
59
|
-
- `log_decorator` 可装饰任意同步或异步函数,自动记录:
|
|
60
|
-
- 函数调用开始
|
|
61
|
-
- 参数、返回值
|
|
62
|
-
- 函数执行耗时
|
|
63
|
-
- 异常信息(可选择是否抛出异常)
|
|
64
|
-
|
|
65
|
-
6. **自定义日志级别**
|
|
66
|
-
- 通过 `add_custom_level` 方法添加额外的日志级别(如 `AUDIT`, `SECURITY` 等),避免与已有日志级别冲突。
|
|
67
|
-
|
|
68
|
-
7. **统一异常处理**
|
|
69
|
-
- 注册全局异常处理 (`sys.excepthook`),捕获任何未处理的异常并记录。
|
|
70
|
-
|
|
71
|
-
---
|
|
72
|
-
|
|
73
|
-
### 目录结构
|
|
74
|
-
|
|
75
|
-
. ├── logs/ # 日志存放目录(默认) ├── my_logger.py # MyLogger 类源码 ├── README.md # 使用说明 └── requirements.txt # Python依赖(如有)
|
|
76
|
-
|
|
77
|
-
yaml
|
|
78
|
-
复制代码
|
|
79
|
-
|
|
80
|
-
> 其中 `logs/` 是默认日志目录,可以通过初始化时的 `log_dir` 参数修改。
|
|
81
|
-
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
### 安装
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
pip install xdeek-logger
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
### 使用示例
|
|
93
|
-
example/main.py
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
### 导入并使用
|
|
97
|
-
|
|
98
|
-
```
|
|
99
|
-
from xdeek_logger import MyLogger
|
|
100
|
-
|
|
101
|
-
"""
|
|
102
|
-
初始化日志记录器
|
|
103
|
-
可自定义:
|
|
104
|
-
- 主日志文件名 (e.g., "app_log")
|
|
105
|
-
- 日志目录 log_dir (默认 "logs")
|
|
106
|
-
- 单个日志文件体积最大值 max_size (MB)
|
|
107
|
-
- 日志保留策略 retention (e.g., "7 days")
|
|
108
|
-
- 远程日志收集地址 remote_log_url (默认 None)
|
|
109
|
-
- 线程池最大工作线程数 max_workers (默认 5)
|
|
110
|
-
"""
|
|
111
|
-
logger = MyLogger(
|
|
112
|
-
file_name="app_log",
|
|
113
|
-
log_dir="logs",
|
|
114
|
-
max_size=50,
|
|
115
|
-
retention="7 days",
|
|
116
|
-
remote_log_url=None,
|
|
117
|
-
max_workers=5,
|
|
118
|
-
language='zh' # 新增:语言选项,默认为中文
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
### 调用日志方法
|
|
124
|
-
|
|
125
|
-
```
|
|
126
|
-
"""直接使用 Loguru 的常见日志方法"""
|
|
127
|
-
logger.info("This is an info message.")
|
|
128
|
-
logger.debug("Debug details here.")
|
|
129
|
-
logger.warning("Be cautious!")
|
|
130
|
-
logger.error("An error occurred.")
|
|
131
|
-
logger.critical("Critical issue!")
|
|
132
|
-
logger.trace("This is a trace message - only if Loguru TRACE level is enabled.")
|
|
133
|
-
|
|
134
|
-
logger.log("CUSTOM_LEVEL", "A special custom message.")
|
|
135
|
-
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### 使用装饰器记录函数调用
|
|
139
|
-
|
|
140
|
-
```
|
|
141
|
-
@logger.log_decorator("A division error occurred.")
|
|
142
|
-
def divide(a, b):
|
|
143
|
-
return a / b
|
|
144
|
-
|
|
145
|
-
try:
|
|
146
|
-
result = divide(10, 0)
|
|
147
|
-
"""# 将触发 ZeroDivisionError"""
|
|
148
|
-
except ZeroDivisionError:
|
|
149
|
-
logger.exception("Handled ZeroDivisionError.")
|
|
150
|
-
|
|
151
|
-
```
|
|
152
|
-
- 此装饰器会自动在函数开始和结束时分别记录函数名、参数、返回值以及耗时。
|
|
153
|
-
- 如果出现异常,则记录 traceback 并打印自定义提示信息。
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
### 记录异步函数调用
|
|
157
|
-
|
|
158
|
-
```
|
|
159
|
-
import asyncio
|
|
160
|
-
|
|
161
|
-
@logger.log_decorator("Async function error.")
|
|
162
|
-
async def async_task():
|
|
163
|
-
await asyncio.sleep(1)
|
|
164
|
-
return "Async result"
|
|
165
|
-
|
|
166
|
-
async def main():
|
|
167
|
-
result = await async_task()
|
|
168
|
-
logger.info(f"Result: {result}")
|
|
169
|
-
|
|
170
|
-
asyncio.run(main())
|
|
171
|
-
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
### 设置和重置 request_id
|
|
175
|
-
|
|
176
|
-
```
|
|
177
|
-
"""# 设置某个上下文的 request_id"""
|
|
178
|
-
token = logger.request_id_var.set("12345")
|
|
179
|
-
|
|
180
|
-
"""# ...执行与你的请求相关的操作,所有日志都带上 request_id=12345"""
|
|
181
|
-
|
|
182
|
-
"""# 结束后重置"""
|
|
183
|
-
logger.request_id_var.reset(token)
|
|
184
|
-
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
### 远程日志收集
|
|
189
|
-
|
|
190
|
-
- 在初始化 MyLogger 时,指定 remote_log_url 即可启用远程日志上报功能:
|
|
191
|
-
|
|
192
|
-
```
|
|
193
|
-
logger = MyLogger(
|
|
194
|
-
file_name="app_log",
|
|
195
|
-
remote_log_url="https://your-logging-endpoint.com/logs"
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
### 常见问题
|
|
205
|
-
|
|
206
|
-
#### 1. 如何关闭日志多文件策略?
|
|
207
|
-
- 如果仅需要一个主日志文件,可去掉或注释掉 `_get_level_log_path()` 相关的 `logger.add(...)` 调用。
|
|
208
|
-
- 如果希望“只按级别分文件、不需要主日志文件”,可以删除对应的添加主日志文件的 `add` 调用。
|
|
209
|
-
|
|
210
|
-
#### 2. 如何自定义轮转策略(按天、按小时等)?
|
|
211
|
-
- 将 `rotation=f"{self.max_size} MB"` 改为 `rotation="1 day"`、`rotation="00:00"` 等,即可使用 Loguru 的时间轮转功能。
|
|
212
|
-
|
|
213
|
-
#### 3. 如何自定义日志输出格式?
|
|
214
|
-
- 修改 `custom_format` 变量,或在 `logger.add()` 中使用你喜欢的格式,如 **JSON** 格式、单行简洁格式等。
|
|
215
|
-
|
|
216
|
-
#### 4. 如何在函数装饰器中抛出异常?
|
|
217
|
-
- 在装饰器里捕获异常后,如果希望装饰器内不“吞掉”异常,可在 `except` 块里添加 `raise`,这样异常会继续向上传递。
|
|
218
|
-
|
|
219
|
-
#### 5. 如何在远程收集中添加鉴权信息?
|
|
220
|
-
- 在 `_send_to_remote` 方法里,可在 `headers` 中添加 `Authorization` token 或其他自定义请求头。
|
|
221
|
-
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
xmi_logger/__init__.py,sha256=VOhz14eFSphmKeNv-r57mAvkITz5H5U1C3EaMSDjSjc,151
|
|
2
|
-
xmi_logger/xmi_logger.py,sha256=Qz1V0fkZuqdFWmDUN_6BJgbsItXi-Ls9hN9iyNJkG7U,23432
|
|
3
|
-
xmi_logger-0.0.1.dist-info/METADATA,sha256=AbhFFMQaaxOO8P2k4PHhRjNoEZ7Gdy2KZsNvobPGh6g,6471
|
|
4
|
-
xmi_logger-0.0.1.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
|
5
|
-
xmi_logger-0.0.1.dist-info/top_level.txt,sha256=utvT64x2C9UI6eB5aJTFpFRldWEvCk2OcZtB8NIMIeU,11
|
|
6
|
-
xmi_logger-0.0.1.dist-info/RECORD,,
|
|
File without changes
|