HTTP Middleware

Automatically trace HTTP requests with middleware

The tracing package provides HTTP middleware for automatic request tracing with any HTTP framework. When using the app package, observability uses StartRequestSpan with the same W3C propagation, sampling rules, and core HTTP attributes. Standalone middleware sets http.route from req.URL.Path and rivaas.router.static_route to true; the app passes the route path and static flag explicitly.

Basic Usage

Wrap your HTTP handler with tracing middleware:

import (
    "net/http"
    "rivaas.dev/tracing"
)

func main() {
    tracer := tracing.MustNew(
        tracing.WithServiceName("my-api"),
        tracing.WithOTLP("localhost:4317"),
    )
    if err := tracer.Start(context.Background()); err != nil {
        log.Fatal(err)
    }
    defer tracer.Shutdown(context.Background())

    mux := http.NewServeMux()
    mux.HandleFunc("/api/users", handleUsers)
    
    // Wrap with middleware (MustMiddleware panics on invalid options)
    handler := tracing.MustMiddleware(tracer)(mux)
    
    http.ListenAndServe(":8080", handler)
}

With OTLP, you must call Start(ctx) before traces are exported; omitting it means no traces.

Middleware Functions

Two functions are available for creating middleware:

Middleware (returns error)

handler, err := tracing.Middleware(tracer,
    tracing.WithExcludePaths("/health"),
    tracing.WithHeaders("X-Request-ID"),
)
if err != nil {
    log.Fatal(err) // e.g. nil tracer, nil option, invalid regex
}
http.ListenAndServe(":8080", handler(mux))

Use Middleware when you need to handle errors (e.g. config-driven setup). It returns an error for nil tracer, nil options, or invalid options (e.g. invalid regex in path exclusion).

MustMiddleware (panics on error)

handler := tracing.MustMiddleware(tracer,
    tracing.WithExcludePaths("/health"),
)(mux)
http.ListenAndServe(":8080", handler)

Use MustMiddleware when invalid options are a programming error. It panics with an error (the same validation errors that Middleware would return); you can recover and use errors.As / errors.Is.

What Gets Traced

The middleware automatically:

  1. Extracts trace context from incoming request headers.
  2. Creates a span for the request with standard attributes.
  3. Propagates context to downstream handlers.
  4. Records HTTP method, URL, status code, and duration.
  5. Finishes the span when the request completes.

Standard Attributes

Every traced request includes:

AttributeDescriptionExample
http.methodHTTP method"GET"
http.urlFull URL"http://localhost:8080/api/users"
http.schemeURL scheme"http"
http.hostHost header"localhost:8080"
http.routeRequest path"/api/users"
http.user_agentUser agent"Mozilla/5.0..."
http.status_codeResponse status200
service.nameService name"my-api"
service.versionService version"v1.0.0"
rivaas.router.static_routeStandalone middleware always sets this to truetrue

For this middleware, http.route is the request path (req.URL.Path), not a framework route template.

Path Exclusion

Exclude specific paths from tracing to reduce noise and overhead.

Exact Path Matching

Exclude specific paths exactly:

handler := tracing.MustMiddleware(tracer,
    tracing.WithExcludePaths("/health", "/metrics", "/ready"),
)(mux)

Requests to /health, /metrics, or /ready won’t create spans.

Prefix Matching

Exclude all paths with a given prefix:

handler := tracing.MustMiddleware(tracer,
    tracing.WithExcludePrefixes("/debug/", "/internal/", "/.well-known/"),
)(mux)

Excludes:

  • /debug/pprof
  • /debug/vars
  • /internal/health
  • /.well-known/acme-challenge

Regex Pattern Matching

Exclude paths matching regex patterns:

handler := tracing.MustMiddleware(tracer,
    tracing.WithExcludePatterns(
        `^/v[0-9]+/internal/.*`,  // Version-prefixed internal routes
        `^/api/health.*`,          // Any health-related endpoint
    ),
)(mux)

Important: Invalid regex patterns make tracing.Middleware return an error. tracing.MustMiddleware panics with that same error during setup.

Combined Exclusions

Use multiple exclusion types together:

