-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy path__init__.py
More file actions
192 lines (165 loc) · 6.6 KB
/
__init__.py
File metadata and controls
192 lines (165 loc) · 6.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
"""日志模块.
提供 uvicorn 兼容的 dictConfig 构建、文件日志字符串格式化器、
以及 gzip 压缩轮转支持。
"""
from __future__ import annotations
import gzip
import logging
import logging.handlers
import os
from pathlib import Path
from .formatters import FileFormatter
# ── 常量 ────────────────────────────────────────────────────────
_DEFAULT_MAX_BYTES = 5 * 1024 * 1024 # 5 MB per file
_DEFAULT_BACKUP_COUNT = 5 # Keep 5 rotated backups
_FILE_LOG_LEVEL = "DEBUG" # File logs capture everything
def _gzip_namer(default_name: str) -> str:
"""RotatingFileHandler namer: 为轮转文件添加 .gz 后缀."""
return default_name + ".gz"
def _gzip_rotator(source: str, dest: str) -> None:
"""RotatingFileHandler rotator: 将源文件 gzip 压缩后写入目标.
流程:
1. 读取 source 文件全部内容
2. gzip 压缩写入 dest 文件
3. 删除 source 原文件
"""
with open(source, "rb") as f_in:
data = f_in.read()
with open(dest, "wb") as f_out:
f_out.write(gzip.compress(data, compresslevel=6))
os.remove(source)
def _create_rotating_file_handler(
*,
filename: str,
maxBytes: int = _DEFAULT_MAX_BYTES,
backupCount: int = _DEFAULT_BACKUP_COUNT,
encoding: str = "utf-8",
) -> logging.handlers.RotatingFileHandler:
"""创建带 gzip 压缩轮转的 RotatingFileHandler(dictConfig 兼容工厂函数).
``logging.config.dictConfig`` 仅支持通过构造函数 kwargs 配置 handler,
而 ``namer`` / ``rotator`` 是实例属性而非构造参数,因此需要通过工厂函数注入。
"""
handler = logging.handlers.RotatingFileHandler(
filename=filename,
maxBytes=maxBytes,
backupCount=backupCount,
encoding=encoding,
)
handler.namer = _gzip_namer
handler.rotator = _gzip_rotator
return handler
def build_log_config(
level: str = "INFO",
file_path: str | None = None,
max_bytes: int = _DEFAULT_MAX_BYTES,
backup_count: int = _DEFAULT_BACKUP_COUNT,
) -> dict:
"""构建 uvicorn log_config,支持双写(控制台 + 文件).
Args:
level: 控制台日志级别(默认 INFO)。
file_path: 文件日志路径。为 ``None`` 时仅输出到控制台(向后兼容)。
max_bytes: 单个日志文件最大字节数(默认 5 MB)。
backup_count: 保留的轮转备份文件数(默认 5)。
Returns:
符合 ``logging.config.dictConfig`` 规范的字典。
双写行为:
- 控制台:人类可读格式,级别由 ``level`` 参数控制(handler 级别过滤)
- 文件:字符串格式(与控制台风格一致),固定 DEBUG 级别(捕获所有日志)
- 当 ``file_path`` 为 ``None`` 或空字符串时,退化为纯控制台模式(向后兼容)
"""
config: dict = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"()": "uvicorn.logging.DefaultFormatter",
"fmt": "%(asctime)s %(levelprefix)s %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
"use_colors": None,
},
"access": {
"()": "uvicorn.logging.AccessFormatter",
"fmt": '%(asctime)s %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s',
"datefmt": "%Y-%m-%d %H:%M:%S",
},
},
"handlers": {
"default": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stderr",
"level": level,
},
"access": {
"formatter": "access",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
"level": "INFO",
},
},
"loggers": {
"uvicorn": {"handlers": ["default"], "level": level, "propagate": False},
"uvicorn.error": {
"handlers": ["default"],
"level": level,
"propagate": False,
},
"uvicorn.access": {
"handlers": ["access"],
"level": "INFO",
"propagate": False,
},
"coding.proxy": {
"handlers": ["default"],
"level": level,
"propagate": False,
},
},
}
# ── 条件注入:文件日志基础设施 ────────────────────────────
if file_path:
# 确保日志目录存在
log_file = Path(file_path)
log_file.parent.mkdir(parents=True, exist_ok=True)
# 注入文件日志字符串 formatter
config["formatters"]["file_fmt"] = {
"()": "coding.proxy.logging.formatters.FileFormatter",
}
# 注入 RotatingFileHandler(gzip 压缩轮转)
# 使用工厂函数(而非 class + namer/rotator kwargs),
# 因为 dictConfig 不支持将 namer/rotator 作为构造参数传递
config["handlers"]["file"] = {
"formatter": "file_fmt",
"()": "coding.proxy.logging._create_rotating_file_handler",
"filename": str(log_file.resolve()),
"maxBytes": max_bytes,
"backupCount": backup_count,
"encoding": "utf-8",
}
# 为每个 logger 添加 file handler
# 注意:uvicorn.error 无 handlers 键(通过 propagate 继承 uvicorn 的 handler)
for logger_name in (
"uvicorn",
"uvicorn.error",
"uvicorn.access",
"coding.proxy",
):
logger_cfg = config["loggers"][logger_name]
handlers = logger_cfg.get("handlers", [])
if isinstance(handlers, list):
handlers.append("file")
logger_cfg["handlers"] = handlers
else:
logger_cfg["handlers"] = [handlers, "file"]
# Logger 级别设为 DEBUG(让所有消息通过到 file handler)
# Console handler 已设 level 过滤,确保控制台仅输出 INFO+
config["loggers"]["coding.proxy"]["level"] = _FILE_LOG_LEVEL
config["loggers"]["uvicorn"]["level"] = _FILE_LOG_LEVEL
config["loggers"]["uvicorn.error"]["level"] = _FILE_LOG_LEVEL
return config
__all__ = [
"build_log_config",
"FileFormatter",
"_gzip_namer",
"_gzip_rotator",
]