备份一下

This commit is contained in:
2025-11-11 15:53:01 +08:00
parent e98dd4d725
commit 39c5b524a9
10 changed files with 4295 additions and 63 deletions

518
pkg/logger/logger_test.go Normal file
View File

@@ -0,0 +1,518 @@
package logger
import (
"os"
"path/filepath"
"testing"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// TestInitLoggers 测试日志初始化T026
func TestInitLoggers(t *testing.T) {
// 创建临时目录用于日志文件
tempDir := t.TempDir()
tests := []struct {
name string
level string
development bool
appLogConfig LogRotationConfig
accessLogConfig LogRotationConfig
wantErr bool
validateFunc func(t *testing.T)
}{
{
name: "production mode with info level",
level: "info",
development: false,
appLogConfig: LogRotationConfig{
Filename: filepath.Join(tempDir, "app-prod.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
},
accessLogConfig: LogRotationConfig{
Filename: filepath.Join(tempDir, "access-prod.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
},
wantErr: false,
validateFunc: func(t *testing.T) {
if appLogger == nil {
t.Error("appLogger should not be nil")
}
if accessLogger == nil {
t.Error("accessLogger should not be nil")
}
// 写入一条日志以触发文件创建
GetAppLogger().Info("test log creation")
Sync()
// 验证日志文件创建
if _, err := os.Stat(filepath.Join(tempDir, "app-prod.log")); os.IsNotExist(err) {
t.Error("app log file should be created after writing")
}
},
},
{
name: "development mode with debug level",
level: "debug",
development: true,
appLogConfig: LogRotationConfig{
Filename: filepath.Join(tempDir, "app-dev.log"),
MaxSize: 5,
MaxBackups: 2,
MaxAge: 3,
Compress: false,
},
accessLogConfig: LogRotationConfig{
Filename: filepath.Join(tempDir, "access-dev.log"),
MaxSize: 5,
MaxBackups: 2,
MaxAge: 3,
Compress: false,
},
wantErr: false,
validateFunc: func(t *testing.T) {
if appLogger == nil {
t.Error("appLogger should not be nil in dev mode")
}
if accessLogger == nil {
t.Error("accessLogger should not be nil in dev mode")
}
},
},
{
name: "warn level logging",
level: "warn",
development: false,
appLogConfig: LogRotationConfig{
Filename: filepath.Join(tempDir, "app-warn.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
},
accessLogConfig: LogRotationConfig{
Filename: filepath.Join(tempDir, "access-warn.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
},
wantErr: false,
validateFunc: func(t *testing.T) {
if appLogger == nil {
t.Error("appLogger should not be nil")
}
},
},
{
name: "error level logging",
level: "error",
development: false,
appLogConfig: LogRotationConfig{
Filename: filepath.Join(tempDir, "app-error.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
},
accessLogConfig: LogRotationConfig{
Filename: filepath.Join(tempDir, "access-error.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
},
wantErr: false,
validateFunc: func(t *testing.T) {
if appLogger == nil {
t.Error("appLogger should not be nil")
}
},
},
{
name: "invalid level defaults to info",
level: "invalid",
development: false,
appLogConfig: LogRotationConfig{
Filename: filepath.Join(tempDir, "app-invalid.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
},
accessLogConfig: LogRotationConfig{
Filename: filepath.Join(tempDir, "access-invalid.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
},
wantErr: false,
validateFunc: func(t *testing.T) {
if appLogger == nil {
t.Error("appLogger should not be nil even with invalid level")
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := InitLoggers(tt.level, tt.development, tt.appLogConfig, tt.accessLogConfig)
if (err != nil) != tt.wantErr {
t.Errorf("InitLoggers() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.validateFunc != nil {
tt.validateFunc(t)
}
})
}
}
// TestGetAppLogger 测试获取应用日志记录器T026
func TestGetAppLogger(t *testing.T) {
// 创建临时目录
tempDir := t.TempDir()
tests := []struct {
name string
setupFunc func()
wantNil bool
}{
{
name: "after initialization",
setupFunc: func() {
InitLoggers("info", false,
LogRotationConfig{
Filename: filepath.Join(tempDir, "app-get.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
},
LogRotationConfig{
Filename: filepath.Join(tempDir, "access-get.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
},
)
},
wantNil: false,
},
{
name: "before initialization returns nop logger",
setupFunc: func() {
// 重置全局变量
appLogger = nil
},
wantNil: false, // GetAppLogger 应该返回 nop logger不是 nil
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.setupFunc()
logger := GetAppLogger()
if logger == nil {
t.Error("GetAppLogger() should never return nil, should return nop logger instead")
}
})
}
}
// TestGetAccessLogger 测试获取访问日志记录器T028
func TestGetAccessLogger(t *testing.T) {
// 创建临时目录
tempDir := t.TempDir()
tests := []struct {
name string
setupFunc func()
wantNil bool
}{
{
name: "after initialization",
setupFunc: func() {
InitLoggers("info", false,
LogRotationConfig{
Filename: filepath.Join(tempDir, "app-access.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
},
LogRotationConfig{
Filename: filepath.Join(tempDir, "access-access.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
},
)
},
wantNil: false,
},
{
name: "before initialization returns nop logger",
setupFunc: func() {
// 重置全局变量
accessLogger = nil
},
wantNil: false, // GetAccessLogger 应该返回 nop logger不是 nil
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.setupFunc()
logger := GetAccessLogger()
if logger == nil {
t.Error("GetAccessLogger() should never return nil, should return nop logger instead")
}
})
}
}
// TestSync 测试日志缓冲区刷新T028
func TestSync(t *testing.T) {
// 创建临时目录
tempDir := t.TempDir()
tests := []struct {
name string
setupFunc func()
wantErr bool
}{
{
name: "sync after initialization",
setupFunc: func() {
InitLoggers("info", false,
LogRotationConfig{
Filename: filepath.Join(tempDir, "app-sync.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
},
LogRotationConfig{
Filename: filepath.Join(tempDir, "access-sync.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
},
)
},
wantErr: false,
},
{
name: "sync before initialization",
setupFunc: func() {
appLogger = nil
accessLogger = nil
},
wantErr: false, // 应该优雅地处理 nil 情况
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.setupFunc()
err := Sync()
if (err != nil) != tt.wantErr {
t.Errorf("Sync() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
// TestParseLevel 测试日志级别解析T026
func TestParseLevel(t *testing.T) {
tests := []struct {
name string
level string
want zapcore.Level
}{
{
name: "debug level",
level: "debug",
want: zapcore.DebugLevel,
},
{
name: "info level",
level: "info",
want: zapcore.InfoLevel,
},
{
name: "warn level",
level: "warn",
want: zapcore.WarnLevel,
},
{
name: "error level",
level: "error",
want: zapcore.ErrorLevel,
},
{
name: "invalid level defaults to info",
level: "invalid",
want: zapcore.InfoLevel,
},
{
name: "empty level defaults to info",
level: "",
want: zapcore.InfoLevel,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := parseLevel(tt.level)
if got != tt.want {
t.Errorf("parseLevel() = %v, want %v", got, tt.want)
}
})
}
}
// TestDualLoggerSystem 测试双日志系统T028
func TestDualLoggerSystem(t *testing.T) {
// 创建临时目录
tempDir := t.TempDir()
appLogFile := filepath.Join(tempDir, "app-dual.log")
accessLogFile := filepath.Join(tempDir, "access-dual.log")
// 初始化双日志系统
err := InitLoggers("info", false,
LogRotationConfig{
Filename: appLogFile,
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: false, // 不压缩以便检查内容
},
LogRotationConfig{
Filename: accessLogFile,
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: false,
},
)
if err != nil {
t.Fatalf("InitLoggers failed: %v", err)
}
// 写入应用日志
appLog := GetAppLogger()
appLog.Info("test app log message",
zap.String("module", "test"),
zap.Int("code", 200),
)
// 写入访问日志
accessLog := GetAccessLogger()
accessLog.Info("test access log message",
zap.String("method", "GET"),
zap.String("path", "/api/test"),
zap.Int("status", 200),
zap.Duration("latency", 100),
)
// 刷新缓冲区
if err := Sync(); err != nil {
t.Fatalf("Sync failed: %v", err)
}
// 验证应用日志文件存在并有内容
appLogContent, err := os.ReadFile(appLogFile)
if err != nil {
t.Fatalf("Failed to read app log file: %v", err)
}
if len(appLogContent) == 0 {
t.Error("App log file should not be empty")
}
// 验证访问日志文件存在并有内容
accessLogContent, err := os.ReadFile(accessLogFile)
if err != nil {
t.Fatalf("Failed to read access log file: %v", err)
}
if len(accessLogContent) == 0 {
t.Error("Access log file should not be empty")
}
// 验证两个日志文件是独立的
if string(appLogContent) == string(accessLogContent) {
t.Error("App log and access log should have different content")
}
}
// TestLoggerReinitialization 测试日志重新初始化T026
func TestLoggerReinitialization(t *testing.T) {
// 创建临时目录
tempDir := t.TempDir()
// 第一次初始化
err := InitLoggers("info", false,
LogRotationConfig{
Filename: filepath.Join(tempDir, "app-reinit1.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
},
LogRotationConfig{
Filename: filepath.Join(tempDir, "access-reinit1.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
},
)
if err != nil {
t.Fatalf("First InitLoggers failed: %v", err)
}
firstAppLogger := GetAppLogger()
// 第二次初始化(重新初始化)
err = InitLoggers("debug", true,
LogRotationConfig{
Filename: filepath.Join(tempDir, "app-reinit2.log"),
MaxSize: 5,
MaxBackups: 2,
MaxAge: 3,
Compress: false,
},
LogRotationConfig{
Filename: filepath.Join(tempDir, "access-reinit2.log"),
MaxSize: 5,
MaxBackups: 2,
MaxAge: 3,
Compress: false,
},
)
if err != nil {
t.Fatalf("Second InitLoggers failed: %v", err)
}
secondAppLogger := GetAppLogger()
// 验证重新初始化后日志记录器已更新
if firstAppLogger == secondAppLogger {
t.Error("Logger should be replaced after reinitialization")
}
}

388
pkg/logger/rotation_test.go Normal file
View File

@@ -0,0 +1,388 @@
package logger
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"go.uber.org/zap"
)
// TestLogRotation 测试日志轮转功能T027
func TestLogRotation(t *testing.T) {
// 创建临时目录
tempDir := t.TempDir()
appLogFile := filepath.Join(tempDir, "app-rotation.log")
// 初始化日志系统,设置较小的 MaxSize 以便测试
err := InitLoggers("info", false,
LogRotationConfig{
Filename: appLogFile,
MaxSize: 1, // 1MB写入足够数据后会触发轮转
MaxBackups: 3,
MaxAge: 7,
Compress: false, // 不压缩以便检查
},
LogRotationConfig{
Filename: filepath.Join(tempDir, "access-rotation.log"),
MaxSize: 1,
MaxBackups: 3,
MaxAge: 7,
Compress: false,
},
)
if err != nil {
t.Fatalf("InitLoggers failed: %v", err)
}
logger := GetAppLogger()
// 写入大量日志数据以触发轮转每条约100字节写入15000条约1.5MB
largeMessage := strings.Repeat("a", 100)
for i := 0; i < 15000; i++ {
logger.Info(largeMessage,
zap.Int("iteration", i),
zap.String("data", largeMessage),
)
}
// 刷新缓冲区
if err := Sync(); err != nil {
t.Fatalf("Sync failed: %v", err)
}
// 等待一小段时间确保文件写入完成
time.Sleep(100 * time.Millisecond)
// 验证主日志文件存在
if _, err := os.Stat(appLogFile); os.IsNotExist(err) {
t.Error("Main log file should exist")
}
// 检查是否有备份文件(轮转后的文件)
files, err := filepath.Glob(filepath.Join(tempDir, "app-rotation-*.log"))
if err != nil {
t.Fatalf("Failed to glob backup files: %v", err)
}
// 由于写入了超过1MB的数据应该触发至少一次轮转
if len(files) == 0 {
// 可能系统写入速度或lumberjack行为导致未立即轮转检查主文件大小
info, err := os.Stat(appLogFile)
if err != nil {
t.Fatalf("Failed to stat main log file: %v", err)
}
if info.Size() == 0 {
t.Error("Log file should have content")
}
// 不强制要求必须轮转,因为取决于具体实现
t.Logf("No rotation occurred, but main log file size: %d bytes", info.Size())
} else {
t.Logf("Found %d rotated backup file(s)", len(files))
}
}
// TestMaxBackups 测试最大备份数限制T027
func TestMaxBackups(t *testing.T) {
// 创建临时目录
tempDir := t.TempDir()
appLogFile := filepath.Join(tempDir, "app-backups.log")
// 初始化日志系统,设置 MaxBackups=2
err := InitLoggers("info", false,
LogRotationConfig{
Filename: appLogFile,
MaxSize: 1, // 1MB
MaxBackups: 2, // 最多保留2个备份
MaxAge: 7,
Compress: false,
},
LogRotationConfig{
Filename: filepath.Join(tempDir, "access-backups.log"),
MaxSize: 1,
MaxBackups: 2,
MaxAge: 7,
Compress: false,
},
)
if err != nil {
t.Fatalf("InitLoggers failed: %v", err)
}
logger := GetAppLogger()
// 写入足够的数据触发多次轮转每次1.5MB共4.5MB应该触发3次轮转
largeMessage := strings.Repeat("b", 100)
for round := 0; round < 3; round++ {
for i := 0; i < 15000; i++ {
logger.Info(largeMessage,
zap.Int("round", round),
zap.Int("iteration", i),
)
}
Sync()
time.Sleep(100 * time.Millisecond)
}
// 等待轮转完成
time.Sleep(200 * time.Millisecond)
// 检查备份文件数量
files, err := filepath.Glob(filepath.Join(tempDir, "app-backups-*.log"))
if err != nil {
t.Fatalf("Failed to glob backup files: %v", err)
}
// 由于 MaxBackups=2即使触发了多次轮转也只应保留最多2个备份文件
// (实际行为取决于 lumberjack 的实现细节可能小于等于2
if len(files) > 2 {
t.Errorf("Expected at most 2 backup files due to MaxBackups=2, got %d", len(files))
}
t.Logf("Found %d backup file(s) with MaxBackups=2", len(files))
}
// TestCompressionConfig 测试压缩配置T027
func TestCompressionConfig(t *testing.T) {
// 创建临时目录
tempDir := t.TempDir()
tests := []struct {
name string
compress bool
}{
{
name: "compression enabled",
compress: true,
},
{
name: "compression disabled",
compress: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logFile := filepath.Join(tempDir, "app-"+tt.name+".log")
err := InitLoggers("info", false,
LogRotationConfig{
Filename: logFile,
MaxSize: 1,
MaxBackups: 3,
MaxAge: 7,
Compress: tt.compress,
},
LogRotationConfig{
Filename: filepath.Join(tempDir, "access-"+tt.name+".log"),
MaxSize: 1,
MaxBackups: 3,
MaxAge: 7,
Compress: tt.compress,
},
)
if err != nil {
t.Fatalf("InitLoggers failed: %v", err)
}
logger := GetAppLogger()
// 写入一些日志
for i := 0; i < 1000; i++ {
logger.Info("test compression",
zap.Int("id", i),
zap.String("data", strings.Repeat("c", 50)),
)
}
Sync()
time.Sleep(100 * time.Millisecond)
// 验证日志文件存在
if _, err := os.Stat(logFile); os.IsNotExist(err) {
t.Error("Log file should exist")
}
})
}
}
// TestMaxAge 测试日志文件保留时间T027
func TestMaxAge(t *testing.T) {
// 创建临时目录
tempDir := t.TempDir()
// 初始化日志系统,设置 MaxAge=1 天
err := InitLoggers("info", false,
LogRotationConfig{
Filename: filepath.Join(tempDir, "app-maxage.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 1, // 1天
Compress: false,
},
LogRotationConfig{
Filename: filepath.Join(tempDir, "access-maxage.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 1,
Compress: false,
},
)
if err != nil {
t.Fatalf("InitLoggers failed: %v", err)
}
logger := GetAppLogger()
// 写入日志
logger.Info("test max age", zap.String("config", "maxage=1"))
Sync()
// 验证配置已应用无法在单元测试中验证实际的清理行为因为需要等待1天
// 这里只验证初始化没有错误
if logger == nil {
t.Error("Logger should be initialized with MaxAge config")
}
}
// TestNewLumberjackLogger 测试 Lumberjack logger 创建T027
func TestNewLumberjackLogger(t *testing.T) {
// 创建临时目录
tempDir := t.TempDir()
tests := []struct {
name string
config LogRotationConfig
}{
{
name: "standard config",
config: LogRotationConfig{
Filename: filepath.Join(tempDir, "test1.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: true,
},
},
{
name: "minimal config",
config: LogRotationConfig{
Filename: filepath.Join(tempDir, "test2.log"),
MaxSize: 1,
MaxBackups: 1,
MaxAge: 1,
Compress: false,
},
},
{
name: "large config",
config: LogRotationConfig{
Filename: filepath.Join(tempDir, "test3.log"),
MaxSize: 100,
MaxBackups: 10,
MaxAge: 30,
Compress: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logger := newLumberjackLogger(tt.config)
if logger == nil {
t.Error("newLumberjackLogger should not return nil")
}
// 验证配置已正确设置
if logger.Filename != tt.config.Filename {
t.Errorf("Filename = %v, want %v", logger.Filename, tt.config.Filename)
}
if logger.MaxSize != tt.config.MaxSize {
t.Errorf("MaxSize = %v, want %v", logger.MaxSize, tt.config.MaxSize)
}
if logger.MaxBackups != tt.config.MaxBackups {
t.Errorf("MaxBackups = %v, want %v", logger.MaxBackups, tt.config.MaxBackups)
}
if logger.MaxAge != tt.config.MaxAge {
t.Errorf("MaxAge = %v, want %v", logger.MaxAge, tt.config.MaxAge)
}
if logger.Compress != tt.config.Compress {
t.Errorf("Compress = %v, want %v", logger.Compress, tt.config.Compress)
}
if !logger.LocalTime {
t.Error("LocalTime should be true")
}
})
}
}
// TestConcurrentLogging 测试并发日志写入T027
func TestConcurrentLogging(t *testing.T) {
// 创建临时目录
tempDir := t.TempDir()
// 初始化日志系统
err := InitLoggers("info", false,
LogRotationConfig{
Filename: filepath.Join(tempDir, "app-concurrent.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: false,
},
LogRotationConfig{
Filename: filepath.Join(tempDir, "access-concurrent.log"),
MaxSize: 10,
MaxBackups: 3,
MaxAge: 7,
Compress: false,
},
)
if err != nil {
t.Fatalf("InitLoggers failed: %v", err)
}
logger := GetAppLogger()
// 启动多个 goroutine 并发写入日志
done := make(chan bool)
goroutines := 10
messagesPerGoroutine := 100
for i := 0; i < goroutines; i++ {
go func(id int) {
for j := 0; j < messagesPerGoroutine; j++ {
logger.Info("concurrent log message",
zap.Int("goroutine", id),
zap.Int("message", j),
)
}
done <- true
}(i)
}
// 等待所有 goroutine 完成
for i := 0; i < goroutines; i++ {
<-done
}
// 刷新缓冲区
if err := Sync(); err != nil {
t.Fatalf("Sync failed: %v", err)
}
// 验证日志文件存在且有内容
logFile := filepath.Join(tempDir, "app-concurrent.log")
info, err := os.Stat(logFile)
if err != nil {
t.Fatalf("Failed to stat log file: %v", err)
}
if info.Size() == 0 {
t.Error("Log file should have content after concurrent writes")
}
t.Logf("Concurrent logging test completed, log file size: %d bytes", info.Size())
}