Files
go-omni/middleware/middleware.go

123 lines
3.7 KiB
Go

package middleware
import (
"errors"
"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)
var parameterErr *apierrors.ParamError
if errors.As(failure, &parameterErr) {
attrs = append(attrs, "errorParameter", parameterErr.Error())
}
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
}
}
}