新增 internal/logging,slog、标准库 log、GORM 与 Gin 访问日志共用同一 Handler,由 CANVAS_LOG_LEVEL / CANVAS_LOG_FORMAT 控制。info 级别不再输出成功的查询类请求、成功 SQL、message_end 等 Agent 流式事件和常规画布自动保存;4xx/5xx、慢请求、慢 SQL、SQL 失败与 panic 仍会记录,SQL 不内联参数值。
103 lines
2.5 KiB
Go
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)"
|
||
}
|