feat(logging): add zerolog + lumberjack structured logging package
This commit is contained in:
@@ -26,6 +26,7 @@ require (
|
|||||||
github.com/klauspost/compress v1.17.6 // indirect
|
github.com/klauspost/compress v1.17.6 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
@@ -33,6 +34,7 @@ require (
|
|||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
github.com/refraction-networking/utls v1.8.2 // indirect
|
github.com/refraction-networking/utls v1.8.2 // indirect
|
||||||
|
github.com/rs/zerolog v1.35.0 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
github.com/tidwall/sjson v1.2.5 // indirect
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
@@ -45,4 +47,5 @@ require (
|
|||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
|
gopkg.in/lumberjack.v2 v2.0.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -65,6 +67,8 @@ github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEv
|
|||||||
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
|
github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
|
||||||
|
github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
@@ -117,6 +121,8 @@ google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j
|
|||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/lumberjack.v2 v2.0.0 h1:IDj6hi8KbNiPQ5VaYNFZ7dBJLF5LFeKvsFrWHjA5aq4=
|
||||||
|
gopkg.in/lumberjack.v2 v2.0.0/go.mod h1:bp5nQ2kK/lLQSmTk29azj9+JB6bWci56xFn/lvd5GLI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"gopkg.in/lumberjack.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config holds logging configuration, mirrors config.LoggingConfig.
|
||||||
|
type Config struct {
|
||||||
|
Level string
|
||||||
|
File string
|
||||||
|
MaxSizeMB int
|
||||||
|
MaxBackups int
|
||||||
|
MaxAgeDays int
|
||||||
|
Compress bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup initializes the global zerolog logger.
|
||||||
|
// - File set: JSON → lumberjack rotating file
|
||||||
|
// - File empty + TTY: colored ConsoleWriter → stderr
|
||||||
|
// - File empty + not TTY: JSON → stderr (for systemd journal)
|
||||||
|
func Setup(cfg Config) zerolog.Logger {
|
||||||
|
// Parse log level
|
||||||
|
level, err := zerolog.ParseLevel(cfg.Level)
|
||||||
|
if err != nil || cfg.Level == "" {
|
||||||
|
level = zerolog.InfoLevel
|
||||||
|
}
|
||||||
|
zerolog.SetGlobalLevel(level)
|
||||||
|
zerolog.TimeFieldFormat = time.RFC3339
|
||||||
|
|
||||||
|
var logger zerolog.Logger
|
||||||
|
|
||||||
|
if cfg.File != "" {
|
||||||
|
// Production: JSON to rotating file
|
||||||
|
jack := &lumberjack.Logger{
|
||||||
|
Filename: cfg.File,
|
||||||
|
MaxSize: cfg.MaxSizeMB,
|
||||||
|
MaxBackups: cfg.MaxBackups,
|
||||||
|
MaxAge: cfg.MaxAgeDays,
|
||||||
|
Compress: cfg.Compress,
|
||||||
|
}
|
||||||
|
logger = zerolog.New(jack).With().Timestamp().Caller().Logger()
|
||||||
|
} else {
|
||||||
|
fi, err := os.Stderr.Stat()
|
||||||
|
isTTY := err == nil && (fi.Mode()&os.ModeCharDevice) != 0
|
||||||
|
if isTTY {
|
||||||
|
// Dev mode: colored console
|
||||||
|
cw := zerolog.ConsoleWriter{
|
||||||
|
Out: os.Stderr,
|
||||||
|
TimeFormat: time.RFC3339,
|
||||||
|
}
|
||||||
|
logger = zerolog.New(cw).With().Timestamp().Caller().Logger()
|
||||||
|
} else {
|
||||||
|
// Systemd journal: JSON to stderr
|
||||||
|
logger = zerolog.New(os.Stderr).With().Timestamp().Caller().Logger()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set global
|
||||||
|
log.Logger = logger
|
||||||
|
return logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// GinRequestLogger returns a Gin middleware that logs every request with zerolog.
|
||||||
|
// Logs AFTER the request completes.
|
||||||
|
// Level: Info for 2xx, Warn for 4xx, Error for 5xx
|
||||||
|
func GinRequestLogger() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
start := time.Now()
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
method := c.Request.Method
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
status := c.Writer.Status()
|
||||||
|
latencyMs := float64(time.Since(start).Microseconds()) / 1000.0
|
||||||
|
clientIP := c.ClientIP()
|
||||||
|
requestID := c.Writer.Header().Get("X-Request-Id")
|
||||||
|
if requestID == "" {
|
||||||
|
requestID = c.GetHeader("x-client-request-id")
|
||||||
|
}
|
||||||
|
|
||||||
|
evt := log.Logger.WithLevel(statusLevel(status)).
|
||||||
|
Str("method", method).
|
||||||
|
Str("path", path).
|
||||||
|
Int("status", status).
|
||||||
|
Float64("latency_ms", latencyMs).
|
||||||
|
Str("client_ip", clientIP)
|
||||||
|
|
||||||
|
if requestID != "" {
|
||||||
|
evt = evt.Str("request_id", requestID)
|
||||||
|
}
|
||||||
|
|
||||||
|
evt.Msg("request")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusLevel(status int) zerolog.Level {
|
||||||
|
switch {
|
||||||
|
case status >= 500:
|
||||||
|
return zerolog.ErrorLevel
|
||||||
|
case status >= 400:
|
||||||
|
return zerolog.WarnLevel
|
||||||
|
default:
|
||||||
|
return zerolog.InfoLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromContext returns the zerolog logger from context, or the global logger.
|
||||||
|
func FromContext(ctx context.Context) *zerolog.Logger {
|
||||||
|
l := zerolog.Ctx(ctx)
|
||||||
|
if l.GetLevel() == zerolog.Disabled {
|
||||||
|
return &log.Logger
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedactHeaders serializes HTTP headers to a JSON string,
|
||||||
|
// replacing Authorization and x-api-key values with "***".
|
||||||
|
func RedactHeaders(h http.Header) string {
|
||||||
|
redacted := make(map[string]string, len(h))
|
||||||
|
for k, v := range h {
|
||||||
|
key := strings.ToLower(k)
|
||||||
|
if key == "authorization" || key == "x-api-key" {
|
||||||
|
redacted[k] = "***"
|
||||||
|
} else {
|
||||||
|
redacted[k] = strings.Join(v, ", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b, _ := json.Marshal(redacted)
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user