Files
ttstd-infinite-canvas/backend/internal/logging/gorm.go
T
ddcat666 a0f025aa96 feat(platform): 日志管理 - 统一日志出口并屏蔽查询与 Agent 流式事件日志
新增 internal/logging,slog、标准库 log、GORM 与 Gin 访问日志共用同一 Handler,由 CANVAS_LOG_LEVEL / CANVAS_LOG_FORMAT 控制。info 级别不再输出成功的查询类请求、成功 SQL、message_end 等 Agent 流式事件和常规画布自动保存;4xx/5xx、慢请求、慢 SQL、SQL 失败与 panic 仍会记录,SQL 不内联参数值。
2026-09-30 03:33:34 +08:00

103 lines
2.5 KiB
Go

package logging
import (
"context"
"errors"
"log/slog"
"time"
"gorm.io/gorm"
gormlogger "gorm.io/gorm/logger"
)
// GormLogger 把 GORM 日志接入统一 Handler:成功的 SQL 一律不输出,只记录失败与慢查询。
// SQL 以参数化形式输出(不内联参数值),避免密码哈希、token 等写进日志。
func GormLogger() gormlogger.Interface {
return gormLogger{level: gormlogger.Warn}
}
type gormLogger struct {
level gormlogger.LogLevel
}
func (l gormLogger) LogMode(level gormlogger.LogLevel) gormlogger.Interface {
l.level = level
return l
}
func (l gormLogger) Info(ctx context.Context, msg string, data ...any) {
if l.level >= gormlogger.Info {
slog.InfoContext(ctx, "gorm: "+msg, "data", data)
}
}
func (l gormLogger) Warn(ctx context.Context, msg string, data ...any) {
if l.level >= gormlogger.Warn {
slog.WarnContext(ctx, "gorm: "+msg, "data", data)
}
}
func (l gormLogger) Error(ctx context.Context, msg string, data ...any) {
if l.level >= gormlogger.Error {
slog.ErrorContext(ctx, "gorm: "+msg, "data", data)
}
}
func (l gormLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
if l.level <= gormlogger.Silent {
return
}
elapsed := time.Since(begin)
var level slog.Level
var msg string
switch {
case err != nil && isExpectedQueryError(err):
return
case err != nil:
if l.level < gormlogger.Error {
return
}
level, msg = slog.LevelWarn, "sql failed"
case slowSQLThreshold() > 0 && elapsed >= slowSQLThreshold():
if l.level < gormlogger.Warn {
return
}
level, msg = slog.LevelWarn, "sql slow"
default:
return
}
if ctx == nil {
ctx = context.Background()
}
if !slog.Default().Enabled(ctx, level) {
return
}
sql, rows := fc()
attrs := []slog.Attr{
slog.Int64("duration_ms", elapsed.Milliseconds()),
slog.Int64("rows", rows),
slog.String("sql", truncate(sql, 2000)),
}
if err != nil {
attrs = append(attrs, slog.String("error", err.Error()))
}
slog.LogAttrs(ctx, level, msg, attrs...)
}
// ParamsFilter 让 GORM 在生成日志用的 SQL 时丢弃参数值。
func (gormLogger) ParamsFilter(_ context.Context, sql string, _ ...any) (string, []any) {
return sql, nil
}
// 查不到记录、请求取消属于正常业务分支,由调用方决定是否需要记录。
func isExpectedQueryError(err error) bool {
return errors.Is(err, gorm.ErrRecordNotFound) || errors.Is(err, context.Canceled)
}
func truncate(value string, limit int) string {
if len(value) <= limit {
return value
}
return value[:limit] + "...(truncated)"
}