135 lines
3.4 KiB
Go
135 lines
3.4 KiB
Go
// Package healthhttp exposes process-local HTTP health endpoints for services
|
|
// whose primary protocol is not HTTP.
|
|
package healthhttp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"hyapp/pkg/grpchealth"
|
|
)
|
|
|
|
const (
|
|
statusOK = "ok"
|
|
statusFail = "fail"
|
|
)
|
|
|
|
// LiveChecker allows services with richer runtime liveness checks, such as
|
|
// room-service, to reuse their existing process probes.
|
|
type LiveChecker interface {
|
|
Live() []grpchealth.Check
|
|
}
|
|
|
|
// Response is intentionally close to gateway health responses so CLB probes and
|
|
// deploy scripts can consume one stable shape across all services.
|
|
type Response struct {
|
|
Status string `json:"status"`
|
|
Service string `json:"service"`
|
|
NodeID string `json:"node_id"`
|
|
Checks []grpchealth.Check `json:"checks"`
|
|
}
|
|
|
|
// Server owns the separate HTTP listener used by CLB health checks for gRPC
|
|
// processes. It does not replace gRPC health; it makes health visible to CLB.
|
|
type Server struct {
|
|
nodeID string
|
|
checker grpchealth.Checker
|
|
server *http.Server
|
|
listener net.Listener
|
|
once sync.Once
|
|
}
|
|
|
|
// New creates a health HTTP server. Empty addr disables the HTTP health port so
|
|
// unit tests and special one-off tools can keep a single listener.
|
|
func New(addr string, nodeID string, checker grpchealth.Checker) (*Server, error) {
|
|
addr = strings.TrimSpace(addr)
|
|
if addr == "" {
|
|
return nil, nil
|
|
}
|
|
if checker == nil {
|
|
return nil, errors.New("health checker is required")
|
|
}
|
|
|
|
listener, err := net.Listen("tcp", addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s := &Server{
|
|
nodeID: strings.TrimSpace(nodeID),
|
|
checker: checker,
|
|
listener: listener,
|
|
}
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/healthz/live", s.live)
|
|
mux.HandleFunc("/healthz/ready", s.ready)
|
|
s.server = &http.Server{
|
|
Handler: mux,
|
|
ReadHeaderTimeout: 2 * time.Second,
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// Run serves health endpoints until Close is called or the listener fails.
|
|
func (s *Server) Run() error {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
err := s.server.Serve(s.listener)
|
|
if errors.Is(err, http.ErrServerClosed) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Close stops the health endpoint. Readiness should already be failing before
|
|
// this is called because the owning service marks its checker draining first.
|
|
func (s *Server) Close(ctx context.Context) error {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
var err error
|
|
s.once.Do(func() {
|
|
err = s.server.Shutdown(ctx)
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (s *Server) live(writer http.ResponseWriter, request *http.Request) {
|
|
checks := []grpchealth.Check{grpchealth.OK("process")}
|
|
if live, ok := s.checker.(LiveChecker); ok {
|
|
// Services with process-local invariants should expose them on live, but
|
|
// live still avoids slow external dependency probes.
|
|
checks = live.Live()
|
|
}
|
|
s.write(writer, checks)
|
|
}
|
|
|
|
func (s *Server) ready(writer http.ResponseWriter, request *http.Request) {
|
|
s.write(writer, s.checker.Ready(request.Context()))
|
|
}
|
|
|
|
func (s *Server) write(writer http.ResponseWriter, checks []grpchealth.Check) {
|
|
status := statusFail
|
|
code := http.StatusServiceUnavailable
|
|
if grpchealth.Healthy(checks) {
|
|
status = statusOK
|
|
code = http.StatusOK
|
|
}
|
|
writer.Header().Set("Content-Type", "application/json")
|
|
writer.WriteHeader(code)
|
|
_ = json.NewEncoder(writer).Encode(Response{
|
|
Status: status,
|
|
Service: s.checker.ServiceName(),
|
|
NodeID: s.nodeID,
|
|
Checks: checks,
|
|
})
|
|
}
|