219 lines
6.6 KiB
Python
219 lines
6.6 KiB
Python
import logging
|
||
import os
|
||
from logging import Logger
|
||
from concurrent_log_handler import ConcurrentRotatingFileHandler
|
||
from logging.handlers import TimedRotatingFileHandler
|
||
import gzip
|
||
import shutil
|
||
import glob
|
||
from datetime import datetime, timedelta
|
||
from pathlib import Path
|
||
|
||
|
||
def getLogger(name: str = 'root') -> Logger:
|
||
"""
|
||
创建一个按2小时滚动、支持多进程安全、自动压缩日志的 Logger
|
||
:param name: 日志器名称
|
||
:return: 单例 Logger 对象
|
||
"""
|
||
logger: Logger = logging.getLogger(name)
|
||
logger.setLevel(logging.DEBUG)
|
||
|
||
if not logger.handlers:
|
||
# 控制台输出
|
||
console_handler = logging.StreamHandler()
|
||
console_handler.setLevel(logging.DEBUG)
|
||
|
||
# 日志目录
|
||
log_dir = "logs"
|
||
os.makedirs(log_dir, exist_ok=True)
|
||
|
||
# 日志文件路径
|
||
log_file = os.path.join(log_dir, f"{name}.log")
|
||
|
||
# 文件处理器:每2小时滚动一次,保留7天,共84个文件,支持多进程写入
|
||
file_handler = TimedRotatingFileHandler(
|
||
filename=log_file,
|
||
when='H',
|
||
interval=2, # 每2小时切一次
|
||
backupCount=84, # 保留7天 = 7 * 24 / 2 = 84个文件
|
||
encoding='utf-8',
|
||
delay=False,
|
||
utc=False # 你也可以改成 True 表示按 UTC 时间切
|
||
)
|
||
|
||
# 设置 Formatter - 简化格式,去掉路径信息
|
||
formatter = logging.Formatter(
|
||
fmt="【{name}】{levelname} {asctime} {message}",
|
||
datefmt="%Y-%m-%d %H:%M:%S",
|
||
style="{"
|
||
)
|
||
console_formatter = logging.Formatter(
|
||
fmt="{levelname} {asctime} {message}",
|
||
datefmt="%Y-%m-%d %H:%M:%S",
|
||
style="{"
|
||
)
|
||
|
||
file_handler.setFormatter(formatter)
|
||
console_handler.setFormatter(console_formatter)
|
||
|
||
logger.addHandler(console_handler)
|
||
logger.addHandler(file_handler)
|
||
|
||
# 添加压缩功能(在第一次创建 logger 时执行一次)
|
||
_compress_old_logs(log_dir, name)
|
||
|
||
return logger
|
||
|
||
|
||
def _compress_old_logs(log_dir: str, name: str):
|
||
"""
|
||
将旧日志压缩成 .gz 格式
|
||
"""
|
||
pattern = os.path.join(log_dir, f"{name}.log.*")
|
||
for filepath in glob.glob(pattern):
|
||
if filepath.endswith('.gz'):
|
||
continue
|
||
try:
|
||
with open(filepath, 'rb') as f_in:
|
||
with gzip.open(filepath + '.gz', 'wb') as f_out:
|
||
shutil.copyfileobj(f_in, f_out)
|
||
os.remove(filepath)
|
||
except Exception as e:
|
||
print(f"日志压缩失败: {filepath}, 原因: {e}")
|
||
|
||
|
||
def compress_old_logs(log_dir: str = None, name: str = "root"):
|
||
"""
|
||
压缩旧的日志文件(公共接口)
|
||
|
||
Args:
|
||
log_dir: 日志目录,如果不指定则使用默认目录
|
||
name: 日志器名称
|
||
"""
|
||
if log_dir is None:
|
||
log_dir = "logs"
|
||
|
||
_compress_old_logs(log_dir, name)
|
||
|
||
|
||
def log_api_call(logger: Logger, user_id: str = None, endpoint: str = None, method: str = None, params: dict = None, response_status: int = None, client_ip: str = None):
|
||
"""
|
||
记录API调用信息,包含用户ID、接口路径、请求方法、参数、响应状态和来源IP
|
||
|
||
Args:
|
||
logger: 日志器对象
|
||
user_id: 用户ID
|
||
endpoint: 接口路径
|
||
method: 请求方法 (GET, POST, PUT, DELETE等)
|
||
params: 请求参数
|
||
response_status: 响应状态码
|
||
client_ip: 客户端IP地址
|
||
"""
|
||
try:
|
||
# 构建日志信息
|
||
log_parts = []
|
||
|
||
if user_id:
|
||
log_parts.append(f"用户={user_id}")
|
||
|
||
if client_ip:
|
||
log_parts.append(f"IP={client_ip}")
|
||
|
||
if method and endpoint:
|
||
log_parts.append(f"{method} {endpoint}")
|
||
elif endpoint:
|
||
log_parts.append(f"接口={endpoint}")
|
||
|
||
if params:
|
||
# 过滤敏感信息
|
||
safe_params = {k: v for k, v in params.items()
|
||
if k.lower() not in ['password', 'token', 'secret', 'key']}
|
||
if safe_params:
|
||
log_parts.append(f"参数={safe_params}")
|
||
|
||
if response_status:
|
||
log_parts.append(f"状态码={response_status}")
|
||
|
||
if log_parts:
|
||
log_message = " ".join(log_parts)
|
||
logger.info(log_message)
|
||
|
||
except Exception as e:
|
||
logger.error(f"记录API调用日志失败: {e}")
|
||
|
||
|
||
def delete_old_compressed_logs(log_dir: str = None, days: int = 7):
|
||
"""
|
||
删除超过指定天数的压缩日志文件
|
||
|
||
Args:
|
||
log_dir: 日志目录,如果不指定则使用默认目录
|
||
days: 保留天数,默认7天
|
||
"""
|
||
try:
|
||
if log_dir is None:
|
||
log_dir = "logs"
|
||
|
||
log_path = Path(log_dir)
|
||
if not log_path.exists():
|
||
return
|
||
|
||
# 计算截止时间
|
||
cutoff_time = datetime.now() - timedelta(days=days)
|
||
|
||
# 获取所有压缩日志文件
|
||
gz_files = [f for f in log_path.iterdir()
|
||
if f.is_file() and f.name.endswith('.log.gz')]
|
||
|
||
deleted_count = 0
|
||
for gz_file in gz_files:
|
||
# 获取文件修改时间
|
||
file_mtime = datetime.fromtimestamp(gz_file.stat().st_mtime)
|
||
|
||
# 如果文件超过保留期限,删除它
|
||
if file_mtime < cutoff_time:
|
||
gz_file.unlink()
|
||
print(f"删除旧压缩日志文件: {gz_file}")
|
||
deleted_count += 1
|
||
|
||
if deleted_count > 0:
|
||
print(f"总共删除了 {deleted_count} 个旧压缩日志文件")
|
||
|
||
except Exception as e:
|
||
print(f"删除旧压缩日志文件失败: {e}")
|
||
|
||
if __name__ == '__main__':
|
||
logger = getLogger('WebAPI')
|
||
|
||
# 基础日志测试
|
||
logger.info("系统启动")
|
||
logger.debug("调试信息")
|
||
logger.warning("警告信息")
|
||
logger.error("错误信息")
|
||
|
||
# API调用日志测试
|
||
log_api_call(
|
||
logger=logger,
|
||
user_id="user123",
|
||
endpoint="/api/users/info",
|
||
method="GET",
|
||
params={"id": 123, "fields": ["name", "email"]},
|
||
response_status=200,
|
||
client_ip="192.168.1.100"
|
||
)
|
||
|
||
log_api_call(
|
||
logger=logger,
|
||
user_id="user456",
|
||
endpoint="/api/users/login",
|
||
method="POST",
|
||
params={"username": "test", "password": "hidden"}, # password会被过滤
|
||
response_status=401,
|
||
client_ip="10.0.0.50"
|
||
)
|
||
|
||
# 单例验证
|
||
logger2 = getLogger('WebAPI')
|
||
print(f"Logger单例验证: {id(logger) == id(logger2)}")
|