Django 上线后的日志方案:三个误区和一份最小配置

作者:程序员白大力 · 2026-09-28 09:14
Django 上线后的日志方案:三个误区和一份最小配置

封面

本地开发时日志这件事几乎不存在:runserver 一跑,所有输出滚在终端里,出了什么错一眼就能看到。上线那天换成了 Gunicorn + systemd,日志的去向一下变了三处——print 的输出不知去向,文件日志没人管轮转,500 错误用户看得见、你自己看不见。

日志是上线后才真正生效的安全网,但三个误区让这张网大多数时候是破的。这篇把三个误区拆开,最后给一份能直接抄走的最小配置。

一、print 调试跟着上了生产

print 写的是标准输出。开发时终端就是 stdout,一切可见;上了生产,进程由 systemd 接管,stdout 会被 journald 收走(journalctl -u 服务名 还能查),但用 nohup 挂后台、塞进 crontab 或某些容器场景下,这些输出要么混进别的文件,要么直接丢失。

print 更根本的问题是它分不出级别:没法在出事后按 ERROR 过滤,没法统一关闭,也没法和其他日志汇成一个体系。正确姿势就一句:用 logging.getLogger(__name__) 取日志器,Django 框架自身和整个 Python 生态的日志都汇在这一个体系里,级别、去向、格式统一在 settings 里管。

顺带记两个框架自带的记录器:django.request 把 5XX 响应记为 ERROR、4XX 记为 WARNING——后面告警链路就挂在它身上。

还有一个常被误解的点:Django 本身不记录访问日志。开发时终端里那一行行 [28/Sep/2026 ... 200 OK] 来自 runserver,上了生产这套没了,访问日志得靠 Gunicorn 自己开(--access-logfile / --access-logformat);走 systemd 的话它跟应用日志一样进 journal。访问日志和错误日志的用途不同——前者看流量和行为,后者看故障——混在一个文件里排查时反而碍事,分开更清楚。

二、500 告警是「内置的」,但收件人不是

Django 确实内置了一条 500 告警链路:默认日志配置里有个叫 mail_admins 的处理器(AdminEmailHandler),带 require_debug_false 过滤——只要 DEBUG=False,框架收到的 ERROR(包括未处理异常产生的 500)就会发邮件给 ADMINS 设置里的人。

但内置的只是链路,不是收件人。默认 ADMINS 是空的,邮件配置也没给,这条链路接好了却无处可发。更麻烦的是它失败得毫无声息,两个经典坑:

■
SERVER_EMAIL 默认是 root@localhost,不少 SMTP 服务器会把这种发件人直接丢弃,不退信、不报错;
■
ADMINS 的格式是元组(或列表)套元组,写成一对括号里直接放姓名和邮箱,收件人格式就错了,同样静默失败。

最小配置长这样(凭据走环境变量,别写死):

ADMINS = [("运维", "ops@example.com")]
SERVER_EMAIL = "django@example.com"
EMAIL_HOST = "smtp.example.com"
EMAIL_HOST_USER = "django@example.com"
EMAIL_HOST_PASSWORD = os.environ["EMAIL_HOST_PASSWORD"]

配完故意触发一个 500(临时路由里抛个异常即可),收到带完整 traceback 的邮件才算通。定位上邮件是兜底——它告诉你出事了,排查还得靠落盘日志;404 这类高噪音事件别往邮件里接,一晚上能把邮箱灌爆。

三、文件日志别让应用自己轮转

单进程下 RotatingFileHandler、TimedRotatingFileHandler 都没问题。但 Gunicorn 多 worker 是多进程:每个 worker 持有自己的处理器,各自判断该不该切割文件,切换瞬间互相踩踏。Python 的 logging 模块是线程安全的,不是进程安全的。有工程师实测过:多 worker 用 TimedRotatingFileHandler 按时间轮转,3000 条日志最后只剩不到 200 条。

两条正路,按需选:

方案 做法 适用
日志走 stdout,交给 journald systemd 默认收集服务 stdout,把 journald 配成持久化后用 journalctl 查询 单机小项目,改动最小,推荐起点
logrotate + WatchedFileHandler logrotate 负责切割和重建文件,WatchedFileHandler 发现文件被换后自动重新打开句柄 必须落普通文件、要长期归档
concurrent-log-handler 第三方库,加锁轮转 不想引入 logrotate 时的折中

走 journald 这条路有个容易忽略的坑:多数发行版默认存储模式是 auto,/var/log/journal 目录不存在时日志只放在内存里(/run/log/journal),重启全部丢失。开机就排查不了上次崩溃的原因,等于白配。开持久化两条命令二选一:

sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journald
# 或在 /etc/systemd/journald.conf 里设 Storage=persistent

磁盘占用可用 SystemMaxUse 限制上限,默认约为所在文件系统的一成。

四、踩坑与教训
■
文件权限:Gunicorn 通常以专用低权用户运行,日志目录要先建好并把属主交给这个用户,否则第一条日志就是 PermissionError——而权限报错本身也可能被吞掉。
■
级别洪水:生产环境从 INFO 起步,别开 DEBUG。访问日志加调试日志,小盘服务几周就能吃穿。另外 journald 有默认限速(同一服务 30 秒内超过一万条会被丢弃),这既是保护,也提醒你日志不是越多越好。
■
配置要实测:日志配置「看起来对」不算数。三路验证——故意触发 500 后,邮件收到、文件落盘、journalctl 能查到,三条都通才算完成。

日志方案的选型逻辑和小站的其他基础设施一样:先用最简单的(stdout + 持久化 journald)把「出事有人知道」这件事兜住,等真的需要归档和检索了,再上 logrotate 或集中式方案。如果你也在做 Django 上线,欢迎把这篇转给一起协作的人。

参考文章
■
Django 官方文档:Error reporting(错误报告)
■
Django 官方文档:Logging 默认配置与 AdminEmailHandler
■
Python 官方文档:logging.handlers(RotatingFileHandler / WatchedFileHandler)
■
systemd-journald.service(8) 手册页
■
Red Hat 客户门户:How to enable persistent logging for the systemd journal
■
How to rotate Gunicorn log files(多进程轮转丢日志实测)

#Django #Python #日志 #运维部署 #后端开发 #服务器

本文由 AI 辅助生成并经人工审核发布,内容仅供参考,不构成法律意见。