Skip to content

Compose gRPC interceptors

transit's grpc package mirrors the HTTP package for gRPC: server interceptors you install on a grpc.Server, and client instrumentation you pass as dial options and client interceptors.

import transitgrpc "gitlab.com/phpboyscout/go/transit/grpc"

Server interceptors

Server interceptors are grouped as an Interceptor{Unary, Stream} pair (either field may be nil). NewInterceptorChain collects them and ServerOptions() turns the chain into the grpc.ServerOption values that install it via ChainUnaryInterceptor / ChainStreamInterceptor.

Constructor Returns Purpose
LoggingInterceptor(log, opts…) Interceptor One structured record per RPC.
RateLimitInterceptor(log, cfg) Interceptor Token-bucket admission control.
OTelStatsHandler(opts…) grpc.ServerOption OpenTelemetry server spans + rpc.server.* metrics.

OTelStatsHandler is a stats handler, not an interceptor — that is the shape the OTel gRPC contrib library ships — so pass it to grpc.NewServer alongside the chain's options rather than into NewInterceptorChain:

chain := transitgrpc.NewInterceptorChain(
    transitgrpc.LoggingInterceptor(log),
    transitgrpc.RateLimitInterceptor(log, transitgrpc.DefaultRateLimitConfig()),
)

opts := append(chain.ServerOptions(), transitgrpc.OTelStatsHandler())
srv := grpc.NewServer(opts...)

LoggingInterceptor takes WithGRPCLogLevel, WithGRPCPathFilter and WithoutGRPCLatency, mirroring the HTTP logger.

Client instrumentation

On the client, OpenTelemetry is a dial option and the circuit breaker is a standard gRPC client interceptor:

Constructor Returns Purpose
OTelClientHandler(opts…) grpc.DialOption Client spans + trace-context propagation.
CircuitBreakerInterceptor(log, cfg) grpc.UnaryClientInterceptor Fail fast on a consistently failing server.
CircuitBreakerStreamInterceptor(log, cfg) grpc.StreamClientInterceptor The streaming equivalent.
conn, err := grpc.NewClient(
    "dns:///orders.internal:443",
    grpc.WithTransportCredentials(creds),
    transitgrpc.OTelClientHandler(),
    grpc.WithUnaryInterceptor(
        transitgrpc.CircuitBreakerInterceptor(log, transitgrpc.DefaultCircuitBreakerConfig()),
    ),
)

OTelClientHandler injects the trace context into outgoing metadata using the global propagator, so a downstream gRPC server continues the same trace rather than starting a new one. Like the server handler, it reads the globally-installed providers and is a noop until those are set up (see go/observability).

Config merging

Both CircuitBreakerConfig and RateLimitConfig have Merge…Config helpers that apply only explicitly-flagged override fields onto a base, leaving function fields (custom failure predicates, key extractors) under your control. This is how a service layers file/env/flag config onto the defaults without a field-by-field copy.