package health
import (
"context"
"sync"
"time"
)
type Registry struct {
checkers []Checker
timeout time.Duration
}
func NewRegistry(timeout time.Duration) *Registry {
return &Registry{timeout: timeout}
}
func (r *Registry) Register(c Checker) {
r.checkers = append(r.checkers, c)
}
func (r *Registry) Run(ctx context.Context) Health {
results := make([]Result, len(r.checkers))
var wg sync.WaitGroup
for i, c := range r.checkers {
wg.Add(1)
go func(idx int, chk Checker) {
defer wg.Done()
cctx, cancel := context.WithTimeout(ctx, r.timeout)
defer cancel()
start := time.Now()
err := chk.Check(cctx)
res := Result{
Name: chk.Name(),
Status: StatusUp,
Latency: time.Since(start) / time.Millisecond,
}
if err != nil {
res.Status = StatusDown
res.Error = err.Error()
}
results[idx] = res
}(i, c)
}
wg.Wait()
return Health{Status: aggregate(results), Checks: results}
}
package health
import (
"context"
"time"
)
type Status string
const (
StatusUp Status = "up"
StatusDegraded Status = "degraded"
StatusDown Status = "down"
)
type Checker interface {
Name() string
Check(ctx context.Context) error
}
type Result struct {
Name string `json:"name"`
Status Status `json:"status"`
Latency time.Duration `json:"latency_ms"`
Error string `json:"error,omitempty"`
}
type Health struct {
Status Status `json:"status"`
Checks []Result `json:"checks"`
}
func aggregate(results []Result) Status {
overall := StatusUp
for _, r := range results {
if r.Status == StatusDown {
overall = StatusDegraded
}
}
return overall
}
type PingChecker struct {
name string
ping func(ctx context.Context) error
}
func NewPingChecker(name string, ping func(ctx context.Context) error) *PingChecker {
return &PingChecker{name: name, ping: ping}
}
func (p *PingChecker) Name() string { return p.name }
func (p *PingChecker) Check(ctx context.Context) error {
return p.ping(ctx)
}
package health
import (
"context"
"database/sql"
"encoding/json"
"net/http"
"github.com/redis/go-redis/v9"
)
func HealthHandler(reg *Registry) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
result := reg.Run(req.Context())
code := http.StatusOK
if result.Status == StatusDown {
code = http.StatusServiceUnavailable
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(result)
}
}
func Build(db *sql.DB, rdb *redis.Client) *Registry {
reg := NewRegistry(2 * time.Second)
reg.Register(NewPingChecker("postgres", func(ctx context.Context) error {
return db.PingContext(ctx)
}))
reg.Register(NewPingChecker("redis", func(ctx context.Context) error {
return rdb.Ping(ctx).Err()
}))
return reg
}
A readiness endpoint answers a deceptively simple question: is this service actually able to serve traffic right now? That means more than "the process is up" — it means the dependencies the service relies on (a database, a cache, an upstream API) are reachable. This snippet builds that endpoint in three collaborating files, with per-dependency timeouts, concurrent probing, and an aggregate status that maps cleanly onto HTTP semantics.
In checker.go, each dependency is expressed as a Checker interface with a single Check(ctx) method, so the health system does not care whether it is talking to Postgres, Redis, or an HTTP service. PingChecker adapts any func(ctx) error into a named checker, which keeps concrete probes small. The Result struct carries a Status, latency, and an optional error string, and Health aggregates results into an overall status where StatusDown for any single check degrades the whole to StatusDegraded. Splitting StatusUp from StatusDegraded lets an operator distinguish a totally broken service from one that lost a non-critical dependency.
In health.go, Registry.Run fans out across all registered checkers using a sync.WaitGroup, so a slow dependency does not serialize the others — total latency is bounded by the slowest single probe, not the sum. Each check receives its own context.WithTimeout, meaning a hung TCP connection cannot stall the whole endpoint; the derived context also inherits cancellation from the incoming request, so a client disconnect stops all in-flight probes. Timing is captured around the call to record real observed latency, which is useful for spotting a dependency that is up but degrading.
In handler.go, HealthHandler runs the registry and translates the aggregate into a status code: 200 for up or degraded, 503 for down. This distinction matters for orchestrators — Kubernetes should pull a 503 pod out of rotation, while a 200-degraded pod stays serving. The JSON body exposes per-check detail for dashboards and humans. A common pitfall this avoids is probing dependencies on the liveness path, which can cause cascading restarts; this handler is meant for readiness only. The pattern scales by registering more Checkers without touching the transport layer.
Share this code
Here's the card — post it anywhere.