- 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>
287 lines
6.7 KiB
Go
287 lines
6.7 KiB
Go
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")
|
|
}
|