HTTP Middleware
5 minute read
The tracing package provides HTTP middleware for automatic request tracing with any HTTP framework.
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"),
)
tracer.Start(context.Background())
defer tracer.Shutdown(context.Background())
mux := http.NewServeMux()
mux.HandleFunc("/api/users", handleUsers)
// Wrap with middleware
handler := tracing.Middleware(tracer)(mux)
http.ListenAndServe(":8080", handler)
}
Middleware Functions
Two functions are available for creating middleware:
Middleware (Panics on Error)
handler := tracing.Middleware(tracer,
tracing.WithExcludePaths("/health"),
)(mux)
Panics if middleware options are invalid (e.g., invalid regex pattern).
MustMiddleware (Alias)
handler := tracing.MustMiddleware(tracer,
tracing.WithExcludePaths("/health"),
)(mux)
Identical to Middleware() - provided for API consistency with MustNew().
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" |
Path Exclusion
Exclude specific paths from tracing to reduce noise and overhead.
Exact Path Matching
Exclude specific paths exactly:
handler := tracing.Middleware(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.Middleware(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.Middleware(tracer,
tracing.WithExcludePatterns(
`^/v[0-9]+/internal/.*`, // Version-prefixed internal routes
`^/api/health.*`, // Any health-related endpoint
),
)(mux)
Important: Invalid regex patterns cause the middleware to panic during initialization.
Combined Exclusions
Use multiple exclusion types together:
handler := tracing.Middleware(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.Middleware(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.Middleware(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.Middleware(tracer)(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.Middleware(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.Middleware(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.Middleware(tracer,
tracing.WithoutParams(),
)(mux)
Useful when parameters may contain sensitive data.
Combined Parameter Options
// Record only safe parameters, explicitly exclude sensitive ones
handler := tracing.Middleware(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:
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() |
| 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.