## 为什么需要结构化与可控的日志

  • 线上诊断依赖稳定的字段与上下文(如请求 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` 控制文件规模,即可满足大多数工程需求。


点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部