Files
ZQYY.FQ-ETL/utils/logging_utils.py

143 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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