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

为什么不用 log 包直接写文件
Go 标准库的 log 包默认输出到 os.Stderr,即使重定向到文件,也缺乏轮转、压缩、并发安全写入等生产必需能力。直接用 log.SetOutput() 写单个大文件,几天后可能生成 GB 级日志,无法按天切分,tail -f 查看会卡死,运维排查时连最近 1 小时的日志都找不到。
- 不支持按大小或时间自动切割(如每天一个
app-2024-06-15.log) - 多 goroutine 并发写同一文件时,需手动加锁,否则日志行错乱或丢失
- 无归档压缩(
.log.gz)、保留天数控制(只留最近 7 天) - 没有结构化输出支持(JSON 格式字段对 ELK 友好)
用 lumberjack 实现带轮转的文件写入
lumberjack 是最轻量且稳定的日志切割方案,被 gin、echo 等框架广泛采用。它本身不处理日志格式或级别,只专注「把日志安全地写进带轮转的文件」,可无缝对接标准 log 或 zap。
- 必须用
io.MultiWriter组合多个输出目标(例如同时写文件 + 控制台) -
lumberjack.Logger的MaxSize单位是 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 日志工具本身不负责采集其他进程日志——那是 filebeat 或 fluent-bit 的职责。但你可以用 Go 写一个轻量代理:监听日志文件变化(inotify/fsevents),读取新增行,打上服务标签后转发到 Kafka/HTTP 接口。
立即学习“go语言免费学习笔记(深入)”;
- 别用
os.Open()+Seek(0, io.SeekEnd)模拟tail -f——文件被 logrotate 切走时句柄失效,会丢日志 - 推荐用
fsnotify监听目录,配合os.Stat()检查 inode 是否变更,再重新打开新文件 - 每行日志必须加时间戳(用系统当前时间,不是解析日志里的字符串时间),否则跨时区或日志延迟写入会导致排序错乱
真正上线时,90% 的团队不会自己造这个轮子,而是用 filebeat 配置 type: filestream + processors 做字段提取。Go 写的采集器只适合嵌入式设备或定制协议场景。










