Configuration¶
There are two ways to configure a client, and they compose:
GrpcClientConfig— a stdlib dataclass describing one channel. This is whatGrpcClientandChannelPoolactually consume.- A settings object — anything carrying the fields of
GrpcClientSettingsProtocol.GrpcClientFactorymaps it onto a pool, a balancer, a health checker and an interceptor chain.
GrpcClientConfig¶
from grpc_client_kit import ConnectivityConfig, GrpcClientConfig
config = GrpcClientConfig(
target="api.internal:50051", # host:port, or a gRPC resolver URI
insecure=False, # False + no credentials = system trust store
credentials=grpc.ssl_channel_credentials(),
options=[("grpc.max_receive_message_length", 8 * 1024 * 1024)],
compression=grpc.Compression.Gzip,
connectivity=ConnectivityConfig(keepalive_time=30.0), # None = gRPC's defaults
)
insecure=True together with credentials raises ValueError at
construction — a channel cannot be both. With insecure=False and no
credentials, grpc.ssl_channel_credentials() is used, i.e. the system trust
store.
Reuse one credentials object rather than rebuilding it per call: gRPC credentials define no equality, so the pool compares them by identity and a fresh object is a fresh channel. See Channel identity.
connectivity spells the keepalive and reconnect channel arguments in seconds
instead of milliseconds; explicit options win over it, key by key, and the
composed list is what the channel is opened with. See
Keepalive and reconnect backoff.
Targets are validated before a channel is created, which turns a typo into an
error at configuration time instead of an opaque UNAVAILABLE at the first
RPC — see Target validation.
Settings objects¶
The factory reads a settings object through structural protocols, so a
pydantic model, a dataclass or a plain class all work. Protocol fields are
declared as read-only properties rather than attributes, which makes them
covariant: a pydantic model may narrow strategy to a Literal and still
satisfy the protocol.
These fields are required (GrpcClientSettingsProtocol):
| Field | Type | Meaning |
|---|---|---|
target |
str \| None |
Single target; mutually exclusive with targets |
targets |
list[str] \| None |
Targets to load-balance across |
insecure |
bool |
Plaintext channels |
tracing_enabled |
bool |
Add the tracing layer |
metrics_enabled |
bool |
Add the metrics layer |
logging_enabled |
bool |
Add the logging layer |
pool |
block or None |
Channel pool |
timeout |
block or None |
Timeouts |
retry |
block or None |
Retries |
circuit_breaker |
block or None |
Circuit breaker |
balancer |
block or None |
Load balancer |
health_checker |
block or None |
Health checker |
A None block means "do not add that layer at all", not "use defaults".
No timeout block, no timeout interceptor — and therefore no deadline.
These are optional and read through getattr, so a minimal settings
object stays valid: credentials, options, compression
(GrpcChannelExtrasProtocol), plus sensitive_headers and
metrics_registry (GrpcObservabilityExtrasProtocol). Both protocols live in
grpc_client_kit.protocols, along with FullGrpcClientSettingsProtocol for
settings that carry everything.
connectivity is read the same way — a ConnectivityConfig under that name
tunes the channels, its absence leaves
gRPC's defaults.
Splitting the optional fields off is what keeps
isinstance(settings, GrpcClientSettingsProtocol) a meaningful check instead
of one that fails on fields nobody defines.
A settings object end to end¶
import grpc
class UpstreamSettings:
target = None
targets = ["api-1.prod:443", "api-2.prod:443"]
insecure = False
credentials = grpc.ssl_channel_credentials()
tracing_enabled = True
metrics_enabled = True
logging_enabled = True
metrics_registry = my_registry
class pool:
max_channels_per_target = 4
idle_timeout = 300.0
class timeout:
default = 10.0
class retry:
max_attempts = 3
initial_backoff = 0.1
max_backoff = 5.0
backoff_multiplier = 2.0
class circuit_breaker:
fail_threshold = 5
recovery_timeout = 60.0
half_open_max_calls = 1
class balancer:
strategy = "round_robin"
weights = None
class health_checker:
check_interval = 30.0
timeout = 5.0
The blocks a settings object can carry are deliberately narrower than the
kit's own config dataclasses: timeout only exposes default, and the
retry block's jitter, retryable_codes, retry_streaming,
idempotent_methods and on_retry are picked up only when it happens to
define them. Two more optional blocks join on the same duck-typed terms — a
wait_for_ready block (default, per_method, require_deadline) and a
deadline_budget block (reserve_for_next) — so every one of the
five resilience layers is reachable from settings alone.
Anything finer — per-method budgets above all — still needs a hand-built
chain, as described in Interceptors:
chain = build_interceptors(
observability=ObservabilityConfig(service_name="users.v1.Users"),
timeout=TimeoutConfig(default=5.0, per_method={"/users.v1.Users/Export": 60.0}),
deadline_budget=DeadlineBudgetConfig(),
wait_for_ready=WaitForReadyConfig(),
retry=RetryConfig(max_attempts=3),
)
client = GrpcClient(UserStub, config=config, pool=pool, interceptors=chain)
Not through create_client(interceptors=...). That argument adds custom
layers to the outer slot of the chain the factory
builds — above logging, tracing, metrics and the timeout — which is the wrong
side of every layer a deadline-shaping interceptor needs to read.
The required surface of the settings object is validated when the factory is
constructed: a missing required field raises TypeError naming it. The
optional blocks stay optional, which also means a typo in an optional
field name silently yields the default — pydantic users should set
model_config = ConfigDict(extra="forbid") on their settings models so typos
fail at model construction instead.
Optional dependencies¶
| Extra | Pulls in | Enables |
|---|---|---|
health |
grpcio-health-checking |
HealthChecker, health-aware balancing and pooling |
tracing |
opentelemetry-api |
AsyncClientTracingInterceptor (a pass-through without it) |
metrics |
prometheus-client |
the default metrics backend; a custom registry needs no extra |
deadline |
deadline-budget |
deadline budget propagation (the layer is skipped without it) |
observability |
metrics + tracing |
both of the above |
all |
deadline + health + metrics + tracing |
everything |
deadline is deliberately not part of observability: propagating a
budget is resilience, not telemetry, and an observability extra should not pull
in a dependency that changes what calls do.
import grpc_client_kit never requires an extra. HealthChecker is the one
gated export: it is resolved on first attribute access, and without
[health] that access raises an ImportError naming the extra to install.