handler := tracing.MustMiddleware(tracer,
    // Exact paths
    tracing.WithExcludePaths("/health", "/metrics"),
    
    // Prefixes
    tracing.WithExcludePrefixes("/debug/", "/internal/"),
    
    // Patterns
    tracing.WithExcludePatterns(`^/v[0-9]+/internal/.*`),
)(mux)

Performance

Path exclusion is highly efficient:

  • Exact paths: O(1) hash map lookup
  • Prefixes: O(n) where n = number of prefixes
  • Patterns: O(p) where p = number of patterns

Even with 100+ excluded paths, overhead is negligible (~9ns per request).

Header Recording

Record specific request headers as span attributes.

Basic Header Recording

handler := tracing.MustMiddleware(tracer,
    tracing.WithHeaders("X-Request-ID", "X-Correlation-ID"),
)(mux)

Headers are recorded as: http.request.header.{name}

Example span attributes:

  • http.request.header.x-request-id: "abc123"
  • http.request.header.x-correlation-id: "xyz789"

Security

Sensitive headers are automatically filtered and never recorded:

  • Authorization
  • Cookie
  • Set-Cookie
  • X-API-Key
  • X-Auth-Token
  • Proxy-Authorization
  • WWW-Authenticate

This protects against accidental credential exposure in traces.

// This is safe - Authorization header is filtered
handler := tracing.MustMiddleware(tracer,
    tracing.WithHeaders(
        "X-Request-ID",
        "Authorization", // ← Automatically filtered, won't be recorded
        "X-Correlation-ID",
    ),
)(mux)

Header Name Normalization

Header names are case-insensitive and normalized to lowercase:

tracing.WithHeaders("X-Request-ID", "x-correlation-id", "User-Agent")

All recorded as lowercase:

  • http.request.header.x-request-id
  • http.request.header.x-correlation-id
  • http.request.header.user-agent

Query Parameter Recording

Record URL query parameters as span attributes.

Default Behavior

By default, all query parameters are recorded:

handler := tracing.MustMiddleware(tracer)(mux)
// Or: mw, err := tracing.Middleware(tracer); if err != nil { ... }; handler := mw(mux)
// All params recorded by default

Request: GET /api/users?page=2&limit=10&user_id=123

Span attributes:

  • http.request.param.page: ["2"]
  • http.request.param.limit: ["10"]
  • http.request.param.user_id: ["123"]

Whitelist Parameters

Record only specific parameters:

handler := tracing.MustMiddleware(tracer,
    tracing.WithRecordParams("page", "limit", "user_id"),
)(mux)

Only page, limit, and user_id are recorded. Others are ignored.

Blacklist Parameters

Exclude sensitive parameters while recording all others:

handler := tracing.MustMiddleware(tracer,
    tracing.WithExcludeParams("password", "token", "api_key", "secret"),
)(mux)

All parameters recorded except password, token, api_key, and secret.

Disable Parameter Recording

Don’t record any query parameters:

handler := tracing.MustMiddleware(tracer,
    tracing.WithoutParams(),
)(mux)

Useful when parameters may contain sensitive data.

Combined Parameter Options

// Record only safe parameters, explicitly exclude sensitive ones
handler := tracing.MustMiddleware(tracer,
    tracing.WithRecordParams("page", "limit", "sort"),
    tracing.WithExcludeParams("api_key", "token"), // Takes precedence
)(mux)

Behavior: Blacklist takes precedence. Even if api_key is in the whitelist, it won’t be recorded.

Complete Middleware Example

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    
    "rivaas.dev/tracing"
)

