github.com/letsencrypt/boulder@v0.20251208.0/grpc/client.go (about) 1 package grpc 2 3 import ( 4 "crypto/tls" 5 "errors" 6 "fmt" 7 8 grpc_prometheus "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus" 9 "github.com/jmhodges/clock" 10 "github.com/prometheus/client_golang/prometheus" 11 "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" 12 "google.golang.org/grpc" 13 14 "github.com/letsencrypt/boulder/cmd" 15 bcreds "github.com/letsencrypt/boulder/grpc/creds" 16 17 // 'grpc/internal/resolver/dns' is imported for its init function, which 18 // registers the SRV resolver. 19 "google.golang.org/grpc/balancer/roundrobin" 20 21 // 'grpc/health' is imported for its init function, which causes clients to 22 // rely on the Health Service for load-balancing as long as a 23 // "healthCheckConfig" is specified in the gRPC service config. 24 _ "google.golang.org/grpc/health" 25 26 _ "github.com/letsencrypt/boulder/grpc/internal/resolver/dns" 27 ) 28 29 // ClientSetup creates a gRPC TransportCredentials that presents 30 // a client certificate and validates the server certificate based 31 // on the provided *tls.Config. 32 // It dials the remote service and returns a grpc.ClientConn if successful. 33 func ClientSetup(c *cmd.GRPCClientConfig, tlsConfig *tls.Config, statsRegistry prometheus.Registerer, clk clock.Clock) (*grpc.ClientConn, error) { 34 if c == nil { 35 return nil, errors.New("nil gRPC client config provided: JSON config is probably missing a fooService section") 36 } 37 if tlsConfig == nil { 38 return nil, errNilTLS 39 } 40 41 metrics, err := newClientMetrics(statsRegistry) 42 if err != nil { 43 return nil, err 44 } 45 46 cmi := clientMetadataInterceptor{c.Timeout.Duration, metrics, clk, !c.NoWaitForReady} 47 48 unaryInterceptors := []grpc.UnaryClientInterceptor{ 49 cmi.Unary, 50 cmi.metrics.grpcMetrics.UnaryClientInterceptor(), 51 } 52 53 streamInterceptors := []grpc.StreamClientInterceptor{ 54 cmi.Stream, 55 cmi.metrics.grpcMetrics.StreamClientInterceptor(), 56 } 57 58 target, hostOverride, err := c.MakeTargetAndHostOverride() 59 if err != nil { 60 return nil, err 61 } 62 63 creds := bcreds.NewClientCredentials(tlsConfig.RootCAs, tlsConfig.Certificates, hostOverride) 64 return grpc.NewClient( 65 target, 66 grpc.WithDefaultServiceConfig( 67 fmt.Sprintf( 68 // By setting the service name to an empty string in 69 // healthCheckConfig, we're instructing the gRPC client to query 70 // the overall health status of each server. The grpc-go health 71 // server, as constructed by health.NewServer(), unconditionally 72 // sets the overall service (e.g. "") status to SERVING. If a 73 // specific service name were set, the server would need to 74 // explicitly transition that service to SERVING; otherwise, 75 // clients would receive a NOT_FOUND status and the connection 76 // would be marked as unhealthy (TRANSIENT_FAILURE). 77 `{"healthCheckConfig": {"serviceName": ""},"loadBalancingConfig": [{"%s":{}}]}`, 78 roundrobin.Name, 79 ), 80 ), 81 grpc.WithTransportCredentials(creds), 82 grpc.WithChainUnaryInterceptor(unaryInterceptors...), 83 grpc.WithChainStreamInterceptor(streamInterceptors...), 84 grpc.WithStatsHandler(otelgrpc.NewClientHandler()), 85 ) 86 } 87 88 // clientMetrics is a struct type used to return registered metrics from 89 // `NewClientMetrics` 90 type clientMetrics struct { 91 grpcMetrics *grpc_prometheus.ClientMetrics 92 // inFlightRPCs is a labelled gauge that slices by service/method the number 93 // of outstanding/in-flight RPCs. 94 inFlightRPCs *prometheus.GaugeVec 95 } 96 97 // newClientMetrics constructs a *grpc_prometheus.ClientMetrics, registered with 98 // the given registry, with timing histogram enabled. It must be called a 99 // maximum of once per registry, or there will be conflicting names. 100 func newClientMetrics(stats prometheus.Registerer) (clientMetrics, error) { 101 // Create the grpc prometheus client metrics instance and register it 102 grpcMetrics := grpc_prometheus.NewClientMetrics( 103 grpc_prometheus.WithClientHandlingTimeHistogram( 104 grpc_prometheus.WithHistogramBuckets([]float64{.01, .025, .05, .1, .5, 1, 2.5, 5, 10, 45, 90}), 105 ), 106 ) 107 err := stats.Register(grpcMetrics) 108 if err != nil { 109 are := prometheus.AlreadyRegisteredError{} 110 if errors.As(err, &are) { 111 grpcMetrics = are.ExistingCollector.(*grpc_prometheus.ClientMetrics) 112 } else { 113 return clientMetrics{}, err 114 } 115 } 116 117 // Create a gauge to track in-flight RPCs and register it. 118 inFlightGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{ 119 Name: "grpc_in_flight", 120 Help: "Number of in-flight (sent, not yet completed) RPCs", 121 }, []string{"method", "service"}) 122 err = stats.Register(inFlightGauge) 123 if err != nil { 124 are := prometheus.AlreadyRegisteredError{} 125 if errors.As(err, &are) { 126 inFlightGauge = are.ExistingCollector.(*prometheus.GaugeVec) 127 } else { 128 return clientMetrics{}, err 129 } 130 } 131 132 return clientMetrics{ 133 grpcMetrics: grpcMetrics, 134 inFlightRPCs: inFlightGauge, 135 }, nil 136 }