代码与工具 4 分钟阅读

日志变早报

起因是一行被忽略的 500 上个月某天早上,我随手翻 nginx 的 access.log,发现前一天晚上有个接口连续吐了十几分 […]

日志变早报

起因是一行被忽略的 500

上个月某天早上,我随手翻 nginx 的 access.log,发现前一天晚上有个接口连续吐了十几分钟 500。问题本身不大,重启个进程就好了,但我当时的第一反应是:这玩意儿昨天就发生了,我今早才知道。

我每天都会看邮件,但不会每天主动去服务器上 tail 日志。于是就冒出一个很朴素的想法:把日志里我真正关心的东西挑出来,每天早上固定时间发一封邮件给我。不用装 ELK,不用上 Prometheus,一个脚本加一条 cron 就够了。

这篇文章记录我怎么做出来的,以及中间踩的几个小坑。

先想清楚要什么

动手前我列了个清单,只保留「早上看一眼有用」的信息:

昨天总请求数、4xx 和 5xx 各多少;出错的 URL Top 5;访问量最高的 URL Top 5;有没有来自陌生 IP 的高频请求;以及证书还有几天到期。

清单之外的东西一律不做。我见过太多监控做着做着变成一个大系统,最后自己都不想看。邮件正文控制在二十行以内,扫一眼就能判断「今天要不要管」。

用 awk 把日志嚼碎

我的日志是 nginx 默认的 combined 格式,一行一条。没上 Python 的解析库,直接用 awk 按空格切。

核心思路是先把「昨天」这个时间窗口切出来。日志行里的时间是 [10/Oct/2026:08:12:33 +0800] 这种,用 substr 截出日期部分,和 date -d yesterday +%d/%b/%Y 生成的字符串比对。这一步比想象中好写,因为格式固定。

统计状态码就一句话的事:按第 9 列($9)分组计数。URL 是第 7 列($7)。我把结果先落到一个临时文件里,格式是「计数 类型 值」,后面拼邮件正文时直接读。

有一个细节值得说:日志里带 query string 的 URL 会让 Top 5 变得很碎,同一个接口不同参数被算成不同条目。我的处理是统计前先用 sub(/?.*/, "", $7) 把问号后面的部分砍掉。砍完之后 Top 5 才有意义。

陌生 IP 的判断我做得比较糙:维护一个 known_ips.txt,每天把出现次数超过阈值的 IP 和它比对,不在文件里的就列出来。不追求精确,只求「有个新面孔我能注意到」。

拼邮件和发出去

正文就是纯文本,不用 HTML。开头一行日期,然后几个小节,每节两三行。我试过 HTML 邮件,排版是好看,但在手机邮件客户端里反而要多点一下「显示图片」,不如纯文本利落。

发送用的是系统自带的 mail 命令,配合本机已经配好的 SMTP 转发。这里踩过一个坑:mail 对非 ASCII 字符的处理很看环境,主题里带中文有时会变成乱码。我干脆把主题写成英文加日期,正文里的中文没问题,因为指定了 UTF-8。

脚本最后会删掉临时文件,并把当天的统计结果追加到一个历史文件里。这样过一阵子我还能回看「上周哪天最忙」,算是白捡的附加价值。

cron 那点事和之后的打算

定时任务没有用 WordPress 的伪 cron,直接写系统 crontab,每天早上七点半跑。时间选在七点半是因为我一般八点前会看手机,太早发容易被淹没在夜间通知里。

跑了两周,我改过两次阈值,加过一次证书到期检查。现在这封邮件已经变成我早上的固定动作,看完顺手决定今天要不要动服务器。

接下来想加的是「异常对比」:把昨天的 5xx 数量和过去七天的均值放一起,差得太多就在主题里加个标记。不过这个得先攒够数据,急不来。

有时候一个想法从冒出来到能用,也就一个周末的事。真正花时间的不是写脚本,是想清楚自己到底想看什么。

留下你的回声