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  }