Skip to content

Commit 1109a18

Browse files
Merge PR #1040
2 parents 4b3d01b + bb3bd59 commit 1109a18

4 files changed

Lines changed: 1030 additions & 8 deletions

File tree

docs/usage-health.md

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
# `GET /api/usage/health` — Usage Subsystem Health Probe
2+
3+
Returns the live operational status of every external dependency that the
4+
`/api/usage` surface area relies on. Designed for load-balancer health
5+
checks, operations dashboards, and automated alerting without requiring
6+
credentials.
7+
8+
## Overview
9+
10+
| Property | Value |
11+
|---|---|
12+
| Method | `GET` |
13+
| Path | `/api/usage/health` |
14+
| Auth required | No (public endpoint) |
15+
| Response type | `application/json` |
16+
| Success status | `200 OK` |
17+
| Error status | `503 Service Unavailable` when a critical dependency is down |
18+
19+
## Dependencies probed
20+
21+
| Key | Dependency | When included |
22+
|---|---|---|
23+
| `database` | PostgreSQL (usage event storage, aggregation, billing) | Always when `DATABASE_URL` / DB env vars are configured |
24+
| `soroban_rpc` | Stellar Soroban RPC (billing deduction & settlement) | Only when `SOROBAN_RPC_ENABLED=true` |
25+
| `horizon` | Stellar Horizon REST API (on-chain settlement sync) | Only when `HORIZON_ENABLED=true` |
26+
27+
Each dependency is probed independently in parallel. A slow or unresponsive
28+
dependency cannot stall the response beyond its own configured timeout.
29+
30+
## HTTP status codes
31+
32+
| Code | Meaning |
33+
|---|---|
34+
| `200` | All probed dependencies are `ok` or at worst `degraded`. The response body contains the rolled-up status. |
35+
| `503` | The critical `database` dependency is `down`. The response body still contains per-dependency details. |
36+
| `500` | An unexpected internal error occurred. Details are not exposed. |
37+
38+
## Response body
39+
40+
```jsonc
41+
{
42+
// Rolled-up status: "ok" | "degraded" | "down"
43+
"status": "ok",
44+
45+
// ISO-8601 timestamp of when the probe was executed
46+
"timestamp": "2026-07-28T22:00:00.000Z",
47+
48+
// Per-dependency status map
49+
"dependencies": {
50+
"database": {
51+
"status": "ok", // "ok" | "degraded" | "down"
52+
"responseTime": 4 // round-trip ms (integer)
53+
},
54+
"soroban_rpc": {
55+
"status": "ok",
56+
"responseTime": 87
57+
},
58+
"horizon": {
59+
"status": "ok",
60+
"responseTime": 112
61+
}
62+
}
63+
}
64+
```
65+
66+
### `status` roll-up rules
67+
68+
| Rule | Result |
69+
|---|---|
70+
| `database` is `down` | `"down"` |
71+
| Any dependency is `degraded` | `"degraded"` |
72+
| All dependencies are `ok` | `"ok"` |
73+
74+
### `dependencies[key].status`
75+
76+
| Value | Meaning |
77+
|---|---|
78+
| `"ok"` | Dependency responded within its timeout with a healthy result. |
79+
| `"degraded"` | Dependency responded but was slow (exceeded the degraded threshold) or returned a non-fatal error (e.g. an unexpected HTTP status). |
80+
| `"down"` | Dependency is unreachable, timed out, or returned a fatal error. |
81+
82+
### `dependencies[key].error`
83+
84+
Present only when `status` is not `"ok"`. Values are sanitised categories —
85+
raw OS / driver error messages (which can contain connection strings,
86+
hostnames, or credentials) are never exposed.
87+
88+
| Value | Meaning |
89+
|---|---|
90+
| `"timeout"` | The probe timed out before a response was received. |
91+
| `"unavailable"` | Connection failed, DNS failed, or an unexpected error occurred. |
92+
| `"unexpected_response"` | The probe completed but the result was semantically wrong (e.g. `SELECT 1` did not return `1`). |
93+
| `"HTTP <code>"` | The remote service returned a non-2xx HTTP status code, e.g. `"HTTP 503"`. |
94+
95+
## Examples
96+
97+
### All dependencies healthy
98+
99+
```
100+
GET /api/usage/health
101+
```
102+
103+
```http
104+
HTTP/1.1 200 OK
105+
Content-Type: application/json
106+
```
107+
108+
```json
109+
{
110+
"status": "ok",
111+
"timestamp": "2026-07-28T22:00:00.000Z",
112+
"dependencies": {
113+
"database": { "status": "ok", "responseTime": 3 },
114+
"soroban_rpc": { "status": "ok", "responseTime": 95 },
115+
"horizon": { "status": "ok", "responseTime": 110 }
116+
}
117+
}
118+
```
119+
120+
### Database unreachable
121+
122+
```http
123+
HTTP/1.1 503 Service Unavailable
124+
Content-Type: application/json
125+
```
126+
127+
```json
128+
{
129+
"status": "down",
130+
"timestamp": "2026-07-28T22:00:00.000Z",
131+
"dependencies": {
132+
"database": { "status": "down", "responseTime": 2001, "error": "timeout" }
133+
}
134+
}
135+
```
136+
137+
### Soroban RPC degraded, database healthy
138+
139+
```http
140+
HTTP/1.1 200 OK
141+
Content-Type: application/json
142+
```
143+
144+
```json
145+
{
146+
"status": "degraded",
147+
"timestamp": "2026-07-28T22:00:00.000Z",
148+
"dependencies": {
149+
"database": { "status": "ok", "responseTime": 4 },
150+
"soroban_rpc": { "status": "degraded", "responseTime": 450, "error": "HTTP 503" }
151+
}
152+
}
153+
```
154+
155+
### No dependencies configured
156+
157+
When the application starts without database or external service environment
158+
variables, the endpoint returns an empty but healthy response:
159+
160+
```http
161+
HTTP/1.1 200 OK
162+
Content-Type: application/json
163+
```
164+
165+
```json
166+
{
167+
"status": "ok",
168+
"timestamp": "2026-07-28T22:00:00.000Z",
169+
"dependencies": {}
170+
}
171+
```
172+
173+
## Security considerations
174+
175+
- **No authentication** is required so that load-balancers and uptime monitors
176+
can poll this endpoint without credential management.
177+
- **Error sanitisation** — raw database connection strings, hostnames,
178+
passwords, and stack traces are never included in the response. Only safe
179+
category strings (`"timeout"`, `"unavailable"`, `"unexpected_response"`, or
180+
`"HTTP <code>"`) are exposed.
181+
- The endpoint is **read-only**. It performs no writes and carries no
182+
side-effects.
183+
184+
## Configuration
185+
186+
The dependencies included in the response are controlled by environment
187+
variables (see main README for full reference):
188+
189+
| Variable | Effect |
190+
|---|---|
191+
| `DATABASE_URL` / `DB_*` | Enables the `database` dependency probe. |
192+
| `SOROBAN_RPC_ENABLED=true` | Enables the `soroban_rpc` probe. |
193+
| `SOROBAN_RPC_URL` | RPC endpoint URL (required when `SOROBAN_RPC_ENABLED=true`). |
194+
| `SOROBAN_RPC_TIMEOUT` | Timeout in ms for the Soroban probe (default `2000`). |
195+
| `HORIZON_ENABLED=true` | Enables the `horizon` probe. |
196+
| `HORIZON_URL` | Horizon endpoint URL (required when `HORIZON_ENABLED=true`). |
197+
| `HORIZON_TIMEOUT` | Timeout in ms for the Horizon probe (default `2000`). |
198+
| `HEALTH_CHECK_DB_TIMEOUT` | Timeout in ms for the database probe (default `2000`). |
199+
200+
## Related endpoints
201+
202+
| Endpoint | Description |
203+
|---|---|
204+
| `GET /api/health` | Aggregate application health (used by load-balancers). |
205+
| `GET /api/health/dependencies` | Per-dependency probe for the whole application (admin-oriented). |
206+
| `GET /api/webhooks/health` | Webhook subsystem health snapshot. |
207+
| `GET /api/rate-limit/health` | Rate-limit subsystem health probe. |
208+
| `GET /api/admin/health/probes` | Detailed per-component probes (admin auth + IP allowlist). |

