找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2006

积分

0

好友

277

主题
发表于 4 天前 | 查看: 13| 回复: 0

Python自带的logging模块功能其实非常强大,之所以让人觉得复杂,主要是因为它的配置看起来有点繁琐。只要理清几个关键概念,再配合一个小封装,你就能彻底告别满屏的print,让调试和问题排查的效率大幅提升。

就像我们之前探讨技术选型一样,让我们从一个真实的开发场景开始。

先别急上手,问一句:你现在还在到处用 print 吗?

我曾经帮一位同事排查一个线上问题,他的代码是典型的“print调试法”,到处充斥着这样的语句:

print("enter handle_request", request_id)
print("user:", user_id)
print("result:", result)

在本地单进程运行时,这确实能立刻看到输出,非常直观。然而,一旦服务部署到 Docker 容器,并且通过 gunicorn 以多进程模式运行,日志输出就会变成混乱的“弹幕”。你根本无法区分哪一行日志属于哪一个具体的用户请求。

更麻烦的是,当问题出现在线上环境时,你会发现:

  • 本地调试与线上运行逻辑脱节:本地用的是print,线上则需要查看日志文件,两套不同的输出方式。
  • 缺乏请求关联性:想为整个请求链路添加一个唯一的trace_id,需要手动在代码各处传递和打印。
  • 日志管理缺失:想要实现日志按天切割、限制单个文件大小等功能,还得自己编写额外的日志轮转脚本。

这时,你再回头去看logging模块那厚厚的文档,面对HandlerFormatterLogger等一堆名词,很可能就打退堂鼓了:“算了,还是先用print顶着吧。”

logging 真正让人头疼的,其实就这三件事

别被它众多的类名吓到。大多数开发者被劝退,其实主要卡在三个地方:

  1. 配置繁琐:每次都需要写一大段配置代码,经常需要复制粘贴。
  2. 多输出目标配置复杂:想同时将日志输出到控制台和文件,需要分别设置handlerformattersetLevel
  3. 多模块日志管理混乱:不同模块希望拥有独立的logger,但又担心配置会相互干扰或重复。

只要理清这三个核心痛点,logging的使用就会变得简单很多。它的核心模型其实只有两句话:

  • Logger:决定“记录什么”,即定义日志级别(debug/info/warning/error)。
  • Handler + Formatter:决定“记录到哪里”以及“记录成什么样子”,例如输出到控制台、文件、或进行轮转,以及日志的格式(如 JSON)。

我们的目标,就是为这套强大的机制包裹一层简便的外衣,让其用起来像print一样简单直观。关于Python的高级用法和最佳实践,你可以在云栈社区的Python板块找到更多深入的讨论和资源。

先写一个最小可用版:10 行代码搞定基础配置

首先,来看一个你今晚就能投入使用的精简版本,不包含任何花哨的功能:

# logging_setup.py
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path

