Files
lingniu-vehicle-ingest/go/vehicle-gateway/internal/health/health.go

139 lines
3.4 KiB
Go

package health
import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/metrics"
)
type Check struct {
Name string
Check func(context.Context) error
}
type Handler struct {
service string
checks []Check
}
type Logger interface {
Info(string, ...any)
Error(string, ...any)
}
func NewHandler(service string, checks []Check) *Handler {
service = strings.TrimSpace(service)
if service == "" {
service = "vehicle-service"
}
return &Handler{service: service, checks: checks}
}
func NewMux(service string, checks []Check, registry *metrics.Registry) *http.ServeMux {
mux := http.NewServeMux()
handler := NewHandler(service, checks)
mux.Handle("/healthz", handler)
mux.Handle("/readyz", handler)
if registry != nil {
metrics.RegisterServiceInfo(registry, handler.service)
mux.Handle("/metrics", metrics.NewHandler(registry))
}
return mux
}
func NewServer(addr string, service string, checks []Check, registry *metrics.Registry) *http.Server {
addr = strings.TrimSpace(addr)
if addr == "" {
return nil
}
return &http.Server{
Addr: addr,
Handler: NewMux(service, checks, registry),
ReadHeaderTimeout: 5 * time.Second,
}
}
func Start(ctx context.Context, logger Logger, server *http.Server) {
if server == nil {
return
}
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = server.Shutdown(shutdownCtx)
}()
go func() {
if logger != nil {
logger.Info("health server started", "addr", server.Addr)
}
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) && logger != nil {
logger.Error("health server failed", "error", err)
}
}()
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeHealth(w, http.StatusMethodNotAllowed, map[string]any{"error": "method not allowed"})
return
}
path := strings.Trim(r.URL.Path, "/")
if path != "healthz" && path != "readyz" {
writeHealth(w, http.StatusNotFound, map[string]any{"error": "route not found"})
return
}
if path == "healthz" {
writeHealth(w, http.StatusOK, map[string]any{
"service": h.service,
"status": "ok",
"time": time.Now().UTC().Format(time.RFC3339),
})
return
}
h.writeReadiness(w, r)
}
func (h *Handler) writeReadiness(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
status := "ok"
code := http.StatusOK
checks := make(map[string]any, len(h.checks))
for _, check := range h.checks {
name := strings.TrimSpace(check.Name)
if name == "" {
continue
}
if check.Check == nil {
checks[name] = map[string]any{"status": "ok"}
continue
}
if err := check.Check(ctx); err != nil {
status = "degraded"
code = http.StatusServiceUnavailable
checks[name] = map[string]any{"status": "error", "error": err.Error()}
continue
}
checks[name] = map[string]any{"status": "ok"}
}
writeHealth(w, code, map[string]any{
"service": h.service,
"status": status,
"time": time.Now().UTC().Format(time.RFC3339),
"checks": checks,
})
}
func writeHealth(w http.ResponseWriter, status int, payload map[string]any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(payload)
}