gRPC client
The grpcclient module provides gRPC client connections, declared by name in the configuration and instrumented with OpenTelemetry.
go get github.com/go-oryn/oryn/grpcclientQuick start
grpcclient: connections: users: target: users:50051 insecure: truefunc main() { core.New( core.WithConfigFS(configs.FS), grpcclient.Module, grpcclient.AsClient("users", pb.NewUsersClient), fx.Provide(NewService), ).Run()}
type Service struct { users pb.UsersClient}
func NewService(users pb.UsersClient) *Service { return &Service{users: users}}grpcclient.Moduleprovides the*grpcclient.Factory, which creates the connections declared undergrpcclient.connections.<name>.grpcclient.AsClient("users", pb.NewUsersClient)provides apb.UsersClienton theusersconnection.- Every call is traced, measured and logged, and the trace context is propagated to the server, see Observability.
Connections
A connection is declared by its name, with at least a target, in the gRPC name syntax: users:50051, dns:///users.internal:443… It uses TLS with the system root certificates, unless insecure is true. See Configuration.
The *grpcclient.Factory creates each connection once, on first use, and shares it. Creating a connection does not connect: gRPC connects on the first call, and reconnects as needed. All the connections are closed when the application stops.
conn, err := factory.Conn("users")if err != nil { return err // grpcclient.connections.users.target is missing, or the connection settings are invalid}
client := pb.NewUsersClient(conn)Clients
AsClient provides the client returned by a generated pb.NewXxxClient constructor, on a named connection. The application fails to start if the connection cannot be created.
As fx identifies values by type, a client type can be provided only once. To call the same service on two connections, build the clients from the factory, and name them:
fx.Provide( fx.Annotate(func(f *grpcclient.Factory) (pb.UsersClient, error) { conn, err := f.Conn("users-eu") if err != nil { return nil, err }
return pb.NewUsersClient(conn), nil }, fx.ResultTags(`name:"users-eu"`)),)Interceptors
Interceptors run code around every call of every connection: authentication, metadata, retries… Register them with AsUnaryInterceptor and AsStreamInterceptor, as values or constructors whose parameters are injected by fx:
func NewAuthInterceptor(tokens *TokenSource) grpc.UnaryClientInterceptor { return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+tokens.Token())
return invoker(ctx, method, req, reply, cc, opts...) }}grpcclient.AsUnaryInterceptor(NewAuthInterceptor)grpcclient.AsStreamInterceptor(NewAuthStreamInterceptor)Interceptors apply in registration order: the first registered interceptor is the outermost one. They run after the built-in call log.
Dial options
AsDialOptions adds dial options to one connection, for what the configuration does not cover: custom TLS credentials, keepalive, a custom dialer…
grpcclient.AsDialOptions("users", grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), grpc.WithKeepaliveParams(keepalive.ClientParameters{Time: 30 * time.Second}),)They are applied after the configured settings, so they override them.
Configuration
The grpcclient.* keys declare the connections, the call logs and the methods excluded from telemetry. See Configuration.
