2026-04-29 23:07:50 +08:00

126 lines
3.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package grpchealth
import (
"context"
"time"
"google.golang.org/grpc/codes"
healthgrpc "google.golang.org/grpc/health/grpc_health_v1"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/status"
)
const (
// StatusOK 表示单个健康检查项通过。
StatusOK = "ok"
// StatusFail 表示单个健康检查项失败。
StatusFail = "fail"
)
// Check 描述一项 readiness 子检查gRPC health 只返回总状态,子项用于服务内聚合。
type Check struct {
Name string
Status string
Message string
}
// Checker 把服务自己的 ready 语义适配到标准 gRPC health protocol。
type Checker interface {
ServiceName() string
Ready(ctx context.Context) []Check
}
// Server 实现 grpc.health.v1.Health并把 SERVING 严格映射到 Checker.Ready。
type Server struct {
healthgrpc.UnimplementedHealthServer
checker Checker
}
// NewServer 创建标准 gRPC health server。
func NewServer(checker Checker) *Server {
return &Server{checker: checker}
}
// Check 返回当前服务是否 ready未知服务名按标准返回 NotFound。
func (s *Server) Check(ctx context.Context, req *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) {
if !s.supports(req.GetService()) {
return nil, status.Error(codes.NotFound, "unknown service")
}
return &healthpb.HealthCheckResponse{Status: s.status(ctx)}, nil
}
// Watch 定时推送 ready 状态变化,供支持 gRPC health watch 的服务发现系统消费。
func (s *Server) Watch(req *healthpb.HealthCheckRequest, stream healthgrpc.Health_WatchServer) error {
if !s.supports(req.GetService()) {
if err := stream.Send(&healthpb.HealthCheckResponse{Status: healthpb.HealthCheckResponse_SERVICE_UNKNOWN}); err != nil {
return status.Error(codes.Canceled, "health watch stream ended")
}
<-stream.Context().Done()
return status.Error(codes.Canceled, "health watch stream ended")
}
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
var last healthpb.HealthCheckResponse_ServingStatus = -1
for {
current := s.status(stream.Context())
if current != last {
if err := stream.Send(&healthpb.HealthCheckResponse{Status: current}); err != nil {
return status.Error(codes.Canceled, "health watch stream ended")
}
last = current
}
select {
case <-ticker.C:
case <-stream.Context().Done():
return status.Error(codes.Canceled, "health watch stream ended")
}
}
}
// Healthy 聚合子检查,任何失败项都会让服务从 ready 池摘除。
func Healthy(checks []Check) bool {
for _, check := range checks {
if check.Status != StatusOK {
return false
}
}
return true
}
// OK 创建通过项。
func OK(name string) Check {
return Check{Name: name, Status: StatusOK}
}
// Fail 创建失败项。
func Fail(name string, message string) Check {
return Check{Name: name, Status: StatusFail, Message: message}
}
// Bool 根据布尔条件创建检查项。
func Bool(name string, ok bool, message string) Check {
if ok {
return OK(name)
}
return Fail(name, message)
}
func (s *Server) supports(service string) bool {
return service == "" || service == s.checker.ServiceName()
}
func (s *Server) status(ctx context.Context) healthpb.HealthCheckResponse_ServingStatus {
if Healthy(s.checker.Ready(ctx)) {
return healthpb.HealthCheckResponse_SERVING
}
return healthpb.HealthCheckResponse_NOT_SERVING
}