Health Checks & Probes
Overview
A set of endpoints that report whether the service is alive and ready to take traffic. They split into two levels by intent: liveness (the process has not died) and readiness (every enabled dependency answers its ping and the process is not draining). Alongside these sit a NestJS Terminus-style endpoint at /api/health and a hello endpoint at GET /api, both ported from the original app.controller.
Business Flow
GET /apireturns text/plain withThis api for {APP_NAME}(parity with the NestJSgetHello). It serves as a smoke test that the engine assembled correctly, and it works even with no backend at all.GET /api/healthcallshealth.Check()and returns a Terminus-shaped HealthCheckResult:{status, info, error, details}. If every indicator is up the response is 200; if any is down it returns 503 (parity withServiceUnavailableException).GET /livezandGET /healthzcheck the process only and must never be tied to a dependency. Restarting a pod does nothing to fix a downed database, and failing liveness because of the DB would put the pod into a restart loop.GET /readyzreturns 503 until every enabled dependency passes its ping and the process is not draining. The response includes a per-dependency breakdown keyed byDependency.Name()— which is exactly why duplicate names are rejected at boot.- All three probes are mounted at the root of the business port rather than under
/api, because that is where k8s probes and the ALB health check land. They are also mirrored on the admin port:9100, backed by the samehealth.Checkerinstance. health.Checkerruns a ticker ping in a background goroutine everyHEALTH_PROBE_INTERVAL(with a 2-second timeout per probe) and caches the result, so endpoints answer quickly and do not hit the DB on every incoming probe.- During shutdown,
SetDraining()is called before the HTTP server closes, so the load balancer withdraws the pod before the service stops accepting work. The delay is governed bySHUTDOWN_DRAIN_DELAY.
Key Files & Functions
| Route | Source |
|---|---|
GET /api | internal/health/handler.go → (*Handler).hello |
GET /api/health | internal/health/handler.go → (*Handler).health plus internal/health/health.go → Check() |
GET /livez, GET /healthz | internal/health/health.go → (*Checker).LivezHandler() (mounted in server.New) |
GET /readyz | (*Checker).ReadyzHandler() |
internal/health/register.go→Register(api gin.IRouter, appName string)mounts/apiand/api/health. It is called directly fromserver.Newrather than through the variadicRegisterFunc, becauseinternal/healthcannot importinternal/serverwithout creating an import cycle.internal/health/entity.goanddomain_test.godefine the Terminus result structure.internal/obs/*is the admin server on:9100, mirroring health alongside/metrics,/version, and gated pprof.
Connections to Other Services
internal/deps.Registry.Probes()converts each dependency'sPingmethod into ahealth.Probe.internal/supervisoruses the same probe set to decide whether to alert or exit.- No database or table is touched beyond the ping itself.
- The related client-web feature is
app-shell, indirectly: the web app never calls the probes itself, but they are the contract with k8s and the ALB that keeps the APIs the web app calls available.