Documentation
¶
Index ¶
- Constants
- Variables
- func ClientIPKey(r *http.Request) string
- func NewRetryTransport(next http.RoundTripper, cfg RetryConfig) http.RoundTripper
- type Chain
- type CircuitBreakerConfig
- type CircuitBreakerConfigOverrides
- type CircuitState
- type ClientChain
- type ClientMiddleware
- func WithBasicAuth(username, password string) ClientMiddleware
- func WithBearerToken(token string) ClientMiddleware
- func WithCircuitBreaker(log *slog.Logger, cfg CircuitBreakerConfig) ClientMiddleware
- func WithRateLimit(requestsPerSecond float64) ClientMiddleware
- func WithRequestLogging(log *slog.Logger) ClientMiddleware
- type LogFormat
- type LoggingOption
- func WithFormat(format LogFormat) LoggingOption
- func WithHeaderFields(headers ...string) LoggingOption
- func WithLogLevel(level slog.Level) LoggingOption
- func WithPathFilter(paths ...string) LoggingOption
- func WithTrustedProxy() LoggingOption
- func WithoutLatency() LoggingOption
- func WithoutUserAgent() LoggingOption
- type Middleware
- type RateLimitConfig
- type RateLimitConfigOverrides
- type RetryConfig
Constants ¶
const ( // StateClosed admits all requests; failures are counted. StateClosed = CircuitState(resilience.StateClosed) // StateOpen rejects all requests immediately with ErrCircuitOpen until the // cooldown elapses, then transitions to StateHalfOpen. StateOpen = CircuitState(resilience.StateOpen) // StateHalfOpen admits a limited number of trial requests; success closes // the breaker, failure re-opens it. StateHalfOpen = CircuitState(resilience.StateHalfOpen) )
The public states are derived from the shared core so the two enumerations can never silently drift out of order.
Variables ¶
var ErrCircuitOpen = errors.New("http: circuit breaker is open")
ErrCircuitOpen is returned by the breaker when it is open. Callers may test for it with errors.Is. It is returned directly (not stack-wrapped per call): an open breaker is an expected control-flow signal on a high-volume reject path, not an exceptional error needing a fresh stack each time.
Functions ¶
func ClientIPKey ¶
ClientIPKey is a ready-made RateLimitConfig.KeyFunc that keys on the client IP using the connection's RemoteAddr.
It deliberately does NOT trust X-Forwarded-For / X-Real-IP: those headers are spoofable by any direct client, so keying a limiter on them would let an attacker both evade their own bucket and churn the bounded key store by rotating fake IPs. A server behind a trusted reverse proxy that terminates XFF should supply its own KeyFunc that reads the proxy-set header. This reuses the logging middleware's client-IP derivation with trustedProxy=false, the same safe default.
func NewRetryTransport ¶
func NewRetryTransport(next http.RoundTripper, cfg RetryConfig) http.RoundTripper
NewRetryTransport wraps next with automatic retry using exponential backoff and full jitter for transient failures. The config is normalised so invalid values are clamped to safe defaults. Client constructors expose this as an option (e.g. a WithRetry(RetryConfig)); transit provides the transport itself.
Types ¶
type Chain ¶
type Chain struct {
// contains filtered or unexported fields
}
Chain composes zero or more Middleware into a single Middleware. Middleware is applied left-to-right: the first middleware in the list is the outermost wrapper (first to see the request, last to see the response).
chain := NewChain(recovery, logging, auth) handler := chain.Then(mux)
func NewChain ¶
func NewChain(middlewares ...Middleware) Chain
NewChain creates a new middleware chain from the given middleware functions. Nil entries are silently skipped.
func (Chain) Append ¶
func (c Chain) Append(middlewares ...Middleware) Chain
Append returns a new Chain with additional middleware appended. The original chain is not modified. Nil entries are silently skipped.
type CircuitBreakerConfig ¶
type CircuitBreakerConfig struct {
// FailureThreshold is the number of consecutive failures (within Closed)
// that trips the breaker open. Must be >= 1. Default: 5.
FailureThreshold int `mapstructure:"failure_threshold" yaml:"failure_threshold" json:"failure_threshold"`
// Cooldown is how long the breaker stays Open before allowing a trial.
// Default: 30s.
Cooldown time.Duration `mapstructure:"cooldown" yaml:"cooldown" json:"cooldown"`
// HalfOpenMaxRequests is the number of trial requests allowed in HalfOpen.
// The first success closes the breaker; any failure re-opens it.
// Must be >= 1. Default: 1.
HalfOpenMaxRequests int `mapstructure:"half_open_max_requests" yaml:"half_open_max_requests" json:"half_open_max_requests"`
// IsFailure classifies a round-trip outcome as a failure for breaker
// accounting. When nil, the default treats transport errors and 5xx
// responses (>=500) as failures; 4xx and 2xx/3xx are successes. A 429
// (client rate-limited) therefore does NOT trip the breaker — that is
// retry's job, not the breaker's.
IsFailure func(resp *http.Response, err error) bool `mapstructure:"-" yaml:"-" json:"-"`
// OnStateChange is invoked on every state transition. Optional; transitions
// are also logged via the constructor's logger.
OnStateChange func(from, to CircuitState) `mapstructure:"-" yaml:"-" json:"-"`
}
CircuitBreakerConfig configures the client-side circuit breaker.
func DefaultCircuitBreakerConfig ¶
func DefaultCircuitBreakerConfig() CircuitBreakerConfig
DefaultCircuitBreakerConfig returns: threshold 5, cooldown 30s, half-open trial 1, default 5xx/transport-error failure classification.
func MergeCircuitBreakerConfig ¶
func MergeCircuitBreakerConfig(base, override CircuitBreakerConfig, fields CircuitBreakerConfigOverrides) CircuitBreakerConfig
MergeCircuitBreakerConfig applies explicitly supplied typed override values to base while leaving code-only function fields under caller control.
type CircuitBreakerConfigOverrides ¶
type CircuitBreakerConfigOverrides struct {
FailureThreshold bool
Cooldown bool
HalfOpenMaxRequests bool
}
CircuitBreakerConfigOverrides records which typed circuit breaker config fields were explicitly supplied by an adapter.
type CircuitState ¶
type CircuitState int
CircuitState is the client circuit breaker's state.
func (CircuitState) String ¶
func (s CircuitState) String() string
String renders the state for logging.
type ClientChain ¶
type ClientChain struct {
// contains filtered or unexported fields
}
ClientChain composes ClientMiddleware in order. Immutable — Append returns a new chain.
func NewClientChain ¶
func NewClientChain(middlewares ...ClientMiddleware) ClientChain
NewClientChain creates a ClientChain from the given middleware.
func (ClientChain) Append ¶
func (c ClientChain) Append(middlewares ...ClientMiddleware) ClientChain
Append returns a new chain with additional middleware appended.
func (ClientChain) Then ¶
func (c ClientChain) Then(rt http.RoundTripper) http.RoundTripper
Then applies the middleware chain to the given RoundTripper and returns the wrapped result.
type ClientMiddleware ¶
type ClientMiddleware func(next http.RoundTripper) http.RoundTripper
ClientMiddleware wraps an http.RoundTripper with additional behaviour. The first middleware in a chain is the outermost wrapper — it executes first on the request and last on the response.
func WithBasicAuth ¶
func WithBasicAuth(username, password string) ClientMiddleware
WithBasicAuth returns middleware that injects an Authorization: Basic header. The header is only sent to the first host the client addresses, so a cross-host redirect cannot capture the credential.
func WithBearerToken ¶
func WithBearerToken(token string) ClientMiddleware
WithBearerToken returns middleware that injects an Authorization: Bearer header. The header is only sent to the first host the client addresses, so a cross-host redirect cannot capture the token.
func WithCircuitBreaker ¶
func WithCircuitBreaker(log *slog.Logger, cfg CircuitBreakerConfig) ClientMiddleware
WithCircuitBreaker returns a ClientMiddleware that fails fast while a downstream is consistently failing, avoiding wasted retry/backoff cycles.
Place it OUTSIDE the retry transport — i.e. in the ClientChain via WithClientMiddleware, which wraps the transport after retry — so the breaker sees the final post-retry verdict: one retry-exhausted logical call counts as a single breaker failure, not one per attempt. Once Open, calls are rejected before entering the retry layer, so no backoff sleeps are spent on a service known to be down.
func WithRateLimit ¶
func WithRateLimit(requestsPerSecond float64) ClientMiddleware
WithRateLimit returns middleware that limits outbound requests to the specified rate using a token-bucket limiter (burst 1). Blocks until a token is available or the request context is cancelled. The limiter is shared across all requests through the transport, so it holds under concurrency — the previous hand-rolled version let concurrent goroutines sleep in parallel and then proceed together, admitting a burst per interval.
func WithRequestLogging ¶
func WithRequestLogging(log *slog.Logger) ClientMiddleware
WithRequestLogging returns middleware that logs each outbound request and response at debug level. Logs method, URL, status code, and duration. Headers and body are NOT logged for security.
type LogFormat ¶
type LogFormat int
LogFormat controls the output format of the logging middleware.
const ( // FormatStructured emits structured key-value fields via *slog.Logger. FormatStructured LogFormat = iota // FormatCommon emits NCSA Common Log Format (CLF). FormatCommon // FormatCombined emits NCSA Combined Log Format (CLF + Referer + User-Agent). FormatCombined // FormatJSON emits a single JSON object per request. FormatJSON )
type LoggingOption ¶
type LoggingOption func(*loggingConfig)
LoggingOption configures transport logging behaviour.
func WithFormat ¶
func WithFormat(format LogFormat) LoggingOption
WithFormat sets the log output format. Defaults to FormatStructured.
func WithHeaderFields ¶
func WithHeaderFields(headers ...string) LoggingOption
WithHeaderFields logs the specified request header values as fields. Header names are normalised to lowercase. Values are truncated to 256 bytes.
Known-sensitive headers (Authorization, Cookie, Set-Cookie, X-Api-Key, X-Auth-Token, X-Csrf-Token, X-Session-Token, Proxy-Authorization) are always redacted regardless of whether they appear in the fields list. This is defence-in-depth against accidental credential leakage.
func WithLogLevel ¶
func WithLogLevel(level slog.Level) LoggingOption
WithLogLevel sets the log level for successful requests. Defaults to slog.LevelInfo. Errors always log at slog.LevelError.
func WithPathFilter ¶
func WithPathFilter(paths ...string) LoggingOption
WithPathFilter excludes requests matching the given paths from logging.
func WithTrustedProxy ¶
func WithTrustedProxy() LoggingOption
WithTrustedProxy makes the logging middleware trust the client-supplied X-Forwarded-For and X-Real-IP headers when deriving the logged client IP.
These headers are trivially spoofable by any direct client, so they are IGNORED by default and the connection's RemoteAddr is logged instead. Enable this option only when the server sits behind a trusted reverse proxy or load balancer that overwrites (rather than appends to) these headers. Enabling it on a directly-exposed server lets clients forge the recorded client IP.
func WithoutLatency ¶
func WithoutLatency() LoggingOption
WithoutLatency disables the "latency" field.
func WithoutUserAgent ¶
func WithoutUserAgent() LoggingOption
WithoutUserAgent disables the "user_agent" field.
type Middleware ¶
Middleware is the standard Go HTTP middleware signature.
func LoggingMiddleware ¶
func LoggingMiddleware(l *slog.Logger, opts ...LoggingOption) Middleware
LoggingMiddleware returns an HTTP Middleware that logs each completed request.
func OTelMiddleware ¶
func OTelMiddleware(server string, opts ...otelhttp.Option) Middleware
OTelMiddleware returns a Middleware that records an OpenTelemetry server span and the standard server metrics (http.server.*) for each request, reading whichever TracerProvider and MeterProvider are installed as the OTel globals (see telemetry.Setup). server names the span operation, identifying this service in the trace.
It composes in a Chain like any other middleware. Put it ahead of the logging middleware so the request log can pick up the active span:
chain := http.NewChain(
http.OTelMiddleware("macguffin"),
http.LoggingMiddleware(log),
)
func RateLimitMiddleware ¶
func RateLimitMiddleware(log *slog.Logger, cfg RateLimitConfig) Middleware
RateLimitMiddleware returns a Middleware that admits requests under a token-bucket limiter and rejects excess traffic with 429 Too Many Requests plus a Retry-After header (which a GTB client's retry layer honours). An invalid config is clamped to defaults rather than rejected.
Because it is an ordinary Middleware it composes into any Chain and can be scoped globally (one entry in the server chain) or per-route (wrap a single handler). Per-client limiting is enabled by setting RateLimitConfig.KeyFunc.
Health endpoints (/healthz, /livez, /readyz) are mounted outside the WithMiddleware chain by Register, so a global limiter never throttles probes.
type RateLimitConfig ¶
type RateLimitConfig struct {
// RequestsPerSecond is the sustained fill rate of the token bucket.
// Must be > 0. Default: 50.
RequestsPerSecond float64 `mapstructure:"requests_per_second" yaml:"requests_per_second" json:"requests_per_second"`
// Burst is the bucket capacity — the maximum number of requests that may
// be admitted in an instantaneous spike. Must be >= 1. Default: 100.
Burst int `mapstructure:"burst" yaml:"burst" json:"burst"`
// KeyFunc derives the limiter key for a request, enabling per-client
// limiting. When nil, a single global bucket is used for all requests.
// A common choice is to key on the client IP (see ClientIPKey).
KeyFunc func(*http.Request) string `mapstructure:"-" yaml:"-" json:"-"`
// MaxTrackedKeys bounds the per-client bucket store. Ignored when KeyFunc
// is nil. Must be >= 1. Default: 8192.
MaxTrackedKeys int `mapstructure:"max_tracked_keys" yaml:"max_tracked_keys" json:"max_tracked_keys"`
// OnLimited is invoked when a request is rejected, before the 429 is
// written. Optional; useful for metrics/telemetry. A structured debug log
// is emitted via the constructor's logger regardless.
OnLimited func(*http.Request) `mapstructure:"-" yaml:"-" json:"-"`
}
RateLimitConfig configures the server-side token-bucket rate limiter.
func DefaultRateLimitConfig ¶
func DefaultRateLimitConfig() RateLimitConfig
DefaultRateLimitConfig returns a RateLimitConfig suitable for a modest management/API server: 50 rps sustained, burst 100, single global bucket.
func MergeRateLimitConfig ¶
func MergeRateLimitConfig(base, override RateLimitConfig, fields RateLimitConfigOverrides) RateLimitConfig
MergeRateLimitConfig applies explicitly supplied typed override values to base while leaving code-only function fields under caller control.
type RateLimitConfigOverrides ¶
RateLimitConfigOverrides records which typed rate limit config fields were explicitly supplied by an adapter.
type RetryConfig ¶
type RetryConfig struct {
// MaxRetries is the maximum number of retry attempts. Zero means no retries.
MaxRetries int
// InitialBackoff is the base delay before the first retry. Default: 500ms.
InitialBackoff time.Duration
// MaxBackoff caps the computed delay. Default: 30s.
MaxBackoff time.Duration
// RetryableStatusCodes defines which HTTP status codes trigger a retry.
// Default: []int{429, 502, 503, 504}.
RetryableStatusCodes []int
// ShouldRetry is an optional custom predicate. When set, it replaces the
// default status-code and network-error checks. The attempt count (0-based)
// and either the response or the transport error are provided.
ShouldRetry func(attempt int, resp *http.Response, err error) bool
}
RetryConfig configures the retry behaviour of the HTTP client.
func DefaultRetryConfig ¶
func DefaultRetryConfig() RetryConfig
DefaultRetryConfig returns a RetryConfig suitable for most use cases.