和青山 奏江河
我知青山江河乐
抚琴为人 无人知我乐
“洋洋兮又复巍峨”
来客忽笑我
声声所念 来人皆可得
徒余留 明月忆往昔
温酒会知音
借问人间 知我者能有几
三尺瑶琴碎骨兮
似绝弦断悲心
孑然一身 苍茫天地兮
🎵 国风堂、哦漏《知我》
在大型 Go 服务中,日志既是排查故障的利器,也是监控与审计的基础。本文带你从零实现一个基于 slog
与 lumberjack
的高性能、可分割、可扩展的结构化日志库,支持 JSON 与文本格式、多级别、调用者信息和文件切割。
一、设计目标
- 结构化输出:内置
JSON
与Text
两种格式,可选切换。 - 日志分割:集成
lumberjack
,支持按大小/备份/天数/压缩自动切割。 - 多级别控制:动态调整日志级别,无需重启服务。
- 调用者定位:可选打印调用者文件名和行号,方便快速定位问题。
- 零侵入初始化:全局单例
Log
,只需调用一次Init
即可使用。
二、核心组件与代码解析
var (
Log *slog.Logger
once sync.Once
handler slog.Handler
level slog.Level = slog.LevelInfo
)
- once 保证全局初始化只执行一次。
- Log 为全局可用的 *slog.Logger 实例。
- handler 与 level 支持后续动态调整。
type Options struct {
Level slog.Level // 日志级别
Format string // "json" 或 "text"
File string // 日志文件路径
MaxSize int // 单文件大小(MB)
MaxBackups int // 保留旧文件数量
MaxAge int // 保留旧文件天数
Compress bool // 是否压缩旧文件
AddSource bool // 是否打印调用者信息
}
- Options 结构体集中所有可配置项,一次性传入 Init。
- 默认 Level=Info、Format=json、不指定 File 则输出到 stdout。
func Init(opts Options) {
once.Do(func() {
// 1. 规范化 opts
// 2. 构造 io.Writer:stdout 或 lumberjack.Logger
// 3. 根据 Format 创建 JSON 或 Text Handler
Log = slog.New(handler)
level = opts.Level
})
}
- 调用 Init 时根据 Options 构建底层 io.Writer。
- lumberjack.Logger 自动完成文件滚动与压缩。
- slog.NewJSONHandler 与 slog.NewTextHandler 切换格式。
三、动态级别与调用者追踪
func SetLevel(l slog.Level) {
// 重新 Init 一次,更新 handlerOptions.Level
}
- 运行时任意位置调用 logger.SetLevel(slog.LevelDebug) 即可开启 Debug 日志,适合线上临时诊断。
func logWithCaller(level slog.Level, msg string, args ...any) {
pc, _, _, ok := runtime.Caller(2)
r := slog.NewRecord(time.Now(), level, msg, pc)
r.Add(args...)
Log.Handler().Handle(context.Background(), r)
}
- 手动获取 runtime.Caller(2),将实际调用位置(文件+行号)注入到 Record 中。
- 无论 JSON 还是 Text,都可以选配 AddSource=true 输出 source":“file.go:123” 字段。
四、常用快捷方法
func Info(msg string, args ...any) { logWithCaller(slog.LevelInfo, msg, args...) }
func Debug(msg string, args ...any) { logWithCaller(slog.LevelDebug, msg, args...) }
// 同时支持携带 context 的 InfoCtx/DebugCtx/…
- 统一入口,使用 logger.Info(“开始处理”, “user”, userID) 既能输出结构化字段,也能打印调用者位置。
五、关闭与资源清理
func Close() error {
if logFile != nil {
return logFile.Close()
}
return nil
}
- 若启用文件输出,可在程序退出前调用 logger.Close() 确保文件句柄释放。
六、完整代码
package logger
import (
"context"
"io"
"log/slog"
"os"
"runtime"
"sync"
"time"
"gopkg.in/natefinch/lumberjack.v2"
)
var (
Log *slog.Logger
once sync.Once
logFile *os.File
handler slog.Handler
level slog.Level = slog.LevelInfo // 默认级别
)
// Options 用于配置日志模块的参数
type Options struct {
Level slog.Level // 日志级别
Format string // "json" 或 "text"
File string // 日志文件路径,若为空则输出到标准输出
MaxSize int // 单个日志文件最大(MB),仅文件输出时有效
MaxBackups int // 保留的旧日志文件数
MaxAge int // 保留旧日志文件的最大天数
Compress bool // 是否压缩旧日志文件
AddSource bool // 是否添加调用者信息
}
// 默认配置
func defaultOptions() Options {
return Options{
Level: slog.LevelInfo,
Format: "json",
AddSource: false,
File: "",
MaxSize: 100,
MaxBackups: 7,
MaxAge: 30,
Compress: false,
}
}
// Init 进行一次性初始化
func Init(opts Options) {
once.Do(func() {
if opts.Format == "" {
opts.Format = "json"
}
if opts.Level == 0 {
opts.Level = slog.LevelInfo
}
var output io.Writer
if opts.File != "" {
// 使用 lumberjack 实现日志分割
output = &lumberjack.Logger{
Filename: opts.File,
MaxSize: opts.MaxSize,
MaxBackups: opts.MaxBackups,
MaxAge: opts.MaxAge,
Compress: opts.Compress,
}
} else {
output = os.Stdout
}
handlerOptions := &slog.HandlerOptions{
Level: opts.Level,
AddSource: opts.AddSource,
}
if opts.Format == "text" {
handler = slog.NewTextHandler(output, handlerOptions)
} else {
handler = slog.NewJSONHandler(output, handlerOptions)
}
Log = slog.New(handler)
level = opts.Level
})
}
// 自动初始化
func ensureInit() {
if Log == nil {
Init(defaultOptions())
}
}
// 动态设置日志级别
func SetLevel(l slog.Level) {
ensureInit()
level = l
if handler != nil {
// 重新设置 handler 的 level
// 只能通过重新初始化 handler 实现
opts := defaultOptions()
opts.Level = l
Init(opts)
}
}
// logWithCaller 是内部方法,用于记录带正确调用者信息的日志
func logWithCaller(level slog.Level, msg string, args ...any) {
ensureInit()
if !Log.Enabled(context.Background(), level) {
return
}
var pc uintptr
var ok bool
// 跳过 2 层调用栈:logWithCaller -> Info/Debug/etc -> 实际调用位置
pc, _, _, ok = runtime.Caller(2)
if !ok {
Log.Log(context.Background(), level, msg, args...)
return
}
// 手动创建 Record 并设置正确的 PC
r := slog.NewRecord(time.Now(), level, msg, pc)
r.Add(args...)
Log.Handler().Handle(context.Background(), r)
}
// logWithCallerCtx 是内部方法,用于记录带正确调用者信息和 context 的日志
func logWithCallerCtx(ctx context.Context, level slog.Level, msg string, args ...any) {
ensureInit()
if !Log.Enabled(ctx, level) {
return
}
var pc uintptr
var ok bool
// 跳过 2 层调用栈:logWithCallerCtx -> InfoCtx/DebugCtx/etc -> 实际调用位置
pc, _, _, ok = runtime.Caller(2)
if !ok {
Log.Log(ctx, level, msg, args...)
return
}
// 手动创建 Record 并设置正确的 PC
r := slog.NewRecord(time.Now(), level, msg, pc)
r.Add(args...)
Log.Handler().Handle(ctx, r)
}
// 快捷方法
func Info(msg string, args ...any) {
logWithCaller(slog.LevelInfo, msg, args...)
}
func Debug(msg string, args ...any) {
logWithCaller(slog.LevelDebug, msg, args...)
}
func Warn(msg string, args ...any) {
logWithCaller(slog.LevelWarn, msg, args...)
}
func Error(msg string, args ...any) {
logWithCaller(slog.LevelError, msg, args...)
}
// 支持 context
func InfoCtx(ctx context.Context, msg string, args ...any) {
logWithCallerCtx(ctx, slog.LevelInfo, msg, args...)
}
func DebugCtx(ctx context.Context, msg string, args ...any) {
logWithCallerCtx(ctx, slog.LevelDebug, msg, args...)
}
func WarnCtx(ctx context.Context, msg string, args ...any) {
logWithCallerCtx(ctx, slog.LevelWarn, msg, args...)
}
func ErrorCtx(ctx context.Context, msg string, args ...any) {
logWithCallerCtx(ctx, slog.LevelError, msg, args...)
}
// Close 用于关闭日志文件(若已打开)
func Close() error {
if logFile != nil {
return logFile.Close()
}
return nil
}
总结
本文介绍了一个完整的 Go 结构化日志库,结合了 slog 的轻量性能和 lumberjack 的文件分割能力,支持:
- JSON/Text 可选格式
- 按大小/备份/天数切割日志
- 动态调整日志级别
- 精准调用者信息追踪
- 零侵入全局单例
在实际项目中,只需在 main 中:
logger.Init(logger.Options{
Level: slog.LevelDebug,
Format: "text",
File: "/var/log/myapp.log",
MaxSize: 200,
MaxBackups: 10,
MaxAge: 7,
Compress: true,
AddSource: true,
})
defer logger.Close()
logger.Log.Info("服务启动", "port", 8080)
logger.Log.Info("Starting application...")
即可获得一套高可用、高性能、易监控的日志方案。