github.com/bufbuild/connect-grpchealth-go@v1.1.1/grpchealth.go (about)

     1  // Copyright 2022-2023 Buf Technologies, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package grpchealth enables any net/http server, including those built with
    16  // Connect, to respond to gRPC-style health checks. This lets load balancers,
    17  // container orchestrators, and other infrastructure systems respond to changes
    18  // in your HTTP server's health.
    19  //
    20  // The exposed health-checking API is wire compatible with Google's gRPC
    21  // implementations, so it works with grpcurl, grpc-health-probe, and Kubernetes
    22  // gRPC liveness probes.
    23  //
    24  // The core Connect package is github.com/bufbuild/connect-go. Documentation is
    25  // available at https://connect.build.
    26  package grpchealth
    27  
    28  import (
    29  	"context"
    30  	"errors"
    31  	"fmt"
    32  	"net/http"
    33  	"sync"
    34  
    35  	"github.com/bufbuild/connect-go"
    36  	healthv1 "github.com/bufbuild/connect-grpchealth-go/internal/gen/go/connectext/grpc/health/v1"
    37  )
    38  
    39  // HealthV1ServiceName is the fully-qualified name of the v1 version of the health service.
    40  const HealthV1ServiceName = "grpc.health.v1.Health"
    41  
    42  // Status describes the health of a service.
    43  type Status uint8
    44  
    45  const (
    46  	// StatusUnknown indicates that the service's health state is indeterminate.
    47  	StatusUnknown Status = 0
    48  
    49  	// StatusServing indicates that the service is ready to accept requests.
    50  	StatusServing Status = 1
    51  
    52  	// StatusNotServing indicates that the process is healthy but the service is
    53  	// not accepting requests. For example, StatusNotServing is often appropriate
    54  	// when your primary database is down or unreachable.
    55  	StatusNotServing Status = 2
    56  )
    57  
    58  // NewHandler wraps the supplied Checker to build an HTTP handler for gRPC's
    59  // health-checking API. It returns the path on which to mount the handler and
    60  // the HTTP handler itself.
    61  //
    62  // Note that the returned handler only supports the unary Check method, not the
    63  // streaming Watch. As suggested in gRPC's health schema, it returns
    64  // connect.CodeUnimplemented for the Watch method.
    65  //
    66  // For more details on gRPC's health checking protocol, see
    67  // https://github.com/grpc/grpc/blob/master/doc/health-checking.md and
    68  // https://github.com/grpc/grpc/blob/master/src/proto/grpc/health/v1/health.proto.
    69  func NewHandler(checker Checker, options ...connect.HandlerOption) (string, http.Handler) {
    70  	const serviceName = "/grpc.health.v1.Health/"
    71  	mux := http.NewServeMux()
    72  	check := connect.NewUnaryHandler(
    73  		serviceName+"Check",
    74  		func(
    75  			ctx context.Context,
    76  			req *connect.Request[healthv1.HealthCheckRequest],
    77  		) (*connect.Response[healthv1.HealthCheckResponse], error) {
    78  			var checkRequest CheckRequest
    79  			if req.Msg != nil {
    80  				checkRequest.Service = req.Msg.Service
    81  			}
    82  			checkResponse, err := checker.Check(ctx, &checkRequest)
    83  			if err != nil {
    84  				return nil, err
    85  			}
    86  			return connect.NewResponse(&healthv1.HealthCheckResponse{
    87  				Status: healthv1.HealthCheckResponse_ServingStatus(checkResponse.Status),
    88  			}), nil
    89  		},
    90  		options...,
    91  	)
    92  	mux.Handle(serviceName+"Check", check)
    93  	watch := connect.NewServerStreamHandler(
    94  		serviceName+"Watch",
    95  		func(
    96  			_ context.Context,
    97  			_ *connect.Request[healthv1.HealthCheckRequest],
    98  			_ *connect.ServerStream[healthv1.HealthCheckResponse],
    99  		) error {
   100  			return connect.NewError(
   101  				connect.CodeUnimplemented,
   102  				errors.New("connect doesn't support watching health state"),
   103  			)
   104  		},
   105  		options...,
   106  	)
   107  	mux.Handle(serviceName+"Watch", watch)
   108  	return serviceName, mux
   109  }
   110  
   111  // CheckRequest is a request for the health of a service. When using protobuf,
   112  // Service will be a fully-qualified service name (for example,
   113  // "acme.ping.v1.PingService"). If the Service is an empty string, the caller
   114  // is asking for the health status of whole process.
   115  type CheckRequest struct {
   116  	Service string
   117  }
   118  
   119  // CheckResponse reports the health of a service (or of the whole process). The
   120  // only valid Status values are StatusUnknown, StatusServing, and
   121  // StatusNotServing. When asked to report on the status of an unknown service,
   122  // Checkers should return a connect.CodeNotFound error.
   123  //
   124  // Often, systems monitoring health respond to errors by restarting the
   125  // process. They often respond to StatusNotServing by removing the process from
   126  // a load balancer pool.
   127  type CheckResponse struct {
   128  	Status Status
   129  }
   130  
   131  // A Checker reports the health of a service. It must be safe to call
   132  // concurrently.
   133  type Checker interface {
   134  	Check(context.Context, *CheckRequest) (*CheckResponse, error)
   135  }
   136  
   137  // StaticChecker is a simple Checker implementation. It always returns
   138  // StatusServing for the process, and it returns a static value for each
   139  // service.
   140  //
   141  // If you have a dynamic list of services, want to ping a database as part of
   142  // your health check, or otherwise need something more specialized, you should
   143  // write a custom Checker implementation.
   144  type StaticChecker struct {
   145  	mu       sync.RWMutex
   146  	statuses map[string]Status
   147  }
   148  
   149  // NewStaticChecker constructs a StaticChecker. By default, each of the
   150  // supplied services has StatusServing.
   151  //
   152  // The supplied strings should be fully-qualified protobuf service names (for
   153  // example, "acme.user.v1.UserService"). Generated Connect service files
   154  // have this declared as a constant.
   155  func NewStaticChecker(services ...string) *StaticChecker {
   156  	statuses := make(map[string]Status, len(services))
   157  	for _, service := range services {
   158  		statuses[service] = StatusServing
   159  	}
   160  	return &StaticChecker{statuses: statuses}
   161  }
   162  
   163  // SetStatus sets the health status of a service, registering a new service if
   164  // necessary. It's safe to call SetStatus and Check concurrently.
   165  func (c *StaticChecker) SetStatus(service string, status Status) {
   166  	c.mu.Lock()
   167  	defer c.mu.Unlock()
   168  	c.statuses[service] = status
   169  }
   170  
   171  // Check implements Checker. It's safe to call concurrently with SetStatus.
   172  func (c *StaticChecker) Check(_ context.Context, req *CheckRequest) (*CheckResponse, error) {
   173  	if req.Service == "" {
   174  		return &CheckResponse{Status: StatusServing}, nil
   175  	}
   176  	c.mu.RLock()
   177  	defer c.mu.RUnlock()
   178  	if status, registered := c.statuses[req.Service]; registered {
   179  		return &CheckResponse{Status: status}, nil
   180  	}
   181  	return nil, connect.NewError(
   182  		connect.CodeNotFound,
   183  		fmt.Errorf("unknown service %s", req.Service),
   184  	)
   185  }