
本地开发时日志这件事几乎不存在: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(多进程轮转丢日志实测)