feat: 添加 zap 日志工厂和 GORM 日志桥接
- NewLogger 工厂函数:支持 JSON/Console 编码、stdout/文件/多输出、lumberjack 轮转 - NewGormLogger 实现 gorm.Interface:Trace 区分错误/慢查询/正常查询 - output_stdout 用 *bool 三态处理(nil=true, true, false) - 默认值:level=info, encoding=json, max_size=100, max_backups=5, max_age=30 - 慢查询阈值 200ms,ErrRecordNotFound 不视为错误 - 编译时接口检查: var _ gormlogger.Interface = (*GormLogger)(nil) - 完整 TDD 测试覆盖 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
286
internal/logger/logger_test.go
Normal file
286
internal/logger/logger_test.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gcy_hpc_server/internal/config"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
func ptrBool(v bool) *bool { return &v }
|
||||
|
||||
// TestNewLogger_JSONConfig creates a logger with JSON encoding and verifies
|
||||
// that log entries are emitted successfully.
|
||||
func TestNewLogger_JSONConfig(t *testing.T) {
|
||||
cfg := config.LogConfig{
|
||||
Level: "debug",
|
||||
Encoding: "json",
|
||||
OutputStdout: ptrBool(true),
|
||||
}
|
||||
|
||||
log, err := NewLogger(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLogger returned error: %v", err)
|
||||
}
|
||||
defer log.Sync()
|
||||
|
||||
// Should not panic when logging
|
||||
log.Info("json logger test", zap.String("key", "value"))
|
||||
}
|
||||
|
||||
// TestNewLogger_ConsoleConfig creates a logger with console encoding.
|
||||
func TestNewLogger_ConsoleConfig(t *testing.T) {
|
||||
cfg := config.LogConfig{
|
||||
Level: "info",
|
||||
Encoding: "console",
|
||||
OutputStdout: ptrBool(true),
|
||||
}
|
||||
|
||||
log, err := NewLogger(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLogger returned error: %v", err)
|
||||
}
|
||||
defer log.Sync()
|
||||
|
||||
log.Info("console logger test", zap.Int("num", 42))
|
||||
}
|
||||
|
||||
// TestNewLogger_InvalidLevel verifies that an invalid log level returns an error.
|
||||
func TestNewLogger_InvalidLevel(t *testing.T) {
|
||||
cfg := config.LogConfig{
|
||||
Level: "bogus",
|
||||
Encoding: "json",
|
||||
}
|
||||
|
||||
_, err := NewLogger(cfg)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid log level, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewLogger_EmptyConfig verifies defaults are applied when config is zero-value.
|
||||
func TestNewLogger_EmptyConfig(t *testing.T) {
|
||||
cfg := config.LogConfig{} // all zero values
|
||||
|
||||
log, err := NewLogger(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLogger returned error: %v", err)
|
||||
}
|
||||
defer log.Sync()
|
||||
|
||||
log.Info("default config test")
|
||||
}
|
||||
|
||||
// TestNewLogger_FileOutput verifies that file output with rotation config works.
|
||||
func TestNewLogger_FileOutput(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
logFile := filepath.Join(tmpDir, "test.log")
|
||||
|
||||
cfg := config.LogConfig{
|
||||
Level: "info",
|
||||
Encoding: "json",
|
||||
FilePath: logFile,
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: true,
|
||||
}
|
||||
|
||||
log, err := NewLogger(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLogger returned error: %v", err)
|
||||
}
|
||||
|
||||
log.Info("file output test", zap.String("msg", "hello"))
|
||||
log.Sync()
|
||||
|
||||
data, err := os.ReadFile(logFile)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read log file: %v", err)
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
t.Fatal("log file is empty, expected output")
|
||||
}
|
||||
|
||||
if !strings.Contains(string(data), "file output test") {
|
||||
t.Fatalf("log file content does not contain expected message;\ngot: %s", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewLogger_MultiWriter verifies that both stdout and file output work together.
|
||||
func TestNewLogger_MultiWriter(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
logFile := filepath.Join(tmpDir, "multi.log")
|
||||
|
||||
cfg := config.LogConfig{
|
||||
Level: "info",
|
||||
Encoding: "json",
|
||||
OutputStdout: ptrBool(true),
|
||||
FilePath: logFile,
|
||||
}
|
||||
|
||||
log, err := NewLogger(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLogger returned error: %v", err)
|
||||
}
|
||||
|
||||
log.Info("multi writer test", zap.String("writer", "both"))
|
||||
log.Sync()
|
||||
|
||||
data, err := os.ReadFile(logFile)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read log file: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(data), "multi writer test") {
|
||||
t.Fatalf("log file content does not contain expected message;\ngot: %s", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewLogger_Observer verifies actual log output content using zaptest.
|
||||
func TestNewLogger_Observer(t *testing.T) {
|
||||
// Use zaptest.NewLogger to capture logs in test output
|
||||
log := zaptest.NewLogger(t,
|
||||
zaptest.WrapOptions(zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel)),
|
||||
zaptest.Level(zapcore.DebugLevel),
|
||||
)
|
||||
|
||||
// These should all succeed without panicking
|
||||
log.Debug("debug msg", zap.String("k", "v"))
|
||||
log.Info("info msg", zap.Int("n", 1))
|
||||
log.Warn("warn msg")
|
||||
log.Error("error msg")
|
||||
}
|
||||
|
||||
// TestNewLogger_AllLevels verifies all valid log levels parse correctly.
|
||||
func TestNewLogger_AllLevels(t *testing.T) {
|
||||
levels := []string{"debug", "info", "warn", "error", "dpanic", "panic", "fatal"}
|
||||
for _, level := range levels {
|
||||
t.Run(level, func(t *testing.T) {
|
||||
cfg := config.LogConfig{
|
||||
Level: level,
|
||||
Encoding: "json",
|
||||
OutputStdout: ptrBool(true),
|
||||
}
|
||||
log, err := NewLogger(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("level %q: NewLogger returned error: %v", level, err)
|
||||
}
|
||||
log.Sync()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewLogger_InvalidEncoding falls back gracefully — the factory should
|
||||
// treat an unrecognized encoding as an error or default to JSON.
|
||||
func TestNewLogger_InvalidEncoding(t *testing.T) {
|
||||
cfg := config.LogConfig{
|
||||
Level: "info",
|
||||
Encoding: "xml",
|
||||
OutputStdout: ptrBool(true),
|
||||
}
|
||||
|
||||
// The implementation should default to JSON for unknown encoding.
|
||||
log, err := NewLogger(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error for invalid encoding: %v", err)
|
||||
}
|
||||
defer log.Sync()
|
||||
|
||||
log.Info("invalid encoding test")
|
||||
}
|
||||
|
||||
// TestNewLogger_DefaultRotation verifies rotation defaults are applied.
|
||||
func TestNewLogger_DefaultRotation(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
logFile := filepath.Join(tmpDir, "rotation.log")
|
||||
|
||||
cfg := config.LogConfig{
|
||||
Level: "info",
|
||||
Encoding: "json",
|
||||
FilePath: logFile,
|
||||
// MaxSize, MaxBackups, MaxAge, Compress all zero → defaults apply
|
||||
}
|
||||
|
||||
log, err := NewLogger(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLogger returned error: %v", err)
|
||||
}
|
||||
|
||||
log.Info("rotation defaults test")
|
||||
log.Sync()
|
||||
|
||||
data, err := os.ReadFile(logFile)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read log file: %v", err)
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
t.Fatal("log file is empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLogger_OutputStdoutNil(t *testing.T) {
|
||||
cfg := config.LogConfig{
|
||||
Level: "info",
|
||||
Encoding: "json",
|
||||
}
|
||||
|
||||
log, err := NewLogger(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLogger returned error: %v", err)
|
||||
}
|
||||
defer log.Sync()
|
||||
|
||||
log.Info("default stdout test")
|
||||
}
|
||||
|
||||
func TestNewLogger_OutputStdoutFalseWithFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
logFile := filepath.Join(tmpDir, "nostdout.log")
|
||||
|
||||
cfg := config.LogConfig{
|
||||
Level: "info",
|
||||
Encoding: "json",
|
||||
OutputStdout: ptrBool(false),
|
||||
FilePath: logFile,
|
||||
}
|
||||
|
||||
log, err := NewLogger(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLogger returned error: %v", err)
|
||||
}
|
||||
log.Info("file only test")
|
||||
log.Sync()
|
||||
|
||||
data, err := os.ReadFile(logFile)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read log file: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(data), "file only test") {
|
||||
t.Fatalf("log file content does not contain expected message;\ngot: %s", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLogger_OutputStdoutFalseFallback(t *testing.T) {
|
||||
cfg := config.LogConfig{
|
||||
Level: "info",
|
||||
Encoding: "json",
|
||||
OutputStdout: ptrBool(false),
|
||||
}
|
||||
|
||||
log, err := NewLogger(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLogger returned error: %v", err)
|
||||
}
|
||||
defer log.Sync()
|
||||
|
||||
log.Info("fallback stdout test")
|
||||
}
|
||||
Reference in New Issue
Block a user