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 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:.6f}秒",
59
- 'ASYNC_FUNCTION_RETURNED': "异步函数 {func} 返回结果: {result},耗时: {duration:.6f}秒",
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:.6f}s",
74
- 'ASYNC_FUNCTION_RETURNED': "Async function {func} returned: {result}, duration: {duration:.6f}s",
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
- # 在 __init__ 方法中添加新的参数
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
- return text.format(**kwargs)
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
- """Configure logger with console, file and remote handlers.
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
- Raises:
176
- OSError: If log directory cannot be created
177
- ValueError: If invalid configuration values are provided
178
- """
179
- """
180
- 配置 Loguru 日志记录器:控制台输出、文件输出、远程日志收集、自定义日志级别。
181
- """
182
- # 移除所有现有的处理器,重新添加
183
- self.logger.remove()
184
-
185
- # 定义日志格式:可根据需要自由增减字段
186
- # 包含时间、进程 ID、线程 ID、日志级别、request_id、调用位置等
187
- # 目前去除进程 ID、线程 ID
188
- """
189
- custom_format = (
190
- "<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
191
- "<cyan>PID:{process}</cyan>/<cyan>TID:{thread}</cyan> | "
192
- "<level>{level: <8}</level> | "
193
- "ReqID:{extra[request_id]} | "
194
- "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
195
- "<level>{message}</level>"
196
- )
197
- """
198
- custom_format = (
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
- log_format = self.custom_format or custom_format
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=True,
287
+ enqueue=self.enqueue,
288
+ diagnose=self.diagnose,
289
+ backtrace=self.backtrace,
215
290
  )
216
-
217
- # 确保日志目录存在
218
- try:
219
- os.makedirs(self.log_dir, exist_ok=True)
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}_{self.file_pattern}.log"),
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=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=True,
237
- diagnose=True,
238
- backtrace=True,
303
+ enqueue=self.enqueue,
304
+ diagnose=self.diagnose,
305
+ backtrace=self.backtrace,
239
306
  )
240
-
241
- # 仅示例演示:为 ERROR 级别单独输出到文件
307
+
308
+ # 错误日志文件
242
309
  self.logger.add(
243
310
  self._get_level_log_path("error"),
244
- format=custom_format,
311
+ format=log_format,
245
312
  level="ERROR",
246
313
  rotation=f"{self.max_size} MB",
247
314
  retention=self.retention,
248
- compression="zip",
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
- if self.remote_log_url:
257
- self._configure_remote_logging()
258
-
259
- # 设置统一异常处理
260
- self.setup_exception_handler()
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
- # 修复:不直接传递 traceback 对象,而是将异常信息格式化为字符串
310
- error_msg = self._msg('UNHANDLED_EXCEPTION') if 'UNHANDLED_EXCEPTION' in self._LANG_MAP[self.language] else "未处理的异常"
311
- self.logger.opt(exception=True).error(
312
- f"{error_msg}: {exc_type.__name__}: {exc_value}"
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
- self._executor.submit(self._send_to_remote, message)
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
- log_method = getattr(self.logger, level.lower(), self.logger.error)
459
-
460
- # 获取消息,如果键不存在则使用默认消息
461
- error_msg = self._msg(msg_key) if msg_key in self._LANG_MAP[self.language] else f"发生异常: {msg_key}"
462
- error_msg += f" [{type(error).__name__}]"
463
-
464
- if trace:
465
- # 修复:避免直接传递 traceback 对象
466
- log_method(f"{error_msg}: {str(error)}")
467
- # 单独记录异常堆栈,但不使用 enqueue
468
- self.logger.opt(exception=True).error("异常堆栈:")
469
- else:
470
- log_method(f"{error_msg}: {str(error)}")
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
- if is_async:
473
- self.logger.info(self._msg('END_ASYNC_FUNCTION_CALL') if 'END_ASYNC_FUNCTION_CALL' in self._LANG_MAP[self.language] else "异步函数调用结束")
474
- else:
475
- self.logger.info(self._msg('END_FUNCTION_CALL') if 'END_FUNCTION_CALL' in self._LANG_MAP[self.language] else "函数调用结束")
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', func=func_name, args=args, kwargs=kwargs)
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', func=func_name, args=args, kwargs=kwargs)
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', func=func_name, result=result, duration=duration)
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', func=func_name, result=result, duration=duration)
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 get_stats(self):
508
- """
509
- 获取日志统计信息。
510
-
511
- Returns:
512
- str: 格式化的日志统计信息字符串
513
- """
514
- # 初始化统计数据
515
- stats = {
516
- 'total': 0,
517
- 'error': 0,
518
- 'warning': 0,
519
- 'info': 0
520
- }
521
-
522
- # 如果启用了统计功能,尝试从日志文件中获取统计数据
523
- if self.enable_stats:
524
- try:
525
- # 这里我们可以尝试从日志文件中读取统计信息
526
- # 简单起见,这里只返回一些示例数据
527
- stats['total'] = 100
528
- stats['error'] = 5
529
- stats['warning'] = 15
530
- stats['info'] = 80
531
- except Exception as e:
532
- self.logger.error(f"获取日志统计信息失败: {e}")
533
-
534
- # 返回统计信息的格式化字符串
535
- return self._msg('LOG_STATS', **stats)
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
- # - language='zh' 输出中文
549
- # - language='en' 输出英文
550
- remote_log_url = None # "https://your-logging-endpoint.com/logs"
551
- log = MyLogger("test_log", remote_log_url=remote_log_url, language='zh')
552
-
553
- @log.log_decorator("ZeroDivisionError occurred.")
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("JSONDecodeError occurred.")
814
+ @log.log_decorator("JSON解析错误", level="WARNING")
558
815
  def test_error():
559
- json.loads("asdasd")
816
+ json.loads("invalid_json")
560
817
 
561
- @log.log_decorator("Function execution took too long.")
818
+ @log.log_decorator("耗时操作", level="INFO", trace=False)
562
819
  def compute_something_sync():
563
820
  time.sleep(1)
564
- return "Sync computation completed"
821
+ return "同步计算完成"
565
822
 
566
- @log.log_decorator("Async function execution took too long.")
823
+ @log.log_decorator("异步耗时操作")
567
824
  async def compute_something_async():
568
825
  await asyncio.sleep(1)
569
- return "Async computation completed"
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
- # 设置 request_id
572
- token = log.request_id_var.set("12345")
832
+ # 设置请求ID
833
+ token = log.request_id_var.set("🦉")
573
834
 
574
835
  try:
575
- # 常见日志级别示例
576
- log.info('This is an info log.')
577
- log.debug('This is a debug log.')
578
- log.warning('This is a warning log.')
579
- log.error('This is an error log.')
580
- log.critical('This is a critical log.')
581
- log.trace('This is a TRACE level log (Loguru default).')
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("Caught a ZeroDivisionError.")
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("Caught a JSONDecodeError.")
865
+ log.exception("捕获到JSON解析错误")
596
866
 
597
867
  # 测试同步函数
598
868
  result = compute_something_sync()
599
- log.info(f"compute_something_sync result: {result}")
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(f"compute_something_async result: {result}")
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
- # 重置 request_id
894
+ # 重置请求ID
610
895
  log.request_id_var.reset(token)
611
- log.info("All done.")
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (79.0.0)
2
+ Generator: setuptools (80.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,,