Go语言实现日志收集工具_Go日志项目实战示例

Go标准库log包缺乏轮转、压缩、并发安全等生产级能力,需用lumberjack实现文件切割,或用zap输出结构化JSON日志;跨进程日志采集应交由filebeat等专用工具。

为什么不用 log 包直接写文件

Go 标准库的 log 包默认输出到 os.Stderr,即使重定向到文件,也缺乏轮转、压缩、并发安全写入等生产必需能力。直接用 log.SetOutput() 写单个大文件,几天后可能生成 GB 级日志,无法按天切分,tail -f 查看会卡死,运维排查时连最近 1 小时的日志都找不到。

  • 不支持按大小或时间自动切割(如每天一个 app-2025-06-15.log
  • 多 goroutine 并发写同一文件时,需手动加锁,否则日志行错乱或丢失
  • 无归档压缩(.log.gz)、保留天数控制(只留最近 7 天)
  • 没有结构化输出支持(JSON 格式字段对 ELK 友好)

lumberjack 实现带轮转的文件写入

lumberjack 是最轻量且稳定的日志切割方案,被 ginecho 等框架广泛采用。它本身不处理日志格式或级别,只专注「把日志安全地写进带轮转的文件」,可无缝对接标准 logzap

  • 必须用 io.MultiWriter 组合多个输出目标(例如同时写文件 + 控制台)
  • lumberjack.LoggerMaxSize 单位是 MB,不是字节;MaxAge 单位是天,不是小时
  • 注意关闭:程序退出前调用 lumberjackLogger.Close(),否则最后一批日志可能未 flush
import (
    "io"
    "log"
    "os"
    "gopkg.in/natefinch/lumberjack.v2"
)

func setupFileLogger() *log.Logger {
    lumberjackLogger := &lumberjack.Logger{
        Filename:   "logs/app.log",
        MaxSize:    10, // MB
        MaxBackups: 7,
        MaxAge:     30, // days
        Compress:   true,
    }
    return log.New(
        io.MultiWriter(lumberjackLogger, os.Stdout),
        "[INFO] ",
        log.Ldate | log.Ltime | log.Lshortfile,
    )
}

zap 输出结构化 JSON 日志

当需要对接 Prometheus、ELK 或做字段级过滤(如查所有 "status":500 请求),纯文本日志效率极低。zap 是 Go 生态事实标准的高性能结构化日志库,比标准 log 快 4–10 倍,且原生支持 JSON 输出。

  • 避免用 zap.Any() 传 map 或 struct——它会反射序列化,性能差且不可控;应显式用 zap.String("key", value)
  • zap.NewProduction() 默认禁用 caller 和 stacktrace,调试时建议用 zap.NewDevelopment() 或自定义 Config
  • 若用 lumberjack 做输出,需包装成 zapcore.WriteSyncer:用 zapcore.AddSync() 转换
import (
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
    "gopkg.in/natefinch/lumberjack.v2"
)

func newZapLogger() (*zap.Logger, error) {
    lumberjackWriter 

:= zapcore.AddSync(&lumberjack.Logger{ Filename: "logs/app.json.log", MaxSize: 10, MaxBackups: 7, MaxAge: 30, Compress: true, }) encoderConfig := zap.NewProductionEncoderConfig() encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder encoder := zapcore.NewJSONEncoder(encoderConfig) core := zapcore.NewCore(encoder, lumberjackWriter, zapcore.InfoLevel) return zap.New(core), nil }

采集非本进程日志(如 Nginx、MySQL)的可行路径

Go 日志工具本身不负责采集其他进程日志——那是 filebeatfluent-bit 的职责。但你可以用 Go 写一个轻量代理:监听日志文件变化(inotify/fsevents),读取新增行,打上服务标签后转发到 Kafka/HTTP 接口。

  • 别用 os.Open() + Seek(0, io.SeekEnd) 模拟 tail -f——文件被 logrotate 切走时句柄失效,会丢日志
  • 推荐用 fsnotify 监听目录,配合 os.Stat() 检查 inode 是否变更,再重新打开新文件
  • 每行日志必须加时间戳(用系统当前时间,不是解析日志里的字符串时间),否则跨时区或日志延迟写入会导致排序错乱

真正上线时,90% 的团队不会自己造这个轮子,而是用 filebeat 配置 type: filestream + processors 做字段提取。Go 写的采集器只适合嵌入式设备或定制协议场景。