选 fsnotify + tail 模式最稳:监听 write/create/chmod/moved_to 事件,每次读前 stat 比对 inode,seek(0, io.seekend) 定位末尾,避免 logrotate 丢失;agent 自身日志须与采集日志分离,禁用 sdk 和 log.setoutput。

Go 日志收集 Agent 怎么选核心库
直接用 fsnotify 监听文件变化 + tail 模式读取追加内容,是轻量日志采集最稳的路。别碰那些封装过重的“日志 SDK”,它们常自带缓冲、重试、队列,反而掩盖行尾截断、inode 复用、权限突变等真实问题。
常见错误现象:tail -f 能看到新日志,Agent 却卡住不动;或某次重启后漏掉几百行;甚至日志轮转(logrotate)后彻底丢失后续内容。
-
fsnotify只监听WRITE和CREATE事件不够——必须同时响应CHMOD(权限变更)和MOVED_TO(logrotate 触发的 rename) - 用
os.OpenFile(path, os.O_RDONLY|os.O_APPEND, 0)打开文件是错的——O_APPEND对只读采集无意义,且可能触发内核缓存异常 - 每次读取后必须调用
file.Seek(0, io.SeekEnd)定位到末尾,否则Read会从开头重复读
怎么安全处理 logrotate 场景
logrotate 默认用 rename + create new,旧文件 inode 改变但路径还在,Agent 若只认路径不认 inode,就会继续读一个已关闭的 fd,导致阻塞或静默失败。
正确做法:用 os.Stat() 每次读前比对当前文件的 dev/inode 是否与打开时一致。不一致就关闭旧 fd,按新路径重新 open —— 这是绕过 rename 陷阱的最小成本方案。
立即学习“go语言免费学习笔记(深入)”;
- 不要依赖
filepath.Base()做文件名匹配(如app.log.1→app.log),logrotate 的dateext或自定义命名会让规则失效 - 避免在
rotated后立刻重读旧文件——它可能正被 gzip 压缩,open会返回permission denied - ELK 场景下,建议在日志行里注入
rotation_id字段(如{"rotation_id":"20240520-152344"}),方便 Kibana 中关联分析
对接 Loki 时 label 设计的关键约束
Loki 不存日志内容,全靠 label 索引。Agent 发送前必须把能区分来源的维度固化为 label,比如 job、host、container_id,否则查不到数据。
常见错误现象:Loki UI 显示 “no logs found”,logcli query '{job="myapp"}' 返回空,但 curl -s http://loki:3100/loki/api/v1/labels 确实有 job 键——说明 label 没打进去,或格式非法(含空格、大写字母、特殊符号)。
- label 值不能含
=、{、}、,、",建议统一用strings.Map过滤非字母数字下划线 - 不要把整条日志当 label(如
message="..."),Loki 会拒绝写入;message 必须走streambody - HTTP POST 到
/loki/api/v1/push时,body 必须是Content-Type: application/json,且 timestamp 字段需是纳秒级整数(不是 RFC3339 字符串)
为什么不用 go-kit/log 或 zerolog 做采集日志输出
Agent 自身运行日志(比如 “failed to connect to Loki”)必须和采集的日志严格分离。用 zerolog.New(os.Stderr) 输出到 stderr 是对的,但千万别把它和采集管道混在一起——否则采集器崩溃时,你连哪条日志触发 panic 都看不到。
性能影响明显:zerolog 默认带时间戳、调用栈、字段结构化,每秒万级日志线程里做 JSON 序列化,CPU 占用翻倍;而采集场景只需纯文本 + 行号 + 时间(用 time.Now().UnixNano() 就够)。
- Agent 自身日志用
fmt.Fprintf(os.Stderr, "[%d] %s\n", time.Now().UnixMilli(), msg)足够,简单、无依赖、易 grep - 禁止用
log.SetOutput()把标准 log 导向采集管道——这等于让 Agent 日志参与转发,造成无限递归或格式污染 - 如果必须结构化 Agent 日志(如上报健康状态),单独起 goroutine + channel +
json.Encoder控制吞吐,别和采集主循环共享资源
真正难的不是发出去,是发出去之后谁来保证不丢、不乱序、不重复。Loki 的 push 接口无 ack,ELK 的 bulk API 有 partial failure,这些都得靠 Agent 自己补——比如内存 buffer + checkpoint file + 基于 offset 的幂等重发。但这部分没标准解,得看你容忍哪类丢失。










