R / Richie全部文章 ↑

Python · 5 分钟阅读

`logging`:记录和管理日志

目录


logging 是 Python 标准库的 日志模块。它提供多级别(DEBUG / INFO /
WARNING / ERROR / CRITICAL)、多 Handler(控制台、文件、网络…)、
结构化输出与线程安全,是写生产代码的标配。

永远用 logging 而不是 print;把日志当作 事件流 看待。


1. 五个级别

级别 数值 使用场景
DEBUG 10 详细诊断信息(开发时打开)
INFO 20 确认流程按预期
WARNING 30 出现意外,但程序还能继续
ERROR 40 更严重问题,部分功能失败
CRITICAL 50 致命错误,程序可能无法继续

设置 level 之后,低于该级别的日志会被丢弃。


2. 最简单的使用

import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")

logging.debug("调试")
logging.info("启动")
logging.warning("磁盘快满了")
logging.error("数据库连接失败")
logging.critical("服务挂了")

basicConfig() 只能 第一次 生效;之后修改配置需要拿 logger 自己设。


3. 四大组件

Loggers       ──→  Filter   ──→  Handler  ──→  Formatter
  入口,按名取             处理器(控制台/文件)    输出格式
  按级别过滤
组件 作用
Logger 入口,按 __name__ 取;可设置级别
Handler 决定日志去哪里(StreamHandler / FileHandler)
Filter 决定哪些日志被接收
Formatter 决定输出格式

4. 模块化推荐写法:每模块一个 logger

# myapp/logger.py
import logging
import sys


def get_logger(name: str = "myapp") -> logging.Logger:
    logger = logging.getLogger(name)
    if logger.handlers:                 # 防止重复添加
        return logger
    logger.setLevel(logging.INFO)

    formatter = logging.Formatter(
        fmt="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",
    )

    # 控制台
    sh = logging.StreamHandler(sys.stdout)
    sh.setFormatter(formatter)
    logger.addHandler(sh)
    return logger

使用:

# myapp/api.py
from .logger import get_logger

log = get_logger(__name__)   # __name__ = "myapp.api"

log.info("请求开始")
log.error("出错了:%s", err)

关键点:

  • 每个模块用自己的 logger(按 __name__),子 logger 自动继承父 logger 的 handler 与 level。
  • 日志消息里 用 %s 占位 而不是 f-string,logging 会自己延迟格式化。
  • 不要 logging.getLogger(__name__).propagate = False,除非有特别理由。

5. 写文件 + 按大小切分

from logging.handlers import RotatingFileHandler

logger = get_logger("file-app")
handler = RotatingFileHandler(
    "app.log",
    maxBytes=10 * 1024 * 1024,    # 10 MiB
    backupCount=5,                # 保留 5 个历史
    encoding="utf-8",
)
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
logger.addHandler(handler)

想按天切分用 TimedRotatingFileHandler("app.log", when="midnight", backupCount=7)。


6. 配置:代码 vs dictConfig vs 配置文件

6.1 代码配置(适合小项目)

logging.basicConfig(
    level=logging.INFO,
    format="%(levelname)s %(name)s :: %(message)s",
    datefmt="%H:%M:%S",
)

6.2 dictConfig(推荐,中大型项目)

import logging.config

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "default": {
            "format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
        }
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "default",
        },
        "file": {
            "class": "logging.handlers.RotatingFileHandler",
            "filename": "app.log",
            "maxBytes": 10 * 1024 * 1024,
            "backupCount": 5,
            "formatter": "default",
        },
    },
    "loggers": {
        "myapp": {"handlers": ["console", "file"], "level": "INFO"},
    },
}

logging.config.dictConfig(LOGGING)

6.3 配置文件(YAML/TOML)

# logging.yaml
version: 1
formatters:
  default:
    format: "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
handlers:
  console:
    class: logging.StreamHandler
    formatter: default
loggers:
  myapp:
    handlers: [console]
    level: INFO
import logging.config, yaml

with open("logging.yaml") as f:
    logging.config.dictConfig(yaml.safe_load(f))

7. 异常日志:带 traceback

try:
    do_something()
except Exception:
    log.exception("do_something 失败")     # 自动带上 traceback
    # 等价于 log.error("xxx", exc_info=True)

8. 结构化日志:JSON

要接入 ELK / Loki / CloudWatch 等系统时,把日志输出成 JSON 最好。

import json
import logging


class JsonFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        payload = {
            "ts": self.formatTime(record, self.datefmt),
            "level": record.levelname,
            "name": record.name,
            "message": record.getMessage(),
        }
        if record.exc_info:
            payload["exc"] = self.formatException(record.exc_info)
        return json.dumps(payload, ensure_ascii=False)

也可以直接用第三方库 python-json-logger /
structlog。


9. 与 print 的关系

情况 推荐
调试 / 快速原型 print 也行
库代码 必须 logging
长时间运行的服务 必须 logging
多进程 / 跨模块 必须 logging
临时一行打印(one-off) print 也行

把 print 当日志,运行时 没法关掉,也无法分流到文件 / 网络。


10. 常见问题

  • basicConfig 第二次调用无效? 用 dictConfig 或在 logger 层面加 handler。
  • 日志重复出现? 多半是 logger 被加了多次 handler;用 if logger.handlers: return
    兜底。
  • 子模块日志没显示? 父 logger 设了 handler 才会传到子;最稳妥是每个模块
    getLogger(__name__)。
  • 时间多了时区偏移? Formatter 加 datefmt="%Y-%m-%dT%H:%M:%S%z",或自己
    在 format 时 datetime.now().astimezone().isoformat()。
  • % 格式化的字符串里有 %? 写 %% 转义。
  • 异步场景:QueueHandler + QueueListener 把 logging 调用扔进队列,
    避免阻塞 event loop。

11. 小结

  • 用 logging.getLogger(__name__) 在每个模块起一个 logger。
  • 消息用 log.info("...%s...", arg),不要用 f-string 提前格式化。
  • 生产环境用 dictConfig / YAML 配置,加 RotatingFileHandler 或集中日志系统。
  • 调级别、调 handler、关闭无关 logger:logging.getLogger("urllib3").setLevel(logging.WARNING)。