Getting started¶
This tutorial wraps an HTTP handler with structured request logging and OpenTelemetry tracing, then makes an outbound call through a retrying client. By the end you will have seen both halves of transit — the server chain and the client round-tripper — working together.
Install¶
The middleware packages are imported as transit/http and transit/grpc. Because
those basenames collide with the standard library, give them an alias:
A logged, traced server¶
NewChain builds an ordered chain of server middleware; Then applies it to any
http.Handler. Middleware runs outermost-first, so put logging first and it will time
the whole request, including everything the inner middleware does.
package main
import (
"log/slog"
"net/http"
"os"
transithttp "gitlab.com/phpboyscout/go/transit/http"
)
func main() {
log := slog.New(slog.NewJSONHandler(os.Stdout, nil))
chain := transithttp.NewChain(
transithttp.LoggingMiddleware(log), // times & logs every request
transithttp.OTelMiddleware("demo"), // one server span per request
)
mux := http.NewServeMux()
mux.HandleFunc("/hello", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("hello"))
})
// chain.Then wraps the whole mux; every route is logged and traced.
_ = http.ListenAndServe(":8080", chain.Then(mux))
}
LoggingMiddleware logs one structured record per request (method, path, status,
byte count, latency, client IP) and derives the client IP safely — it ignores
spoofable X-Forwarded-For/X-Real-IP headers unless you opt in with
transithttp.WithTrustedProxy(). OTelMiddleware reads whichever TracerProvider is
installed as the OTel global (wire one with
go/observability); until then it is a noop.
A resilient client¶
The client half is a set of http.RoundTripper decorators. NewRetryTransport retries
transient failures (429/502/503/504 and transient network errors) with exponential
backoff and full jitter; a ClientChain layers on per-request behaviour such as a
bearer token.
base := http.DefaultTransport
retrying := transithttp.NewRetryTransport(base, transithttp.DefaultRetryConfig())
chain := transithttp.NewClientChain(
transithttp.WithBearerToken(os.Getenv("API_TOKEN")),
)
client := &http.Client{Transport: chain.Then(retrying)}
resp, err := client.Get("https://api.internal/things")
Order matters: wrapping the chain around the retry transport means the bearer token is attached once per logical call and retries reuse the raw transport. The middleware model explains why, and where the circuit breaker belongs relative to retry.
Where next¶
- Compose HTTP middleware — the full server and client toolboxes, including rate limiting and the circuit breaker.
- Compose gRPC interceptors — the same concerns for a gRPC service.