diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 5c4dd56..f4fca48 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -23,6 +23,7 @@ import ( "github.com/hhs/camtalk/internal/ratelimit" "github.com/hhs/camtalk/internal/session" "github.com/hhs/camtalk/internal/store" + "github.com/hhs/camtalk/internal/trace" "github.com/hhs/camtalk/internal/ws" migrations "github.com/hhs/camtalk/migrations" ) @@ -226,7 +227,9 @@ func main() { } r := gin.New() - r.Use(gin.Recovery()) + r.Use(trace.TraceMiddleware()) // 第一层:生成 trace ID + r.Use(trace.GinLogger()) // 第二层:记录请求 + r.Use(trace.GinRecovery()) // 第三层:panic 恢复 // REST API apiGroup := r.Group("/api") diff --git a/backend/internal/trace/gin_logger.go b/backend/internal/trace/gin_logger.go new file mode 100644 index 0000000..1125a08 --- /dev/null +++ b/backend/internal/trace/gin_logger.go @@ -0,0 +1,63 @@ +package trace + +import ( + "time" + + "github.com/gin-gonic/gin" +) + +// GinLogger 记录每个 HTTP 请求的 method/path/status/latency +func GinLogger() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + path := c.Request.URL.Path + query := c.Request.URL.RawQuery + + c.Next() + + latency := time.Since(start).Milliseconds() + status := c.Writer.Status() + log := FromContext(c.Request.Context()) + + fields := []interface{}{ + "method", c.Request.Method, + "path", path, + "status", status, + "latency_ms", latency, + "client_ip", c.ClientIP(), + } + if query != "" { + fields = append(fields, "query", query) + } + if errStr := c.Errors.String(); errStr != "" { + fields = append(fields, "errors", errStr) + } + + switch { + case status >= 500: + log.Errorw("request completed", fields...) + case status >= 400: + log.Warnw("request completed", fields...) + default: + log.Infow("request completed", fields...) + } + } +} + +// GinRecovery 自定义 panic 恢复中间件,使用 zap 记录 +func GinRecovery() gin.HandlerFunc { + return func(c *gin.Context) { + defer func() { + if err := recover(); err != nil { + log := FromContext(c.Request.Context()) + log.Errorw("panic recovered", + "error", err, + "path", c.Request.URL.Path, + "method", c.Request.Method, + "client_ip", c.ClientIP()) + c.AbortWithStatus(500) + } + }() + c.Next() + } +}