func main() {
    ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
    defer cancel()

    // Create tracer
    tracer := tracing.MustNew(
        tracing.WithServiceName("user-api"),
        tracing.WithServiceVersion("v1.2.3"),
        tracing.WithOTLP("localhost:4317"),
        tracing.WithSampleRate(0.1), // 10% sampling
    )
    
    if err := tracer.Start(ctx); err != nil {
        log.Fatal(err)
    }
    defer tracer.Shutdown(context.Background())

    // Create HTTP handlers
    mux := http.NewServeMux()
    mux.HandleFunc("/api/users", handleUsers)
    mux.HandleFunc("/api/orders", handleOrders)
    mux.HandleFunc("/health", handleHealth)
    mux.HandleFunc("/metrics", handleMetrics)
    
    // Wrap with tracing middleware
    handler := tracing.MustMiddleware(tracer,
        // Exclude health/metrics endpoints
        tracing.WithExcludePaths("/health", "/metrics", "/ready", "/live"),
        
        // Exclude debug and internal routes
        tracing.WithExcludePrefixes("/debug/", "/internal/"),
        
        // Record correlation headers
        tracing.WithHeaders("X-Request-ID", "X-Correlation-ID", "User-Agent"),
        
        // Whitelist safe parameters
        tracing.WithRecordParams("page", "limit", "sort", "filter"),
        
        // Blacklist sensitive parameters
        tracing.WithExcludeParams("password", "token", "api_key"),
    )(mux)

    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", handler))
}

func handleUsers(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.Write([]byte(`{"users": []}`))
}

func handleOrders(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.Write([]byte(`{"orders": []}`))
}

func handleHealth(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("OK"))
}

func handleMetrics(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/plain")
    w.Write([]byte("# Metrics"))
}

Integration with Custom Context

Access the span from within your handlers. When you only have context (e.g. from the middleware), use SetSpanAttributeFromContext and AddSpanEventFromContext; they are equivalent to tracer.SetSpanAttribute and tracer.AddSpanEvent.

func handleUser(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    
    // Add custom attributes to the current span
    tracing.SetSpanAttributeFromContext(ctx, "user.action", "view_profile")
    tracing.SetSpanAttributeFromContext(ctx, "user.id", getUserID(r))
    
    // Add events
    tracing.AddSpanEventFromContext(ctx, "profile_viewed",
        attribute.String("profile_id", "123"),
    )
    
    // Your handler logic...
}

Comparison with Metrics Middleware

The tracing middleware follows the same pattern as the metrics middleware:

AspectMetricsTracing
Main Functionmetrics.Middleware()tracing.Middleware() returns (handler, error)
Panic Versionmetrics.MustMiddleware()tracing.MustMiddleware()
Path Exclusionmetrics.WithExcludePaths()tracing.WithExcludePaths()
Prefix Exclusionmetrics.WithExcludePrefixes()tracing.WithExcludePrefixes()
Regex Exclusion✗ Not availabletracing.WithExcludePatterns()
Header Recordingmetrics.WithHeaders()tracing.WithHeaders()
Parameter Recording✗ Not availabletracing.WithRecordParams()

Performance

OperationTimeMemoryAllocations
Request overhead (100% sampling)~1.6 µs2.3 KB23
Path exclusion (100 paths)~9 ns0 B0
Start/Finish span~160 ns240 B3
Set attribute~3 ns0 B0

Best Practices

Always Exclude Health Checks

tracing.WithExcludePaths("/health", "/metrics", "/ready", "/live")

Health checks are high-frequency and low-value for tracing.

Use Sampling for High Traffic

tracer := tracing.MustNew(
    tracing.WithServiceName("my-api"),
    tracing.WithSampleRate(0.1), // 10% sampling
    tracing.WithOTLP("collector:4317"),
)

Reduces overhead and trace storage costs.

Record Correlation IDs

tracing.WithHeaders("X-Request-ID", "X-Correlation-ID", "X-Trace-ID")

Helps correlate traces with logs and other observability data.

Blacklist Sensitive Parameters

tracing.WithExcludeParams("password", "token", "api_key", "secret", "credit_card")

Prevents accidental exposure of credentials in traces.

Combine with Span Hooks

startHook := func(ctx context.Context, span trace.Span, req *http.Request) {
    // Add business context from request
    if tenantID := extractTenant(req); tenantID != "" {
        span.SetAttributes(attribute.String("tenant.id", tenantID))
    }
}

tracer := tracing.MustNew(
    tracing.WithServiceName("my-api"),
    tracing.WithSpanStartHook(startHook),
    tracing.WithOTLP("collector:4317"),
)

Next Steps