初始提交:飞球 ETL 系统全量代码

This commit is contained in:
Neo
2026-02-13 08:05:34 +08:00
commit 3c51f5485d
441 changed files with 117631 additions and 0 deletions

142
utils/logging_utils.py Normal file
View File

@@ -0,0 +1,142 @@
# -*- 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