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:
1
go.mod
1
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user