Health checks
Liveness and readiness endpoints discoverable via decorators.
Kubernetes, ECS, and most reverse proxies expect /healthz and
/readyz to return 200 when the app is OK and 503 when it isn't.
Gebweb mounts both, runs every registered probe on a hit, and
aggregates the results into a JSON body.
Wiring
import gebweb;
let app = gebweb.app([HomeController()]);
gebweb.useHealth(app, [DatabaseProbe(conn), QueueProbe(brokerConn)]);
Each instance is scanned for @HealthCheck methods. The default
endpoints are /healthz (liveness) and /readyz (readiness);
override via {"livenessPath": "...", "readinessPath": "..."}.
Writing probes
class DatabaseProbe {
db.Conn conn;
func DatabaseProbe(db.Conn conn) { this.conn = conn; }
@HealthCheck(name: "db", kind: "readiness", timeout: 2000)
func ping(): gebweb.ProbeResult {
try {
this.conn.queryOne("SELECT 1");
return gebweb.passing();
} catch (Error e) {
return gebweb.failing("DB ping failed: " + e.message);
}
}
}
class MemoryProbe {
@HealthCheck(name: "memory", kind: "liveness")
func memory(): bool {
return sys.freeMemoryMb() > 64;
}
}
@HealthCheck accepts three named args:
| Arg | Default | Meaning |
|---|---|---|
name |
<Class>.<method> |
Identifier surfaced on the response body. |
kind |
"readiness" |
"liveness" or "readiness". Liveness runs on /healthz, readiness on /readyz. |
timeout |
5000 |
Per-probe timeout in ms. A timing-out probe fails the overall check with status: "timeout". |
A probe method returns gebweb.ProbeResult for the structured
case (status + message + details) or a plain bool. Throwing is
equivalent to returning a failing result whose message is the
exception text.
Response shape
{
"status": "ok",
"checks": {
"db": {"status": "ok", "duration_ms": 4},
"memory": {"status": "ok", "duration_ms": 0}
}
}
When any probe fails the response uses status 503 and the
top-level status becomes "fail". Each entry carries the probe
duration so observability tools can latch onto degradation
trends.
Liveness vs readiness
- Liveness answers "should this process be killed?" Probes should be cheap and never touch external dependencies; failing liveness will cause the orchestrator to restart the pod.
- Readiness answers "is this process ready to serve traffic?" Probes touch the database, message broker, downstream services. Failing readiness pulls the pod out of the load balancer pool without restarting it.
The default kind is readiness because that's the more useful
probe in practice; mark the cheap process-health probes as
liveness explicitly.
Reference
| Helper | Purpose |
|---|---|
gebweb.useHealth(app, instances, opts) |
Mount /healthz + /readyz and discover probes. |
@HealthCheck(name, kind, timeout) |
Mark a method as a probe. |
gebweb.ProbeResult |
bool ok, string message, dict<string, any> details. |
gebweb.passing(message) |
Convenience constructor. |
gebweb.failing(message) |
Convenience constructor. |
The ops bundle: useOps
gebweb.useOps(app, instances, opts) mounts everything a production
deployment expects in one call: the liveness and readiness probes
above plus a Prometheus metrics endpoint:
gebweb.useOps(app, [DatabaseProbe(conn)]);
Metrics
gebweb.useMetrics(app) (included in useOps) serves Prometheus
text format at /metrics (override with {"path": "..."}):
gebweb_http_requests_total{method,route,status}- request counts labelled by route template (bounded cardinality, not raw paths).gebweb_http_request_duration_milliseconds_sum/_count- latency totals per route for rate and average-latency queries.gebweb_http_requests_in_flight- current in-flight gauge.
Every route records automatically, including early rejections from rate limiting or CSRF. Protect the endpoint at the proxy or with middleware if it should not be public.
Graceful shutdown and drain
gebweb.shutdown(app, {"timeoutMs": 10000}) drains the app: the
readiness endpoint flips to 503 with {"status": "draining"} so
load balancers and rolling deploys stop sending traffic, in-flight
requests finish within the deadline, then the listener closes and
gebweb.serve (or http.wait on a gebweb.listen handle) returns.
It is safe to call from a signal handler; gebweb.cli installs
exactly that for SIGINT and SIGTERM.
Long-running loops (job workers, scheduler ticks) should poll
gebweb.isDraining(app) and exit when it flips.