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 }