【Go】高性能结构化日志库设计与实现


和青山 奏江河
我知青山江河乐
抚琴为人 无人知我乐
“洋洋兮又复巍峨”
来客忽笑我
声声所念 来人皆可得
徒余留 明月忆往昔
温酒会知音
借问人间 知我者能有几
三尺瑶琴碎骨兮
似绝弦断悲心
孑然一身 苍茫天地兮
                     🎵 国风堂、哦漏《知我》


在大型 Go 服务中,日志既是排查故障的利器,也是监控与审计的基础。本文带你从零实现一个基于 sloglumberjack 的高性能、可分割、可扩展的结构化日志库,支持 JSON 与文本格式、多级别、调用者信息和文件切割。


一、设计目标
  1. 结构化输出:内置 JSONText 两种格式,可选切换。
  2. 日志分割:集成 lumberjack,支持按大小/备份/天数/压缩自动切割。
  3. 多级别控制:动态调整日志级别,无需重启服务。
  4. 调用者定位:可选打印调用者文件名和行号,方便快速定位问题。
  5. 零侵入初始化:全局单例 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...")

即可获得一套高可用、高性能、易监控的日志方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值