Skip to content

Dial a service with middleware

grpcclient.Dial builds the connection and selects credentials. The per-call behaviour — circuit breaking, OpenTelemetry — comes from go/transit's gRPC client interceptors, which you pass to Dial as ordinary grpc.DialOptions.

grpcclient does not depend on transit, so add it to your own module first:

go get gitlab.com/phpboyscout/go/transit
import (
    "gitlab.com/phpboyscout/go/grpcclient"
    transitgrpc "gitlab.com/phpboyscout/go/transit/grpc"
    "google.golang.org/grpc"
)

Which transit interceptors work on a client

go/transit/grpc exposes three client-side helpers. The rest of the package — LoggingInterceptor, RateLimitInterceptor, OTelStatsHandler — is server-side and will not compile as a dial option.

Helper Type Wire it with
CircuitBreakerInterceptor(log, cfg) grpc.UnaryClientInterceptor grpc.WithChainUnaryInterceptor(...)
CircuitBreakerStreamInterceptor(log, cfg) grpc.StreamClientInterceptor grpc.WithChainStreamInterceptor(...)
OTelClientHandler(opts...) grpc.DialOption pass directly to Dial

There is no client-side request-logging interceptor in transit. If you want per-call client logs, write your own grpc.UnaryClientInterceptor or read the spans OTelClientHandler emits.

Compose a full client stack

cfg := transitgrpc.DefaultCircuitBreakerConfig()

conn, err := grpcclient.Dial(
    grpcclient.Target{Host: "svc.internal", Port: 443, TLS: pair},

    // circuit breaking on both unary and streaming calls
    grpc.WithChainUnaryInterceptor(transitgrpc.CircuitBreakerInterceptor(log, cfg)),
    grpc.WithChainStreamInterceptor(transitgrpc.CircuitBreakerStreamInterceptor(log, cfg)),

    // OpenTelemetry client instrumentation
    transitgrpc.OTelClientHandler(),
)
if err != nil {
    return err
}
defer conn.Close()

DefaultCircuitBreakerConfig() trips after 5 consecutive failures, stays open for 30 seconds, then allows 1 trial call. Only Unavailable and DeadlineExceeded count as failures — ResourceExhausted deliberately does not, so a server's own rate limiter cannot trip its callers' breakers. While the breaker is open, calls return Unavailable: circuit breaker is open immediately, without attempting a connection.

Where transport credentials come from

You do not pass grpc.WithTransportCredentials yourself: Dial derives it from Target.TLS. Set TLS.Enabled and, if you need a private CA, TLS.Cert — see Target fields.

Be aware that Dial places its credentials first in the option list, and gRPC lets a later option overwrite an earlier one. A grpc.WithTransportCredentials in your own option list therefore replaces the ones Target.TLS produced — including replacing TLS with plaintext, silently. Do not build a client's dial options from untrusted configuration, and see Confirm a connection is actually encrypted if you need to prove which credentials were used.

Add your own dial options

Any grpc.DialOption is accepted — keepalive, message-size limits, a custom resolver, a service config — and applied on top of the credentials Dial set:

conn, err := grpcclient.Dial(
    grpcclient.Target{Port: 8080},
    grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(16<<20)),
)

Two options are rejected outright, with an error from Dial rather than a broken connection: grpc.WithCredentialsBundle (it cannot coexist with the individual transport credentials Dial sets) and grpc.WithPerRPCCredentials while Target.TLS.Enabled is false (token credentials refuse to travel in the clear). Both are listed under Errors returned by Dial.