Files
health/CLAUDE.md
Rene Nochebuena e1b6b7ddd7 feat(health): initial stable release v0.9.0
HTTP health check handler with parallel goroutine-per-check execution, 5 s request-derived timeout, and two-level criticality (LevelCritical → 503, LevelDegraded → 200).

What's included:
- `Checkable` interface (HealthCheck / Name / Priority) and `Level` type with LevelCritical and LevelDegraded constants
- `NewHandler(logger, checks...)` returning http.Handler; runs all checks concurrently via buffered channel, returns JSON with per-component status and latency
- `ComponentStatus` and `Response` types for the JSON response body

Tested-via: todo-api POC integration
Reviewed-against: docs/adr/
2026-03-18 14:06:17 -06:00

3.3 KiB

health

HTTP health check handler with parallel checks, timeouts, and two-level criticality.

Purpose

Provides a single http.Handler that interrogates any number of Checkable infrastructure components concurrently and returns a JSON response with per-component status and an overall service status. Designed to be mounted at /health and consumed by load balancers and orchestrators.

Tier & Dependencies

Tier 1 — depends only on Go stdlib (context, encoding/json, net/http, time). No external or internal module imports.

Key Design Decisions

  • Parallel checks (ADR-001): every registered component is checked in its own goroutine. A context.WithTimeout(r.Context(), 5*time.Second) provides the deadline for the entire request. The handler blocks until all goroutines report via a buffered channel.
  • Two-level criticality (ADR-002): LevelCritical (value 0) → DOWN + HTTP 503 on failure; LevelDegraded (value 1) → DEGRADED + HTTP 200 on failure. The zero-value default is LevelCritical.
  • Checkable interface (ADR-003): infrastructure components implement HealthCheck(ctx) error, Name() string, and Priority() Level. The health package defines the interface; infra packages satisfy it — not the reverse.
  • Duck-typed Logger (global ADR-001): the Logger interface is defined locally in this package. Any logger that matches the method set (including logz.Logger) is accepted without an import of logz.

Patterns

Construct the handler at app bootstrap and pass all Checkable components:

db  := postgres.New(logger, cfg)   // satisfies health.Checkable
rdb := redis.New(logger, cfg)      // satisfies health.Checkable

h := health.NewHandler(logger, db, rdb)
router.Get("/health", h)

Implement Checkable on a custom type:

type myService struct{}

func (s *myService) HealthCheck(ctx context.Context) error { return s.ping(ctx) }
func (s *myService) Name() string                          { return "my-service" }
func (s *myService) Priority() health.Level                { return health.LevelDegraded }

What to Avoid

  • Do not add a global/package-level handler or registry. NewHandler is the only constructor; use dependency injection.
  • Do not call NewHandler with nil logger — the handler will panic on the first request when it calls logger.WithContext.
  • Do not import infra modules (postgres, mysql, etc.) from this package. The dependency must flow one way: infra → health.
  • Do not remove the buffered channel (make(chan result, len(h.checks))). Making it unbuffered would leak goroutines if the handler returns before draining all results.

Testing Notes

  • health_test.go covers: no checks (UP), all UP, critical DOWN (503), degraded DOWN (200), mixed DOWN+DEGRADED (503), parallel execution timing, JSON shape, and context timeout propagation.
  • compliance_test.go contains compile-time interface satisfaction checks for Logger and Checkable. Run go build ./... to verify them without executing any runtime code.
  • Tests use an in-process httptest.NewRecorder — no real network or infrastructure required.
  • The parallelism test uses a 100 ms delay per check and asserts total elapsed < 300 ms. It is timing-sensitive; extremely slow CI runners may produce false negatives.