src/routes/index.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import { InMemoryRestRateLimiter } from "../middleware/restRateLimit.js";
1717
import { createUsageCsvRouter } from "./usage/csv.js";
1818
import { createUsageByEndpointRouter } from "./usage/byEndpoint.js";
1919
import { createUsageAggregateRouter } from "./usage/aggregate.js";
20+
import { createUsageHealthRouter } from "./usage/health.js";
21+
import type { HealthCheckConfig } from "../services/healthCheck.js";
2022
import { createExportSchedulesRouter } from "./exports/schedules.js";
2123
import { createExportsRouter } from "./exports.js";
2224
import { createUsageAccessLogMiddleware } from "../middleware/usageAccessLog.js";
@@ -50,6 +52,8 @@ export interface ApiRouterDeps
5052
apiRepository?: ApiRepository;
5153
usageSseBroadcaster?: UsageSseBroadcaster;
5254
auditService?: AuditService;
55+
/** Health-check configuration forwarded to GET /api/usage/health. */
56+
healthCheckConfig?: HealthCheckConfig;
5357
}
5458

5559
export function createApiRouter(deps: ApiRouterDeps = {}): Router {
@@ -70,14 +74,7 @@ export function createApiRouter(deps: ApiRouterDeps = {}): Router {
7074
}),
7175
);
7276

73-
// Structured JSON access log for all /api/usage/* routes.
74-
// Applied at the parent level so sub-routers (csv, by-endpoint, aggregate, sse)
75-
// are automatically covered without needing per-route middleware.
76-
const usageAccessLogMiddleware = createUsageAccessLogMiddleware({
77-
redactFields: config.usageAccessLog.redactFields,
78-
});
79-
80-
// Mounted before '/usage' so the more specific CSV export path matches first.
77+
// Mounted before '/usage' so the more specific paths match first.
8178
router.use(
8279
"/usage/csv",
8380
usageAccessLogMiddleware,
@@ -110,6 +107,13 @@ export function createApiRouter(deps: ApiRouterDeps = {}): Router {
110107
}),
111108
);
112109

110+
// Usage subsystem external-dependency health probe (GrantFox FWC26).
111+
// Mounted before the generic /usage handler to avoid path shadowing.
112+
router.use(
113+
"/usage/health",
114+
createUsageHealthRouter({ config: deps.healthCheckConfig }),
115+
);
116+
113117
router.use(
114118
"/usage",
115119
usageAccessLogMiddleware,

0 commit comments

Comments
 (0)