所属专题:Python 专题导航:基础语法、工具与实践

Django 部署后的日志排查与性能监控:从请求链路到故障定位

Django 部署后的日志排查与性能监控:从请求链路到故障定位 - 暂无配图,技术文章默认封面

为什么上线后要重新设计日志

开发环境里把日志打印到终端很方便,但生产环境的进程会重启、请求会经过 Nginx 和 uWSGI,单看 Django 控制台往往无法回答三个问题:请求从哪里进入、在哪一步变慢、错误是否会再次发生。日志设计的目标不是输出越多越好,而是让一次请求可以被唯一标识,让错误、耗时和依赖调用能够放在同一条检索链路上。

站内的 Django 日志配置实战指南 已经介绍了 logging 的组件;本文补充部署后的排查方法。部署层面的进程和静态文件问题可以结合 Django 项目部署全攻略 查看,查询耗时则可对照 Django 性能调优实战 和 Web 与 API 性能优化。

先把日志分成四层

建议保留四类日志,并使用不同的保留策略。

  1. 访问日志:由 Nginx 记录状态码、请求路径、响应大小和总耗时。
  2. 应用日志:Django 记录业务事件、异常堆栈和关键状态变化。
  3. 依赖日志:数据库、缓存、邮件或第三方 API 的调用结果和耗时。
  4. 进程日志:uWSGI、Celery 或定时任务启动、退出和崩溃原因。

应用日志里不要写入密码、Cookie、Authorization 请求头和完整的个人信息。日志文件应由运行用户可写,目录权限保持最小化,并配合 logrotate 或日志平台完成切割。

一个可直接落地的 logging 配置

下面的配置使用标准库的 WatchedFileHandler,适合由 logrotate 重命名日志文件的 Linux 环境。开发环境仍然输出到控制台,生产环境写入独立的应用日志。

# settings.py
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent
LOG_DIR = BASE_DIR / "var" / "log"
LOG_DIR.mkdir(parents=True, exist_ok=True)

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "verbose": {
            "format": "{asctime} {levelname} {name} {message}",
            "style": "{",
        },
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "verbose",
        },
        "app_file": {
            "class": "logging.handlers.WatchedFileHandler",
            "filename": str(LOG_DIR / "app.log"),
            "formatter": "verbose",
            "encoding": "utf-8",
        },
    },
    "loggers": {
        "django": {
            "handlers": ["console", "app_file"],
            "level": "INFO",
            "propagate": False,
        },
        "django.request": {
            "handlers": ["console", "app_file"],
            "level": "WARNING",
            "propagate": False,
        },
        "myapp": {
            "handlers": ["console", "app_file"],
            "level": "INFO",
            "propagate": False,
        },
    },
}

业务代码使用模块级 logger,并把可检索的字段放进消息中:

# orders/services.py
import logging

logger = logging.getLogger(__name__)

def cancel_order(order, actor_id):
    logger.info("order_cancel order_id=%s actor_id=%s", order.id, actor_id)
    order.status = "cancelled"
    order.save(update_fields=["status", "updated_at"])

给请求加关联 ID

Nginx 日志和 Django 日志如果没有共同字段,排查一次请求会很慢。可以用中间件读取上游传来的 X-Request-ID,没有时生成一个,并在响应头中返回。生产环境若由可信的网关生成 ID,应校验长度和字符集,避免把任意长字符串写入日志。

# core/middleware.py
import logging
import uuid

logger = logging.getLogger("myapp")

class RequestIdMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        request_id = request.headers.get("X-Request-ID", "")[:64]
        if not request_id:
            request_id = uuid.uuid4().hex
        request.request_id = request_id
        response = self.get_response(request)
        response["X-Request-ID"] = request_id
        logger.info(
            "request_done request_id=%s method=%s path=%s status=%s",
            request_id, request.method, request.path, response.status_code,
        )
        return response

如果需要记录总耗时,可以在进入视图前保存单调时钟,返回时输出毫秒数。不要使用系统时间计算耗时,因为系统校时会造成负数。对于慢请求,建议单独使用 WARNING 级别,便于告警:

import time

started = time.perf_counter()
response = self.get_response(request)
elapsed_ms = (time.perf_counter() - started) * 1000
level = logger.warning if elapsed_ms > 800 else logger.info
level("request_done request_id=%s elapsed_ms=%.1f", request.request_id, elapsed_ms)

按排查顺序定位 500 和变慢

第一步看 Nginx:确认请求是否到达、状态码是 499、502 还是 504。499 通常表示客户端提前断开,502 多与上游进程不可用有关,504 则要结合上游超时配置和应用耗时判断。第二步用 X-Request-ID 在应用日志中定位异常堆栈。第三步检查同一时间段的数据库慢查询、缓存连接和外部 API。最后再看 uWSGI worker 是否频繁重启、内存是否持续增长。

不要只盯着平均响应时间。平均值会掩盖少量极慢请求,至少同时记录请求数、错误数、P50 和 P95。没有监控平台时,可以先通过结构化日志导出这些指标;有 Prometheus 等平台后,再把同样的字段转为指标。

一个安全的健康检查

健康检查要区分“进程活着”和“依赖可用”。负载均衡器使用轻量的存活检查,运维告警使用包含数据库连接的就绪检查,避免数据库短暂故障时不断重启所有 Web 进程。

# core/views.py
from django.db import connection
from django.http import JsonResponse

def readiness(request):
    try:
        with connection.cursor() as cursor:
            cursor.execute("SELECT 1")
            cursor.fetchone()
    except Exception:
        return JsonResponse({"status": "unready"}, status=503)
    return JsonResponse({"status": "ready"})

健康接口不要返回环境变量、版本密钥或完整异常。应用部署与性能优化还可以参考 网站性能优化实战,SEO 数据监控的日志指标整理可结合 SEO 数据监控实战指南。当日志能够关联请求、错误和依赖耗时后,排障就从猜测变成了有证据的定位。

分享这篇文章:

评论 (0)

请 登录 后发表评论, 还没有账户?立即注册

暂无评论,快来抢沙发吧!