用 Python logging 模块给小项目做结构化日志

举报
yd_240066378 发表于 2026/11/13 08:44:48 2026/11/13
【摘要】 小项目不必一上来上重型日志框架。用 Python 标准库 logging 做 JSON 结构化日志,配上按天滚动、request_id 追踪和几个常见坑,就能支撑日常排查。

小项目一开始常常用 print 调试,跑通了也懒得改。等线上出问题,才发现既没有时间戳,也分不清哪条是错误、哪条是普通信息,排查起来全靠猜。其实 Python 标准库里的 logging 就够用,不必一上来引入重型框架。下面用一个最小可运行的例子,把结构化日志接到一个小服务里,并说明滚动切割、请求追踪和几个常见坑。

为什么不要只用 print

print 有三个常见问题:无法按级别过滤、没有统一格式、不容易接到文件或集中采集。开发时把所有信息都打出来还勉强能看,部署到服务器后,既没法只保留 WARNING 以上的记录,也没法按天归档。logging 把“记什么”和“写到哪里”拆开:业务代码只负责 logger 的 info 或 error 方法,格式、输出位置、级别都在配置里改。以后若要接入云日志,通常只需要换 Handler,不必回头改业务代码。

对于接口服务,结构化日志还有一层好处:把 request_id、user_id、耗时、状态码写成字段,而不是拼在一句自然语言里。这样用 jq 或日志平台过滤时,可以直接按字段查询,不必写复杂的正则。

可复用的最小配置

把下面这段放到项目的 logutil 模块里,进程启动时调用一次即可。日志同时输出到控制台和按天滚动的文件,每条记录都是一行 JSON。

import json
import logging
from logging.handlers import TimedRotatingFileHandler

class JsonFormatter(logging.Formatter):
    def format(self, record):
        payload = {
            "ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S"),
            "level": record.levelname,
            "logger": record.name,
            "msg": record.getMessage(),
        }
        extra = getattr(record, "extra_fields", None)
        if isinstance(extra, dict):
            payload.update(extra)
        return json.dumps(payload, ensure_ascii=False)

def setup_logging(log_path="app_log.txt"):
    root = logging.getLogger()
    if root.handlers:
        return
    root.setLevel(getattr(logging, "INFO"))
    fmt = JsonFormatter()
    stream = logging.StreamHandler()
    stream.setFormatter(fmt)
    file_handler = TimedRotatingFileHandler(
        log_path, when="midnight", backupCount=7, encoding="utf-8"
    )
    file_handler.setFormatter(fmt)
    root.addHandler(stream)
    root.addHandler(file_handler)

JSON 一行一条,后续用 jq 或 grep 都能查。TimedRotatingFileHandler 会在每天零点切割文件,并保留最近 7 天,避免日志把磁盘写满。

在业务代码里怎么打

按模块名取 logger,不要到处用 root logger。需要追踪一次请求时,把 request_id 放进 extra。失败时用 exception 方法,它会自动带上堆栈。

import logging
from logutil import setup_logging

setup_logging()
logger = logging.getLogger("order")

def create_order(user_id, amount):
    extra = {"extra_fields": {"request_id": "req-1024", "user_id": user_id}}
    logger.info("create order start", extra=extra)
    if amount <= 0:
        logger.exception("create order failed", extra=extra)
        raise ValueError("amount must be positive")
    logger.info("create order ok", extra=extra)

几个容易踩的坑

第一,不要反复调用 basicConfig 或反复 addHandler,否则同一条日志会打印多次。上面用 if root.handlers: return 就是为了防止这一点。

第二,extra 的键不能和 LogRecord 已有属性冲突,例如 name、message、args。所以这里统一放到 extra_fields 再合并进 JSON,比较稳妥。

第三,生产环境把级别设为 INFO,调试时再用环境变量改成 DEBUG。不要把密码、token、身份证号写进日志;金额、手机号如需记录,最好做掩码。

第四,多进程各自写同一个文件时可能交错或缺行。更省事的做法是只打到标准输出,由进程管理器或容器采集。

把 print 换成这样一套小配置,项目还很小的时候就能养成统一的日志习惯。等以后要接集中式日志平台,也只是换 Handler,不用改业务代码。先把级别、格式和滚动策略定下来,比事后补日志要轻松得多。

补充一点:如果暂时没有日志平台,先用纯文本格式也完全可以,把时间、级别、模块名固定在 Formatter 的模板里即可。等后面要做检索和统计,再把同一套 logger 调用切到 JSON 输出。关键是尽早把级别和输出位置从业务代码里拆出来,而不是等项目变大后再到处替换 print。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。