Health Primer
Health Primer
Section titled “Health Primer”Open this page after Stable Roots when the change still belongs to the default service path and the real question has narrowed to how the service reports its own health: what types describe liveness vs. readiness, how individual components signal their state, and what the canonical health model looks like.
health owns health status types, readiness helpers, and component health models. It is intentionally not an HTTP package and not an ops analytics package. HTTP health endpoints live in the owning service’s handler layer; orchestration lives in reference apps or extension families.
checker := health.ComponentCheckerFunc(func(ctx context.Context) health.HealthState { if err := db.PingContext(ctx); err != nil { return health.StateUnhealthy } return health.StateHealthy})agg := health.NewAggregate(checker)Start here when
Section titled “Start here when”- you are defining or checking
HealthStatusorHealthStatevalues - you are implementing
ComponentCheckerfor a dependency (database, cache, downstream service) - you are building a
ComponentHealthaggregate from multiple checkers - you are reading or writing
ReadinessStatusin a readiness probe handler
Do not start here when
Section titled “Do not start here when”- the change is about the HTTP endpoint that exposes health — that belongs in the handler layer of your service
- the change introduces check orchestration, retry policy, or health manager abstractions
- the work is about build metadata exposure, health history retention, or trend reporting
- the change adds metrics, analytics, or export backends — those belong in
x/observability
First files to read in the current repository
Section titled “First files to read in the current repository”health/module.yamlhealth/core.gohealth/readiness.goreference/standard-service/internal/handler/health.go
Concrete ownership examples
Section titled “Concrete ownership examples”Keep it in health when the work is about | Move out when the work becomes |
|---|---|
HealthStatus — the typed enum: Healthy, Degraded, Unhealthy | feature-specific status catalogs with domain vocabulary |
ComponentChecker interface and ComponentHealth aggregate | health manager that executes checks on a schedule with retry |
ReadinessStatus — a small explicit type for readiness probes | HTTP endpoint wiring, transport-specific response shaping |
| Checking a single component’s state inside a handler | ops reporting, health history export, or trend analytics |
Import
Section titled “Import”import "github.com/spcent/plumego/health"Liveness and readiness handlers
Section titled “Liveness and readiness handlers”The canonical pattern is to expose /healthz (liveness) and /readyz (readiness) from your handler layer, using health types to model the state:
type HealthHandler struct { ServiceName string db health.ComponentChecker}
func (h HealthHandler) Live(w http.ResponseWriter, r *http.Request) { contract.WriteResponse(w, r, http.StatusOK, map[string]string{ "status": "ok", "service": h.ServiceName, }, nil)}
func (h HealthHandler) Ready(w http.ResponseWriter, r *http.Request) { ctx := r.Context() status := health.ReadinessStatus{ Ready: true, Timestamp: time.Now(), Components: map[string]bool{}, }
if err := h.db.Check(ctx); err != nil { status.Ready = false status.Reason = "database unavailable" status.Components["db"] = false contract.WriteResponse(w, r, http.StatusServiceUnavailable, status, nil) return } status.Components["db"] = true contract.WriteResponse(w, r, http.StatusOK, status, nil)}Implement ComponentChecker
Section titled “Implement ComponentChecker”type DBChecker struct { db *sql.DB}
func (c DBChecker) Name() string { return "database" }
func (c DBChecker) Check(ctx context.Context) error { return c.db.PingContext(ctx)}HealthState constants
Section titled “HealthState constants”| Constant | Meaning | Ready to serve? |
|---|---|---|
health.StatusHealthy | All systems nominal | Yes |
health.StatusDegraded | Partial failure, degraded service | Yes |
health.StatusUnhealthy | Component down, cannot serve | No |
state := health.StatusDegradedif state.IsReady() { // service can accept traffic}Wire health routes
Section titled “Wire health routes”healthH := handler.HealthHandler{ ServiceName: "my-service", db: DBChecker{db: sqlDB},}app.Get("/healthz", http.HandlerFunc(healthH.Live))app.Get("/readyz", http.HandlerFunc(healthH.Ready))Why this primer exists
Section titled “Why this primer exists”health is a support package, not a feature catalog. Services frequently conflate “the type that represents health” with “the system that manages and exposes health.” Keeping health as pure types and checkers means the same model works whether health is exposed over HTTP, gRPC, or a metrics scrape endpoint — the format is not the model’s concern.