## 为什么需要结构化与可控的日志
- 线上诊断依赖稳定的字段与上下文(如请求 ID、用户 ID)。
- 阻塞式文件写入会拖慢主路径;使用队列把 I/O 异步化可降低延迟。
- 滚动策略避免单个日志文件无限增长并影响磁盘与备份。
## 版本与标准库范围
- `logging`/`logging.config.dictConfig`:Python 2.7+/3.x。
- `RotatingFileHandler`/`TimedRotatingFileHandler`:标准库自带。
- `QueueHandler`/`QueueListener`:Python 3.2+。
- `contextvars`:Python 3.7+(适合异步/协程上下文传递)。
## 方案一:dictConfig + 上下文 Filter(结构化格式)
import logging
import logging.config
import contextvars
request_id = contextvars.ContextVar("request_id", default="-")
class ContextFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
rid = request_id.get()
if not hasattr(record, "request_id"):
record.request_id = rid
return True
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"filters": {"context": {"()": ContextFilter}},
"formatters": {
"plain": {"format": "%(asctime)s %(levelname)s %(name)s %(message)s %(request_id)s"},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "plain",
"filters": ["context"],
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"level": "INFO",
"formatter": "plain",
"filters": ["context"],
"filename": "app.log",
"maxBytes": 10 * 1024 * 1024,
"backupCount": 5,
"encoding": "utf-8",
},
},
"loggers": {
"app": {"handlers": ["console", "file"], "level": "INFO", "propagate": False},
},
}
logging.config.dictConfig(LOGGING)
logger = logging.getLogger("app")
def handle_one_request():
request_id.set("req-123")
logger.info("user login", extra={"request_id": request_id.get()})
handle_one_request()
要点:
- `Filter` 注入 `request_id`,保证格式化器能安全渲染该字段。
- 通过 `extra` 可覆盖或补充字段;使用 `logger.info("... %s", x)` 的懒格式化以降低禁用级别时的开销。
## 方案二:QueueHandler + QueueListener(异步落盘,提升吞吐)
import logging
import logging.handlers
import queue
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s %(request_id)s"))
file = logging.handlers.RotatingFileHandler("app.log", maxBytes=10 * 1024 * 1024, backupCount=5, encoding="utf-8")
file.setLevel(logging.INFO)
file.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s %(request_id)s"))
class ContextFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
if not hasattr(record, "request_id"):
record.request_id = "-"
return True
console.addFilter(ContextFilter())
file.addFilter(ContextFilter())
q = queue.Queue(-1)
listener = logging.handlers.QueueListener(q, console, file)
root = logging.getLogger()
root.setLevel(logging.INFO)
root.handlers = [logging.handlers.QueueHandler(q)]
listener.start()
app = logging.getLogger("app")
app.info("boot")
要点:
- 主线程只把 `LogRecord` 入队,I/O 由监听器线程处理,减少阻塞。
- 在多进程环境中,请将队列与监听器放在独立写入进程,其他工作进程仅入队,避免文件写并发竞争。
## TimedRotating 与归档策略建议
from logging.handlers import TimedRotatingFileHandler
handler = TimedRotatingFileHandler("app.log", when="midnight", interval=1, backupCount=7, encoding="utf-8")
建议:
- 日滚策略常用 `when="midnight"`;保留周期由合规要求决定(如 7/30/180 天)。
- 配合外部压缩/归档任务对历史日志打包与上传(如对象存储)。
## 结构化 JSON 日志(便于检索与聚合)
import logging
import json
class JsonFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
payload = {
"ts": self.formatTime(record),
"level": record.levelname,
"logger": record.name,
"msg": record.getMessage(),
"request_id": getattr(record, "request_id", "-"),
}
return json.dumps(payload, ensure_ascii=False)
logger = logging.getLogger("app.json")
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.info("ok", extra={"request_id": "req-1"})
## 性能与工程实践清单
- 使用懒格式化:`logger.debug("x=%s", x)`,避免禁用级别时构造字符串。
- 为第三方库降噪:`logging.getLogger("urllib3").setLevel(logging.WARNING)`。
- 在高吞吐场景中使用 `QueueHandler`/`QueueListener`,把 I/O 与格式化放到专用线程/进程。
- 统一字段命名(`request_id`、`user_id`、`trace_id`)与格式,保证检索与可视化一致。
- 文件滚动与归档策略要与合规/审计要求对齐;避免无限增长。
## 结论
- 仅用标准库即可实现结构化、低阻塞、高可用的日志系统。
- 结合 `Filter` 注入上下文、`QueueHandler/QueueListener` 异步落盘、`Rotating/TimedRotating` 控制文件规模,即可满足大多数工程需求。

发表评论 取消回复