or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced-types.mdcontextual-logging.mdcore-logging.mddiode-writer.mdfield-methods.mdglobal-configuration.mdglobal-logger.mdhooks.mdhttp-middleware.mdindex.mdjournald-integration.mdpkgerrors-integration.mdsampling.mdwriters-and-output.md
tile.json

http-middleware.mddocs/

HTTP Middleware

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.

Package Imports

import (
    "net/http"
    "github.com/rs/zerolog/hlog"
)

Logger Injection

NewHandler

Inject a logger into the request context.

// Inject logger into request context
func NewHandler(log zerolog.Logger) func(http.Handler) http.Handler

Example:

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)

FromRequest

Retrieve the logger from the request context.

// Get logger from request context
func FromRequest(r *http.Request) *zerolog.Logger

Example:

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()).

Request Metadata Handlers

These handlers add request metadata to the logger's context.

RequestIDHandler

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.Handler

Parameters:

  • 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 header

Retrieving 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.Context

Example:

func handler(w http.ResponseWriter, r *http.Request) {
    if id, ok := hlog.IDFromRequest(r); ok {
        fmt.Fprintf(w, "Request ID: %s", id)
    }
}

URLHandler

Add the request URL to the logger context.

// Add request URL to logger context
func URLHandler(fieldKey string) func(next http.Handler) http.Handler

Example:

chain := hlog.NewHandler(logger)
chain = hlog.URLHandler("url")(chain)
// Logs include: "url":"/api/users?page=1"

MethodHandler

Add the HTTP method to the logger context.

// Add HTTP method to logger context
func MethodHandler(fieldKey string) func(next http.Handler) http.Handler

Example:

chain := hlog.MethodHandler("method")(chain)
// Logs include: "method":"GET"

RequestHandler

Add both method and URL to the logger context.

// Add "METHOD URL" to logger context
func RequestHandler(fieldKey string) func(next http.Handler) http.Handler

Example:

chain := hlog.RequestHandler("request")(chain)
// Logs include: "request":"GET /api/users"

RemoteAddrHandler

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.Handler

Example:

chain := hlog.RemoteAddrHandler("remote")(chain)
// Logs include: "remote":"192.168.1.100:54321"

RemoteIPHandler

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.Handler

Example:

chain := hlog.RemoteIPHandler("ip")(chain)
// Logs include: "ip":"192.168.1.100"

UserAgentHandler

Add the User-Agent header to the logger context.

// Add User-Agent to logger context
func UserAgentHandler(fieldKey string) func(next http.Handler) http.Handler

Example:

chain := hlog.UserAgentHandler("user_agent")(chain)
// Logs include: "user_agent":"Mozilla/5.0..."

RefererHandler

Add the Referer header to the logger context.

// Add Referer to logger context
func RefererHandler(fieldKey string) func(next http.Handler) http.Handler

Example:

chain := hlog.RefererHandler("referer")(chain)
// Logs include: "referer":"https://example.com/page"

HostHandler

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.Handler

Example:

// 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"

ProtoHandler

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.Handler

Example:

chain := hlog.ProtoHandler("protocol")(chain)
// Logs include: "protocol":"HTTP/1.1"

HTTPVersionHandler

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.Handler

Example:

chain := hlog.HTTPVersionHandler("http_version")(chain)
// Logs include: "http_version":"1.1"

CustomHeaderHandler

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.Handler

Example:

chain := hlog.CustomHeaderHandler("correlation_id", "X-Correlation-ID")(chain)
// Logs include: "correlation_id":"abc-123"

Response Metadata Handlers

EtagHandler

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.Handler

Example:

chain := hlog.EtagHandler("etag")(chain)
// Logs include: "etag":"33a64df551425fcc55e4d42a148795d9f25f89d4"

ResponseHeaderHandler

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.Handler

Example:

chain := hlog.ResponseHeaderHandler("content_type", "Content-Type")(chain)
// Logs include: "content_type":"application/json"

Access Logging

AccessHandler

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.Handler

Example:

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)

Complete Middleware Example

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"
}

Middleware Ordering

Order matters when chaining middleware. Generally:

  1. NewHandler - Must be first to inject logger
  2. RequestIDHandler - Early, so ID is available to other handlers
  3. Request metadata - Method, URL, headers, etc.
  4. Your application handler - Business logic
  5. AccessHandler - Should be last to capture complete information
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.

Usage Patterns

Multiple Routers

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
}

Per-Route Middleware

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)

Conditional Middleware

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)
}

With Third-Party Routers

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)

Best Practices

1. Always Use NewHandler First

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)

2. Generate Request IDs Early

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 handlers

3. Use AccessHandler for Metrics

Combine 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)

4. Consistent Field Names

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)

5. Don't Log Sensitive Headers

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)       // BAD

Performance Considerations

  • Middleware overhead is minimal (nanoseconds per request)
  • UpdateContext is used internally (not thread-safe, but safe here)
  • Request ID generation uses github.com/rs/xid (fast, unique)
  • AccessHandler captures response metadata without buffering
  • Logging is deferred until handler completes (non-blocking)

Thread Safety

All hlog middleware is thread-safe and can be used with concurrent requests. The logger is copied per-request to avoid data races.

See Also

  • Core Logging - Logger basics
  • Contextual Logging - Using context with loggers
  • Global Logger - Global logger convenience functions
  • Writers and Output - Output configuration