tail -f 之外怎么追日志:grep 过滤与 journalctl 的取舍
本文使用 CC BY 4.0 协议,无须申请转载
无须申请转载;允许转载、改编和商业使用,需署名、标注原文地址并附协议链接。
- 允许转载、改编和商业使用
- 需署名
- 需标注原文地址
- 需附上本协议链接
协议链接:https://creativecommons.org/licenses/by/4.0/deed.zh-hans
自愿登记用于生成可选授权码和追踪短链,不是转载前置条件。
先判断日志从哪里来
追日志的第一步不是选命令,而是确认日志的落点。服务由 systemd 启动时,应用写到标准输出、标准错误的内容默认进入 journal,journalctl -u 能直接查;应用自己配置了文件输出(log4j、logback、zap、Python logging 的 FileHandler 等),日志就在文件里,tail 和 grep 才有对象。两者也可能同时存在:有些服务既写文件,又把部分输出交给 systemd。这时要先确认你看的是哪一份,避免在两份日志之间来回切换,也避免把“文件里没有”误判成“服务没打日志”。
文件日志:优先用 tail -F,而不是只记 -f
tail -f 跟随的是文件描述符。日志轮转时,如果 logrotate 采用“改名 + 新建”的方式,旧文件被移走,新文件是新的 inode,tail -f 仍然盯着旧 inode,看起来就像日志停了。tail -F 等价于 --follow=name --retry,会按文件名重新打开,轮转后能继续跟。
tail -n 200 -F /var/log/myapp/app.log
-n 200 先给出最近 200 行作为上下文,避免只看到新日志却不知道之前发生了什么。如果目录下多个文件,tail -F 也支持一次跟多个,输出会带文件名分隔。
但 tail -F 不解决“按时间窗口回看”。要查昨天 14:00 到 15:00 的日志,文件可能已经被轮转成 app.log.1 或压缩成 .gz,需要逐个文件处理。zcat 配合 grep 能查压缩历史,但跨多个文件拼接时间线仍然麻烦。这是文件日志的固有成本:它把“存储”交给应用和 logrotate,查询能力就要自己补。
grep 过滤:把“追”变成“筛”
最直接的组合是:
tail -n 500 -F /var/log/myapp/app.log | grep --line-buffered -E 'ERROR|Exception|timeout'
--line-buffered 很关键。grep 输出到管道或文件时默认使用块缓冲,日志行可能积攒到几 KB 才吐出来,看起来像“日志延迟”。加上它以后,每匹配一行就输出一行,适合实时观察。代价是更频繁的系统调用,日志量极大时会增加一点 CPU,但通常可以接受。
需要上下文时加 -C:
tail -n 500 -F /var/log/myapp/app.log | grep --line-buffered -C 3 -E 'ERROR|Exception'
要注意,流式场景下的 -C 只对已经到达的行生效。异常堆栈往往以多行形式出现,grep 按行匹配,可能只命中第一行 Exception,后面的 at ... 不会自动带出来。想保留完整堆栈,要么扩大 -C,要么先不过滤、把原始流写入文件再用 less 查看,要么让应用输出单行结构化日志(JSON)后再用 jq 过滤。
grep 还有几个实用参数:-n 显示行号,便于回到文件定位;--color=auto 在终端高亮匹配;-v 反向过滤,排除健康检查、心跳等噪声;-F 按固定字符串匹配,避免正则元字符干扰;-m 限制匹配数量,适合查历史文件而不是 -f 流式场景。
如果只是想边看边翻历史,less +F 往往比 tail -f | grep 更舒服:
less +F /var/log/myapp/app.log
它先进入跟随模式,按 Ctrl+C 暂停后可以用 / 搜索、上下翻页,再按 Shift+F 回到跟随。缺点是过滤能力弱,适合人工浏览而不是脚本化筛选。
journalctl:按单元、时间和优先级切片
当服务由 systemd 管理时,journalctl 的优势不在“跟随”,而在“带索引地查”。常用入口是单元:
journalctl -u myapp.service -f
这条命令跟随该单元的新日志。查历史时间窗口:
journalctl -u myapp.service \
--since '2024-05-01 09:00:00' \
--until '2024-05-01 10:00:00'
--since 和 --until 也接受相对时间,例如 --since '1 hour ago'、--since today。排障时先锁定时间窗口,再叠加优先级:
journalctl -u myapp.service -p warning --since '30 min ago'
-p 按 syslog 优先级过滤,warning 会包含更严重的级别。注意,应用必须把级别信息以 syslog 优先级或 journal 字段的形式传进来,-p 才有意义;如果应用只是把 WARN 写在消息正文里,-p 不会识别,仍要用 -g 或管道 grep。
较新版本的 journalctl 支持 -g/--grep 对消息做正则过滤:
journalctl -u myapp.service -g 'trace_id=abc123' --since '1 hour ago'
需要结构化输出时用 -o json,再交给 jq:
journalctl -u myapp.service -o json --since '1 hour ago' \
| jq -r 'select(.MESSAGE | contains("timeout")) | [.__REALTIME_TIMESTAMP, .MESSAGE] | @tsv'
这里有两个限定条件:journal 的字段名以 systemd 版本文档为准,不同版本可能有差异;-g 也不是所有旧版本都支持。生产环境先 journalctl --version 确认,或者退回 journalctl ... | grep。
journalctl 还有几个排障时常用的开关:-b 只看本次启动,-b -1 看上一次启动;-k 看内核消息;-n 100 只看最后 100 行;--no-pager 避免进入分页器,便于管道处理;-o short-iso-precise 输出带毫秒的 ISO 时间,便于和其他系统对时;_PID=1234 或 _SYSTEMD_UNIT=... 按字段过滤。
两种方式的盲区
先说文件日志这一侧。tail -F 解决了轮转后的重开问题,但没有解决:历史分散在 .1、.2、.gz 中,跨文件按时间过滤不直观;grep 以行为单位,Java 堆栈、Python traceback 这类多行事件容易被切断;没有统一优先级字段,只能靠正则猜 ERROR、WARN,日志格式一变规则就失效;应用侧如果开了写入缓冲,文件里可能迟迟不出现日志,tail 再快也看不到;在容器里 PID 1 不是应用、日志由 sidecar 收集时,直接 tail 宿主机文件可能跟实际服务不是同一份。
再说 journalctl 这一侧。它也不是“系统里所有日志都能查”:只覆盖进入 journal 的日志,应用自己写文件、没有把 stdout/stderr 交给 systemd 的部分看不到;默认可能不持久化,很多发行版在 /var/log/journal 不存在时只保留运行时日志,重启即丢,需要确认 Storage= 配置;systemd-journald 会对短时间内大量日志做限流,突发流量下可能丢消息,需要查 RateLimitIntervalSec、RateLimitBurst 的配置;旧日志会被 journalctl --vacuum-time、--vacuum-size 或磁盘压力清理,查很久以前的时间窗口可能已经没有数据;journal 记录的接收时间与应用自己打印的时间可能不同,跨时区、批量导入、容器时钟偏差时,--since/--until 的边界要以实际字段为准;macOS、多数容器基础镜像、Windows 不适用,这时还是文件加 grep。
一条可执行的决策路径
可以把选择压缩成下面几步:
- 先问服务是不是 systemd 单元。是,先试
journalctl -u <unit> -f;不是,直接看应用配置的日志文件。 - 需要实时跟还是按时间回看。实时跟:文件用
tail -F,journal 用journalctl -f。按时间回看:journal 用--since/--until;文件用grep按时间前缀,或用awk/sed按格式提取。 - 需要按优先级、单元、PID 过滤吗。需要,优先
journalctl的-p、-u、_PID=;不需要,文件侧grep更直接。 - 需要结构化字段吗。需要,
journalctl -o json配jq;文件侧只能靠应用输出 JSON 再jq。 - 日志是否被轮转、压缩、限流、清理。文件侧注意
tail -F和.gz;journal 侧注意持久化、rate limit 和 vacuum。
一个实用的组合是:先用 journalctl 锁定“哪个时间点、哪个单元、什么级别”,再用文件日志看应用内部的详细上下文。两者不是替代关系,而是索引和正文的关系。
几个可以直接套用的命令
实时看服务错误,排除健康检查噪声:
journalctl -u myapp.service -f -p warning
文件日志实时过滤,保留前后文:
tail -n 300 -F /var/log/myapp/app.log \
| grep --line-buffered -C 2 -E 'ERROR|Exception|timeout'
查某个请求 ID 在 journal 中的完整轨迹:
journalctl -u myapp.service --since '2 hours ago' --no-pager \
| grep 'request_id=abc123'
查文件日志中某个小时段的记录(假设行首是 2024-05-01 14: 这样的时间):
grep '^2024-05-01 14:' /var/log/myapp/app.log
如果要精确到分钟,grep 就不够了,需要按日志格式写 awk 条件。没有通用的一行命令能适配所有日志格式,这也是文件日志始终需要“知道格式”的原因。
最后提醒一句:追日志时先保留原始流。过滤命令适合定位,不适合作为唯一证据;把 tail -F 的完整输出重定向到临时文件,或者用 tee 留一份,再在副本上 grep,排障结束后会省很多事。
tail -n 500 -F /var/log/myapp/app.log | tee /tmp/myapp-follow.log
这样即使 grep 规则漏掉了关键行,原始记录还在。
评论
参与讨论
请保持友善,共同维护良好的讨论环境。
讨论列表
0 条评论