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:
dailz
2026-04-10 08:39:21 +08:00
parent 7550e75945
commit f7a21ee455
4 changed files with 1036 additions and 0 deletions

93
internal/logger/logger.go Normal file
View File

@@ -0,0 +1,93 @@
package logger
import (
"fmt"
"os"
"gcy_hpc_server/internal/config"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
func NewLogger(cfg config.LogConfig) (*zap.Logger, error) {
level := applyDefault(cfg.Level, "info")
var zapLevel zapcore.Level
if err := zapLevel.UnmarshalText([]byte(level)); err != nil {
return nil, fmt.Errorf("invalid log level %q: %w", level, err)
}
encoding := applyDefault(cfg.Encoding, "json")
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.TimeKey = "ts"
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
var encoder zapcore.Encoder
switch encoding {
case "console":
encoder = zapcore.NewConsoleEncoder(encoderConfig)
default:
encoder = zapcore.NewJSONEncoder(encoderConfig)
}
var syncers []zapcore.WriteSyncer
stdout := true
if cfg.OutputStdout != nil {
stdout = *cfg.OutputStdout
}
if stdout {
syncers = append(syncers, zapcore.AddSync(os.Stdout))
}
if cfg.FilePath != "" {
maxSize := applyDefaultInt(cfg.MaxSize, 100)
maxBackups := applyDefaultInt(cfg.MaxBackups, 5)
maxAge := applyDefaultInt(cfg.MaxAge, 30)
compress := cfg.Compress || cfg.MaxSize == 0 && cfg.MaxBackups == 0 && cfg.MaxAge == 0
lj := &lumberjack.Logger{
Filename: cfg.FilePath,
MaxSize: maxSize,
MaxBackups: maxBackups,
MaxAge: maxAge,
Compress: compress,
}
syncers = append(syncers, zapcore.AddSync(lj))
}
if len(syncers) == 0 {
syncers = append(syncers, zapcore.AddSync(os.Stdout))
}
writeSyncer := syncers[0]
if len(syncers) > 1 {
writeSyncer = zapcore.NewMultiWriteSyncer(syncers...)
}
core := zapcore.NewCore(encoder, writeSyncer, zapLevel)
opts := []zap.Option{
zap.AddCaller(),
zap.AddStacktrace(zapcore.ErrorLevel),
}
return zap.New(core, opts...), nil
}
func applyDefault(val, def string) string {
if val == "" {
return def
}
return val
}
func applyDefaultInt(val, def int) int {
if val == 0 {
return def
}
return val
}