使用Golang实现简单的日志收集Agent_对接ELK与Loki

2次阅读

选 fsnotify + tail 模式最稳:监听 WRITE/CREATE/CHMOD/MOVED_TO 事件,每次读前 stat 比对 inode,Seek(0, io.SeekEnd) 定位末尾,避免 logrotate 丢失;Agent 自身日志须与采集日志分离,禁用 SDK 和 log.SetOutput。

使用Golang实现简单的日志收集Agent_对接ELK与Loki

go 日志收集 Agent 怎么选核心库

直接用 fsnotify 监听文件变化 + tail 模式读取追加内容,是轻量日志采集最稳的路。别碰那些封装过重的“日志 SDK”,它们常自带缓冲、重试、队列,反而掩盖行尾截断、inode 复用、权限突变等真实问题。

常见错误现象:tail -f 能看到新日志,Agent 却卡住不动;或某次重启后漏掉几百行;甚至日志轮转(logrotate)后彻底丢失后续内容。

  • fsnotify 只监听 WRITECREATE 事件不够——必须同时响应 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.1app.log),logrotate 的 dateext 或自定义命名会让规则失效
  • 避免在 rotated 后立刻重读旧文件——它可能正被 gzip 压缩,open 会返回 permission denied
  • elk 场景下,建议在日志行里注入 rotation_id 字段(如 {"rotation_id":"20240520-152344"}),方便 Kibana 中关联分析

对接 Loki 时 label 设计的关键约束

Loki 不存日志内容,全靠 label 索引。Agent 发送前必须把能区分来源的维度固化为 label,比如 jobhostcontainer_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 必须走 stream body
  • 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] %sn", 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 的幂等重发。但这部分没标准解,得看你容忍哪类丢失。

text=ZqhQzanResources