From 5d256e495164279e61986ab5f43502575cb58559 Mon Sep 17 00:00:00 2001 From: RainbowBird Date: Fri, 15 May 2026 18:59:03 +0800 Subject: [PATCH] feat(server): replace legacy health endpoints with K8s-style /livez and /readyz probes --- apps/server/docker-compose.otel.yml | 2 +- apps/server/docker-compose.yml | 2 +- .../docs/ai-context/observability-metrics.md | 2 +- .../docs/ai-context/transport-and-routes.md | 3 +- .../ai-context/verifications/llm-router.md | 20 +++++---- .../docs/ai-context/workers-and-runtime.md | 2 +- ...at-llm-tts-router-replacing-knoway-plan.md | 42 +++++++++---------- apps/server/otel/grafana/dashboards/build.ts | 2 +- apps/server/src/app.ts | 38 +++++++---------- 9 files changed, 56 insertions(+), 57 deletions(-) diff --git a/apps/server/docker-compose.otel.yml b/apps/server/docker-compose.otel.yml index 0756783f4..c8e539084 100644 --- a/apps/server/docker-compose.otel.yml +++ b/apps/server/docker-compose.otel.yml @@ -105,7 +105,7 @@ services: tempo: condition: service_healthy healthcheck: - test: ['CMD', 'wget', '--spider', '-q', 'http://localhost:3000/api/health'] + test: ['CMD', 'wget', '--spider', '-q', 'http://localhost:3000/livez'] interval: 10s timeout: 5s retries: 5 diff --git a/apps/server/docker-compose.yml b/apps/server/docker-compose.yml index 6ad16820e..754c6c00d 100644 --- a/apps/server/docker-compose.yml +++ b/apps/server/docker-compose.yml @@ -48,7 +48,7 @@ services: ports: - '6112:3000' healthcheck: - test: ['CMD-SHELL', 'curl -f http://localhost:3000/health || exit 1'] + test: ['CMD-SHELL', 'curl -f http://localhost:3000/livez || exit 1'] interval: 10s timeout: 5s retries: 5 diff --git a/apps/server/docs/ai-context/observability-metrics.md b/apps/server/docs/ai-context/observability-metrics.md index d73dc832c..9da8f8729 100644 --- a/apps/server/docs/ai-context/observability-metrics.md +++ b/apps/server/docs/ai-context/observability-metrics.md @@ -30,7 +30,7 @@ OTel SDK 在导出到 Prometheus 时做两件事: > > **STABLE-only**:[instrumentation.ts](../../instrumentation.ts) 把 `OTEL_SEMCONV_STABILITY_OPT_IN=http` 提前注入。OLD 系列(`http.server.duration` in ms)不再发射。详见 [`observability-conventions.md` 的 SemconvStability 章节](./observability-conventions.md#semconvstability-迁移说明)。 > -> `/health` 路径在 [app.ts](../../src/app.ts) 的 @hono/otel 包装层被显式 skip,Railway 健康检查不进 metric。 +> `/livez` 和 `/readyz` 在 [app.ts](../../src/app.ts) 的 @hono/otel 包装层被显式 skip,K8s 风格探针不进 metric。 ## Auth & Users diff --git a/apps/server/docs/ai-context/transport-and-routes.md b/apps/server/docs/ai-context/transport-and-routes.md index 88f32e399..79f835c4a 100644 --- a/apps/server/docs/ai-context/transport-and-routes.md +++ b/apps/server/docs/ai-context/transport-and-routes.md @@ -4,7 +4,8 @@ 应用在 `src/app.ts` 中挂载以下路由: -- `GET /health` +- `GET /livez` — K8s 风格 liveness 探针,纯静态 200,不碰任何外部依赖 +- `GET /readyz` — K8s 风格 readiness 探针,并发 ping Postgres + Redis;任一失败回 503。**不**检查上游 LLM key 健康(R14) - `GET /` — 服务标识 JSON,避免邮件链接拼错落到框架默认 404 - `/api/auth/*` - `/api/v1/characters` diff --git a/apps/server/docs/ai-context/verifications/llm-router.md b/apps/server/docs/ai-context/verifications/llm-router.md index 230055d8f..d54962052 100644 --- a/apps/server/docs/ai-context/verifications/llm-router.md +++ b/apps/server/docs/ai-context/verifications/llm-router.md @@ -45,25 +45,31 @@ Verification artifacts for the in-process router shipped across U1-U9 of ## Liveness probe -- **Scenario**: `GET /healthz/live` returns 200 with `{status: "live"}` even - when external dependencies are degraded. -- **Command**: `curl http://localhost:3000/healthz/live` +- **Scenario**: `GET /livez` returns 200 with `{status: "live"}` even + when external dependencies are degraded. K8s-style flat naming; legacy + `/health` and nested `/healthz/live` removed in this revision. +- **Command**: `curl -i http://localhost:3000/livez` - **Expected output**: HTTP 200, body `{"status":"live"}`. -- **Actual output** (2026-05-15): +- **Actual output** (commit `cfad87757` + uncommitted route rename, 2026-05-15): ``` + HTTP 200 {"status":"live"} ``` + Cross-check: `curl http://localhost:3000/health` → HTTP 404 (legacy + endpoint removed); `curl http://localhost:3000/healthz/live` → HTTP 404 + (nested form removed). - **Last verified**: 2026-05-15. ## Readiness probe -- **Scenario**: `GET /healthz/ready` returns 200 when Postgres + Redis both +- **Scenario**: `GET /readyz` returns 200 when Postgres + Redis both respond; 503 otherwise. Gateway-internal key health does NOT block readiness (R14). -- **Command**: `curl http://localhost:3000/healthz/ready` +- **Command**: `curl -i http://localhost:3000/readyz` - **Expected output**: HTTP 200, body `{"status":"ready","checks":{"db":"ok","redis":"ok"}}`. -- **Actual output** (2026-05-15): +- **Actual output** (commit `cfad87757` + uncommitted route rename, 2026-05-15): ``` + HTTP 200 {"status":"ready","checks":{"db":"ok","redis":"ok"}} ``` - **Last verified**: 2026-05-15. diff --git a/apps/server/docs/ai-context/workers-and-runtime.md b/apps/server/docs/ai-context/workers-and-runtime.md index aca1d23af..cd626f1c6 100644 --- a/apps/server/docs/ai-context/workers-and-runtime.md +++ b/apps/server/docs/ai-context/workers-and-runtime.md @@ -105,7 +105,7 @@ 重要实现细节: - `sdk.start()` 必须发生在 `metrics.getMeter()` 之前 -- `/health` 会被 HTTP instrumentation 忽略 +- `/livez` 和 `/readyz` 会被 HTTP instrumentation 忽略 ## 运行时修改建议 diff --git a/apps/server/docs/plans/2026-05-15-001-feat-llm-tts-router-replacing-knoway-plan.md b/apps/server/docs/plans/2026-05-15-001-feat-llm-tts-router-replacing-knoway-plan.md index 0d3ae9d82..bdb9431f6 100644 --- a/apps/server/docs/plans/2026-05-15-001-feat-llm-tts-router-replacing-knoway-plan.md +++ b/apps/server/docs/plans/2026-05-15-001-feat-llm-tts-router-replacing-knoway-plan.md @@ -10,7 +10,7 @@ deepened: ## Summary -在 `apps/server` 内新建一个 in-process 路由模块替换 knoway sidecar:LLM `/v1/chat/completions` 走 SSE passthrough + 请求内多 key fallback + 跨 upstream fallback;TTS `/v1/audio/speech` 走 adapter interface(v1 三家:Azure / DashScope cosyvoice / Volcengine,非流式 REST 实现);`/v1/audio/voices` 由仓库内静态 JSON 提供;configKV 增 `LLM_ROUTER_CONFIG` composite 条目承载整棵路由器配置;新 envelope encryption 工具加密存储 provider key;OTel 用 `airi.gen_ai.gateway.*` 自定义属性,新增 fallback / key 健康相关 metrics;新增 `/healthz/live` + `/healthz/ready`;一次性切流 + 数据驱动决定何时删 knoway compose。 +在 `apps/server` 内新建一个 in-process 路由模块替换 knoway sidecar:LLM `/v1/chat/completions` 走 SSE passthrough + 请求内多 key fallback + 跨 upstream fallback;TTS `/v1/audio/speech` 走 adapter interface(v1 三家:Azure / DashScope cosyvoice / Volcengine,非流式 REST 实现);`/v1/audio/voices` 由仓库内静态 JSON 提供;configKV 增 `LLM_ROUTER_CONFIG` composite 条目承载整棵路由器配置;新 envelope encryption 工具加密存储 provider key;OTel 用 `airi.gen_ai.gateway.*` 自定义属性,新增 fallback / key 健康相关 metrics;新增 `/livez` + `/readyz`;一次性切流 + 数据驱动决定何时删 knoway compose。 --- @@ -61,7 +61,7 @@ R-IDs 沿用 origin 文档(详见 origin 中 R1-R19 描述): | KTD-9 | Voice catalog 用 **静态 JSON 提交仓库**(`apps/server/src/services/tts-adapters/voices/*.json`),不在运行时跨服务聚合 | origin D12 | | KTD-10 | LLM/TTS 路由 logic **全部下沉到 `src/services/llm-router/`**,路由层只做 param validation + auth guard + 调 service + 处理响应;现有 `routes/openai/v1/index.ts` 的 TODO `:97-98` 同期解决 | apps/server/CLAUDE.md "Routes: thin — no business logic" | | KTD-11 | 路由器**不持久化** key 死活状态(origin D33 risk-accepted),但 OTel 上报支持 SLO 触发器(fallback.depth > 0.5 / 24h, 单 key > 80% 错误 / 30min)以便后续手动促 v2 | origin Success Criteria + D29 | -| KTD-12 | `/healthz/live` 和 `/healthz/ready` 是**新路径**(不替换现有 `/health` —— 后者保留兼容),按现有 `httpInstrumentationMiddleware` 的 `/health` 排除规则同样跳过 | apps/server/src/app.ts:126-128 模式 | +| KTD-12 | `/livez` 和 `/readyz` 是**新路径**(K8s 风格,post-implementation 决定不保留 legacy `/health`),按现有 `httpInstrumentationMiddleware` 的探针排除规则同样跳过 | apps/server/src/app.ts:126-128 模式 | | KTD-13 | 跨 upstream fallback 在**同一请求内**触发:upstream A 全 key 失败后切 upstream B 全 key 试,全 upstream 都失败才返 5xx | origin R5 | --- @@ -220,7 +220,7 @@ apps/server/ │ │ └── index.ts # MODIFY: prime new gateway metrics; new GatewayMetrics bundle │ ├── services/ │ │ └── config-kv.ts # MODIFY: add LLM_ROUTER_CONFIG to ConfigEntrySchemas -│ ├── app.ts # MODIFY: DI wiring, /healthz routes, remove GATEWAY_BASE_URL +│ ├── app.ts # MODIFY: DI wiring, /livez + /readyz routes, remove GATEWAY_BASE_URL │ ├── libs/env.ts # MODIFY: add LLM_ROUTER_MASTER_KEY env var; remove GATEWAY_BASE_URL │ └── libs/env.test.ts # MODIFY ├── otel/ @@ -232,7 +232,7 @@ apps/server/ │ └── ai-context/ │ ├── observability-metrics.md # MODIFY: register new metrics │ ├── observability-conventions.md # MODIFY: gen_ai.system values + airi.gen_ai.gateway.* namespace -│ ├── transport-and-routes.md # MODIFY: new /healthz routes; route → service mapping +│ ├── transport-and-routes.md # MODIFY: new /livez + /readyz routes; route → service mapping │ ├── redis-boundaries-and-pubsub.md # MODIFY: configkv:invalidate channel contract │ └── verifications/ │ └── llm-router.md # NEW: verification doc per AGENTS.md template @@ -559,9 +559,9 @@ apps/server/ --- -### U7. Pub/Sub config invalidation + `/healthz/live` + `/healthz/ready` +### U7. Pub/Sub config invalidation + `/livez` + `/readyz` -**Goal**: Wire Pub/Sub-driven invalidation of the LLM router config in-memory cache (KTD-4). Add per-instance `config.reload` OTel counter. Add `/healthz/live` (always 200) and `/healthz/ready` (Postgres + Redis ping) endpoints. **Gateway key health does NOT affect readiness** (R14). +**Goal**: Wire Pub/Sub-driven invalidation of the LLM router config in-memory cache (KTD-4). Add per-instance `config.reload` OTel counter. Add `/livez` (always 200) and `/readyz` (Postgres + Redis ping) endpoints. **Gateway key health does NOT affect readiness** (R14). **Requirements**: R13, R14, R16, R16a (acknowledged as Outstanding Question — not actively delivered, see "Resolve before merging" below), KTD-4, KTD-12. @@ -570,7 +570,7 @@ apps/server/ **Files**: - `apps/server/src/services/llm-router/config-loader.ts` (MODIFY — add `subscribeToInvalidations(redis)` wiring) - `apps/server/src/utils/redis-keys.ts` (MODIFY — add `configKvInvalidateChannel()` helper) -- `apps/server/src/app.ts` (MODIFY — register `/healthz/live`, `/healthz/ready`, exclude both from `httpInstrumentationMiddleware`; wire config-loader to redis subscriber; admin endpoint for `set LLM_ROUTER_CONFIG` publishes invalidation) +- `apps/server/src/app.ts` (MODIFY — register `/livez`, `/readyz`, exclude both from `httpInstrumentationMiddleware`; wire config-loader to redis subscriber; admin endpoint for `set LLM_ROUTER_CONFIG` publishes invalidation) - `apps/server/src/routes/admin/...` (MODIFY — if admin set endpoint exists for configKV; publish on write) - `apps/server/src/app.test.ts` (NEW — health endpoint tests) - `apps/server/docs/ai-context/redis-boundaries-and-pubsub.md` (MODIFY — declare `configkv:invalidate` channel) @@ -580,9 +580,9 @@ apps/server/ - On `configKV.set('LLM_ROUTER_CONFIG', value)`: publish to channel. - In `createLlmRouterService` init: subscribe via separate `ioredis` instance (Redis Pub/Sub requires dedicated subscriber connection per ioredis docs). On message matching `key === 'LLM_ROUTER_CONFIG'`: call `config-loader.invalidate()` and increment `gateway.configReload.add(1, {service_instance_id, source: 'pubsub'})`. - TTL fallback: in-memory cache has TTL = 5s. On TTL expiry next request reloads from configKV (Postgres+Redis source-of-truth chain) and increments counter with `source: 'ttl'`. -- `/healthz/live`: route returns `200 {status: 'live'}` always. No DB / Redis touch. Excluded from `httpInstrumentationMiddleware` (`apps/server/src/app.ts:126-128` pattern). -- `/healthz/ready`: route pings Postgres (`SELECT 1`) + Redis (`PING`). Returns 200 if both ok; 503 otherwise. **Does not check gateway key health** (R14 — single key flap can't take instance out of pool). -- Existing `/health` endpoint at `apps/server/src/app.ts:187` stays (keep external monitors stable); declared deprecated in docs in U8. +- `/livez`: route returns `200 {status: 'live'}` always. No DB / Redis touch. Excluded from `httpInstrumentationMiddleware` (`apps/server/src/app.ts:126-128` pattern). +- `/readyz`: route pings Postgres (`SELECT 1`) + Redis (`PING`). Returns 200 if both ok; 503 otherwise. **Does not check gateway key health** (R14 — single key flap can't take instance out of pool). +- Legacy `/health` endpoint removed post-implementation in favor of K8s-style `/livez` + `/readyz` (single source of truth, no overlap). **Patterns to follow**: - ioredis Pub/Sub: dedicated subscriber connection (search for existing pubsub usage in `apps/server` — `redis-boundaries-and-pubsub.md` references this) @@ -593,17 +593,17 @@ apps/server/ - (1) Config-loader subscribes on init; on Pub/Sub message for `LLM_ROUTER_CONFIG`: cache cleared, next read fetches fresh. - (2) Pub/Sub message for unrelated key: no invalidation, no counter increment. - (3) TTL expiry path: cache populated → 5s elapse (mock clock or vitest fake timers) → next read fetches fresh + counter incremented with `source: 'ttl'`. -- (4) `GET /healthz/live` returns 200 + `{status: 'live'}` even when Redis is down (Redis client error mocked). -- (5) `GET /healthz/ready` returns 200 when both Postgres + Redis ping ok. -- (6) `GET /healthz/ready` returns 503 when Postgres down (mock pool query throws). -- (7) `GET /healthz/ready` returns 503 when Redis down (mock ping throws). -- (8) `GET /healthz/ready` returns 200 even with `LLM_ROUTER_CONFIG` missing (gateway state does not block readiness per R14). -- (9) `httpInstrumentationMiddleware` does NOT instrument `/healthz/*` requests (assert OTel http span count after probe = 0). +- (4) `GET /livez` returns 200 + `{status: 'live'}` even when Redis is down (Redis client error mocked). +- (5) `GET /readyz` returns 200 when both Postgres + Redis ping ok. +- (6) `GET /readyz` returns 503 when Postgres down (mock pool query throws). +- (7) `GET /readyz` returns 503 when Redis down (mock ping throws). +- (8) `GET /readyz` returns 200 even with `LLM_ROUTER_CONFIG` missing (gateway state does not block readiness per R14). +- (9) `httpInstrumentationMiddleware` does NOT instrument `/livez` or `/readyz` requests (assert OTel http span count after probe = 0). **Verification**: - `pnpm -F @proj-airi/server typecheck` passes - `pnpm exec vitest run apps/server/src/app.test.ts` green -- Manual: `curl /healthz/live` → 200; `curl /healthz/ready` → 200 with Postgres + Redis up +- Manual: `curl /livez` → 200; `curl /readyz` → 200 with Postgres + Redis up **Resolve before merging**: - **R16a admin permission model** is an explicit Outstanding Question in origin; if it's not resolved before this unit ships, the admin set-config endpoint stays behind existing flat-admin-role auth. Note as known limitation in PR description: "Admin endpoint for `set LLM_ROUTER_CONFIG` uses existing flat admin role; role-scoping is follow-up work". @@ -675,7 +675,7 @@ apps/server/ - `apps/server/src/libs/env.test.ts` (MODIFY) - `apps/server/scripts/verify-router-config.ts` (NEW — operator script referenced in U1 Migration step; decrypts current `LLM_ROUTER_CONFIG` against `LLM_ROUTER_MASTER_KEY` to validate boot-time correctness) - `apps/server/src/scripts/otel/llm-router-smoke.ts` (NEW — new smoke fixture that produces traces tagged with `airi.gen_ai.gateway.*` attrs; **note**: the previously-referenced `apps/server/src/scripts/otel/smoke.ts` does not exist — the actual existing file is `ws-smoke.ts` for WebSocket smoke; gateway-specific smoke is new work) -- `apps/server/docs/ai-context/transport-and-routes.md` (MODIFY — route → service mapping update; `/healthz/live` + `/healthz/ready` documented; `/health` marked deprecated) +- `apps/server/docs/ai-context/transport-and-routes.md` (MODIFY — route → service mapping update; `/livez` + `/readyz` documented; legacy `/health` removed) - `apps/server/docs/ai-context/observability-conventions.md` (MODIFY) - `apps/server/docs/ai-context/verifications/llm-router.md` (FINALIZE — full verification with real evidence) - (Possibly) `apps/server/scripts/...` (NEW — knoway compose retention policy doc / data-driven trigger criteria) @@ -684,7 +684,7 @@ apps/server/ - Grafana dashboard JSON: add 3 panels (key exhausted count time series, fallback depth distribution, upstream errors by status code) + 3 alert rules (P0 key.exhausted > 0 in 5min, P1 fallback ratio > 30% in 15min, P2 single key > 80% errors in 30min). Use existing `build.ts` to assemble. Thresholds are placeholders — refine post-launch. - DI wiring: extend `AppDeps` interface (`apps/server/src/app.ts:70-88`) with `llmRouter` field. Register via `injeca.provide('services:llmRouter', { dependsOn: ['services:configKV', 'libs:redis', 'otel'], build: ... })`. Thread into `createV1CompletionsRoutes` factory. - `GATEWAY_BASE_URL` removal: delete from env schema; verify no remaining consumers via grep — current consumers per existing brainstorm context: `apps/server/src/libs/env.ts`, `apps/server/src/libs/env.test.ts`, `apps/server/src/routes/openai/v1/index.ts`, `apps/server/src/routes/openai/v1/route.test.ts`, `apps/server/src/scripts/otel/smoke.ts`. Update each. -- Verification doc (`apps/server/docs/ai-context/verifications/llm-router.md`): follow AGENTS.md template — for each user path (chat completions happy / chat completions fallback / TTS speech happy / voices listing / healthz live / healthz ready), include scenario / command / expected output / actual output (curl response snippets) / environment (commit SHA + deploy env) / last verified date. +- Verification doc (`apps/server/docs/ai-context/verifications/llm-router.md`): follow AGENTS.md template — for each user path (chat completions happy / chat completions fallback / TTS speech happy / voices listing / livez / readyz), include scenario / command / expected output / actual output (curl response snippets) / environment (commit SHA + deploy env) / last verified date. - knoway compose: do not delete yet. Document retention criteria in transport-and-routes.md and PR description: "knoway compose stays until: 14 days post-deploy without P1+ incidents OR 1 peak-traffic event without P1+ incidents. Reset on any P1." **Patterns to follow**: @@ -772,8 +772,8 @@ Verification doc lives at `apps/server/docs/ai-context/verifications/llm-router. 4. **TTS speech (cosyvoice)**: same against cosyvoice model. 5. **TTS speech (Volcengine)**: same against Volcengine model. 6. **Voices listing**: `curl GET /api/v1/openai/audio/voices?model=azure-tts` returns voice catalog JSON. -7. **Liveness**: `curl /healthz/live` returns 200 + `{status: 'live'}`. -8. **Readiness**: `curl /healthz/ready` returns 200 with Postgres+Redis up; 503 otherwise. +7. **Liveness**: `curl /livez` returns 200 + `{status: 'live'}`. +8. **Readiness**: `curl /readyz` returns 200 with Postgres+Redis up; 503 otherwise. 9. **Pre-upstream validation**: `curl POST /api/v1/openai/chat/completions model=unknown` returns 400 with `unknown_model` error code. 10. **All-keys exhaustion**: with all keys invalid, returns 502 (per KTD-1 final-cause mapping). diff --git a/apps/server/otel/grafana/dashboards/build.ts b/apps/server/otel/grafana/dashboards/build.ts index 0a2a8ad68..789683883 100644 --- a/apps/server/otel/grafana/dashboards/build.ts +++ b/apps/server/otel/grafana/dashboards/build.ts @@ -454,7 +454,7 @@ elements['panel-15'] = statPanel( elements['panel-3'] = statPanel( 3, 'Req/s (5m)', - '5-minute average inbound HTTP request rate. /health (Railway probe) is excluded at the @hono/otel middleware level so this reflects real user traffic. **Fixed 5m window — intentionally does not follow the dashboard time picker** (see row-level note). For trends, see panel-14 (HTTP Request Rate by Route).', + '5-minute average inbound HTTP request rate. /livez and /readyz (K8s probes) are excluded at the @hono/otel middleware level so this reflects real user traffic.', [query(`sum(rate(http_server_request_duration_seconds_count{${SERVICE_FILTER}, http_request_method!="OPTIONS"}[5m]))`, 'req/s')], { unit: 'reqps', steps: [{ color: 'green', value: 0 }, { color: 'yellow', value: 100 }, { color: 'red', value: 500 }], decimals: 2 }, ) diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 32d47c17e..82d6cd214 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -124,19 +124,16 @@ export async function buildApp(deps: AppDeps) { // @hono/otel records `http.server.request.duration` and // `http.server.active_requests` with the matched Hono route pattern // (auto-instrumentation can't see Hono's router, so it would emit empty - // `http.route` and concrete URLs — the previous Latency-by-Route bug). + // `http.route` and concrete URLs, the previous Latency-by-Route bug). // - // /health is Railway's healthcheck pinger — high frequency, zero signal, - // skip outright. + // K8s-style probes are high-frequency and zero-signal for product + // metrics; skip outright so they don't pollute http.* dashboards. const otelMw = httpInstrumentationMiddleware({ serviceName: deps.env.OTEL_SERVICE_NAME, serviceVersion: process.env.npm_package_version || '0.0.0', }) app.use('*', async (c, next) => { - // Skip /health (legacy Railway probe) and /healthz/* (new liveness + - // readiness routes added in U7) so high-frequency probes don't pollute - // http.* metrics or the Latency-by-Route panel. - if (c.req.path === '/health' || c.req.path.startsWith('/healthz/')) + if (c.req.path === '/livez' || c.req.path === '/readyz') return next() return otelMw(c, next) }) @@ -224,25 +221,20 @@ export async function buildApp(deps: AppDeps) { }) /** - * Health check route (legacy — kept for backward compatibility with the - * existing Railway probe configuration). New deployments should target - * /healthz/live (liveness) and /healthz/ready (readiness) separately. + * Liveness probe (K8s convention). Returns 200 as long as the Node + * process is alive. Must not touch Postgres, Redis, or any external + * dependency: a single upstream blip should NOT cause Railway to + * recycle the pod (R13/R14). */ - .on('GET', '/health', c => c.json({ status: 'ok' })) + .on('GET', '/livez', c => c.json({ status: 'live' })) /** - * Liveness probe — returns 200 as long as the Node process is alive. - * Must not touch Postgres, Redis, or any external dependency: a single - * upstream blip should NOT cause Railway to recycle the pod (R13/R14). + * Readiness probe (K8s convention). Verifies the instance can serve + * traffic by pinging Postgres + Redis (the only two infra dependencies + * that, if down, mean we genuinely can't serve). Gateway-internal key + * health is intentionally NOT checked (R14): one bad upstream key + * must not pull the whole instance out of the load balancer pool. */ - .on('GET', '/healthz/live', c => c.json({ status: 'live' })) - /** - * Readiness probe — verifies the instance can serve traffic. Pings - * Postgres + Redis (the only two infra dependencies that, if down, mean - * we genuinely can't serve). Gateway-internal key health is intentionally - * NOT checked (R14): one bad upstream key must not pull the whole instance - * out of the load balancer pool. - */ - .on('GET', '/healthz/ready', async (c) => { + .on('GET', '/readyz', async (c) => { // Run both checks in parallel and let either fail independently. const [dbResult, redisResult] = await Promise.allSettled([ deps.db.execute('SELECT 1'),