mirror of
https://github.com/labstack/echo.git
synced 2025-11-27 22:38:25 +02:00
Addresses issue #2665 by providing comprehensive documentation for the Logger middleware including: **Configuration Examples:** - Basic usage with default settings - Custom simple and JSON formats - Custom time formatting - Header, query, form, and cookie logging - File output configuration - Custom tag functions - Conditional logging with Skipper - External logging service integration **Detailed Tag Reference:** - Complete list of all available tags (time, request, response, dynamic) - Clear explanations of each tag's purpose and format - Examples showing proper usage **Enhanced Field Documentation:** - Detailed descriptions for all LoggerConfig fields - Examples for each configuration option - Default values and behavior **Troubleshooting Section:** - Common issues and solutions - Performance optimization tips - Best practices for high-traffic applications **Function Documentation:** - Enhanced Logger() and LoggerWithConfig() documentation - Example outputs and usage patterns This makes the Logger middleware much more accessible to new users while providing advanced configuration guidance for experienced developers. Fixes #2665 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
412 lines
13 KiB
Go
412 lines
13 KiB
Go
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
|
|
|
|
package middleware
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/labstack/gommon/color"
|
|
"github.com/valyala/fasttemplate"
|
|
)
|
|
|
|
// LoggerConfig defines the config for Logger middleware.
|
|
//
|
|
// # Configuration Examples
|
|
//
|
|
// ## Basic Usage with Default Settings
|
|
//
|
|
// e.Use(middleware.Logger())
|
|
//
|
|
// This uses the default JSON format that logs all common request/response details.
|
|
//
|
|
// ## Custom Simple Format
|
|
//
|
|
// e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
|
// Format: "${time_rfc3339_nano} ${status} ${method} ${uri} ${latency_human}\n",
|
|
// }))
|
|
//
|
|
// ## JSON Format with Custom Fields
|
|
//
|
|
// e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
|
// Format: `{"timestamp":"${time_rfc3339_nano}","level":"info","remote_ip":"${remote_ip}",` +
|
|
// `"method":"${method}","uri":"${uri}","status":${status},"latency":"${latency_human}",` +
|
|
// `"user_agent":"${user_agent}","error":"${error}"}` + "\n",
|
|
// }))
|
|
//
|
|
// ## Custom Time Format
|
|
//
|
|
// e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
|
// Format: "${time_custom} ${method} ${uri} ${status}\n",
|
|
// CustomTimeFormat: "2006-01-02 15:04:05",
|
|
// }))
|
|
//
|
|
// ## Logging Headers and Parameters
|
|
//
|
|
// e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
|
// Format: `{"time":"${time_rfc3339_nano}","method":"${method}","uri":"${uri}",` +
|
|
// `"status":${status},"auth":"${header:Authorization}","user":"${query:user}",` +
|
|
// `"form_data":"${form:action}","session":"${cookie:session_id}"}` + "\n",
|
|
// }))
|
|
//
|
|
// ## Custom Output (File Logging)
|
|
//
|
|
// file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
|
// if err != nil {
|
|
// log.Fatal(err)
|
|
// }
|
|
// defer file.Close()
|
|
//
|
|
// e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
|
// Output: file,
|
|
// }))
|
|
//
|
|
// ## Custom Tag Function
|
|
//
|
|
// e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
|
// Format: `{"time":"${time_rfc3339_nano}","user_id":"${custom}","method":"${method}"}` + "\n",
|
|
// CustomTagFunc: func(c echo.Context, buf *bytes.Buffer) (int, error) {
|
|
// userID := getUserIDFromContext(c) // Your custom logic
|
|
// return buf.WriteString(strconv.Itoa(userID))
|
|
// },
|
|
// }))
|
|
//
|
|
// ## Conditional Logging (Skip Certain Requests)
|
|
//
|
|
// e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
|
// Skipper: func(c echo.Context) bool {
|
|
// // Skip logging for health check endpoints
|
|
// return c.Request().URL.Path == "/health" || c.Request().URL.Path == "/metrics"
|
|
// },
|
|
// }))
|
|
//
|
|
// ## Integration with External Logging Service
|
|
//
|
|
// logBuffer := &SyncBuffer{} // Thread-safe buffer for external service
|
|
//
|
|
// e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
|
// Format: `{"timestamp":"${time_rfc3339_nano}","service":"my-api","level":"info",` +
|
|
// `"method":"${method}","uri":"${uri}","status":${status},"latency_ms":${latency},` +
|
|
// `"remote_ip":"${remote_ip}","user_agent":"${user_agent}","error":"${error}"}` + "\n",
|
|
// Output: logBuffer,
|
|
// }))
|
|
//
|
|
// # Available Tags
|
|
//
|
|
// ## Time Tags
|
|
// - time_unix: Unix timestamp (seconds)
|
|
// - time_unix_milli: Unix timestamp (milliseconds)
|
|
// - time_unix_micro: Unix timestamp (microseconds)
|
|
// - time_unix_nano: Unix timestamp (nanoseconds)
|
|
// - time_rfc3339: RFC3339 format (2006-01-02T15:04:05Z07:00)
|
|
// - time_rfc3339_nano: RFC3339 with nanoseconds
|
|
// - time_custom: Uses CustomTimeFormat field
|
|
//
|
|
// ## Request Information
|
|
// - id: Request ID from X-Request-ID header
|
|
// - remote_ip: Client IP address (respects proxy headers)
|
|
// - uri: Full request URI with query parameters
|
|
// - host: Host header value
|
|
// - method: HTTP method (GET, POST, etc.)
|
|
// - path: URL path without query parameters
|
|
// - route: Echo route pattern (e.g., /users/:id)
|
|
// - protocol: HTTP protocol version
|
|
// - referer: Referer header value
|
|
// - user_agent: User-Agent header value
|
|
//
|
|
// ## Response Information
|
|
// - status: HTTP status code
|
|
// - error: Error message if request failed
|
|
// - latency: Request processing time in nanoseconds
|
|
// - latency_human: Human-readable processing time
|
|
// - bytes_in: Request body size in bytes
|
|
// - bytes_out: Response body size in bytes
|
|
//
|
|
// ## Dynamic Tags
|
|
// - header:<NAME>: Value of specific header (e.g., header:Authorization)
|
|
// - query:<NAME>: Value of specific query parameter (e.g., query:user_id)
|
|
// - form:<NAME>: Value of specific form field (e.g., form:username)
|
|
// - cookie:<NAME>: Value of specific cookie (e.g., cookie:session_id)
|
|
// - custom: Output from CustomTagFunc
|
|
//
|
|
// # Troubleshooting
|
|
//
|
|
// ## Common Issues
|
|
//
|
|
// 1. **Missing logs**: Check if Skipper function is filtering out requests
|
|
// 2. **Invalid JSON**: Ensure CustomTagFunc outputs valid JSON content
|
|
// 3. **Performance issues**: Consider using a buffered writer for high-traffic applications
|
|
// 4. **File permission errors**: Ensure write permissions when logging to files
|
|
//
|
|
// ## Performance Tips
|
|
//
|
|
// - Use time_unix formats for better performance than time_rfc3339
|
|
// - Minimize the number of dynamic tags (header:, query:, form:, cookie:)
|
|
// - Use Skipper to exclude high-frequency, low-value requests (health checks, etc.)
|
|
// - Consider async logging for very high-traffic applications
|
|
type LoggerConfig struct {
|
|
// Skipper defines a function to skip middleware.
|
|
// Use this to exclude certain requests from logging (e.g., health checks).
|
|
//
|
|
// Example:
|
|
// Skipper: func(c echo.Context) bool {
|
|
// return c.Request().URL.Path == "/health"
|
|
// },
|
|
Skipper Skipper
|
|
|
|
// Format defines the logging format using template tags.
|
|
// Tags are enclosed in ${} and replaced with actual values.
|
|
// See the detailed tag documentation above for all available options.
|
|
//
|
|
// Default: JSON format with common fields
|
|
// Example: "${time_rfc3339_nano} ${status} ${method} ${uri} ${latency_human}\n"
|
|
Format string `yaml:"format"`
|
|
|
|
// CustomTimeFormat specifies the time format used by ${time_custom} tag.
|
|
// Uses Go's reference time: Mon Jan 2 15:04:05 MST 2006
|
|
//
|
|
// Default: "2006-01-02 15:04:05.00000"
|
|
// Example: "2006-01-02 15:04:05" or "15:04:05.000"
|
|
CustomTimeFormat string `yaml:"custom_time_format"`
|
|
|
|
// CustomTagFunc is called when ${custom} tag is encountered.
|
|
// Use this to add application-specific information to logs.
|
|
// The function should write valid content for your log format.
|
|
//
|
|
// Example:
|
|
// CustomTagFunc: func(c echo.Context, buf *bytes.Buffer) (int, error) {
|
|
// userID := getUserFromContext(c)
|
|
// return buf.WriteString(`"user_id":"` + userID + `"`)
|
|
// },
|
|
CustomTagFunc func(c echo.Context, buf *bytes.Buffer) (int, error)
|
|
|
|
// Output specifies where logs are written.
|
|
// Can be any io.Writer: files, buffers, network connections, etc.
|
|
//
|
|
// Default: os.Stdout
|
|
// Example: Custom file, syslog, or external logging service
|
|
Output io.Writer
|
|
|
|
template *fasttemplate.Template
|
|
colorer *color.Color
|
|
pool *sync.Pool
|
|
}
|
|
|
|
// DefaultLoggerConfig is the default Logger middleware config.
|
|
var DefaultLoggerConfig = LoggerConfig{
|
|
Skipper: DefaultSkipper,
|
|
Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}",` +
|
|
`"host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}",` +
|
|
`"status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}"` +
|
|
`,"bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n",
|
|
CustomTimeFormat: "2006-01-02 15:04:05.00000",
|
|
colorer: color.New(),
|
|
}
|
|
|
|
// Logger returns a middleware that logs HTTP requests using the default configuration.
|
|
//
|
|
// The default format logs requests as JSON with the following fields:
|
|
// - time: RFC3339 nano timestamp
|
|
// - id: Request ID from X-Request-ID header
|
|
// - remote_ip: Client IP address
|
|
// - host: Host header
|
|
// - method: HTTP method
|
|
// - uri: Request URI
|
|
// - user_agent: User-Agent header
|
|
// - status: HTTP status code
|
|
// - error: Error message (if any)
|
|
// - latency: Processing time in nanoseconds
|
|
// - latency_human: Human-readable processing time
|
|
// - bytes_in: Request body size
|
|
// - bytes_out: Response body size
|
|
//
|
|
// Example output:
|
|
//
|
|
// {"time":"2023-01-15T10:30:45.123456789Z","id":"","remote_ip":"127.0.0.1",
|
|
// "host":"localhost:8080","method":"GET","uri":"/users/123","user_agent":"curl/7.81.0",
|
|
// "status":200,"error":"","latency":1234567,"latency_human":"1.234567ms",
|
|
// "bytes_in":0,"bytes_out":42}
|
|
//
|
|
// For custom configurations, use LoggerWithConfig instead.
|
|
func Logger() echo.MiddlewareFunc {
|
|
return LoggerWithConfig(DefaultLoggerConfig)
|
|
}
|
|
|
|
// LoggerWithConfig returns a Logger middleware with custom configuration.
|
|
//
|
|
// This function allows you to customize all aspects of request logging including:
|
|
// - Log format and fields
|
|
// - Output destination
|
|
// - Time formatting
|
|
// - Custom tags and logic
|
|
// - Request filtering
|
|
//
|
|
// See LoggerConfig documentation for detailed configuration examples and options.
|
|
//
|
|
// Example:
|
|
//
|
|
// e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
|
// Format: "${time_rfc3339} ${status} ${method} ${uri} ${latency_human}\n",
|
|
// Output: customLogWriter,
|
|
// Skipper: func(c echo.Context) bool {
|
|
// return c.Request().URL.Path == "/health"
|
|
// },
|
|
// }))
|
|
func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
|
|
// Defaults
|
|
if config.Skipper == nil {
|
|
config.Skipper = DefaultLoggerConfig.Skipper
|
|
}
|
|
if config.Format == "" {
|
|
config.Format = DefaultLoggerConfig.Format
|
|
}
|
|
if config.Output == nil {
|
|
config.Output = DefaultLoggerConfig.Output
|
|
}
|
|
|
|
config.template = fasttemplate.New(config.Format, "${", "}")
|
|
config.colorer = color.New()
|
|
config.colorer.SetOutput(config.Output)
|
|
config.pool = &sync.Pool{
|
|
New: func() interface{} {
|
|
return bytes.NewBuffer(make([]byte, 256))
|
|
},
|
|
}
|
|
|
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) (err error) {
|
|
if config.Skipper(c) {
|
|
return next(c)
|
|
}
|
|
|
|
req := c.Request()
|
|
res := c.Response()
|
|
start := time.Now()
|
|
if err = next(c); err != nil {
|
|
c.Error(err)
|
|
}
|
|
stop := time.Now()
|
|
buf := config.pool.Get().(*bytes.Buffer)
|
|
buf.Reset()
|
|
defer config.pool.Put(buf)
|
|
|
|
if _, err = config.template.ExecuteFunc(buf, func(w io.Writer, tag string) (int, error) {
|
|
switch tag {
|
|
case "custom":
|
|
if config.CustomTagFunc == nil {
|
|
return 0, nil
|
|
}
|
|
return config.CustomTagFunc(c, buf)
|
|
case "time_unix":
|
|
return buf.WriteString(strconv.FormatInt(time.Now().Unix(), 10))
|
|
case "time_unix_milli":
|
|
// go 1.17 or later, it supports time#UnixMilli()
|
|
return buf.WriteString(strconv.FormatInt(time.Now().UnixNano()/1000000, 10))
|
|
case "time_unix_micro":
|
|
// go 1.17 or later, it supports time#UnixMicro()
|
|
return buf.WriteString(strconv.FormatInt(time.Now().UnixNano()/1000, 10))
|
|
case "time_unix_nano":
|
|
return buf.WriteString(strconv.FormatInt(time.Now().UnixNano(), 10))
|
|
case "time_rfc3339":
|
|
return buf.WriteString(time.Now().Format(time.RFC3339))
|
|
case "time_rfc3339_nano":
|
|
return buf.WriteString(time.Now().Format(time.RFC3339Nano))
|
|
case "time_custom":
|
|
return buf.WriteString(time.Now().Format(config.CustomTimeFormat))
|
|
case "id":
|
|
id := req.Header.Get(echo.HeaderXRequestID)
|
|
if id == "" {
|
|
id = res.Header().Get(echo.HeaderXRequestID)
|
|
}
|
|
return buf.WriteString(id)
|
|
case "remote_ip":
|
|
return buf.WriteString(c.RealIP())
|
|
case "host":
|
|
return buf.WriteString(req.Host)
|
|
case "uri":
|
|
return buf.WriteString(req.RequestURI)
|
|
case "method":
|
|
return buf.WriteString(req.Method)
|
|
case "path":
|
|
p := req.URL.Path
|
|
if p == "" {
|
|
p = "/"
|
|
}
|
|
return buf.WriteString(p)
|
|
case "route":
|
|
return buf.WriteString(c.Path())
|
|
case "protocol":
|
|
return buf.WriteString(req.Proto)
|
|
case "referer":
|
|
return buf.WriteString(req.Referer())
|
|
case "user_agent":
|
|
return buf.WriteString(req.UserAgent())
|
|
case "status":
|
|
n := res.Status
|
|
s := config.colorer.Green(n)
|
|
switch {
|
|
case n >= 500:
|
|
s = config.colorer.Red(n)
|
|
case n >= 400:
|
|
s = config.colorer.Yellow(n)
|
|
case n >= 300:
|
|
s = config.colorer.Cyan(n)
|
|
}
|
|
return buf.WriteString(s)
|
|
case "error":
|
|
if err != nil {
|
|
// Error may contain invalid JSON e.g. `"`
|
|
b, _ := json.Marshal(err.Error())
|
|
b = b[1 : len(b)-1]
|
|
return buf.Write(b)
|
|
}
|
|
case "latency":
|
|
l := stop.Sub(start)
|
|
return buf.WriteString(strconv.FormatInt(int64(l), 10))
|
|
case "latency_human":
|
|
return buf.WriteString(stop.Sub(start).String())
|
|
case "bytes_in":
|
|
cl := req.Header.Get(echo.HeaderContentLength)
|
|
if cl == "" {
|
|
cl = "0"
|
|
}
|
|
return buf.WriteString(cl)
|
|
case "bytes_out":
|
|
return buf.WriteString(strconv.FormatInt(res.Size, 10))
|
|
default:
|
|
switch {
|
|
case strings.HasPrefix(tag, "header:"):
|
|
return buf.Write([]byte(c.Request().Header.Get(tag[7:])))
|
|
case strings.HasPrefix(tag, "query:"):
|
|
return buf.Write([]byte(c.QueryParam(tag[6:])))
|
|
case strings.HasPrefix(tag, "form:"):
|
|
return buf.Write([]byte(c.FormValue(tag[5:])))
|
|
case strings.HasPrefix(tag, "cookie:"):
|
|
cookie, err := c.Cookie(tag[7:])
|
|
if err == nil {
|
|
return buf.Write([]byte(cookie.Value))
|
|
}
|
|
}
|
|
}
|
|
return 0, nil
|
|
}); err != nil {
|
|
return
|
|
}
|
|
|
|
if config.Output == nil {
|
|
_, err = c.Logger().Output().Write(buf.Bytes())
|
|
return
|
|
}
|
|
_, err = config.Output.Write(buf.Bytes())
|
|
return
|
|
}
|
|
}
|
|
}
|