真正可靠的日志增量采集必须基于操作系统事件,fsnotify 跨平台封装 inotify/FSEvents;需监听目录、区分 inode、安全读取、处理轮转、持久化偏移量。

如何用 fsnotify 监控日志文件增量变化
直接轮询 os.Stat() 查大小或修改时间,既浪费 CPU 又容易漏掉秒级内多次写入的日志行。真正可靠的增量采集必须基于操作系统事件——Linux 用 inotify,macOS 用 FSEvents,fsnotify 这个库做了跨平台封装,是 Go 日志采集的起点。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 初始化时对日志文件所在目录调用
watcher.Add(),不是对单个文件(因为日志轮转后新文件不在原路径) - 监听
fsnotify.Write和fsnotify.Create事件:前者捕获追加写入,后者捕获轮转后的新文件生成 - 收到
Create事件后,需用os.SameFile()判断是否为同一 inode(防止重命名误判),再决定是否切换读取句柄 - 避免在事件回调里做耗时操作(如网络发送),应把文件名/偏移量发到 channel,由 worker goroutine 处理
如何安全读取正在被写入的日志文件
直接 os.Open() + bufio.Scanner 会卡住或丢行——因为日志进程可能用 O_APPEND 写入,而 Go 的 Scanner 默认按行缓冲,遇到未结束的行就阻塞。必须自己控制读取节奏和偏移位置。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
os.OpenFile(path, os.O_RDONLY, 0)打开,配合io.Seek()跳到上次记录的偏移量(如从offset.txt读取) - 每次读取用
bufio.NewReader().ReadBytes('\n'),而非Scan();遇到 EOF 立即返回,不等待新行 - 每成功读取一行,立刻更新本地偏移量(
file.Seek(0, io.SeekCurrent)获取当前位置),并原子写入持久化存储 - 若日志行超长(如 JSON 日志含大字段),需设足够大的
bufio.Reader缓冲区(默认 4KB 不够)
如何处理日志轮转(logrotate)场景
典型错误是监听旧文件句柄,轮转后新文件产生但采集器仍读空文件。本质问题是没区分“文件路径”和“文件实体”。logrotate 默认用 rename(),inode 不变;但启用 copytruncate 时旧文件被清空、新文件新建,inode 改变。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 持续检查当前打开文件的
os.File.Stat().Sys().(*syscall.Stat_t).Ino,与os.Stat(path)获取的 inode 对比 - 发现 inode 不一致,且新文件 size > 0 → 触发重新打开,从 offset 0 开始读(避免跳过轮转后首行)
- 若使用
copytruncate,旧文件 size 归零但句柄仍有效,此时应主动file.Close()并放弃该句柄 - 不要依赖文件名后缀(如
.1、.2)判断轮转,logrotate配置可任意定制
为什么不用 tail -f 或现成工具(如 filebeat)
不是不能用,而是当你要嵌入采集逻辑(比如过滤敏感字段、打标、对接私有协议)或资源受限(只跑一个极轻量二进制)时,调外部命令或引入重型 agent 就成了负担。Go 编译出的静态二进制,本身已满足多数边缘采集需求。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 若只是转发到 Kafka/Elasticsearch,优先用
filebeat——它内置背压、重试、ACK 机制,fsnotify+ 自研代码很难做到同等健壮 - 若需在采集链路中做正则脱敏、字段提取、采样降频,Go 自研更可控,但必须实现至少两级 buffer(内存 channel + 本地磁盘 checkpoint)防丢日志
- 注意
fsnotify在 inotify 资源耗尽时静默失败(too many open files),需监控/proc/sys/fs/inotify/max_user_watches并预设 fallback 轮询
真正的难点从来不在“怎么读到新行”,而在于“怎么确保每一行只被发一次,且不因进程重启、磁盘满、网络抖动而丢失”。偏移量持久化、inode 校验、事件去重,这三个点漏掉任何一个,上线后都会变成半夜告警。

















