1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
|
// 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
}
|