Skip to content

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

go get gitlab.com/phpboyscout/go/transit

The middleware packages are imported as transit/http and transit/grpc. Because those basenames collide with the standard library, give them an alias:

import (
    transithttp "gitlab.com/phpboyscout/go/transit/http"
)

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