HTTP Server Observability
Complete example of instrumenting an HTTP server with aperture.
Signals and Keys
package signals
import "github.com/zoobz-io/capitan"
// HTTP request lifecycle
var (
HTTPRequestReceived = capitan.NewSignal("http.request.received", "HTTP request received")
HTTPRequestCompleted = capitan.NewSignal("http.request.completed", "HTTP request completed")
)
// Field keys
var (
RequestID = capitan.NewStringKey("request_id")
Method = capitan.NewStringKey("method")
Path = capitan.NewStringKey("path")
StatusCode = capitan.NewIntKey("status")
Duration = capitan.NewDurationKey("duration")
UserID = capitan.NewStringKey("user_id")
)
Configuration
package main
import "github.com/zoobz-io/aperture"
func observabilitySchema() aperture.Schema {
return aperture.Schema{
Metrics: []aperture.MetricSchema{
// Count requests
{
Signal: "http.request.completed",
Name: "http_requests_total",
Type: "counter",
Description: "Total HTTP requests",
},
// Latency histogram
{
Signal: "http.request.completed",
Name: "http_request_duration_ms",
Type: "histogram",
ValueKey: "duration",
Description: "HTTP request latency distribution",
},
},
Traces: []aperture.TraceSchema{
{
Start: "http.request.received",
End: "http.request.completed",
CorrelationKey: "request_id",
SpanName: "http_request",
},
},
Logs: &aperture.LogSchema{
Whitelist: []string{
"http.request.received",
"http.request.completed",
},
},
}
}
Middleware
package middleware
import (
"context"
"net/http"
"time"
"github.com/google/uuid"
"github.com/zoobz-io/capitan"
"myapp/signals"
)
type ctxKey string
const requestIDKey ctxKey = "request_id"
func Observability(cap *capitan.Capitan) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
start := time.Now()
// Generate request ID
reqID := r.Header.Get("X-Request-ID")
if reqID == "" {
reqID = uuid.New().String()
}
ctx = context.WithValue(ctx, requestIDKey, reqID)
// Emit request received
cap.Emit(ctx, signals.HTTPRequestReceived,
signals.RequestID.Field(reqID),
signals.Method.Field(r.Method),
signals.Path.Field(r.URL.Path),
)
// Wrap response writer to capture status
wrapped := &statusWriter{ResponseWriter: w, status: http.StatusOK}
// Process request
next.ServeHTTP(wrapped, r.WithContext(ctx))
// Emit request completed
cap.Emit(ctx, signals.HTTPRequestCompleted,
signals.RequestID.Field(reqID),
signals.Method.Field(r.Method),
signals.Path.Field(r.URL.Path),
signals.StatusCode.Field(wrapped.status),
signals.Duration.Field(time.Since(start)),
)
})
}
}
type statusWriter struct {
http.ResponseWriter
status int
}
func (w *statusWriter) WriteHeader(status int) {
w.status = status
w.ResponseWriter.WriteHeader(status)
}
Main Application
package main
import (
"context"
"log"
"net/http"
"time"
"github.com/zoobz-io/aperture"
"github.com/zoobz-io/capitan"
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
sdklog "go.opentelemetry.io/otel/sdk/log"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.28.0"
"myapp/middleware"
)
func main() {
ctx := context.Background()
// Create resource
res, _ := resource.Merge(
resource.Default(),
resource.NewSchemaless(
semconv.ServiceName("my-api"),
semconv.ServiceVersion("v1.0.0"),
),
)
// Create providers
logExporter, _ := otlploghttp.New(ctx, otlploghttp.WithEndpoint("localhost:4318"), otlploghttp.WithInsecure())
logProvider := sdklog.NewLoggerProvider(
sdklog.WithResource(res),
sdklog.WithProcessor(sdklog.NewBatchProcessor(logExporter)),
)
defer logProvider.Shutdown(ctx)
metricExporter, _ := otlpmetrichttp.New(ctx, otlpmetrichttp.WithEndpoint("localhost:4318"), otlpmetrichttp.WithInsecure())
meterProvider := sdkmetric.NewMeterProvider(
sdkmetric.WithResource(res),
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExporter, sdkmetric.WithInterval(60*time.Second))),
)
defer meterProvider.Shutdown(ctx)
traceExporter, _ := otlptracehttp.New(ctx, otlptracehttp.WithEndpoint("localhost:4318"), otlptracehttp.WithInsecure())
traceProvider := sdktrace.NewTracerProvider(
sdktrace.WithResource(res),
sdktrace.WithSpanProcessor(sdktrace.NewBatchSpanProcessor(traceExporter)),
sdktrace.WithSampler(sdktrace.AlwaysSample()),
)
defer traceProvider.Shutdown(ctx)
// Create capitan and aperture
cap := capitan.Default()
defer cap.Shutdown()
ap, err := aperture.New(cap, logProvider, meterProvider, traceProvider)
if err != nil {
log.Fatal(err)
}
defer ap.Close()
// Apply observability schema
ap.Apply(observabilitySchema())
// Create HTTP server with observability middleware
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
})
handler := middleware.Observability(cap)(mux)
log.Println("Starting server on :8080")
http.ListenAndServe(":8080", handler)
}
Resulting Telemetry
Metrics
http_requests_total{method="GET", path="/", status="200"} 42
http_request_duration_ms_bucket{method="GET", path="/", le="10"} 5
http_request_duration_ms_bucket{method="GET", path="/", le="50"} 35
http_request_duration_ms_bucket{method="GET", path="/", le="100"} 41
http_request_duration_ms_bucket{method="GET", path="/", le="+Inf"} 42
Logs
{
"timestamp": "2025-01-15T10:30:00Z",
"severity": "INFO",
"body": "HTTP request received",
"attributes": {
"capitan.signal": "http.request.received",
"request_id": "abc-123",
"method": "GET",
"path": "/"
}
}
Traces
Span: http_request
TraceID: abc123...
Duration: 45ms
Attributes:
- request_id: abc-123
- method: GET
- path: /
- status: 200
- duration: 45000000
With Context Extraction
Add user ID from authentication:
type ctxKey string
const userIDKey ctxKey = "user_id"
ap.RegisterContextKey("user_id", userIDKey)
schema := observabilitySchema()
schema.Context = &aperture.ContextSchema{
Logs: []string{"user_id"},
Traces: []string{"user_id"},
}
ap.Apply(schema)
Then in auth middleware:
ctx = context.WithValue(ctx, userIDKey, authenticatedUserID)
All logs and traces automatically include user_id.