xmi-logger 0.0.6__py3-none-any.whl → 0.0.8__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/__version__.py +10 -0
- xmi_logger/advanced_features.py +743 -579
- xmi_logger/xmi_logger.py +253 -171
- {xmi_logger-0.0.6.dist-info → xmi_logger-0.0.8.dist-info}/METADATA +151 -69
- xmi_logger-0.0.8.dist-info/RECORD +8 -0
- xmi_logger-0.0.6.dist-info/RECORD +0 -7
- {xmi_logger-0.0.6.dist-info → xmi_logger-0.0.8.dist-info}/WHEEL +0 -0
- {xmi_logger-0.0.6.dist-info → xmi_logger-0.0.8.dist-info}/top_level.txt +0 -0
xmi_logger/xmi_logger.py
CHANGED
|
@@ -14,12 +14,12 @@ import requests
|
|
|
14
14
|
import asyncio
|
|
15
15
|
import aiohttp
|
|
16
16
|
|
|
17
|
-
from typing import Optional, Dict, Any,
|
|
17
|
+
from typing import Optional, Dict, Any, List, Tuple
|
|
18
18
|
|
|
19
|
-
from functools import wraps
|
|
19
|
+
from functools import wraps
|
|
20
20
|
from time import perf_counter
|
|
21
21
|
from contextvars import ContextVar
|
|
22
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
22
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
23
23
|
from collections import defaultdict
|
|
24
24
|
from datetime import datetime, timedelta
|
|
25
25
|
import threading
|
|
@@ -81,14 +81,6 @@ class XmiLogger:
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
# 添加类级别的缓存,使用 LRU 缓存提高性能
|
|
85
|
-
_format_cache: Dict[str, str] = {}
|
|
86
|
-
_message_cache: Dict[str, str] = {}
|
|
87
|
-
_location_cache: Dict[str, str] = {}
|
|
88
|
-
_stats_cache: Dict[str, Any] = {}
|
|
89
|
-
_stats_cache_time = 0
|
|
90
|
-
_stats_cache_ttl = 5 # 统计缓存TTL(秒)
|
|
91
|
-
|
|
92
84
|
def __init__(
|
|
93
85
|
self,
|
|
94
86
|
file_name: str,
|
|
@@ -108,6 +100,7 @@ class XmiLogger:
|
|
|
108
100
|
cache_size: int = 128, # 新增:缓存大小配置
|
|
109
101
|
adaptive_level: bool = False, # 新增:自适应日志级别
|
|
110
102
|
performance_mode: bool = False, # 新增:性能模式
|
|
103
|
+
enable_exception_hook: bool = False,
|
|
111
104
|
) -> None:
|
|
112
105
|
"""
|
|
113
106
|
初始化日志记录器。
|
|
@@ -136,12 +129,23 @@ class XmiLogger:
|
|
|
136
129
|
self.enable_stats = enable_stats
|
|
137
130
|
self.categories = categories or []
|
|
138
131
|
self._cache_size = cache_size
|
|
132
|
+
self._format_cache: Dict[Any, str] = {}
|
|
133
|
+
self._message_cache: Dict[Any, str] = {}
|
|
134
|
+
self._location_cache: Dict[Any, str] = {}
|
|
135
|
+
self._stats_cache: Dict[str, Any] = {}
|
|
136
|
+
self._stats_cache_time = 0.0
|
|
137
|
+
self._stats_cache_ttl = 5
|
|
139
138
|
self.adaptive_level = adaptive_level
|
|
140
139
|
self.performance_mode = performance_mode
|
|
141
|
-
self.
|
|
142
|
-
self.
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
self._handler_ids: List[int] = []
|
|
141
|
+
self._removed_default_handler = False
|
|
142
|
+
self._exception_hook_enabled = bool(enable_exception_hook)
|
|
143
|
+
self._exception_hook = None
|
|
144
|
+
self._prev_excepthook = None
|
|
145
|
+
self._remote_loop = None
|
|
146
|
+
self._remote_thread = None
|
|
147
|
+
self._remote_queue = None
|
|
148
|
+
self._remote_ready = threading.Event()
|
|
145
149
|
|
|
146
150
|
# 语言选项
|
|
147
151
|
self.language = language if language in ('zh', 'en') else 'zh'
|
|
@@ -165,7 +169,11 @@ class XmiLogger:
|
|
|
165
169
|
self.backtrace = True
|
|
166
170
|
|
|
167
171
|
# 用于远程日志发送的线程池
|
|
168
|
-
self.
|
|
172
|
+
self._max_workers = max_workers
|
|
173
|
+
self._executor = None
|
|
174
|
+
if self.remote_log_url:
|
|
175
|
+
self._executor = ThreadPoolExecutor(max_workers=max_workers)
|
|
176
|
+
self._start_remote_sender()
|
|
169
177
|
|
|
170
178
|
# 初始化 Logger 配置
|
|
171
179
|
self.configure_logger()
|
|
@@ -188,18 +196,14 @@ class XmiLogger:
|
|
|
188
196
|
def _msg(self, key: str, **kwargs) -> str:
|
|
189
197
|
"""消息格式化处理,优化性能"""
|
|
190
198
|
try:
|
|
191
|
-
# 使用缓存键
|
|
192
|
-
cache_key = f"{self.language}:{key}:{hash(frozenset(kwargs.items()))}"
|
|
193
|
-
|
|
194
|
-
# 检查缓存
|
|
195
|
-
if cache_key in self._message_cache:
|
|
196
|
-
return self._message_cache[cache_key]
|
|
197
|
-
|
|
198
199
|
# 获取消息模板
|
|
199
200
|
text = self._LANG_MAP.get(self.language, {}).get(key, key)
|
|
200
201
|
|
|
201
202
|
# 如果没有参数,直接返回模板
|
|
202
203
|
if not kwargs:
|
|
204
|
+
cache_key = (self.language, key, None)
|
|
205
|
+
if cache_key in self._message_cache:
|
|
206
|
+
return self._message_cache[cache_key]
|
|
203
207
|
self._message_cache[cache_key] = text
|
|
204
208
|
return text
|
|
205
209
|
|
|
@@ -208,13 +212,23 @@ class XmiLogger:
|
|
|
208
212
|
for k, v in kwargs.items():
|
|
209
213
|
try:
|
|
210
214
|
if isinstance(v, (list, tuple)):
|
|
211
|
-
str_kwargs[k] =
|
|
215
|
+
str_kwargs[k] = tuple(str(item) for item in v)
|
|
212
216
|
elif isinstance(v, dict):
|
|
213
217
|
str_kwargs[k] = {str(kk): str(vv) for kk, vv in v.items()}
|
|
214
218
|
else:
|
|
215
219
|
str_kwargs[k] = str(v)
|
|
216
220
|
except Exception:
|
|
217
221
|
str_kwargs[k] = f"<{type(v).__name__}>"
|
|
222
|
+
|
|
223
|
+
frozen_kwargs = tuple(
|
|
224
|
+
sorted(
|
|
225
|
+
(k, tuple(sorted(v.items())) if isinstance(v, dict) else v)
|
|
226
|
+
for k, v in str_kwargs.items()
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
cache_key = (self.language, key, frozen_kwargs)
|
|
230
|
+
if cache_key in self._message_cache:
|
|
231
|
+
return self._message_cache[cache_key]
|
|
218
232
|
|
|
219
233
|
# 格式化消息
|
|
220
234
|
result = text.format(**str_kwargs)
|
|
@@ -237,11 +251,25 @@ class XmiLogger:
|
|
|
237
251
|
text = self._LANG_MAP.get(self.language, {}).get(key, key)
|
|
238
252
|
return f"{text} (格式化错误: {str(e)})"
|
|
239
253
|
|
|
254
|
+
def _remove_handlers(self) -> None:
|
|
255
|
+
handler_ids = list(self._handler_ids)
|
|
256
|
+
self._handler_ids.clear()
|
|
257
|
+
for handler_id in handler_ids:
|
|
258
|
+
try:
|
|
259
|
+
self.logger.remove(handler_id)
|
|
260
|
+
except Exception:
|
|
261
|
+
continue
|
|
262
|
+
|
|
240
263
|
def configure_logger(self) -> None:
|
|
241
264
|
"""配置日志记录器,添加错误处理和安全性检查"""
|
|
242
265
|
try:
|
|
243
|
-
|
|
244
|
-
self.
|
|
266
|
+
self._remove_handlers()
|
|
267
|
+
if not self._removed_default_handler:
|
|
268
|
+
try:
|
|
269
|
+
self.logger.remove(0)
|
|
270
|
+
except Exception:
|
|
271
|
+
pass
|
|
272
|
+
self._removed_default_handler = True
|
|
245
273
|
|
|
246
274
|
# 验证配置参数
|
|
247
275
|
self._validate_config()
|
|
@@ -263,7 +291,8 @@ class XmiLogger:
|
|
|
263
291
|
self._configure_remote_logging()
|
|
264
292
|
|
|
265
293
|
# 设置异常处理器
|
|
266
|
-
self.
|
|
294
|
+
if self._exception_hook_enabled:
|
|
295
|
+
self.setup_exception_handler()
|
|
267
296
|
|
|
268
297
|
except Exception as e:
|
|
269
298
|
# 如果配置失败,使用基本配置
|
|
@@ -315,7 +344,7 @@ class XmiLogger:
|
|
|
315
344
|
|
|
316
345
|
def _add_console_handler(self, log_format: str) -> None:
|
|
317
346
|
"""添加控制台处理器"""
|
|
318
|
-
self.logger.add(
|
|
347
|
+
handler_id = self.logger.add(
|
|
319
348
|
sys.stdout,
|
|
320
349
|
format=log_format,
|
|
321
350
|
level=self.filter_level,
|
|
@@ -323,11 +352,12 @@ class XmiLogger:
|
|
|
323
352
|
diagnose=self.diagnose,
|
|
324
353
|
backtrace=self.backtrace,
|
|
325
354
|
)
|
|
355
|
+
self._handler_ids.append(handler_id)
|
|
326
356
|
|
|
327
357
|
def _add_file_handlers(self, log_format: str) -> None:
|
|
328
358
|
"""添加文件处理器"""
|
|
329
359
|
# 主日志文件
|
|
330
|
-
self.logger.add(
|
|
360
|
+
handler_id = self.logger.add(
|
|
331
361
|
os.path.join(self.log_dir, f"{self.file_name}.log"),
|
|
332
362
|
format=log_format,
|
|
333
363
|
level=self.filter_level,
|
|
@@ -339,9 +369,10 @@ class XmiLogger:
|
|
|
339
369
|
diagnose=self.diagnose,
|
|
340
370
|
backtrace=self.backtrace,
|
|
341
371
|
)
|
|
372
|
+
self._handler_ids.append(handler_id)
|
|
342
373
|
|
|
343
374
|
# 错误日志文件
|
|
344
|
-
self.logger.add(
|
|
375
|
+
handler_id = self.logger.add(
|
|
345
376
|
self._get_level_log_path("error"),
|
|
346
377
|
format=log_format,
|
|
347
378
|
level="ERROR",
|
|
@@ -353,26 +384,29 @@ class XmiLogger:
|
|
|
353
384
|
diagnose=self.diagnose,
|
|
354
385
|
backtrace=self.backtrace,
|
|
355
386
|
)
|
|
387
|
+
self._handler_ids.append(handler_id)
|
|
356
388
|
|
|
357
389
|
def _fallback_configuration(self) -> None:
|
|
358
390
|
"""配置失败时的后备方案"""
|
|
359
|
-
self.
|
|
360
|
-
self.logger.add(
|
|
391
|
+
self._remove_handlers()
|
|
392
|
+
handler_id = self.logger.add(
|
|
361
393
|
sys.stderr,
|
|
362
394
|
format="<red>{time:YYYY-MM-DD HH:mm:ss}</red> | <level>{level: <8}</level> | <level>{message}</level>",
|
|
363
395
|
level="ERROR"
|
|
364
396
|
)
|
|
397
|
+
self._handler_ids.append(handler_id)
|
|
365
398
|
|
|
366
399
|
def _configure_remote_logging(self):
|
|
367
400
|
"""
|
|
368
401
|
配置远程日志收集。
|
|
369
402
|
"""
|
|
370
403
|
# 当远程日志收集启用时,只发送 ERROR 及以上级别的日志
|
|
371
|
-
self.logger.add(
|
|
404
|
+
handler_id = self.logger.add(
|
|
372
405
|
self.remote_sink,
|
|
373
406
|
level="ERROR",
|
|
374
407
|
enqueue=self.enqueue,
|
|
375
408
|
)
|
|
409
|
+
self._handler_ids.append(handler_id)
|
|
376
410
|
|
|
377
411
|
def log_with_tag(self, level: str, message: str, tag: str):
|
|
378
412
|
"""
|
|
@@ -383,6 +417,7 @@ class XmiLogger:
|
|
|
383
417
|
message: 日志消息
|
|
384
418
|
tag: 标签名称
|
|
385
419
|
"""
|
|
420
|
+
self._update_stats(level)
|
|
386
421
|
log_method = getattr(self.logger, level.lower(), self.logger.info)
|
|
387
422
|
tagged_message = self._msg('LOG_TAGGED', tag=tag, message=message)
|
|
388
423
|
log_method(tagged_message)
|
|
@@ -396,6 +431,7 @@ class XmiLogger:
|
|
|
396
431
|
message: 日志消息
|
|
397
432
|
category: 分类名称
|
|
398
433
|
"""
|
|
434
|
+
self._update_stats(level, category=category)
|
|
399
435
|
log_method = getattr(self.logger, level.lower(), self.logger.info)
|
|
400
436
|
categorized_message = self._msg('LOG_CATEGORY', category=category, message=message)
|
|
401
437
|
log_method(categorized_message)
|
|
@@ -452,6 +488,9 @@ class XmiLogger:
|
|
|
452
488
|
# 如果格式化失败,使用最基本的错误记录
|
|
453
489
|
self.logger.opt(exception=True).error(f"未处理的异常: {exc_type.__name__}")
|
|
454
490
|
|
|
491
|
+
if self._prev_excepthook is None:
|
|
492
|
+
self._prev_excepthook = sys.excepthook
|
|
493
|
+
self._exception_hook = exception_handler
|
|
455
494
|
sys.excepthook = exception_handler
|
|
456
495
|
|
|
457
496
|
def _get_level_log_path(self, level_name):
|
|
@@ -468,117 +507,136 @@ class XmiLogger:
|
|
|
468
507
|
log_file = f"{log_level}.log"
|
|
469
508
|
return os.path.join(self.log_dir, log_file)
|
|
470
509
|
|
|
471
|
-
def
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
510
|
+
def _start_remote_sender(self) -> None:
|
|
511
|
+
if self._remote_thread and self._remote_thread.is_alive():
|
|
512
|
+
return
|
|
513
|
+
|
|
514
|
+
def remote_thread_target():
|
|
515
|
+
loop = asyncio.new_event_loop()
|
|
516
|
+
asyncio.set_event_loop(loop)
|
|
517
|
+
queue = asyncio.Queue()
|
|
518
|
+
|
|
519
|
+
async def remote_worker():
|
|
520
|
+
connector = aiohttp.TCPConnector(limit=10, limit_per_host=5)
|
|
521
|
+
timeout = aiohttp.ClientTimeout(total=5)
|
|
522
|
+
session = aiohttp.ClientSession(connector=connector, timeout=timeout)
|
|
475
523
|
try:
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
524
|
+
while True:
|
|
525
|
+
payload = await queue.get()
|
|
526
|
+
if payload is None:
|
|
527
|
+
queue.task_done()
|
|
528
|
+
break
|
|
529
|
+
try:
|
|
530
|
+
await self._post_remote_payload(session, payload)
|
|
531
|
+
finally:
|
|
532
|
+
queue.task_done()
|
|
533
|
+
finally:
|
|
534
|
+
try:
|
|
535
|
+
await session.close()
|
|
536
|
+
except Exception:
|
|
537
|
+
pass
|
|
538
|
+
loop.stop()
|
|
539
|
+
|
|
540
|
+
self._remote_loop = loop
|
|
541
|
+
self._remote_queue = queue
|
|
542
|
+
self._remote_ready.set()
|
|
543
|
+
loop.create_task(remote_worker())
|
|
544
|
+
loop.run_forever()
|
|
545
|
+
try:
|
|
546
|
+
loop.close()
|
|
547
|
+
except Exception:
|
|
548
|
+
pass
|
|
549
|
+
|
|
550
|
+
self._remote_ready.clear()
|
|
551
|
+
self._remote_thread = threading.Thread(target=remote_thread_target, daemon=True)
|
|
552
|
+
self._remote_thread.start()
|
|
553
|
+
self._remote_ready.wait(timeout=2)
|
|
554
|
+
|
|
555
|
+
async def _post_remote_payload(self, session: aiohttp.ClientSession, payload: Dict[str, Any]) -> None:
|
|
556
|
+
for attempt in range(3):
|
|
557
|
+
try:
|
|
558
|
+
async with session.post(
|
|
559
|
+
self.remote_log_url,
|
|
560
|
+
json=payload,
|
|
561
|
+
headers={"Content-Type": "application/json"},
|
|
562
|
+
) as response:
|
|
563
|
+
response.raise_for_status()
|
|
564
|
+
return
|
|
565
|
+
except Exception as e:
|
|
566
|
+
if attempt == 2:
|
|
567
|
+
self.logger.warning(self._msg('FAILED_REMOTE', error=f"最终尝试失败: {e}"))
|
|
568
|
+
return
|
|
569
|
+
await asyncio.sleep(1 * (attempt + 1))
|
|
570
|
+
|
|
571
|
+
def _build_remote_payload(self, message: Any) -> Dict[str, Any]:
|
|
487
572
|
log_entry = message.record
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
# 优化文件路径处理
|
|
573
|
+
try:
|
|
574
|
+
time_str = log_entry["time"].strftime("%Y-%m-%d %H:%M:%S")
|
|
575
|
+
except Exception:
|
|
576
|
+
time_str = str(log_entry.get("time"))
|
|
577
|
+
|
|
495
578
|
file_path = ""
|
|
496
|
-
|
|
579
|
+
file_obj = log_entry.get("file")
|
|
580
|
+
if file_obj:
|
|
497
581
|
try:
|
|
498
|
-
file_path = os.path.basename(
|
|
499
|
-
except
|
|
500
|
-
file_path = str(
|
|
501
|
-
|
|
502
|
-
|
|
582
|
+
file_path = os.path.basename(file_obj.path)
|
|
583
|
+
except Exception:
|
|
584
|
+
file_path = str(file_obj)
|
|
585
|
+
|
|
586
|
+
return {
|
|
503
587
|
"time": time_str,
|
|
504
|
-
"level":
|
|
505
|
-
"message": log_entry
|
|
588
|
+
"level": getattr(log_entry.get("level"), "name", str(log_entry.get("level"))),
|
|
589
|
+
"message": log_entry.get("message", ""),
|
|
506
590
|
"file": file_path,
|
|
507
|
-
"line": log_entry
|
|
508
|
-
"function": log_entry
|
|
509
|
-
"request_id": request_id
|
|
591
|
+
"line": log_entry.get("line"),
|
|
592
|
+
"function": log_entry.get("function"),
|
|
593
|
+
"request_id": log_entry.get("extra", {}).get("request_id", "no-request-id"),
|
|
510
594
|
}
|
|
511
|
-
|
|
512
|
-
# 使用连接池优化网络请求
|
|
513
|
-
connector = aiohttp.TCPConnector(limit=10, limit_per_host=5)
|
|
514
|
-
timeout = aiohttp.ClientTimeout(total=5)
|
|
515
|
-
|
|
516
|
-
async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
|
|
517
|
-
for attempt in range(3):
|
|
518
|
-
try:
|
|
519
|
-
async with session.post(
|
|
520
|
-
self.remote_log_url,
|
|
521
|
-
json=payload,
|
|
522
|
-
headers={"Content-Type": "application/json"}
|
|
523
|
-
) as response:
|
|
524
|
-
response.raise_for_status()
|
|
525
|
-
return
|
|
526
|
-
except Exception as e:
|
|
527
|
-
if attempt == 2:
|
|
528
|
-
self.logger.warning(
|
|
529
|
-
self._msg('FAILED_REMOTE', error=f"最终尝试失败: {e}")
|
|
530
|
-
)
|
|
531
|
-
await asyncio.sleep(1 * (attempt + 1))
|
|
532
|
-
|
|
595
|
+
|
|
533
596
|
def remote_sink(self, message):
|
|
534
|
-
|
|
535
|
-
if self.
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
597
|
+
payload = self._build_remote_payload(message)
|
|
598
|
+
if self._remote_loop and self._remote_queue and self._remote_ready.is_set():
|
|
599
|
+
try:
|
|
600
|
+
self._remote_loop.call_soon_threadsafe(self._remote_queue.put_nowait, payload)
|
|
601
|
+
return
|
|
602
|
+
except Exception:
|
|
603
|
+
pass
|
|
604
|
+
if self._executor is None:
|
|
605
|
+
self._executor = ThreadPoolExecutor(max_workers=self._max_workers)
|
|
606
|
+
self._executor.submit(self._send_payload_sync, payload)
|
|
539
607
|
|
|
540
|
-
def
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
"time": log_entry["time"].strftime("%Y-%m-%d %H:%M:%S"),
|
|
555
|
-
"level": log_entry["level"].name,
|
|
556
|
-
"message": log_entry["message"],
|
|
557
|
-
"file": os.path.basename(log_entry["file"].path) if log_entry["file"] else "",
|
|
558
|
-
"line": log_entry["line"],
|
|
559
|
-
"function": log_entry["function"],
|
|
560
|
-
"request_id": log_entry["extra"].get("request_id", "no-request-id")
|
|
561
|
-
}
|
|
562
|
-
headers = {"Content-Type": "application/json"}
|
|
608
|
+
def _stop_remote_sender(self) -> None:
|
|
609
|
+
loop = self._remote_loop
|
|
610
|
+
queue = self._remote_queue
|
|
611
|
+
thread = self._remote_thread
|
|
612
|
+
if loop and queue and thread and thread.is_alive():
|
|
613
|
+
try:
|
|
614
|
+
loop.call_soon_threadsafe(queue.put_nowait, None)
|
|
615
|
+
except Exception:
|
|
616
|
+
pass
|
|
617
|
+
thread.join(timeout=2)
|
|
618
|
+
self._remote_loop = None
|
|
619
|
+
self._remote_queue = None
|
|
620
|
+
self._remote_thread = None
|
|
621
|
+
self._remote_ready.clear()
|
|
563
622
|
|
|
623
|
+
def _send_payload_sync(self, payload: Dict[str, Any]) -> None:
|
|
624
|
+
headers = {"Content-Type": "application/json"}
|
|
564
625
|
max_retries = 3
|
|
565
|
-
retry_delay = 1
|
|
566
|
-
|
|
626
|
+
retry_delay = 1
|
|
567
627
|
for attempt in range(max_retries):
|
|
568
628
|
try:
|
|
569
629
|
response = requests.post(
|
|
570
630
|
self.remote_log_url,
|
|
571
631
|
headers=headers,
|
|
572
632
|
json=payload,
|
|
573
|
-
timeout=5
|
|
633
|
+
timeout=5,
|
|
574
634
|
)
|
|
575
635
|
response.raise_for_status()
|
|
576
636
|
return
|
|
577
637
|
except requests.RequestException as e:
|
|
578
|
-
if attempt == max_retries - 1:
|
|
579
|
-
self.logger.warning(
|
|
580
|
-
self._msg('FAILED_REMOTE', error=f"Final attempt failed: {e}")
|
|
581
|
-
)
|
|
638
|
+
if attempt == max_retries - 1:
|
|
639
|
+
self.logger.warning(self._msg('FAILED_REMOTE', error=f"Final attempt failed: {e}"))
|
|
582
640
|
else:
|
|
583
641
|
time.sleep(retry_delay * (attempt + 1))
|
|
584
642
|
|
|
@@ -608,6 +666,34 @@ class XmiLogger:
|
|
|
608
666
|
"""
|
|
609
667
|
return getattr(self.logger, level)
|
|
610
668
|
|
|
669
|
+
def log(self, level: str, message: str, *args, **kwargs):
|
|
670
|
+
self._update_stats(level)
|
|
671
|
+
return self.logger.log(level, message, *args, **kwargs)
|
|
672
|
+
|
|
673
|
+
def debug(self, message: str, *args, **kwargs):
|
|
674
|
+
self._update_stats("DEBUG")
|
|
675
|
+
return self.logger.debug(message, *args, **kwargs)
|
|
676
|
+
|
|
677
|
+
def info(self, message: str, *args, **kwargs):
|
|
678
|
+
self._update_stats("INFO")
|
|
679
|
+
return self.logger.info(message, *args, **kwargs)
|
|
680
|
+
|
|
681
|
+
def warning(self, message: str, *args, **kwargs):
|
|
682
|
+
self._update_stats("WARNING")
|
|
683
|
+
return self.logger.warning(message, *args, **kwargs)
|
|
684
|
+
|
|
685
|
+
def error(self, message: str, *args, **kwargs):
|
|
686
|
+
self._update_stats("ERROR")
|
|
687
|
+
return self.logger.error(message, *args, **kwargs)
|
|
688
|
+
|
|
689
|
+
def critical(self, message: str, *args, **kwargs):
|
|
690
|
+
self._update_stats("CRITICAL")
|
|
691
|
+
return self.logger.critical(message, *args, **kwargs)
|
|
692
|
+
|
|
693
|
+
def exception(self, message: str, *args, **kwargs):
|
|
694
|
+
self._update_stats("ERROR")
|
|
695
|
+
return self.logger.exception(message, *args, **kwargs)
|
|
696
|
+
|
|
611
697
|
def log_decorator(self, msg: Optional[str] = None, level: str = "ERROR", trace: bool = True):
|
|
612
698
|
"""
|
|
613
699
|
增强版日志装饰器,支持自定义日志级别和跟踪配置
|
|
@@ -815,9 +901,8 @@ class XmiLogger:
|
|
|
815
901
|
self._stats['last_error_time'] = datetime.now()
|
|
816
902
|
|
|
817
903
|
# 计算错误率
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
self._stats['error_rate'] = self._stats['error'] / total_time
|
|
904
|
+
if self._stats['total'] > 0:
|
|
905
|
+
self._stats['error_rate'] = self._stats['error'] / self._stats['total']
|
|
821
906
|
|
|
822
907
|
def get_stats(self) -> Dict[str, Any]:
|
|
823
908
|
"""获取详细的日志统计信息,优化性能"""
|
|
@@ -903,47 +988,30 @@ class XmiLogger:
|
|
|
903
988
|
|
|
904
989
|
def get_current_location(self) -> str:
|
|
905
990
|
"""获取当前调用位置信息,优化性能"""
|
|
906
|
-
import inspect
|
|
907
|
-
import threading
|
|
908
|
-
|
|
909
|
-
# 使用线程本地缓存
|
|
910
|
-
thread_id = threading.get_ident()
|
|
911
|
-
cache_key = f"location_{thread_id}"
|
|
912
|
-
|
|
913
|
-
# 检查缓存
|
|
914
|
-
if cache_key in self._location_cache:
|
|
915
|
-
return self._location_cache[cache_key]
|
|
916
|
-
|
|
917
|
-
frame = inspect.currentframe()
|
|
918
991
|
try:
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
function = caller.function
|
|
927
|
-
location = f"{filename}:{lineno}:{function}"
|
|
928
|
-
|
|
929
|
-
# 缓存结果
|
|
930
|
-
self._location_cache[cache_key] = location
|
|
931
|
-
|
|
932
|
-
# 限制缓存大小
|
|
933
|
-
if len(self._location_cache) > self._cache_size:
|
|
934
|
-
# 清除最旧的缓存项
|
|
935
|
-
oldest_key = next(iter(self._location_cache))
|
|
936
|
-
del self._location_cache[oldest_key]
|
|
937
|
-
|
|
938
|
-
return location
|
|
939
|
-
else:
|
|
992
|
+
frame = inspect.currentframe()
|
|
993
|
+
if not frame:
|
|
994
|
+
return "未知位置"
|
|
995
|
+
caller = frame.f_back
|
|
996
|
+
if caller and caller.f_code.co_name == "log_with_location":
|
|
997
|
+
caller = caller.f_back
|
|
998
|
+
if not caller:
|
|
940
999
|
return "未知位置"
|
|
1000
|
+
filename = caller.f_code.co_filename
|
|
1001
|
+
lineno = caller.f_lineno
|
|
1002
|
+
function = caller.f_code.co_name
|
|
1003
|
+
return f"{filename}:{lineno}:{function}"
|
|
1004
|
+
except Exception:
|
|
1005
|
+
return "未知位置"
|
|
941
1006
|
finally:
|
|
942
|
-
|
|
943
|
-
|
|
1007
|
+
try:
|
|
1008
|
+
del frame
|
|
1009
|
+
except Exception:
|
|
1010
|
+
pass
|
|
944
1011
|
|
|
945
1012
|
def log_with_location(self, level: str, message: str, include_location: bool = True):
|
|
946
1013
|
"""带位置信息的日志记录"""
|
|
1014
|
+
self._update_stats(level)
|
|
947
1015
|
if include_location:
|
|
948
1016
|
location = self.get_current_location()
|
|
949
1017
|
full_message = f"[{location}] {message}"
|
|
@@ -1002,8 +1070,7 @@ class XmiLogger:
|
|
|
1002
1070
|
elif category:
|
|
1003
1071
|
self.log_with_category(level, message, category)
|
|
1004
1072
|
else:
|
|
1005
|
-
|
|
1006
|
-
log_method(message)
|
|
1073
|
+
self.log(level, message)
|
|
1007
1074
|
|
|
1008
1075
|
async def async_batch_log(self, logs: List[Dict[str, Any]]) -> None:
|
|
1009
1076
|
"""异步批量记录日志"""
|
|
@@ -1018,14 +1085,14 @@ class XmiLogger:
|
|
|
1018
1085
|
elif category:
|
|
1019
1086
|
self.log_with_category(level, message, category)
|
|
1020
1087
|
else:
|
|
1021
|
-
|
|
1022
|
-
log_method(message)
|
|
1088
|
+
self.log(level, message)
|
|
1023
1089
|
|
|
1024
1090
|
# 小延迟避免阻塞
|
|
1025
1091
|
await asyncio.sleep(0.001)
|
|
1026
1092
|
|
|
1027
1093
|
def log_with_context(self, level: str, message: str, context: Dict[str, Any] = None):
|
|
1028
1094
|
"""带上下文的日志记录"""
|
|
1095
|
+
self._update_stats(level)
|
|
1029
1096
|
if context:
|
|
1030
1097
|
context_str = " | ".join([f"{k}={v}" for k, v in context.items()])
|
|
1031
1098
|
full_message = f"{message} | {context_str}"
|
|
@@ -1037,6 +1104,7 @@ class XmiLogger:
|
|
|
1037
1104
|
|
|
1038
1105
|
def log_with_timing(self, level: str, message: str, timing_data: Dict[str, float]):
|
|
1039
1106
|
"""带计时信息的日志记录"""
|
|
1107
|
+
self._update_stats(level)
|
|
1040
1108
|
timing_str = " | ".join([f"{k}={v:.3f}s" for k, v in timing_data.items()])
|
|
1041
1109
|
full_message = f"{message} | {timing_str}"
|
|
1042
1110
|
|
|
@@ -1075,8 +1143,7 @@ class XmiLogger:
|
|
|
1075
1143
|
def _update_logger_level(self) -> None:
|
|
1076
1144
|
"""更新日志记录器级别"""
|
|
1077
1145
|
try:
|
|
1078
|
-
|
|
1079
|
-
self.logger.remove()
|
|
1146
|
+
self._remove_handlers()
|
|
1080
1147
|
# 重新配置日志记录器
|
|
1081
1148
|
self.configure_logger()
|
|
1082
1149
|
except Exception as e:
|
|
@@ -1300,13 +1367,28 @@ class XmiLogger:
|
|
|
1300
1367
|
|
|
1301
1368
|
def cleanup(self) -> None:
|
|
1302
1369
|
"""清理资源"""
|
|
1303
|
-
# 如果有聚合器,停止
|
|
1304
1370
|
if hasattr(self, 'aggregator'):
|
|
1305
1371
|
self.aggregator.stop()
|
|
1306
|
-
|
|
1372
|
+
try:
|
|
1373
|
+
self._stop_remote_sender()
|
|
1374
|
+
except Exception:
|
|
1375
|
+
pass
|
|
1376
|
+
if self._exception_hook and self._prev_excepthook and sys.excepthook is self._exception_hook:
|
|
1377
|
+
try:
|
|
1378
|
+
sys.excepthook = self._prev_excepthook
|
|
1379
|
+
except Exception:
|
|
1380
|
+
pass
|
|
1381
|
+
if self._executor is not None:
|
|
1382
|
+
try:
|
|
1383
|
+
self._executor.shutdown(wait=False, cancel_futures=True)
|
|
1384
|
+
except Exception:
|
|
1385
|
+
try:
|
|
1386
|
+
self._executor.shutdown(wait=False)
|
|
1387
|
+
except Exception:
|
|
1388
|
+
pass
|
|
1389
|
+
self._executor = None
|
|
1390
|
+
self._remove_handlers()
|
|
1307
1391
|
self.clear_caches()
|
|
1308
|
-
# 强制垃圾回收
|
|
1309
1392
|
import gc
|
|
1310
1393
|
gc.collect()
|
|
1311
1394
|
print("XmiLogger 资源清理完成")
|
|
1312
|
-
|