HTTP Middleware
6 minute read
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:
- Extracts trace context from incoming request headers.
- Creates a span for the request with standard attributes.
- Propagates context to downstream handlers.
- Records HTTP method, URL, status code, and duration.
- Finishes the span when the request completes.
Standard Attributes
Every traced request includes:
| Attribute | Description | Example |
|---|---|---|
http.method | HTTP method | "GET" |
http.url | Full URL | "http://localhost:8080/api/users" |
http.scheme | URL scheme | "http" |
http.host | Host header | "localhost:8080" |
http.route | Request path | "/api/users" |
http.user_agent | User agent | "Mozilla/5.0..." |
http.status_code | Response status | 200 |
service.name | Service name | "my-api" |
service.version | Service version | "v1.0.0" |
rivaas.router.static_route | Standalone middleware always sets this to true | true |
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:
AuthorizationCookieSet-CookieX-API-KeyX-Auth-TokenProxy-AuthorizationWWW-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-idhttp.request.header.x-correlation-idhttp.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:
| Aspect | Metrics | Tracing |
|---|---|---|
| Main Function | metrics.Middleware() | tracing.Middleware() returns (handler, error) |
| Panic Version | metrics.MustMiddleware() | tracing.MustMiddleware() |
| Path Exclusion | metrics.WithExcludePaths() | tracing.WithExcludePaths() |
| Prefix Exclusion | metrics.WithExcludePrefixes() | tracing.WithExcludePrefixes() |
| Regex Exclusion | ✗ Not available | tracing.WithExcludePatterns() |
| Header Recording | metrics.WithHeaders() | tracing.WithHeaders() |
| Parameter Recording | ✗ Not available | tracing.WithRecordParams() |
Performance
| Operation | Time | Memory | Allocations |
|---|---|---|---|
| Request overhead (100% sampling) | ~1.6 µs | 2.3 KB | 23 |
| Path exclusion (100 paths) | ~9 ns | 0 B | 0 |
| Start/Finish span | ~160 ns | 240 B | 3 |
| Set attribute | ~3 ns | 0 B | 0 |
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
- Learn Context Propagation for distributed tracing
- Check Middleware Options for all options
- See Examples for production-ready configurations
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.