用 Python logging 模块给小项目做结构化日志
小项目一开始常常用 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。
- 点赞
- 收藏
- 关注作者
评论(0)