tretool 0.2.1__py3-none-any.whl → 1.0.0__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.
tretool/logger.py ADDED
@@ -0,0 +1,712 @@
1
+ import sys
2
+ import os
3
+ import datetime
4
+ import traceback
5
+ import threading
6
+ import gzip
7
+ import json
8
+ from typing import (
9
+ Any, Tuple, List, Optional, Union, TextIO, Callable, Dict,
10
+ TypeVar, Generic, Type
11
+ )
12
+ from enum import Enum, auto
13
+ from pathlib import Path
14
+ from dataclasses import dataclass, asdict
15
+ from abc import ABC, abstractmethod
16
+ from queue import Queue, Empty, Full
17
+ from collections import deque
18
+ import inspect
19
+ import socket
20
+ import uuid
21
+
22
+ # 类型变量
23
+ T = TypeVar('T')
24
+
25
+ class LogLevel(Enum):
26
+ """日志级别枚举"""
27
+ DEBUG = auto()
28
+ INFO = auto()
29
+ WARNING = auto()
30
+ ERROR = auto()
31
+ CRITICAL = auto()
32
+ AUDIT = auto() # 审计日志
33
+
34
+ @classmethod
35
+ def from_string(cls, level_str: str) -> 'LogLevel':
36
+ """从字符串转换日志级别"""
37
+ try:
38
+ return cls[level_str.upper()]
39
+ except KeyError:
40
+ raise ValueError(f"无效的日志级别: {level_str}") from None
41
+
42
+ @dataclass
43
+ class LogRecord:
44
+ """日志记录数据结构"""
45
+ level: LogLevel
46
+ message: str
47
+ timestamp: str
48
+ logger_name: str
49
+ user: Optional[str] = None
50
+ module: Optional[str] = None
51
+ func_name: Optional[str] = None
52
+ lineno: Optional[int] = None
53
+ process_id: Optional[int] = None
54
+ thread_id: Optional[int] = None
55
+ hostname: Optional[str] = None
56
+ request_id: Optional[str] = None
57
+ extra: Optional[Dict[str, Any]] = None
58
+ exc_info: Optional[Tuple[Type[BaseException], BaseException, Any]] = None
59
+
60
+ class LogHandler(ABC):
61
+ """日志处理器抽象基类"""
62
+
63
+ def __init__(self, level: LogLevel = LogLevel.INFO):
64
+ self.level = level
65
+ self._lock = threading.Lock()
66
+ self._formatter = self.default_formatter
67
+
68
+ @abstractmethod
69
+ def emit(self, record: LogRecord) -> None:
70
+ """处理日志记录"""
71
+ pass
72
+
73
+ def set_level(self, level: LogLevel) -> None:
74
+ """设置处理器级别"""
75
+ with self._lock:
76
+ self.level = level
77
+
78
+ def filter(self, record: LogRecord) -> bool:
79
+ """过滤日志记录"""
80
+ return record.level.value >= self.level.value
81
+
82
+ @staticmethod
83
+ def default_formatter(record: LogRecord) -> str:
84
+ """默认日志格式化方法"""
85
+ parts = [
86
+ f"[{record.timestamp}]",
87
+ f"({record.level.name})",
88
+ f"{record.logger_name}:",
89
+ ]
90
+
91
+ if record.user:
92
+ parts.append(f"user={record.user}")
93
+
94
+ if record.module:
95
+ parts.append(f"module={record.module}")
96
+
97
+ if record.func_name:
98
+ parts.append(f"func={record.func_name}")
99
+
100
+ if record.lineno:
101
+ parts.append(f"line={record.lineno}")
102
+
103
+ parts.append(record.message)
104
+
105
+ if record.exc_info:
106
+ exc_type, exc_value, exc_tb = record.exc_info
107
+ tb_lines = traceback.format_exception(exc_type, exc_value, exc_tb)
108
+ parts.append('\n' + ''.join(tb_lines).strip())
109
+
110
+ return ' '.join(parts)
111
+
112
+ def format(self, record: LogRecord) -> str:
113
+ """格式化日志记录"""
114
+ return self.default_formatter(record)
115
+
116
+ def close(self) -> None:
117
+ """关闭处理器"""
118
+ pass
119
+
120
+ class StreamHandler(LogHandler):
121
+ """流处理器(控制台输出)"""
122
+
123
+ def __init__(
124
+ self,
125
+ stream: TextIO = sys.stdout,
126
+ level: LogLevel = LogLevel.INFO,
127
+ formatter: Optional[Callable[[LogRecord], str]] = None
128
+ ):
129
+ super().__init__(level)
130
+ self.stream = stream
131
+ if formatter is not None:
132
+ self._formatter = formatter
133
+
134
+ def emit(self, record: LogRecord) -> None:
135
+ """输出日志到流"""
136
+ if not self.filter(record):
137
+ return
138
+
139
+ formatted = self.format(record)
140
+
141
+ with self._lock:
142
+ try:
143
+ self.stream.write(formatted + '\n')
144
+ self.stream.flush()
145
+ except (ValueError, AttributeError):
146
+ # 流已关闭
147
+ pass
148
+
149
+ def close(self) -> None:
150
+ """关闭流"""
151
+ if hasattr(self.stream, 'close') and self.stream not in (sys.stdout, sys.stderr):
152
+ self.stream.close()
153
+
154
+ class FileHandler(LogHandler):
155
+ """文件处理器"""
156
+
157
+ def __init__(
158
+ self,
159
+ filename: Union[str, Path],
160
+ mode: str = 'a',
161
+ encoding: str = 'utf-8',
162
+ level: LogLevel = LogLevel.INFO,
163
+ formatter: Optional[Callable[[LogRecord], str]] = None,
164
+ delay: bool = False
165
+ ):
166
+ super().__init__(level)
167
+ self.filename = Path(filename)
168
+ self.mode = mode
169
+ self.encoding = encoding
170
+ self.delay = delay
171
+ self._file: Optional[TextIO] = None
172
+ if formatter is not None:
173
+ self._formatter = formatter
174
+
175
+ if not delay:
176
+ self._open_file()
177
+
178
+ def _open_file(self) -> None:
179
+ """打开日志文件"""
180
+ self.filename.parent.mkdir(parents=True, exist_ok=True)
181
+ self._file = open(self.filename, self.mode, encoding=self.encoding)
182
+
183
+ def emit(self, record: LogRecord) -> None:
184
+ """写入日志到文件"""
185
+ if not self.filter(record):
186
+ return
187
+
188
+ if self._file is None and self.delay:
189
+ self._open_file()
190
+
191
+ formatted = self.format(record)
192
+
193
+ with self._lock:
194
+ if self._file is not None:
195
+ try:
196
+ self._file.write(formatted + '\n')
197
+ self._file.flush()
198
+ except (OSError, ValueError):
199
+ # 文件已关闭或不可写
200
+ self._file = None
201
+
202
+ def close(self) -> None:
203
+ """关闭文件"""
204
+ if self._file is not None:
205
+ with self._lock:
206
+ if self._file is not None:
207
+ self._file.close()
208
+ self._file = None
209
+
210
+ class RotatingFileHandler(FileHandler):
211
+ """滚动文件处理器"""
212
+
213
+ def __init__(
214
+ self,
215
+ filename: Union[str, Path],
216
+ max_bytes: int = 10 * 1024 * 1024, # 10MB
217
+ backup_count: int = 5,
218
+ mode: str = 'a',
219
+ encoding: str = 'utf-8',
220
+ level: LogLevel = LogLevel.INFO,
221
+ formatter: Optional[Callable[[LogRecord], str]] = None,
222
+ delay: bool = False
223
+ ):
224
+ super().__init__(filename, mode, encoding, level, formatter, delay)
225
+ self.max_bytes = max_bytes
226
+ self.backup_count = backup_count
227
+
228
+ def emit(self, record: LogRecord) -> None:
229
+ """处理日志记录,必要时滚动文件"""
230
+ if not self.filter(record):
231
+ return
232
+
233
+ if self._file is None and self.delay:
234
+ self._open_file()
235
+
236
+ formatted = self.format(record)
237
+
238
+ with self._lock:
239
+ if self._file is not None:
240
+ try:
241
+ # 检查是否需要滚动
242
+ if self._file.tell() + len(formatted) > self.max_bytes:
243
+ self._do_rollover()
244
+
245
+ self._file.write(formatted + '\n')
246
+ self._file.flush()
247
+ except (OSError, ValueError):
248
+ # 文件已关闭或不可写
249
+ self._file = None
250
+
251
+ def _do_rollover(self) -> None:
252
+ """执行文件滚动"""
253
+ if self._file is not None:
254
+ self._file.close()
255
+ self._file = None
256
+
257
+ # 重命名现有日志文件
258
+ for i in range(self.backup_count - 1, 0, -1):
259
+ src = self.filename.with_suffix(f'.{i}')
260
+ dst = self.filename.with_suffix(f'.{i+1}')
261
+ if src.exists():
262
+ if dst.exists():
263
+ dst.unlink()
264
+ src.rename(dst)
265
+
266
+ # 重命名主日志文件
267
+ dst = self.filename.with_suffix('.1')
268
+ if dst.exists():
269
+ dst.unlink()
270
+ if self.filename.exists():
271
+ self.filename.rename(dst)
272
+
273
+ # 重新打开主日志文件
274
+ self._open_file()
275
+
276
+ class JSONFormatter:
277
+ """JSON格式化器"""
278
+
279
+ def __call__(self, record: LogRecord) -> str:
280
+ """将日志记录转换为JSON字符串"""
281
+ record_dict = asdict(record)
282
+ record_dict['level'] = record.level.name
283
+ if record.exc_info:
284
+ exc_type, exc_value, exc_tb = record.exc_info
285
+ record_dict['exception'] = {
286
+ 'type': exc_type.__name__,
287
+ 'message': str(exc_value),
288
+ 'traceback': traceback.format_exception(exc_type, exc_value, exc_tb)
289
+ }
290
+ return json.dumps(record_dict, ensure_ascii=False)
291
+
292
+ class Logger:
293
+ """
294
+ 终极版日志记录器
295
+
296
+ 特性:
297
+ - 多级别日志记录
298
+ - 多处理器支持
299
+ - 线程安全
300
+ - 异步日志支持
301
+ - 上下文信息记录
302
+ - 结构化日志(JSON)
303
+ - 日志轮转
304
+ - 审计日志
305
+ """
306
+
307
+ _loggers: Dict[str, 'Logger'] = {}
308
+ _lock = threading.Lock()
309
+
310
+ def __init__(
311
+ self,
312
+ name: str,
313
+ level: LogLevel = LogLevel.INFO,
314
+ handlers: Optional[List[LogHandler]] = None,
315
+ propagate: bool = True,
316
+ async_mode: bool = False,
317
+ queue_size: int = 1000
318
+ ):
319
+ """
320
+ 初始化日志记录器
321
+
322
+ 参数:
323
+ name: 日志器名称
324
+ level: 日志级别阈值
325
+ handlers: 日志处理器列表
326
+ propagate: 是否传播到父日志器
327
+ async_mode: 是否启用异步模式
328
+ queue_size: 异步队列大小
329
+ """
330
+ self.name = name
331
+ self.level = level
332
+ self.handlers = handlers or []
333
+ self.propagate = propagate
334
+ self.parent: Optional['Logger'] = None
335
+ self._async_mode = async_mode
336
+ self._queue: Optional[Queue] = None
337
+ self._worker_thread: Optional[threading.Thread] = None
338
+ self._stop_event = threading.Event()
339
+
340
+ if async_mode:
341
+ self._init_async_logging(queue_size)
342
+
343
+ def _init_async_logging(self, queue_size: int) -> None:
344
+ """初始化异步日志记录"""
345
+ self._queue = Queue(maxsize=queue_size)
346
+ self._worker_thread = threading.Thread(
347
+ target=self._process_log_records,
348
+ name=f"Logger-{self.name}-Worker",
349
+ daemon=True
350
+ )
351
+ self._worker_thread.start()
352
+
353
+ def _process_log_records(self) -> None:
354
+ """处理日志记录的线程函数"""
355
+ while not self._stop_event.is_set():
356
+ try:
357
+ record = self._queue.get(timeout=0.1)
358
+ self._handle_record(record)
359
+ except Empty:
360
+ continue
361
+ except Exception:
362
+ # 防止工作线程因异常退出
363
+ continue
364
+
365
+ def _handle_record(self, record: LogRecord) -> None:
366
+ """处理日志记录"""
367
+ for handler in self.handlers:
368
+ try:
369
+ handler.emit(record)
370
+ except Exception:
371
+ # 处理器错误不应中断日志系统
372
+ continue
373
+
374
+ # 传播到父日志器
375
+ if self.propagate and self.parent is not None:
376
+ self.parent._handle_record(record)
377
+
378
+ def _make_record(
379
+ self,
380
+ level: LogLevel,
381
+ message: str,
382
+ user: Optional[str] = None,
383
+ extra: Optional[Dict[str, Any]] = None,
384
+ exc_info: Optional[Tuple[Type[BaseException], BaseException, Any]] = None,
385
+ stack_level: int = 1
386
+ ) -> LogRecord:
387
+ """创建日志记录对象"""
388
+ # 获取调用者信息
389
+ frame = inspect.currentframe()
390
+ for _ in range(stack_level + 1):
391
+ if frame is None:
392
+ break
393
+ frame = frame.f_back
394
+
395
+ module = None
396
+ func_name = None
397
+ lineno = None
398
+
399
+ if frame is not None:
400
+ module = inspect.getmodule(frame)
401
+ if module is not None:
402
+ module = module.__name__
403
+ func_name = frame.f_code.co_name
404
+ lineno = frame.f_lineno
405
+
406
+ # 获取进程和线程信息
407
+ process_id = os.getpid()
408
+ thread_id = threading.get_ident()
409
+
410
+ # 获取主机名
411
+ try:
412
+ hostname = socket.gethostname()
413
+ except:
414
+ hostname = None
415
+
416
+ return LogRecord(
417
+ level=level,
418
+ message=message,
419
+ timestamp=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f'),
420
+ logger_name=self.name,
421
+ user=user,
422
+ module=module,
423
+ func_name=func_name,
424
+ lineno=lineno,
425
+ process_id=process_id,
426
+ thread_id=thread_id,
427
+ hostname=hostname,
428
+ request_id=Logger.get_request_id(),
429
+ extra=extra,
430
+ exc_info=exc_info
431
+ )
432
+
433
+ @staticmethod
434
+ def get_request_id() -> str:
435
+ """获取当前请求ID(用于分布式追踪)"""
436
+ # 可以从线程局部存储或上下文获取
437
+ return str(uuid.uuid4())
438
+
439
+ def add_handler(self, handler: LogHandler) -> None:
440
+ """添加日志处理器"""
441
+ with self._lock:
442
+ self.handlers.append(handler)
443
+
444
+ def remove_handler(self, handler: LogHandler) -> None:
445
+ """移除日志处理器"""
446
+ with self._lock:
447
+ if handler in self.handlers:
448
+ self.handlers.remove(handler)
449
+
450
+ def set_level(self, level: LogLevel) -> None:
451
+ """设置日志级别"""
452
+ with self._lock:
453
+ self.level = level
454
+
455
+ def log(
456
+ self,
457
+ level: LogLevel,
458
+ message: str,
459
+ user: Optional[str] = None,
460
+ extra: Optional[Dict[str, Any]] = None,
461
+ exc_info: Optional[Tuple[Type[BaseException], BaseException, Any]] = None,
462
+ stack_level: int = 1
463
+ ) -> None:
464
+ """记录日志"""
465
+ if level.value < self.level.value:
466
+ return
467
+
468
+ record = self._make_record(level, message, user, extra, exc_info, stack_level + 1)
469
+
470
+ if self._async_mode and self._queue is not None:
471
+ try:
472
+ self._queue.put_nowait(record)
473
+ except Full:
474
+ # 队列已满,同步处理
475
+ self._handle_record(record)
476
+ else:
477
+ self._handle_record(record)
478
+
479
+ def debug(
480
+ self,
481
+ message: str,
482
+ user: Optional[str] = None,
483
+ extra: Optional[Dict[str, Any]] = None,
484
+ stack_level: int = 0
485
+ ) -> None:
486
+ """记录DEBUG级别日志"""
487
+ self.log(LogLevel.DEBUG, message, user, extra, None, stack_level + 1)
488
+
489
+ def info(
490
+ self,
491
+ message: str,
492
+ user: Optional[str] = None,
493
+ extra: Optional[Dict[str, Any]] = None,
494
+ stack_level: int = 0
495
+ ) -> None:
496
+ """记录INFO级别日志"""
497
+ self.log(LogLevel.INFO, message, user, extra, None, stack_level + 1)
498
+
499
+ def warning(
500
+ self,
501
+ message: str,
502
+ user: Optional[str] = None,
503
+ extra: Optional[Dict[str, Any]] = None,
504
+ stack_level: int = 0
505
+ ) -> None:
506
+ """记录WARNING级别日志"""
507
+ self.log(LogLevel.WARNING, message, user, extra, None, stack_level + 1)
508
+
509
+ def error(
510
+ self,
511
+ message: str,
512
+ user: Optional[str] = None,
513
+ extra: Optional[Dict[str, Any]] = None,
514
+ exc_info: Optional[Tuple[Type[BaseException], BaseException, Any]] = None,
515
+ stack_level: int = 0
516
+ ) -> None:
517
+ """记录ERROR级别日志"""
518
+ self.log(LogLevel.ERROR, message, user, extra, exc_info, stack_level + 1)
519
+
520
+ def critical(
521
+ self,
522
+ message: str,
523
+ user: Optional[str] = None,
524
+ extra: Optional[Dict[str, Any]] = None,
525
+ exc_info: Optional[Tuple[Type[BaseException], BaseException, Any]] = None,
526
+ stack_level: int = 0
527
+ ) -> None:
528
+ """记录CRITICAL级别日志"""
529
+ self.log(LogLevel.CRITICAL, message, user, extra, exc_info, stack_level + 1)
530
+
531
+ def audit(
532
+ self,
533
+ message: str,
534
+ user: Optional[str] = None,
535
+ extra: Optional[Dict[str, Any]] = None,
536
+ stack_level: int = 0
537
+ ) -> None:
538
+ """记录审计日志"""
539
+ self.log(LogLevel.AUDIT, message, user, extra, None, stack_level + 1)
540
+
541
+ def exception(
542
+ self,
543
+ message: str,
544
+ user: Optional[str] = None,
545
+ extra: Optional[Dict[str, Any]] = None,
546
+ stack_level: int = 0
547
+ ) -> None:
548
+ """记录当前异常"""
549
+ exc_info = sys.exc_info()
550
+ if exc_info[0] is not None:
551
+ self.log(LogLevel.ERROR, message, user, extra, exc_info, stack_level + 1)
552
+
553
+ def close(self) -> None:
554
+ """关闭日志记录器"""
555
+ if self._async_mode:
556
+ self._stop_event.set()
557
+ if self._worker_thread is not None:
558
+ self._worker_thread.join(timeout=5)
559
+
560
+ # 处理队列中剩余日志
561
+ if self._queue is not None:
562
+ while not self._queue.empty():
563
+ try:
564
+ record = self._queue.get_nowait()
565
+ self._handle_record(record)
566
+ except Empty:
567
+ break
568
+
569
+ for handler in self.handlers:
570
+ handler.close()
571
+
572
+ @classmethod
573
+ def get_logger(
574
+ cls,
575
+ name: str = 'root',
576
+ level: Optional[LogLevel] = None,
577
+ handlers: Optional[List[LogHandler]] = None,
578
+ propagate: bool = True,
579
+ async_mode: bool = False,
580
+ queue_size: int = 1000
581
+ ) -> 'Logger':
582
+ """获取或创建日志记录器"""
583
+ with cls._lock:
584
+ if name in cls._loggers:
585
+ logger = cls._loggers[name]
586
+ # 更新配置
587
+ if level is not None:
588
+ logger.set_level(level)
589
+ if handlers is not None:
590
+ logger.handlers = handlers
591
+ logger.propagate = propagate
592
+ return logger
593
+ else:
594
+ # 创建层次结构
595
+ parts = name.split('.')
596
+ parent = None
597
+
598
+ for i in range(1, len(parts)):
599
+ parent_name = '.'.join(parts[:i])
600
+ if parent_name not in cls._loggers:
601
+ cls._loggers[parent_name] = Logger(
602
+ name=parent_name,
603
+ level=LogLevel.INFO,
604
+ propagate=True
605
+ )
606
+
607
+ if len(parts) > 1:
608
+ parent_name = '.'.join(parts[:-1])
609
+ parent = cls._loggers.get(parent_name)
610
+
611
+ logger = Logger(
612
+ name=name,
613
+ level=level or LogLevel.INFO,
614
+ handlers=handlers,
615
+ propagate=propagate,
616
+ async_mode=async_mode,
617
+ queue_size=queue_size
618
+ )
619
+ logger.parent = parent
620
+ cls._loggers[name] = logger
621
+ return logger
622
+
623
+ @classmethod
624
+ def shutdown(cls) -> None:
625
+ """关闭所有日志记录器"""
626
+ with cls._lock:
627
+ for logger in cls._loggers.values():
628
+ logger.close()
629
+ cls._loggers.clear()
630
+
631
+ # 全局默认日志记录器
632
+ _default_logger = Logger.get_logger('root')
633
+
634
+ # 便捷函数
635
+ def debug(message: str, user: Optional[str] = None, **kwargs) -> None:
636
+ """记录DEBUG级别日志"""
637
+ _default_logger.debug(message, user, kwargs)
638
+
639
+ def info(message: str, user: Optional[str] = None, **kwargs) -> None:
640
+ """记录INFO级别日志"""
641
+ _default_logger.info(message, user, kwargs)
642
+
643
+ def warning(message: str, user: Optional[str] = None, **kwargs) -> None:
644
+ """记录WARNING级别日志"""
645
+ _default_logger.warning(message, user, kwargs)
646
+
647
+ def error(message: str, user: Optional[str] = None, **kwargs) -> None:
648
+ """记录ERROR级别日志"""
649
+ _default_logger.error(message, user, kwargs)
650
+
651
+ def critical(message: str, user: Optional[str] = None, **kwargs) -> None:
652
+ """记录CRITICAL级别日志"""
653
+ _default_logger.critical(message, user, kwargs)
654
+
655
+ def audit(message: str, user: Optional[str] = None, **kwargs) -> None:
656
+ """记录审计日志"""
657
+ _default_logger.audit(message, user, kwargs)
658
+
659
+ def exception(message: str, user: Optional[str] = None, **kwargs) -> None:
660
+ """记录当前异常"""
661
+ _default_logger.exception(message, user, kwargs)
662
+
663
+ def get_logger(name: str = 'root', **kwargs) -> Logger:
664
+ """获取命名的日志记录器"""
665
+ return Logger.get_logger(name, **kwargs)
666
+
667
+ def setup_default_logger(
668
+ level: Union[LogLevel, str] = LogLevel.INFO,
669
+ filename: Optional[Union[str, Path]] = None,
670
+ console: bool = True,
671
+ json_format: bool = False,
672
+ max_bytes: Optional[int] = None,
673
+ backup_count: int = 5,
674
+ async_mode: bool = False,
675
+ queue_size: int = 1000
676
+ ) -> None:
677
+ """设置全局默认日志记录器"""
678
+ global _default_logger
679
+
680
+ if isinstance(level, str):
681
+ level = LogLevel.from_string(level)
682
+
683
+ handlers = []
684
+ formatter = JSONFormatter() if json_format else None
685
+
686
+ if console:
687
+ handlers.append(StreamHandler(formatter=formatter))
688
+
689
+ if filename is not None:
690
+ if max_bytes is not None:
691
+ handlers.append(
692
+ RotatingFileHandler(
693
+ filename,
694
+ max_bytes=max_bytes,
695
+ backup_count=backup_count,
696
+ formatter=formatter
697
+ )
698
+ )
699
+ else:
700
+ handlers.append(FileHandler(filename, formatter=formatter))
701
+
702
+ _default_logger = Logger.get_logger(
703
+ 'root',
704
+ level=level,
705
+ handlers=handlers,
706
+ async_mode=async_mode,
707
+ queue_size=queue_size
708
+ )
709
+
710
+ def shutdown() -> None:
711
+ """关闭日志系统"""
712
+ Logger.shutdown()