重构日志与可观测性体系
新增单行文本编码器与结构化 GORM 日志,统一错误记录与请求日志策略,收紧日志文件权限并修复按天切分与压缩,支付回调参数脱敏,生产强制阿里云短信,RequestID 校验防注入,日志文案中文化。
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
appLogging "hfb_sys/backend/internal/logging"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
gormLogger "gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
type structuredGormLogger struct {
|
||||
logger *zap.Logger
|
||||
level gormLogger.LogLevel
|
||||
slowThreshold time.Duration
|
||||
}
|
||||
|
||||
func newGormLogger(logLevel string, logger *zap.Logger) gormLogger.Interface {
|
||||
level := gormLogger.Warn
|
||||
if strings.EqualFold(strings.TrimSpace(logLevel), "debug") {
|
||||
level = gormLogger.Info
|
||||
}
|
||||
if logger == nil {
|
||||
logger = zap.L()
|
||||
}
|
||||
return &structuredGormLogger{
|
||||
logger: logger.With(zap.String("module", "database")),
|
||||
level: level,
|
||||
slowThreshold: 500 * time.Millisecond,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *structuredGormLogger) LogMode(level gormLogger.LogLevel) gormLogger.Interface {
|
||||
cloned := *l
|
||||
cloned.level = level
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func (l *structuredGormLogger) Info(ctx context.Context, message string, args ...any) {
|
||||
if l.level >= gormLogger.Info {
|
||||
l.withContext(ctx).Debug("数据库信息", zap.String("detail", fmt.Sprintf(message, args...)))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *structuredGormLogger) Warn(ctx context.Context, message string, args ...any) {
|
||||
if l.level >= gormLogger.Warn {
|
||||
l.withContext(ctx).Warn("数据库警告", zap.String("detail", fmt.Sprintf(message, args...)))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *structuredGormLogger) Error(ctx context.Context, message string, args ...any) {
|
||||
if l.level >= gormLogger.Error {
|
||||
l.withContext(ctx).Error("数据库错误", zap.String("detail", fmt.Sprintf(message, args...)))
|
||||
}
|
||||
}
|
||||
|
||||
func (l *structuredGormLogger) Trace(ctx context.Context, begin time.Time, query func() (string, int64), err error) {
|
||||
if l.level == gormLogger.Silent {
|
||||
return
|
||||
}
|
||||
elapsed := time.Since(begin)
|
||||
switch {
|
||||
case err != nil && !errors.Is(err, gorm.ErrRecordNotFound) && l.level >= gormLogger.Error:
|
||||
sql, rows := query()
|
||||
l.withContext(ctx).Error("数据库查询失败", queryFields(sql, rows, elapsed, err)...)
|
||||
case elapsed >= l.slowThreshold && l.level >= gormLogger.Warn:
|
||||
sql, rows := query()
|
||||
l.withContext(ctx).Warn("数据库慢查询", queryFields(sql, rows, elapsed, nil)...)
|
||||
case l.level == gormLogger.Info:
|
||||
sql, rows := query()
|
||||
l.withContext(ctx).Debug("数据库查询", queryFields(sql, rows, elapsed, nil)...)
|
||||
}
|
||||
}
|
||||
|
||||
// ParamsFilter 让 GORM 保留 SQL 占位符,避免查询参数进入日志。
|
||||
func (l *structuredGormLogger) ParamsFilter(_ context.Context, sql string, _ ...any) (string, []any) {
|
||||
return sql, nil
|
||||
}
|
||||
|
||||
func (l *structuredGormLogger) withContext(ctx context.Context) *zap.Logger {
|
||||
fields := make([]zap.Field, 0, 2)
|
||||
if requestID := appLogging.RequestIDFromContext(ctx); requestID != "" {
|
||||
fields = append(fields, zap.String("request_id", requestID))
|
||||
}
|
||||
if adminID := appLogging.AdminIDFromContext(ctx); adminID != 0 {
|
||||
fields = append(fields, zap.Uint64("admin_id", adminID))
|
||||
}
|
||||
return l.logger.With(fields...)
|
||||
}
|
||||
|
||||
func queryFields(sql string, rows int64, elapsed time.Duration, err error) []zap.Field {
|
||||
fields := []zap.Field{
|
||||
zap.Float64("duration_ms", float64(elapsed.Microseconds())/1000),
|
||||
zap.Int64("rows", rows),
|
||||
zap.String("sql", sql),
|
||||
}
|
||||
if err != nil {
|
||||
fields = append(fields, zap.Error(err))
|
||||
}
|
||||
return fields
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zaptest/observer"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestGormLoggerDoesNotLogQueryParameters(t *testing.T) {
|
||||
core, observed := observer.New(zap.DebugLevel)
|
||||
logger := zap.New(core)
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
Logger: newGormLogger("debug", logger),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
type secretRecord struct {
|
||||
ID uint64
|
||||
Phone string
|
||||
}
|
||||
if err := db.AutoMigrate(&secretRecord{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const secretPhone = "13812345678"
|
||||
if err := db.Create(&secretRecord{Phone: secretPhone}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, entry := range observed.All() {
|
||||
for _, value := range entry.ContextMap() {
|
||||
if strings.Contains(valueString(value), secretPhone) {
|
||||
t.Fatalf("query parameter leaked into log: %v", entry.ContextMap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func valueString(value any) string {
|
||||
if text, ok := value.(string); ok {
|
||||
return text
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,19 +1,16 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func OpenMySQL(dsn string, logLevel string) (*gorm.DB, error) {
|
||||
func OpenMySQL(dsn string, logLevel string, appLogger *zap.Logger) (*gorm.DB, error) {
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
Logger: newGormLogger(logLevel),
|
||||
Logger: newGormLogger(logLevel, appLogger),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -31,19 +28,3 @@ func OpenMySQL(dsn string, logLevel string) (*gorm.DB, error) {
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func newGormLogger(logLevel string) logger.Interface {
|
||||
level := logger.Warn
|
||||
if strings.EqualFold(strings.TrimSpace(logLevel), "debug") {
|
||||
level = logger.Info
|
||||
}
|
||||
return logger.New(
|
||||
log.New(os.Stdout, "\r\n", log.LstdFlags),
|
||||
logger.Config{
|
||||
SlowThreshold: 500 * time.Millisecond,
|
||||
LogLevel: level,
|
||||
IgnoreRecordNotFoundError: true,
|
||||
Colorful: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user