392 lines
14 KiB
Go
392 lines
14 KiB
Go
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
|
|
|
|
package middleware
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
// Example for `slog` https://pkg.go.dev/log/slog
|
|
// logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
|
// e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
|
|
// LogStatus: true,
|
|
// LogURI: true,
|
|
// LogError: true,
|
|
// HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
|
|
// LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
|
|
// if v.Error == nil {
|
|
// logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST",
|
|
// slog.String("uri", v.URI),
|
|
// slog.Int("status", v.Status),
|
|
// )
|
|
// } else {
|
|
// logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR",
|
|
// slog.String("uri", v.URI),
|
|
// slog.Int("status", v.Status),
|
|
// slog.String("err", v.Error.Error()),
|
|
// )
|
|
// }
|
|
// return nil
|
|
// },
|
|
// }))
|
|
//
|
|
// Example for `fmt.Printf`
|
|
// e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
|
|
// LogStatus: true,
|
|
// LogURI: true,
|
|
// LogError: true,
|
|
// HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
|
|
// LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
|
|
// if v.Error == nil {
|
|
// fmt.Printf("REQUEST: uri: %v, status: %v\n", v.URI, v.Status)
|
|
// } else {
|
|
// fmt.Printf("REQUEST_ERROR: uri: %v, status: %v, err: %v\n", v.URI, v.Status, v.Error)
|
|
// }
|
|
// return nil
|
|
// },
|
|
// }))
|
|
//
|
|
// Example for Zerolog (https://github.com/rs/zerolog)
|
|
// logger := zerolog.New(os.Stdout)
|
|
// e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
|
|
// LogURI: true,
|
|
// LogStatus: true,
|
|
// LogError: true,
|
|
// HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
|
|
// LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
|
|
// if v.Error == nil {
|
|
// logger.Info().
|
|
// Str("URI", v.URI).
|
|
// Int("status", v.Status).
|
|
// Msg("request")
|
|
// } else {
|
|
// logger.Error().
|
|
// Err(v.Error).
|
|
// Str("URI", v.URI).
|
|
// Int("status", v.Status).
|
|
// Msg("request error")
|
|
// }
|
|
// return nil
|
|
// },
|
|
// }))
|
|
//
|
|
// Example for Zap (https://github.com/uber-go/zap)
|
|
// logger, _ := zap.NewProduction()
|
|
// e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
|
|
// LogURI: true,
|
|
// LogStatus: true,
|
|
// LogError: true,
|
|
// HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
|
|
// LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
|
|
// if v.Error == nil {
|
|
// logger.Info("request",
|
|
// zap.String("URI", v.URI),
|
|
// zap.Int("status", v.Status),
|
|
// )
|
|
// } else {
|
|
// logger.Error("request error",
|
|
// zap.String("URI", v.URI),
|
|
// zap.Int("status", v.Status),
|
|
// zap.Error(v.Error),
|
|
// )
|
|
// }
|
|
// return nil
|
|
// },
|
|
// }))
|
|
//
|
|
// Example for Logrus (https://github.com/sirupsen/logrus)
|
|
// log := logrus.New()
|
|
// e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
|
|
// LogURI: true,
|
|
// LogStatus: true,
|
|
// LogError: true,
|
|
// HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
|
|
// LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
|
|
// if v.Error == nil {
|
|
// log.WithFields(logrus.Fields{
|
|
// "URI": v.URI,
|
|
// "status": v.Status,
|
|
// }).Info("request")
|
|
// } else {
|
|
// log.WithFields(logrus.Fields{
|
|
// "URI": v.URI,
|
|
// "status": v.Status,
|
|
// "error": v.Error,
|
|
// }).Error("request error")
|
|
// }
|
|
// return nil
|
|
// },
|
|
// }))
|
|
|
|
// RequestLoggerConfig is configuration for Request Logger middleware.
|
|
type RequestLoggerConfig struct {
|
|
// Skipper defines a function to skip middleware.
|
|
Skipper Skipper
|
|
|
|
// BeforeNextFunc defines a function that is called before next middleware or handler is called in chain.
|
|
BeforeNextFunc func(c echo.Context)
|
|
// LogValuesFunc defines a function that is called with values extracted by logger from request/response.
|
|
// Mandatory.
|
|
LogValuesFunc func(c echo.Context, v RequestLoggerValues) error
|
|
|
|
// HandleError instructs logger to call global error handler when next middleware/handler returns an error.
|
|
// This is useful when you have custom error handler that can decide to use different status codes.
|
|
//
|
|
// A side-effect of calling global error handler is that now Response has been committed and sent to the client
|
|
// and middlewares up in chain can not change Response status code or response body.
|
|
HandleError bool
|
|
|
|
// LogLatency instructs logger to record duration it took to execute rest of the handler chain (next(c) call).
|
|
LogLatency bool
|
|
// LogProtocol instructs logger to extract request protocol (i.e. `HTTP/1.1` or `HTTP/2`)
|
|
LogProtocol bool
|
|
// LogRemoteIP instructs logger to extract request remote IP. See `echo.Context.RealIP()` for implementation details.
|
|
LogRemoteIP bool
|
|
// LogHost instructs logger to extract request host value (i.e. `example.com`)
|
|
LogHost bool
|
|
// LogMethod instructs logger to extract request method value (i.e. `GET` etc)
|
|
LogMethod bool
|
|
// LogURI instructs logger to extract request URI (i.e. `/list?lang=en&page=1`)
|
|
LogURI bool
|
|
// LogURIPath instructs logger to extract request URI path part (i.e. `/list`)
|
|
LogURIPath bool
|
|
// LogRoutePath instructs logger to extract route path part to which request was matched to (i.e. `/user/:id`)
|
|
LogRoutePath bool
|
|
// LogRequestID instructs logger to extract request ID from request `X-Request-ID` header or response if request did not have value.
|
|
LogRequestID bool
|
|
// LogReferer instructs logger to extract request referer values.
|
|
LogReferer bool
|
|
// LogUserAgent instructs logger to extract request user agent values.
|
|
LogUserAgent bool
|
|
// LogStatus instructs logger to extract response status code. If handler chain returns an echo.HTTPError,
|
|
// the status code is extracted from the echo.HTTPError returned
|
|
LogStatus bool
|
|
// LogError instructs logger to extract error returned from executed handler chain.
|
|
LogError bool
|
|
// LogContentLength instructs logger to extract content length header value. Note: this value could be different from
|
|
// actual request body size as it could be spoofed etc.
|
|
LogContentLength bool
|
|
// LogResponseSize instructs logger to extract response content length value. Note: when used with Gzip middleware
|
|
// this value may not be always correct.
|
|
LogResponseSize bool
|
|
// LogHeaders instructs logger to extract given list of headers from request. Note: request can contain more than
|
|
// one header with same value so slice of values is been logger for each given header.
|
|
//
|
|
// Note: header values are converted to canonical form with http.CanonicalHeaderKey as this how request parser converts header
|
|
// names to. For example, the canonical key for "accept-encoding" is "Accept-Encoding".
|
|
LogHeaders []string
|
|
// LogQueryParams instructs logger to extract given list of query parameters from request URI. Note: request can
|
|
// contain more than one query parameter with same name so slice of values is been logger for each given query param name.
|
|
LogQueryParams []string
|
|
// LogFormValues instructs logger to extract given list of form values from request body+URI. Note: request can
|
|
// contain more than one form value with same name so slice of values is been logger for each given form value name.
|
|
LogFormValues []string
|
|
|
|
timeNow func() time.Time
|
|
}
|
|
|
|
// RequestLoggerValues contains extracted values from logger.
|
|
type RequestLoggerValues struct {
|
|
// StartTime is time recorded before next middleware/handler is executed.
|
|
StartTime time.Time
|
|
// Latency is duration it took to execute rest of the handler chain (next(c) call).
|
|
Latency time.Duration
|
|
// Protocol is request protocol (i.e. `HTTP/1.1` or `HTTP/2`)
|
|
Protocol string
|
|
// RemoteIP is request remote IP. See `echo.Context.RealIP()` for implementation details.
|
|
RemoteIP string
|
|
// Host is request host value (i.e. `example.com`)
|
|
Host string
|
|
// Method is request method value (i.e. `GET` etc)
|
|
Method string
|
|
// URI is request URI (i.e. `/list?lang=en&page=1`)
|
|
URI string
|
|
// URIPath is request URI path part (i.e. `/list`)
|
|
URIPath string
|
|
// RoutePath is route path part to which request was matched to (i.e. `/user/:id`)
|
|
RoutePath string
|
|
// RequestID is request ID from request `X-Request-ID` header or response if request did not have value.
|
|
RequestID string
|
|
// Referer is request referer values.
|
|
Referer string
|
|
// UserAgent is request user agent values.
|
|
UserAgent string
|
|
// Status is response status code. Then handler returns an echo.HTTPError then code from there.
|
|
Status int
|
|
// Error is error returned from executed handler chain.
|
|
Error error
|
|
// ContentLength is content length header value. Note: this value could be different from actual request body size
|
|
// as it could be spoofed etc.
|
|
ContentLength string
|
|
// ResponseSize is response content length value. Note: when used with Gzip middleware this value may not be always correct.
|
|
ResponseSize int64
|
|
// Headers are list of headers from request. Note: request can contain more than one header with same value so slice
|
|
// of values is been logger for each given header.
|
|
// Note: header values are converted to canonical form with http.CanonicalHeaderKey as this how request parser converts header
|
|
// names to. For example, the canonical key for "accept-encoding" is "Accept-Encoding".
|
|
Headers map[string][]string
|
|
// QueryParams are list of query parameters from request URI. Note: request can contain more than one query parameter
|
|
// with same name so slice of values is been logger for each given query param name.
|
|
QueryParams map[string][]string
|
|
// FormValues are list of form values from request body+URI. Note: request can contain more than one form value with
|
|
// same name so slice of values is been logger for each given form value name.
|
|
FormValues map[string][]string
|
|
}
|
|
|
|
// RequestLoggerWithConfig returns a RequestLogger middleware with config.
|
|
func RequestLoggerWithConfig(config RequestLoggerConfig) echo.MiddlewareFunc {
|
|
mw, err := config.ToMiddleware()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return mw
|
|
}
|
|
|
|
// ToMiddleware converts RequestLoggerConfig into middleware or returns an error for invalid configuration.
|
|
func (config RequestLoggerConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
|
|
if config.Skipper == nil {
|
|
config.Skipper = DefaultSkipper
|
|
}
|
|
now := time.Now
|
|
if config.timeNow != nil {
|
|
now = config.timeNow
|
|
}
|
|
|
|
if config.LogValuesFunc == nil {
|
|
return nil, errors.New("missing LogValuesFunc callback function for request logger middleware")
|
|
}
|
|
|
|
logHeaders := len(config.LogHeaders) > 0
|
|
headers := append([]string(nil), config.LogHeaders...)
|
|
for i, v := range headers {
|
|
headers[i] = http.CanonicalHeaderKey(v)
|
|
}
|
|
|
|
logQueryParams := len(config.LogQueryParams) > 0
|
|
logFormValues := len(config.LogFormValues) > 0
|
|
|
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
if config.Skipper(c) {
|
|
return next(c)
|
|
}
|
|
|
|
req := c.Request()
|
|
res := c.Response()
|
|
start := now()
|
|
|
|
if config.BeforeNextFunc != nil {
|
|
config.BeforeNextFunc(c)
|
|
}
|
|
err := next(c)
|
|
if err != nil && config.HandleError {
|
|
c.Error(err)
|
|
}
|
|
|
|
v := RequestLoggerValues{
|
|
StartTime: start,
|
|
}
|
|
if config.LogLatency {
|
|
v.Latency = now().Sub(start)
|
|
}
|
|
if config.LogProtocol {
|
|
v.Protocol = req.Proto
|
|
}
|
|
if config.LogRemoteIP {
|
|
v.RemoteIP = c.RealIP()
|
|
}
|
|
if config.LogHost {
|
|
v.Host = req.Host
|
|
}
|
|
if config.LogMethod {
|
|
v.Method = req.Method
|
|
}
|
|
if config.LogURI {
|
|
v.URI = req.RequestURI
|
|
}
|
|
if config.LogURIPath {
|
|
p := req.URL.Path
|
|
if p == "" {
|
|
p = "/"
|
|
}
|
|
v.URIPath = p
|
|
}
|
|
if config.LogRoutePath {
|
|
v.RoutePath = c.Path()
|
|
}
|
|
if config.LogRequestID {
|
|
id := req.Header.Get(echo.HeaderXRequestID)
|
|
if id == "" {
|
|
id = res.Header().Get(echo.HeaderXRequestID)
|
|
}
|
|
v.RequestID = id
|
|
}
|
|
if config.LogReferer {
|
|
v.Referer = req.Referer()
|
|
}
|
|
if config.LogUserAgent {
|
|
v.UserAgent = req.UserAgent()
|
|
}
|
|
if config.LogStatus {
|
|
v.Status = res.Status
|
|
if err != nil && !config.HandleError {
|
|
// this block should not be executed in case of HandleError=true as the global error handler will decide
|
|
// the status code. In that case status code could be different from what err contains.
|
|
var httpErr *echo.HTTPError
|
|
if errors.As(err, &httpErr) {
|
|
v.Status = httpErr.Code
|
|
}
|
|
}
|
|
}
|
|
if config.LogError && err != nil {
|
|
v.Error = err
|
|
}
|
|
if config.LogContentLength {
|
|
v.ContentLength = req.Header.Get(echo.HeaderContentLength)
|
|
}
|
|
if config.LogResponseSize {
|
|
v.ResponseSize = res.Size
|
|
}
|
|
if logHeaders {
|
|
v.Headers = map[string][]string{}
|
|
for _, header := range headers {
|
|
if values, ok := req.Header[header]; ok {
|
|
v.Headers[header] = values
|
|
}
|
|
}
|
|
}
|
|
if logQueryParams {
|
|
queryParams := c.QueryParams()
|
|
v.QueryParams = map[string][]string{}
|
|
for _, param := range config.LogQueryParams {
|
|
if values, ok := queryParams[param]; ok {
|
|
v.QueryParams[param] = values
|
|
}
|
|
}
|
|
}
|
|
if logFormValues {
|
|
v.FormValues = map[string][]string{}
|
|
for _, formValue := range config.LogFormValues {
|
|
if values, ok := req.Form[formValue]; ok {
|
|
v.FormValues[formValue] = values
|
|
}
|
|
}
|
|
}
|
|
|
|
if errOnLog := config.LogValuesFunc(c, v); errOnLog != nil {
|
|
return errOnLog
|
|
}
|
|
|
|
// in case of HandleError=true we are returning the error that we already have handled with global error handler
|
|
// this is deliberate as this error could be useful for upstream middlewares and default global error handler
|
|
// will ignore that error when it bubbles up in middleware chain.
|
|
return err
|
|
}
|
|
}, nil
|
|
}
|