gitlab.com/phpboyscout/go/transit — the reusable transport middleware shared by a
Go service's HTTP and gRPC edges: structured request logging, OpenTelemetry
instrumentation, circuit breaking, rate limiting and client-side retry, plus the
resilience primitives (a circuit breaker and a keyed token-bucket store) they are
built on.
It is the middleware layer extracted from
go-tool-base, which consumes it behind
its pkg/http and pkg/grpc server/client constructors. It is framework-free: it
carries the gRPC SDK and the OpenTelemetry gRPC/HTTP contrib instrumentation (that is
the middleware's job) but nothing else — no config framework, no TUI, no go-tool-base.
Design
- Framework-free. Dependencies are the gRPC SDK, the OTel contrib
instrumentation,
golang.org/x/time/rate, go/redact
and cockroachdb/errors. A depfootprint_test.go guard forbids go-tool-base,
Viper/Cobra, Charm and the cloud SDKs.
*slog.Logger at the seam. Middleware that logs takes a plain *slog.Logger,
so nothing here depends on a logging framework.
- Client and server, both transports. Each of
http and grpc provides server
handlers/interceptors and client round-trippers/interceptors, so a service and the
clients it calls share one implementation of each concern.
- Resilience is one package. The circuit breaker and the rate-limiter store live
together in
resilience; the HTTP and gRPC layers wrap them into transport-shaped
middleware.
Install
go get gitlab.com/phpboyscout/go/transit
Quick start
package main
import (
"log/slog"
"net/http"
transithttp "gitlab.com/phpboyscout/go/transit/http"
)
func main() {
log := slog.Default()
// OTel goes first: it puts the span in the context the logger reads,
// so the access log carries trace_id and span_id.
chain := transithttp.NewChain(
transithttp.OTelMiddleware("my-service"),
transithttp.LoggingMiddleware(log),
)
mux := http.NewServeMux()
// mux.Handle("/", ...)
_ = http.ListenAndServe(":8080", chain.Then(mux))
}
What's inside
resilience — the transport-neutral primitives: a Breaker (closed → open →
half-open state machine) and a keyed token-bucket Store for rate limiting.
http — server middleware (NewChain, LoggingMiddleware, OTelMiddleware,
RateLimitMiddleware) and client round-trippers (NewRetryTransport, the ClientChain
with WithCircuitBreaker / WithBearerToken / WithBasicAuth / WithRateLimit /
WithRequestLogging). The circuit breaker is client-side only — there is no server-side
load shedding.
grpc — server interceptors (NewInterceptorChain, LoggingInterceptor,
OTelStatsHandler, CircuitBreakerInterceptor, RateLimitInterceptor) and the
OTelClientHandler client dial option.
Compatibility
The gRPC and OpenTelemetry dependency versions are pinned and kept in lockstep with
go-tool-base (currently grpc v1.82.0,
otel v1.44.0 and the gRPC/HTTP contrib instrumentation) so a service and this module
share one build graph. Grouped Renovate updates keep them aligned.
What it does not do
- No distributed state. Circuit breakers and rate limiters are per-process and
in-memory: a 50 rps limit across ten replicas admits 500, and a breaker trips per
replica.
- No configuration loading. Constructors take Go structs. The config types carry
mapstructure/yaml/json tags so a service can decode onto them, but transit reads
no files and no environment variables, and nothing can be reconfigured after
construction.
- No
*http.Client constructor. TLS settings, redirect policy and timeouts are
connection policy and belong to the layer above.
- No gRPC retry. The gRPC SDK already implements it as a service config policy.
- No metrics, no server-side circuit breaker, no panic recovery, no timeouts, no CORS.
The full list with reasoning is at
What transit does not do.
Documentation
Full guides, the middleware model, and a reference tier covering every config field and
its default: transit.go.phpboyscout.uk.
API reference: pkg.go.dev.
License
See LICENSE.