feat(log): B-03 日志轮转配置 - 添加 lumberjack 支持

- 添加 lumberjack.v2 依赖实现日志轮转
- 支持配置文件输出(stdout/stderr/file)
- 支持文件轮转(100MB/3备份/7天/压缩)
- 添加 Config 结构体灵活配置
- 添加完整测试用例

测试验证:
- TestInitWithConfig PASS
- TestInitWithConfigFileOutput PASS
- TestDefaultConfig PASS
- 全量日志测试通过
This commit is contained in:
phamnazage-jpg
2026-06-01 22:06:56 +08:00
parent 714c4acbe4
commit cf7dd35e1d
4 changed files with 120 additions and 2 deletions

1
go.mod
View File

@@ -13,6 +13,7 @@ require (
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
lukechampine.com/uint128 v1.1.1 // indirect
modernc.org/cc/v3 v3.36.0 // indirect
modernc.org/ccgo/v3 v3.16.8 // indirect

2
go.sum
View File

@@ -42,6 +42,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.36.0 h1:0kmRkTmqNidmu3c7BNDSdVHCxXCkWLmWmCIVX4LUboo=

View File

@@ -9,10 +9,35 @@ import (
"time"
"log/slog"
"gopkg.in/natefinch/lumberjack.v2"
)
var logger *slog.Logger
// Config holds logging configuration
type Config struct {
Level string
Output string // "stdout", "stderr", or file path
Rotation bool // enable file rotation
MaxSize int // MB
MaxBackups int
MaxAge int // days
Compress bool
}
// DefaultConfig returns default logging configuration
func DefaultConfig() Config {
return Config{
Level: "INFO",
Output: "stdout",
Rotation: false,
MaxSize: 100,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
}
}
// sensitiveFields contains field names that should be sanitized in logs
var sensitiveFields = []string{
"token",
@@ -35,14 +60,47 @@ func Init() {
// InitWithLevel initializes the logger with specified level
func InitWithLevel(level string) {
levelVar := parseLevel(level)
cfg := DefaultConfig()
cfg.Level = level
InitWithConfig(cfg)
}
// InitWithConfig initializes the logger with full configuration
func InitWithConfig(cfg Config) {
levelVar := parseLevel(cfg.Level)
opts := &slog.HandlerOptions{
Level: levelVar,
ReplaceAttr: sanitizeAttrs,
}
handler := slog.NewJSONHandler(os.Stdout, opts)
var handler slog.Handler
switch cfg.Output {
case "stdout":
handler = slog.NewJSONHandler(os.Stdout, opts)
case "stderr":
handler = slog.NewJSONHandler(os.Stderr, opts)
default:
// File output with optional rotation
if cfg.Rotation {
lumberjackLogger := &lumberjack.Logger{
Filename: cfg.Output,
MaxSize: cfg.MaxSize,
MaxBackups: cfg.MaxBackups,
MaxAge: cfg.MaxAge,
Compress: cfg.Compress,
}
handler = slog.NewJSONHandler(lumberjackLogger, opts)
} else {
file, err := os.OpenFile(cfg.Output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
panic(err)
}
handler = slog.NewJSONHandler(file, opts)
}
}
logger = slog.New(handler)
slog.SetDefault(logger)
}

View File

@@ -2,6 +2,7 @@ package log
import (
"log/slog"
"os"
"testing"
)
@@ -130,3 +131,59 @@ func TestRequestLogger(t *testing.T) {
t.Error("RequestLogger should not return nil")
}
}
func TestInitWithConfig(t *testing.T) {
// Test with stdout output
cfg := DefaultConfig()
cfg.Output = "stdout"
cfg.Level = "DEBUG"
InitWithConfig(cfg)
if logger == nil {
t.Error("logger should not be nil after InitWithConfig")
}
}
func TestInitWithConfigFileOutput(t *testing.T) {
// Test with file output (no rotation)
tmpFile := t.TempDir() + "/test.log"
cfg := DefaultConfig()
cfg.Output = tmpFile
cfg.Rotation = false
InitWithConfig(cfg)
Info("test message for file")
// Verify file was created
if _, err := os.Stat(tmpFile); os.IsNotExist(err) {
t.Errorf("log file %s should exist", tmpFile)
}
}
func TestDefaultConfig(t *testing.T) {
cfg := DefaultConfig()
if cfg.Level != "INFO" {
t.Errorf("default Level = %s, want INFO", cfg.Level)
}
if cfg.Output != "stdout" {
t.Errorf("default Output = %s, want stdout", cfg.Output)
}
if cfg.MaxSize != 100 {
t.Errorf("default MaxSize = %d, want 100", cfg.MaxSize)
}
if cfg.MaxBackups != 3 {
t.Errorf("default MaxBackups = %d, want 3", cfg.MaxBackups)
}
if cfg.MaxAge != 7 {
t.Errorf("default MaxAge = %d, want 7", cfg.MaxAge)
}
if !cfg.Compress {
t.Error("default Compress should be true")
}
}