def setup_logger(
    name: str = "app",
    level: int = logging.INFO,
    log_file: str = "app.log",
    max_bytes: int = 10 * 1024 * 1024,  # 10MB
    backup_count: int = 5,
) -> logging.Logger:
    logger = logging.getLogger(name)
    logger.setLevel(level)
    logger.propagate = False  # 不往 root 传递,避免重复输出

    if logger.handlers:
        # 已经配置过了,直接返回
        return logger

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

    # 控制台输出
    ch = logging.StreamHandler()
    ch.setFormatter(fmt)
    ch.setLevel(level)
    logger.addHandler(ch)

    # 文件输出 + 按大小滚动
    Path(log_file).parent.mkdir(parents=True, exist_ok=True)
    fh = RotatingFileHandler(log_file, maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8")
    fh.setFormatter(fmt)
    fh.setLevel(level)
    logger.addHandler(fh)

    return logger

在业务代码中,你可以这样使用它:

# main.py
from logging_setup import setup_logger

logger = setup_logger("order-service", level=10)  # 10 = logging.DEBUG

def create_order(user_id: int, amount: float):
    logger.info("start create order user_id=%s amount=%.2f", user_id, amount)
    try:
        # 这里是你的业务逻辑
        1 / 0
    except Exception:
        logger.exception("create order failed user_id=%s", user_id)

if __name__ == "__main__":
    create_order(123, 99.9)

这个简单的版本已经比满屏的print要强大得多:

  • 本地开发:可以在控制台实时看到结构化的日志。
  • 线上环境:日志会同时写入文件,并自动按设定的文件大小进行轮转切割。
  • 异常追踪:使用logger.exception可以自动记录完整的异常堆栈信息。

再进阶一点:为每一条日志自动添加“请求 ID”

在实际项目中,最令人头疼的是那些链路冗长的 Bug。一个 HTTP 请求可能经过多个服务、消息队列和异步任务,查看日志就像在玩拼图游戏。

此时,你最需要的是一个能将所有相关日志串联起来的关联标识

下面介绍一个非常实用的技巧:为logging添加一个“上下文过滤器”,自动为同一次请求的所有日志打上trace_id,无需手动传递和打印。

# context_logger.py
import logging
import contextvars
from logging.handlers import RotatingFileHandler

_current_trace_id = contextvars.ContextVar("trace_id", default="-")

class TraceIdFilter(logging.Filter):
    def filter(self, record: logging.LogRecord) -> bool:
        record.trace_id = _current_trace_id.get()
        return True

def set_trace_id(trace_id: str):
    _current_trace_id.set(trace_id)

def get_logger(name: str = "app") -> logging.Logger:
    logger = logging.getLogger(name)
    if logger.handlers:
        return logger

    logger.setLevel(logging.INFO)
    logger.propagate = False

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

    fh = RotatingFileHandler("logs/app.log", maxBytes=5 * 1024 * 1024, backupCount=3, encoding="utf-8")
    fh.setFormatter(fmt)
    fh.addFilter(TraceIdFilter())
    logger.addHandler(fh)

    ch = logging.StreamHandler()
    ch.setFormatter(fmt)
    ch.addFilter(TraceIdFilter())
    logger.addHandler(ch)

    return logger

结合 FastAPI 或 Flask 这样的 Web 框架,你可以在请求开始时轻松设置trace_id

# web_example.py
import uuid
from fastapi import FastAPI, Request
from context_logger import set_trace_id, get_logger

app = FastAPI()
logger = get_logger("web")

@app.middleware("http")
async def add_trace_id(request: Request, call_next):
    trace_id = request.headers.get("X-Trace-Id") or uuid.uuid4().hex[:16]
    set_trace_id(trace_id)
    logger.info("incoming request %s %s", request.method, request.url.path)
    try:
        response = await call_next(request)
        logger.info("request done status=%s", response.status_code)
        return response
    finally:
        # 请求结束,可以清理 trace_id(根据习惯选择是否清理)
        set_trace_id("-")

@app.get("/hello")
async def hello():
    logger.debug("handling /hello logic")
    return {"msg": "ok"}

现在,当你在日志系统中搜索某个特定的trace_id时,与该请求相关的所有日志都会被筛选出来,整个请求链路一目了然,排查问题的体验直接提升数个等级。

调试时,究竟应该记录哪些日志?

许多人对日志记录有一个误解:只要将日志级别设为DEBUG,然后把所有信息都打出来就行了。结果往往是日志文件体积暴增,真正有价值的信息反而被海量冗余数据所淹没。

我个人的使用习惯大致如下(这里说的是习惯而非“规范”,更灵活一些):

  • logger.debug:仅用于开发或排查阶段需要查看的细节,例如函数内部参数、中间变量值等。
  • logger.info:记录关键的业务节点,例如“订单创建成功”、“开始同步用户数据”。
  • logger.warning:记录非预期但仍可继续运行的情况,例如“第三方接口调用超时,已启用备用缓存数据”。
  • logger.error:记录真正的错误,但系统整体仍能响应。
  • logger.exception:在except代码块中使用,等价于logger.error并自动附加异常堆栈信息。

来看一个具体的例子,感受会更直观:

import time
from context_logger import get_logger

logger = get_logger("payment")

def pay(user_id: int, amount: float):
    start = time.time()
    logger.info("pay start user_id=%s amount=%.2f", user_id, amount)
    try:
        if amount <= 0:
            logger.warning("invalid amount user_id=%s amount=%.2f", user_id, amount)
            return False

        # 调用第三方支付网关
        logger.debug("calling payment gateway...")
        # 模拟一个HTTP调用
        raise TimeoutError("gateway timeout")

    except TimeoutError:
        logger.exception("pay timeout user_id=%s amount=%.2f", user_id, amount)
        return False
    finally:
        cost = (time.time() - start) * 1000
        logger.info("pay end user_id=%s cost=%.1fms", user_id, cost)

当某天你需要排查“为什么支付流程变慢了”时,只需按name=payment进行过滤,并查看cost字段,问题很可能就清晰了。这类性能监控和日志分析是保障系统稳定性的重要环节,更多相关经验可以在运维 & 测试板块交流学习。

为日志添加一点提升“可读性”的调味剂

这部分属于体验优化,没有它也能正常工作,但有了它会让你的日常工作更舒适。

例如,在命令行查看日志时,很多人喜欢有颜色的输出,这样WARNINGERROR可以一眼扫到。你可以使用colorlog这样的第三方库,也可以自己实现一个轻量级的包装:

# color_formatter.py
import logging
import sys

RESET = "\033[0m"
LEVEL_COLORS = {
    logging.DEBUG: "\033[37m",    # 灰色
    logging.INFO: "\033[36m",     # 青色
    logging.WARNING: "\033[33m",  # 黄色
    logging.ERROR: "\033[31m",    # 红色
    logging.CRITICAL: "\033[41m"  # 红底白字
}

class ColorFormatter(logging.Formatter):
    def format(self, record):
        color = LEVEL_COLORS.get(record.levelno, "")
        message = super().format(record)
        if not sys.stderr.isatty() and not sys.stdout.isatty():
            # 非终端环境(例如重定向到文件)不加颜色
            return message
        return f"{color}{message}{RESET}"

然后,在前面的get_logger函数中,将控制台handlerformatter替换成这个彩色版本即可:

from color_formatter import ColorFormatter

# ...
ch = logging.StreamHandler()
ch.setFormatter(ColorFormatter(fmt._fmt, fmt.datefmt))
ch.addFilter(TraceIdFilter())
logger.addHandler(ch)

这样,当你在本地或测试环境使用tail -f命令跟踪日志时,错误信息会以醒目的红色标识出来,大大提升了视觉辨识度。

何时该将日志配置升级为“生产环境”版本?

有些同学会问:开发环境和生产环境是否需要两套不同的日志配置?

一个简单且有效的方法是,使用环境变量来控制日志级别和输出目标。例如:

# config_aware_logger.py
import logging
import os
from logging.handlers import RotatingFileHandler

def get_logger(name: str = "app") -> logging.Logger:
    logger = logging.getLogger(name)
    if logger.handlers:
        return logger

    level_name = os.getenv("APP_LOG_LEVEL", "INFO")
    level = getattr(logging, level_name.upper(), logging.INFO)

    log_to_file = os.getenv("APP_LOG_TO_FILE", "true").lower() == "true"
    log_file = os.getenv("APP_LOG_FILE", "logs/app.log")

    logger.setLevel(level)
    logger.propagate = False

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

    ch = logging.StreamHandler()
    ch.setFormatter(fmt)
    ch.setLevel(level)
    logger.addHandler(ch)

    if log_to_file:
        fh = RotatingFileHandler(log_file, maxBytes=20 * 1024 * 1024, backupCount=10, encoding="utf-8")
        fh.setFormatter(fmt)
        fh.setLevel(level)
        logger.addHandler(fh)

    return logger

在开发时,你可以这样设置环境变量:

export APP_LOG_LEVEL=DEBUG
export APP_LOG_TO_FILE=false

而在生产环境部署时,则使用:

APP_LOG_LEVEL=INFO
APP_LOG_TO_FILE=true
APP_LOG_FILE=/var/log/myapp/app.log

代码本身完全无需改动,日志行为就已经根据环境自动切换到了最适合的模式。

实际上,Python的日志模块并不“玄学”,其大部分复杂性都集中在初始配置的几十行代码里。只要你静下心来,为自己的项目创建一个统一的logger工具模块,并约定所有代码都从这个模块获取logger实例,坚持使用一两周,你就再也回不去那个到处是print的“原始时代”了。

一旦你习惯了将关键节点和信息记录到日志中,许多以往需要反复打断点调试的场景,现在只需翻看日志就能理清整个事件脉络。这种感觉,确实会让人上瘾。




上一篇:Word加密宏绕过原理与CTF解题实践:从VBA工程密码到内容解密
下一篇:如何绕过WAF:将反射型XSS漏洞升级为键盘记录钓鱼攻击
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-10 08:51 , Processed in 0.446527 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表