Skip to main content

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 /api returns text/plain with This api for {APP_NAME} (parity with the NestJS getHello). It serves as a smoke test that the engine assembled correctly, and it works even with no backend at all.
  • GET /api/health calls health.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 with ServiceUnavailableException).
  • GET /livez and GET /healthz check 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 /readyz returns 503 until every enabled dependency passes its ping and the process is not draining. The response includes a per-dependency breakdown keyed by Dependency.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 same health.Checker instance.
  • health.Checker runs a ticker ping in a background goroutine every HEALTH_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 by SHUTDOWN_DRAIN_DELAY.

Key Files & Functions

RouteSource
GET /apiinternal/health/handler.go(*Handler).hello
GET /api/healthinternal/health/handler.go(*Handler).health plus internal/health/health.goCheck()
GET /livez, GET /healthzinternal/health/health.go(*Checker).LivezHandler() (mounted in server.New)
GET /readyz(*Checker).ReadyzHandler()
  • internal/health/register.goRegister(api gin.IRouter, appName string) mounts /api and /api/health. It is called directly from server.New rather than through the variadic RegisterFunc, because internal/health cannot import internal/server without creating an import cycle.
  • internal/health/entity.go and domain_test.go define 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's Ping method into a health.Probe.
  • internal/supervisor uses 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.