143 lines
3.7 KiB
Python
143 lines
3.7 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""日志配置工具
|
||
|
||
提供统一的日志配置和格式化。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
import sys
|
||
from contextlib import contextmanager
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from typing import Iterator, TextIO
|
||
|
||
|
||
# 统一日志格式(中文友好)
|
||
UNIFIED_FORMAT = "[%(asctime)s] %(levelname)-5s | %(name)s | %(message)s"
|
||
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||
|
||
|
||
class TeeStream:
|
||
"""同时输出到多个流"""
|
||
|
||
def __init__(self, *streams: TextIO) -> None:
|
||
self._streams = streams
|
||
|
||
def write(self, data: str) -> int:
|
||
for stream in self._streams:
|
||
stream.write(data)
|
||
return len(data)
|
||
|
||
def flush(self) -> None:
|
||
for stream in self._streams:
|
||
stream.flush()
|
||
|
||
def isatty(self) -> bool:
|
||
return False
|
||
|
||
def fileno(self) -> int:
|
||
return self._streams[0].fileno()
|
||
|
||
|
||
def build_log_path(log_dir: Path, prefix: str, tag: str = "") -> Path:
|
||
"""构建日志文件路径"""
|
||
suffix = f"_{tag}" if tag else ""
|
||
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
return log_dir / f"{prefix}{suffix}_{stamp}.log"
|
||
|
||
|
||
def get_unified_formatter() -> logging.Formatter:
|
||
"""获取统一格式的日志格式器"""
|
||
return logging.Formatter(UNIFIED_FORMAT, DATE_FORMAT)
|
||
|
||
|
||
@contextmanager
|
||
def configure_logging(
|
||
name: str,
|
||
log_file: Path | None,
|
||
*,
|
||
level: str = "INFO",
|
||
console: bool = True,
|
||
tee_std: bool = True,
|
||
) -> Iterator[logging.Logger]:
|
||
"""
|
||
配置日志
|
||
|
||
Args:
|
||
name: 日志器名称
|
||
log_file: 日志文件路径,None 表示不写文件
|
||
level: 日志级别
|
||
console: 是否输出到控制台
|
||
tee_std: 是否将 stdout/stderr 也写入日志文件
|
||
|
||
Yields:
|
||
配置好的日志器
|
||
"""
|
||
logger = logging.getLogger(name)
|
||
logger.handlers.clear()
|
||
logger.setLevel(getattr(logging, level.upper(), logging.INFO))
|
||
logger.propagate = False
|
||
|
||
formatter = get_unified_formatter()
|
||
|
||
original_stdout = sys.stdout
|
||
original_stderr = sys.stderr
|
||
log_fp: TextIO | None = None
|
||
|
||
try:
|
||
if log_file:
|
||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||
log_fp = open(log_file, "a", encoding="utf-8", buffering=1)
|
||
if tee_std:
|
||
if console:
|
||
sys.stdout = TeeStream(original_stdout, log_fp)
|
||
sys.stderr = TeeStream(original_stderr, log_fp)
|
||
else:
|
||
sys.stdout = log_fp
|
||
sys.stderr = log_fp
|
||
file_handler = logging.StreamHandler(log_fp)
|
||
file_handler.setFormatter(formatter)
|
||
logger.addHandler(file_handler)
|
||
|
||
if console:
|
||
console_handler = logging.StreamHandler(original_stdout)
|
||
console_handler.setFormatter(formatter)
|
||
logger.addHandler(console_handler)
|
||
|
||
yield logger
|
||
finally:
|
||
for handler in list(logger.handlers):
|
||
handler.flush()
|
||
handler.close()
|
||
logger.removeHandler(handler)
|
||
if log_fp:
|
||
log_fp.flush()
|
||
log_fp.close()
|
||
sys.stdout = original_stdout
|
||
sys.stderr = original_stderr
|
||
|
||
|
||
def setup_root_logger(level: str = "INFO") -> logging.Logger:
|
||
"""
|
||
配置根日志器
|
||
|
||
Args:
|
||
level: 日志级别
|
||
|
||
Returns:
|
||
根日志器
|
||
"""
|
||
root = logging.getLogger()
|
||
root.setLevel(getattr(logging, level.upper(), logging.INFO))
|
||
|
||
# 清除已有处理器
|
||
root.handlers.clear()
|
||
|
||
# 添加控制台处理器
|
||
handler = logging.StreamHandler()
|
||
handler.setFormatter(get_unified_formatter())
|
||
root.addHandler(handler)
|
||
|
||
return root
|