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:
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.
Related pages¶
- Dial options: what Dial sets and what you can override
- Confirm a connection is actually encrypted
- The transit middleware model — interceptor ordering, from transit's own docs.