package middleware import ( "fmt" "runtime/debug" "time" apierrors "git.yuntuo.hk/k2safe/go-omni/errors" "git.yuntuo.hk/k2safe/go-omni/httpx" "git.yuntuo.hk/k2safe/go-omni/logx" ) // RequestIDKey is the value key used by the request ID middleware. const RequestIDKey = "go-omni.requestId" // RequestID reads or creates a request ID and returns it in the response // header. It is a transport-neutral GoOmni middleware. func RequestID(header string) httpx.Middleware { return func(next httpx.Handler) httpx.Handler { return func(ctx *httpx.Context) error { requestID := ctx.Header(header) if requestID == "" { requestID = newRequestID() } ctx.Set(RequestIDKey, requestID) ctx.SetResponseHeader(header, requestID) ctx.WithContext(logx.WithRequestID(ctx.Context(), requestID)) return next(ctx) } } } // Recovery converts panics from handlers and middleware into the standard // internal error. Recovery belongs to GoOmni's transport-neutral middleware // rather than a router-specific hook. func Recovery() httpx.Middleware { return func(next httpx.Handler) httpx.Handler { return func(ctx *httpx.Context) (err error) { defer func() { if recovered := recover(); recovered != nil { logx.ErrorContext(ctx.Context(), "http.panic", "panic", recovered, "stack", string(debug.Stack()), "method", ctx.Request().Method, "route", httpx.RoutePattern(ctx.Context())) err = apierrors.Internal(fmt.Errorf("panic recovered: %v", recovered)) } }() return next(ctx) } } } // AccessLog records one structured event after each request. It deliberately // lives in GoOmni instead of using a router-specific logger so business // applications get the same log format with every transport adapter. type AccessLogOption func(*accessLogConfig) type accessLogConfig struct{ slowThreshold time.Duration } // WithSlowRequestThreshold logs slow requests at WARN. Zero disables detection. func WithSlowRequestThreshold(threshold time.Duration) AccessLogOption { return func(c *accessLogConfig) { if threshold >= 0 { c.slowThreshold = threshold } } } func AccessLog(options ...AccessLogOption) httpx.Middleware { settings := accessLogConfig{} for _, option := range options { if option != nil { option(&settings) } } return func(next httpx.Handler) httpx.Handler { return func(ctx *httpx.Context) error { started := time.Now() err := next(ctx) if err != nil && !ctx.Responded() { ctx.WriteError(err) } status := ctx.Status() duration := time.Since(started) slow := settings.slowThreshold > 0 && duration >= settings.slowThreshold attrs := []any{ "method", ctx.Request().Method, "path", ctx.Request().URL.Path, "route", httpx.RoutePattern(ctx.Context()), "status", status, "durationMs", duration.Seconds() * 1000, "clientIp", ctx.ClientIP(), } failure := ctx.Failure() if failure == nil && err != nil { failure = ctx.ResolveError(err) } if failure != nil { attrs = append(attrs, "failed", true, "errorKey", failure.Code) if failure.BusinessCode != nil { attrs = append(attrs, "businessCode", failure.BusinessCode) } } if slow { attrs = append(attrs, "slow", true) } if err != nil { attrs = append(attrs, "error", err) } switch { case status >= 500: logx.ErrorContext(ctx.Context(), "http.request", attrs...) case slow: logx.WarnContext(ctx.Context(), "http.request", attrs...) default: logx.InfoContext(ctx.Context(), "http.request", attrs...) } return err } } }