为什么上线后要重新设计日志
开发环境里把日志打印到终端很方便,但生产环境的进程会重启、请求会经过 Nginx 和 uWSGI,单看 Django 控制台往往无法回答三个问题:请求从哪里进入、在哪一步变慢、错误是否会再次发生。日志设计的目标不是输出越多越好,而是让一次请求可以被唯一标识,让错误、耗时和依赖调用能够放在同一条检索链路上。
站内的 Django 日志配置实战指南 已经介绍了 logging 的组件;本文补充部署后的排查方法。部署层面的进程和静态文件问题可以结合 Django 项目部署全攻略 查看,查询耗时则可对照 Django 性能调优实战 和 Web 与 API 性能优化。
先把日志分成四层
建议保留四类日志,并使用不同的保留策略。
- 访问日志:由 Nginx 记录状态码、请求路径、响应大小和总耗时。
- 应用日志:Django 记录业务事件、异常堆栈和关键状态变化。
- 依赖日志:数据库、缓存、邮件或第三方 API 的调用结果和耗时。
- 进程日志: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)
暂无评论,快来抢沙发吧!