Zero Allocation JSON Logger - High-performance structured logging library for Go
The hlog subpackage provides HTTP middleware for zerolog, making it easy to add request logging with automatic metadata capture. All middleware functions follow the standard Go http.Handler pattern.
import (
"net/http"
"github.com/rs/zerolog/hlog"
)Inject a logger into the request context.
// Inject logger into request context
func NewHandler(log zerolog.Logger) func(http.Handler) http.HandlerExample:
logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
handler := hlog.NewHandler(logger)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
hlog.FromRequest(r).Info().Msg("handling request")
}))
http.ListenAndServe(":8080", handler)Retrieve the logger from the request context.
// Get logger from request context
func FromRequest(r *http.Request) *zerolog.LoggerExample:
func myHandler(w http.ResponseWriter, r *http.Request) {
logger := hlog.FromRequest(r)
logger.Info().Msg("processing request")
}This is a convenience wrapper around zerolog.Ctx(r.Context()).
These handlers add request metadata to the logger's context.
Generate and track unique request IDs.
// Add request ID to logger context and optionally to response header
func RequestIDHandler(fieldKey, headerName string) func(next http.Handler) http.HandlerParameters:
fieldKey - Field name for logger (empty to skip logging)headerName - Response header name (empty to skip header)Example:
chain := hlog.NewHandler(logger)
chain = hlog.RequestIDHandler("request_id", "X-Request-ID")(chain)
http.ListenAndServe(":8080", chain)
// Each request gets unique ID logged and sent in headerRetrieving request ID:
// Get request ID from request
func IDFromRequest(r *http.Request) (id xid.ID, ok bool)
// Get request ID from context
func IDFromCtx(ctx context.Context) (id xid.ID, ok bool)
// Add request ID to context
func CtxWithID(ctx context.Context, id xid.ID) context.ContextExample:
func handler(w http.ResponseWriter, r *http.Request) {
if id, ok := hlog.IDFromRequest(r); ok {
fmt.Fprintf(w, "Request ID: %s", id)
}
}Add the request URL to the logger context.
// Add request URL to logger context
func URLHandler(fieldKey string) func(next http.Handler) http.HandlerExample:
chain := hlog.NewHandler(logger)
chain = hlog.URLHandler("url")(chain)
// Logs include: "url":"/api/users?page=1"Add the HTTP method to the logger context.
// Add HTTP method to logger context
func MethodHandler(fieldKey string) func(next http.Handler) http.HandlerExample:
chain := hlog.MethodHandler("method")(chain)
// Logs include: "method":"GET"Add both method and URL to the logger context.
// Add "METHOD URL" to logger context
func RequestHandler(fieldKey string) func(next http.Handler) http.HandlerExample:
chain := hlog.RequestHandler("request")(chain)
// Logs include: "request":"GET /api/users"Add the remote address (IP and port) to the logger context.
// Add remote address to logger context
func RemoteAddrHandler(fieldKey string) func(next http.Handler) http.HandlerExample:
chain := hlog.RemoteAddrHandler("remote")(chain)
// Logs include: "remote":"192.168.1.100:54321"Add only the remote IP (without port) to the logger context.
// Add remote IP only to logger context
func RemoteIPHandler(fieldKey string) func(next http.Handler) http.HandlerExample:
chain := hlog.RemoteIPHandler("ip")(chain)
// Logs include: "ip":"192.168.1.100"Add the User-Agent header to the logger context.
// Add User-Agent to logger context
func UserAgentHandler(fieldKey string) func(next http.Handler) http.HandlerExample:
chain := hlog.UserAgentHandler("user_agent")(chain)
// Logs include: "user_agent":"Mozilla/5.0..."Add the Referer header to the logger context.
// Add Referer to logger context
func RefererHandler(fieldKey string) func(next http.Handler) http.HandlerExample:
chain := hlog.RefererHandler("referer")(chain)
// Logs include: "referer":"https://example.com/page"Add the request host to the logger context.
// Add host to logger context (optionally trim port)
func HostHandler(fieldKey string, trimPort ...bool) func(next http.Handler) http.HandlerExample:
// With port
chain := hlog.HostHandler("host")(chain)
// Logs include: "host":"example.com:8080"
// Without port
chain := hlog.HostHandler("host", true)(chain)
// Logs include: "host":"example.com"Add the protocol version to the logger context.
// Add protocol version (e.g., "HTTP/1.1") to logger context
func ProtoHandler(fieldKey string) func(next http.Handler) http.HandlerExample:
chain := hlog.ProtoHandler("protocol")(chain)
// Logs include: "protocol":"HTTP/1.1"Add the HTTP version without the "HTTP/" prefix.
// Add HTTP version (e.g., "1.1") to logger context
func HTTPVersionHandler(fieldKey string) func(next http.Handler) http.HandlerExample:
chain := hlog.HTTPVersionHandler("http_version")(chain)
// Logs include: "http_version":"1.1"Add any custom request header to the logger context.
// Add custom request header to logger context
func CustomHeaderHandler(fieldKey, header string) func(next http.Handler) http.HandlerExample:
chain := hlog.CustomHeaderHandler("correlation_id", "X-Correlation-ID")(chain)
// Logs include: "correlation_id":"abc-123"Add the Etag from the response headers to the logger context.
// Add Etag from response to logger context
func EtagHandler(fieldKey string) func(next http.Handler) http.HandlerExample:
chain := hlog.EtagHandler("etag")(chain)
// Logs include: "etag":"33a64df551425fcc55e4d42a148795d9f25f89d4"Add any custom response header to the logger context.
// Add custom response header to logger context
func ResponseHeaderHandler(fieldKey, headerName string) func(next http.Handler) http.HandlerExample:
chain := hlog.ResponseHeaderHandler("content_type", "Content-Type")(chain)
// Logs include: "content_type":"application/json"Log HTTP access information after each request.
// Call function after each request with access information
func AccessHandler(f func(r *http.Request, status, size int, duration time.Duration)) func(next http.Handler) http.HandlerExample:
chain := hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
hlog.FromRequest(r).Info().
Int("status", status).
Int("size", size).
Dur("duration", duration).
Msg("request completed")
})(chain)Complete access logging:
accessLogger := func(r *http.Request, status, size int, duration time.Duration) {
logger := hlog.FromRequest(r)
var event *zerolog.Event
if status >= 500 {
event = logger.Error()
} else if status >= 400 {
event = logger.Warn()
} else {
event = logger.Info()
}
event.
Int("status", status).
Int("size", size).
Dur("duration", duration).
Msg("access")
}
handler := hlog.AccessHandler(accessLogger)(myHandler)package main
import (
"net/http"
"os"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/hlog"
)
func main() {
// Create logger
logger := zerolog.New(os.Stdout).With().
Timestamp().
Str("service", "api").
Logger()
// Define handler
myHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get logger from request
hlog.FromRequest(r).Info().Msg("handling request")
// Your handler logic
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello World"))
})
// Chain middleware
handler := hlog.NewHandler(logger)(myHandler)
handler = hlog.RequestIDHandler("request_id", "X-Request-ID")(handler)
handler = hlog.MethodHandler("method")(handler)
handler = hlog.URLHandler("url")(handler)
handler = hlog.RemoteIPHandler("ip")(handler)
handler = hlog.UserAgentHandler("user_agent")(handler)
handler = hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
hlog.FromRequest(r).Info().
Int("status", status).
Int("size", size).
Dur("duration", duration).
Msg("access")
})(handler)
// Start server
http.ListenAndServe(":8080", handler)
}Output example:
{
"level":"info",
"service":"api",
"request_id":"c0umghhqb5jnvr5jq9kg",
"method":"GET",
"url":"/api/users",
"ip":"192.168.1.100",
"user_agent":"Mozilla/5.0...",
"time":"2024-01-15T10:30:00Z",
"message":"handling request"
}
{
"level":"info",
"service":"api",
"request_id":"c0umghhqb5jnvr5jq9kg",
"method":"GET",
"url":"/api/users",
"ip":"192.168.1.100",
"user_agent":"Mozilla/5.0...",
"status":200,
"size":11,
"duration":5.123,
"time":"2024-01-15T10:30:00Z",
"message":"access"
}Order matters when chaining middleware. Generally:
handler := myHandler
handler = hlog.AccessHandler(accessLog)(handler)
handler = hlog.UserAgentHandler("user_agent")(handler)
handler = hlog.RemoteIPHandler("ip")(handler)
handler = hlog.URLHandler("url")(handler)
handler = hlog.MethodHandler("method")(handler)
handler = hlog.RequestIDHandler("request_id", "X-Request-ID")(handler)
handler = hlog.NewHandler(logger)(handler)Note: Middleware wraps from bottom to top in the chain above.
func setupRoutes(logger zerolog.Logger) http.Handler {
mux := http.NewServeMux()
// Apply middleware to all routes
handler := hlog.NewHandler(logger)(mux)
handler = hlog.RequestIDHandler("request_id", "X-Request-ID")(handler)
handler = hlog.AccessHandler(accessLog)(handler)
// Add routes
mux.HandleFunc("/api/users", usersHandler)
mux.HandleFunc("/api/posts", postsHandler)
return handler
}mux := http.NewServeMux()
// Global middleware
handler := hlog.NewHandler(logger)(mux)
handler = hlog.RequestIDHandler("request_id", "X-Request-ID")(handler)
// Per-route middleware
adminHandler := http.HandlerFunc(adminFunc)
adminHandler = hlog.CustomHeaderHandler("admin_id", "X-Admin-ID")(adminHandler)
mux.Handle("/admin", adminHandler)
mux.HandleFunc("/public", publicFunc)var handler http.Handler = myHandler
if production {
// Production: minimal logging
handler = hlog.NewHandler(logger)(handler)
handler = hlog.RequestIDHandler("request_id", "X-Request-ID")(handler)
handler = hlog.AccessHandler(accessLog)(handler)
} else {
// Development: detailed logging
handler = hlog.NewHandler(logger)(handler)
handler = hlog.RequestIDHandler("request_id", "X-Request-ID")(handler)
handler = hlog.MethodHandler("method")(handler)
handler = hlog.URLHandler("url")(handler)
handler = hlog.RemoteIPHandler("ip")(handler)
handler = hlog.UserAgentHandler("user_agent")(handler)
handler = hlog.RefererHandler("referer")(handler)
handler = hlog.AccessHandler(verboseAccessLog)(handler)
}Chi router:
import "github.com/go-chi/chi/v5"
r := chi.NewRouter()
r.Use(hlog.NewHandler(logger))
r.Use(hlog.RequestIDHandler("request_id", "X-Request-ID"))
r.Use(hlog.AccessHandler(accessLog))
r.Get("/", homeHandler)Gorilla mux:
import "github.com/gorilla/mux"
r := mux.NewRouter()
r.Use(hlog.NewHandler(logger))
r.Use(hlog.RequestIDHandler("request_id", "X-Request-ID"))
r.Use(hlog.AccessHandler(accessLog))
r.HandleFunc("/", homeHandler)NewHandler must be first in the chain to inject the logger:
// Good
handler := hlog.NewHandler(logger)(myHandler)
handler = hlog.RequestIDHandler("request_id", "X-Request-ID")(handler)
// Bad - RequestIDHandler won't find logger
handler := hlog.RequestIDHandler("request_id", "X-Request-ID")(myHandler)
handler = hlog.NewHandler(logger)(handler)Generate request IDs early so they're available in all logs:
handler := hlog.NewHandler(logger)(myHandler)
handler = hlog.RequestIDHandler("request_id", "X-Request-ID")(handler)
// RequestID now available to all subsequent middleware and handlersCombine access logging with metrics:
accessHandler := func(r *http.Request, status, size int, duration time.Duration) {
// Log
hlog.FromRequest(r).Info().
Int("status", status).
Dur("duration", duration).
Msg("access")
// Metrics
metrics.RequestDuration.Observe(duration.Seconds())
metrics.RequestCount.WithLabelValues(strconv.Itoa(status)).Inc()
}
handler = hlog.AccessHandler(accessHandler)(handler)Use consistent field names across your application:
const (
RequestIDField = "request_id"
MethodField = "method"
URLField = "url"
StatusField = "status"
DurationField = "duration"
)
handler = hlog.RequestIDHandler(RequestIDField, "X-Request-ID")(handler)
handler = hlog.MethodHandler(MethodField)(handler)Avoid logging headers that may contain sensitive data:
// Good
handler = hlog.UserAgentHandler("user_agent")(handler)
handler = hlog.CustomHeaderHandler("api_version", "X-API-Version")(handler)
// Avoid
handler = hlog.CustomHeaderHandler("auth", "Authorization")(handler) // BAD
handler = hlog.CustomHeaderHandler("cookie", "Cookie")(handler) // BADUpdateContext is used internally (not thread-safe, but safe here)github.com/rs/xid (fast, unique)All hlog middleware is thread-safe and can be used with concurrent requests. The logger is copied per-request to avoid data races.
Install with Tessl CLI
npx tessl i tessl/golang-github-com-rs-zerolog