@astroscope/health 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +283 -0
- package/dist/index.d.ts +163 -0
- package/dist/index.js +236 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# @astroscope/health
|
|
2
|
+
|
|
3
|
+
> **Note:** This package is in active development. APIs may change between versions.
|
|
4
|
+
|
|
5
|
+
Kubernetes-style health check endpoints for Astro SSR. Provides `/livez`, `/readyz`, `/startupz`, and `/healthz` probes on a separate HTTP server.
|
|
6
|
+
|
|
7
|
+
## Examples
|
|
8
|
+
|
|
9
|
+
See the [demo/health](../../demo/health) directory for a working example.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @astroscope/health
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
This package is designed to work with [@astroscope/boot](../boot) for lifecycle management.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// src/boot.ts
|
|
23
|
+
import type { BootContext } from "@astroscope/boot";
|
|
24
|
+
import { checks, probes, server } from "@astroscope/health";
|
|
25
|
+
|
|
26
|
+
export async function onStartup({ dev, host, port }: BootContext) {
|
|
27
|
+
// start health server on a separate port
|
|
28
|
+
server.start({ port: 9090 });
|
|
29
|
+
|
|
30
|
+
// enable liveness immediately
|
|
31
|
+
probes.livez.enable();
|
|
32
|
+
|
|
33
|
+
// initialize your app...
|
|
34
|
+
await connectToDatabase();
|
|
35
|
+
|
|
36
|
+
// register health checks
|
|
37
|
+
checks.register("database", () => db.ping());
|
|
38
|
+
|
|
39
|
+
// enable startup probe (initialization complete)
|
|
40
|
+
probes.startupz.enable();
|
|
41
|
+
|
|
42
|
+
// enable readiness probe (ready for traffic)
|
|
43
|
+
probes.readyz.enable();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function onShutdown() {
|
|
47
|
+
// disable readiness first (stop receiving traffic)
|
|
48
|
+
probes.readyz.disable();
|
|
49
|
+
|
|
50
|
+
await disconnectFromDatabase();
|
|
51
|
+
|
|
52
|
+
// stop health server
|
|
53
|
+
await server.stop();
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Probes
|
|
58
|
+
|
|
59
|
+
### `/livez` — Liveness Probe
|
|
60
|
+
|
|
61
|
+
Indicates if the process is running. If this fails, Kubernetes will restart the container.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
probes.livez.enable(); // returns 200 OK
|
|
65
|
+
probes.livez.disable(); // returns 503 Service Unavailable
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### `/startupz` — Startup Probe
|
|
69
|
+
|
|
70
|
+
Indicates if the application has finished initializing. Kubernetes waits for this before sending traffic or checking liveness.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
probes.startupz.enable();
|
|
74
|
+
probes.startupz.disable();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `/readyz` — Readiness Probe
|
|
78
|
+
|
|
79
|
+
Indicates if the application is ready to receive traffic. When disabled or when required health checks fail, Kubernetes removes the pod from load balancer rotation.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
probes.readyz.enable();
|
|
83
|
+
probes.readyz.disable();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The readiness probe automatically runs all non-optional health checks and returns 503 if any fail.
|
|
87
|
+
|
|
88
|
+
### `/healthz` — Health Status
|
|
89
|
+
|
|
90
|
+
Returns detailed JSON status of all probes and health checks. Useful for debugging and dashboards.
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"status": "healthy",
|
|
95
|
+
"probes": {
|
|
96
|
+
"livez": true,
|
|
97
|
+
"startupz": true,
|
|
98
|
+
"readyz": true
|
|
99
|
+
},
|
|
100
|
+
"checks": {
|
|
101
|
+
"database": {
|
|
102
|
+
"status": "healthy",
|
|
103
|
+
"latency": 12
|
|
104
|
+
},
|
|
105
|
+
"redis": {
|
|
106
|
+
"status": "unhealthy",
|
|
107
|
+
"latency": 5003,
|
|
108
|
+
"error": "check \"redis\" timed out after 5000ms"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Status values:
|
|
115
|
+
- `healthy` — all checks pass
|
|
116
|
+
- `degraded` — optional checks failing, required checks pass
|
|
117
|
+
- `unhealthy` — required checks failing
|
|
118
|
+
|
|
119
|
+
## Health Checks
|
|
120
|
+
|
|
121
|
+
Register health checks to verify dependencies are working:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { checks } from "@astroscope/health";
|
|
125
|
+
|
|
126
|
+
// return result (recommended for boolean checks)
|
|
127
|
+
checks.register("database", () => ({
|
|
128
|
+
status: db.isConnected() ? "healthy" : "unhealthy",
|
|
129
|
+
error: db.isConnected() ? undefined : "connection lost",
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
// throw-based (classic pattern)
|
|
133
|
+
checks.register("cache", async () => {
|
|
134
|
+
await redis.ping(); // throws if fails
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// with options
|
|
138
|
+
checks.register({
|
|
139
|
+
name: "external-api",
|
|
140
|
+
check: () => fetch("https://api.example.com/health").then(() => {}),
|
|
141
|
+
optional: true, // doesn't affect /readyz, only /healthz status
|
|
142
|
+
timeout: 10000, // custom timeout (default: 5000ms)
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The check function can either:
|
|
147
|
+
- Return `HealthCheckResult` with status and optional error
|
|
148
|
+
- Return `void` (completing without error = healthy)
|
|
149
|
+
- Throw an error (= unhealthy with error message)
|
|
150
|
+
|
|
151
|
+
### Unregistering Checks
|
|
152
|
+
|
|
153
|
+
`register()` returns an unregister function:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const unregister = checks.register({
|
|
157
|
+
name: "database",
|
|
158
|
+
check: () => db.ping(),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// later...
|
|
162
|
+
unregister();
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Server Options
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
server.start({
|
|
169
|
+
host: "0.0.0.0", // default: "localhost"
|
|
170
|
+
port: 9090, // default: 9090
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Kubernetes Configuration
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
apiVersion: v1
|
|
178
|
+
kind: Pod
|
|
179
|
+
spec:
|
|
180
|
+
containers:
|
|
181
|
+
- name: app
|
|
182
|
+
ports:
|
|
183
|
+
- containerPort: 4321 # astro
|
|
184
|
+
- containerPort: 9090 # health
|
|
185
|
+
livenessProbe:
|
|
186
|
+
httpGet:
|
|
187
|
+
path: /livez
|
|
188
|
+
port: 9090
|
|
189
|
+
initialDelaySeconds: 0
|
|
190
|
+
periodSeconds: 10
|
|
191
|
+
startupProbe:
|
|
192
|
+
httpGet:
|
|
193
|
+
path: /startupz
|
|
194
|
+
port: 9090
|
|
195
|
+
failureThreshold: 30
|
|
196
|
+
periodSeconds: 2
|
|
197
|
+
readinessProbe:
|
|
198
|
+
httpGet:
|
|
199
|
+
path: /readyz
|
|
200
|
+
port: 9090
|
|
201
|
+
periodSeconds: 5
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## API Reference
|
|
205
|
+
|
|
206
|
+
### Server
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { server } from "@astroscope/health";
|
|
210
|
+
|
|
211
|
+
server.start(options?: HealthServerOptions): void;
|
|
212
|
+
server.stop(): Promise<void>;
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Probes
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { probes } from "@astroscope/health";
|
|
219
|
+
|
|
220
|
+
probes.livez.enable(): void;
|
|
221
|
+
probes.livez.disable(): void;
|
|
222
|
+
probes.livez.get(): Promise<HealthProbeResult>;
|
|
223
|
+
probes.livez.response(): Promise<Response>;
|
|
224
|
+
|
|
225
|
+
// same for startupz, readyz
|
|
226
|
+
|
|
227
|
+
probes.healthz.get(): Promise<HealthzResult>;
|
|
228
|
+
probes.healthz.response(): Promise<Response>;
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Checks
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
import { checks } from "@astroscope/health";
|
|
235
|
+
|
|
236
|
+
checks.register(name: string, check: CheckFn): () => void;
|
|
237
|
+
checks.register(check: HealthCheck): () => void;
|
|
238
|
+
|
|
239
|
+
// CheckFn = () => Promise<HealthCheckResult | void> | HealthCheckResult | void
|
|
240
|
+
checks.getChecks(): HealthCheck[];
|
|
241
|
+
checks.runAll(): Promise<Record<string, HealthCheckResult>>;
|
|
242
|
+
checks.runRequired(): Promise<boolean>;
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Types
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
interface HealthCheck {
|
|
249
|
+
name: string;
|
|
250
|
+
check: () => Promise<HealthCheckResult | void> | HealthCheckResult | void;
|
|
251
|
+
optional?: boolean; // default: false
|
|
252
|
+
timeout?: number; // default: 5000
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
interface HealthCheckResult {
|
|
256
|
+
status: "healthy" | "unhealthy";
|
|
257
|
+
latency?: number;
|
|
258
|
+
error?: string;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
interface HealthProbeResult {
|
|
262
|
+
passing: boolean;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
interface HealthzResult {
|
|
266
|
+
status: "healthy" | "degraded" | "unhealthy";
|
|
267
|
+
probes: {
|
|
268
|
+
livez: boolean;
|
|
269
|
+
startupz: boolean;
|
|
270
|
+
readyz: boolean;
|
|
271
|
+
};
|
|
272
|
+
checks: Record<string, HealthCheckResult>;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
interface HealthServerOptions {
|
|
276
|
+
host?: string; // default: "localhost"
|
|
277
|
+
port?: number; // default: 9090
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## License
|
|
282
|
+
|
|
283
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server configuration options.
|
|
3
|
+
*/
|
|
4
|
+
interface HealthServerOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Host to bind the health server to.
|
|
7
|
+
* @default 'localhost'
|
|
8
|
+
*/
|
|
9
|
+
host?: string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Port to bind the health server to.
|
|
12
|
+
* @default 9090
|
|
13
|
+
*/
|
|
14
|
+
port?: number | undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Health check definition.
|
|
18
|
+
*/
|
|
19
|
+
interface HealthCheck {
|
|
20
|
+
/**
|
|
21
|
+
* Unique name for this health check.
|
|
22
|
+
*/
|
|
23
|
+
name: string;
|
|
24
|
+
/**
|
|
25
|
+
* Function that performs the health check.
|
|
26
|
+
* Return HealthCheckResult or throw an error to indicate status.
|
|
27
|
+
* Returning void or completing without error means healthy.
|
|
28
|
+
*/
|
|
29
|
+
check: () => Promise<HealthCheckResult | void> | HealthCheckResult | void;
|
|
30
|
+
/**
|
|
31
|
+
* Whether this check is optional.
|
|
32
|
+
* Optional checks don't affect the /readyz endpoint.
|
|
33
|
+
* @default false
|
|
34
|
+
*/
|
|
35
|
+
optional?: boolean | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Maximum time in ms for the check to complete.
|
|
38
|
+
* @default 5000
|
|
39
|
+
*/
|
|
40
|
+
timeout?: number | undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Result of a single health check.
|
|
44
|
+
*/
|
|
45
|
+
interface HealthCheckResult {
|
|
46
|
+
status: 'healthy' | 'unhealthy';
|
|
47
|
+
latency?: number | undefined;
|
|
48
|
+
error?: string | undefined;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Result of a probe query.
|
|
52
|
+
*/
|
|
53
|
+
interface HealthProbeResult {
|
|
54
|
+
passing: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Full health status including all checks.
|
|
58
|
+
*/
|
|
59
|
+
interface HealthzResult {
|
|
60
|
+
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
61
|
+
probes: {
|
|
62
|
+
livez: boolean;
|
|
63
|
+
startupz: boolean;
|
|
64
|
+
readyz: boolean;
|
|
65
|
+
};
|
|
66
|
+
checks: Record<string, HealthCheckResult>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Single probe interface.
|
|
70
|
+
*/
|
|
71
|
+
interface HealthProbe {
|
|
72
|
+
/**
|
|
73
|
+
* Enable this probe (will return 200 when called).
|
|
74
|
+
*/
|
|
75
|
+
enable(): void;
|
|
76
|
+
/**
|
|
77
|
+
* Disable this probe (will return 503 when called).
|
|
78
|
+
*/
|
|
79
|
+
disable(): void;
|
|
80
|
+
/**
|
|
81
|
+
* Get the current probe result.
|
|
82
|
+
*/
|
|
83
|
+
get(): Promise<HealthProbeResult>;
|
|
84
|
+
/**
|
|
85
|
+
* Get a Response object for this probe.
|
|
86
|
+
*/
|
|
87
|
+
response(): Promise<Response>;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Healthz probe interface (always returns data, used for debugging).
|
|
91
|
+
*/
|
|
92
|
+
interface HealthzProbe {
|
|
93
|
+
/**
|
|
94
|
+
* Get the full health status.
|
|
95
|
+
*/
|
|
96
|
+
get(): Promise<HealthzResult>;
|
|
97
|
+
/**
|
|
98
|
+
* Get a Response object with JSON health data.
|
|
99
|
+
*/
|
|
100
|
+
response(): Promise<Response>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Manages the HTTP server for health endpoints.
|
|
105
|
+
*/
|
|
106
|
+
declare class HealthServer {
|
|
107
|
+
private instance;
|
|
108
|
+
/**
|
|
109
|
+
* Start the health check HTTP server.
|
|
110
|
+
*/
|
|
111
|
+
start(options?: HealthServerOptions): void;
|
|
112
|
+
/**
|
|
113
|
+
* Stop the health check HTTP server.
|
|
114
|
+
*/
|
|
115
|
+
stop(): Promise<void>;
|
|
116
|
+
private handleRequest;
|
|
117
|
+
}
|
|
118
|
+
declare const server: HealthServer;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Manages probe state and provides probe endpoints.
|
|
122
|
+
*/
|
|
123
|
+
declare class HealthProbes {
|
|
124
|
+
private state;
|
|
125
|
+
private createProbeResponse;
|
|
126
|
+
readonly livez: HealthProbe;
|
|
127
|
+
readonly startupz: HealthProbe;
|
|
128
|
+
readonly readyz: HealthProbe;
|
|
129
|
+
readonly healthz: HealthzProbe;
|
|
130
|
+
}
|
|
131
|
+
declare const probes: HealthProbes;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Manages health check registration and execution.
|
|
135
|
+
*/
|
|
136
|
+
declare class HealthChecks {
|
|
137
|
+
private readonly registered;
|
|
138
|
+
/**
|
|
139
|
+
* Register a health check.
|
|
140
|
+
* Returns an unregister function.
|
|
141
|
+
*/
|
|
142
|
+
register(name: string, check: () => Promise<HealthCheckResult | void> | HealthCheckResult | void): () => void;
|
|
143
|
+
register(check: HealthCheck): () => void;
|
|
144
|
+
/**
|
|
145
|
+
* Run a single check with timeout.
|
|
146
|
+
*/
|
|
147
|
+
private run;
|
|
148
|
+
/**
|
|
149
|
+
* Run all registered health checks.
|
|
150
|
+
*/
|
|
151
|
+
runAll(): Promise<Record<string, HealthCheckResult>>;
|
|
152
|
+
/**
|
|
153
|
+
* Run only required checks and return whether they all pass.
|
|
154
|
+
*/
|
|
155
|
+
runRequired(): Promise<boolean>;
|
|
156
|
+
/**
|
|
157
|
+
* Get all registered checks.
|
|
158
|
+
*/
|
|
159
|
+
getChecks(): HealthCheck[];
|
|
160
|
+
}
|
|
161
|
+
declare const checks: HealthChecks;
|
|
162
|
+
|
|
163
|
+
export { type HealthCheck, type HealthCheckResult, HealthChecks, type HealthProbe, type HealthProbeResult, HealthProbes, HealthServer, type HealthServerOptions, type HealthzProbe, type HealthzResult, checks, probes, server };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import { createServer } from "http";
|
|
3
|
+
|
|
4
|
+
// src/checks.ts
|
|
5
|
+
var DEFAULT_TIMEOUT = 5e3;
|
|
6
|
+
var HealthChecks = class {
|
|
7
|
+
registered = /* @__PURE__ */ new Map();
|
|
8
|
+
register(nameOrCheck, checkFn) {
|
|
9
|
+
const check = typeof nameOrCheck === "string" ? { name: nameOrCheck, check: checkFn } : nameOrCheck;
|
|
10
|
+
if (this.registered.has(check.name)) {
|
|
11
|
+
console.warn(`[health] overwriting existing check "${check.name}"`);
|
|
12
|
+
}
|
|
13
|
+
this.registered.set(check.name, check);
|
|
14
|
+
return () => {
|
|
15
|
+
this.registered.delete(check.name);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Run a single check with timeout.
|
|
20
|
+
*/
|
|
21
|
+
async run(check) {
|
|
22
|
+
const timeout = check.timeout ?? DEFAULT_TIMEOUT;
|
|
23
|
+
const start = performance.now();
|
|
24
|
+
try {
|
|
25
|
+
const result = await Promise.race([
|
|
26
|
+
Promise.resolve(check.check()),
|
|
27
|
+
new Promise((_, reject) => {
|
|
28
|
+
setTimeout(() => reject(new Error(`check "${check.name}" timed out after ${timeout}ms`)), timeout);
|
|
29
|
+
})
|
|
30
|
+
]);
|
|
31
|
+
const latency = Math.round(performance.now() - start);
|
|
32
|
+
if (result) {
|
|
33
|
+
return { ...result, latency };
|
|
34
|
+
}
|
|
35
|
+
return { status: "healthy", latency };
|
|
36
|
+
} catch (error) {
|
|
37
|
+
const latency = Math.round(performance.now() - start);
|
|
38
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
39
|
+
return { status: "unhealthy", latency, error: errorMessage };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Run all registered health checks.
|
|
44
|
+
*/
|
|
45
|
+
async runAll() {
|
|
46
|
+
const results = {};
|
|
47
|
+
const checks2 = [...this.registered.values()];
|
|
48
|
+
const checkPromises = checks2.map(async (check) => {
|
|
49
|
+
const result = await this.run(check);
|
|
50
|
+
results[check.name] = result;
|
|
51
|
+
});
|
|
52
|
+
await Promise.all(checkPromises);
|
|
53
|
+
return results;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Run only required checks and return whether they all pass.
|
|
57
|
+
*/
|
|
58
|
+
async runRequired() {
|
|
59
|
+
const requiredChecks = [...this.registered.values()].filter((check) => !check.optional);
|
|
60
|
+
if (requiredChecks.length === 0) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
const checkPromises = requiredChecks.map(async (check) => {
|
|
64
|
+
const result = await this.run(check);
|
|
65
|
+
return result.status === "healthy";
|
|
66
|
+
});
|
|
67
|
+
const results = await Promise.all(checkPromises);
|
|
68
|
+
return results.every(Boolean);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get all registered checks.
|
|
72
|
+
*/
|
|
73
|
+
getChecks() {
|
|
74
|
+
return [...this.registered.values()];
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
var checks = new HealthChecks();
|
|
78
|
+
|
|
79
|
+
// src/probes.ts
|
|
80
|
+
var HealthProbes = class {
|
|
81
|
+
state = {
|
|
82
|
+
livez: false,
|
|
83
|
+
startupz: false,
|
|
84
|
+
readyz: false
|
|
85
|
+
};
|
|
86
|
+
createProbeResponse(result) {
|
|
87
|
+
return new Response(result.passing ? "OK" : "Service Unavailable", {
|
|
88
|
+
status: result.passing ? 200 : 503,
|
|
89
|
+
headers: { "Content-Type": "text/plain" }
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
livez = {
|
|
93
|
+
enable: () => {
|
|
94
|
+
this.state.livez = true;
|
|
95
|
+
},
|
|
96
|
+
disable: () => {
|
|
97
|
+
this.state.livez = false;
|
|
98
|
+
},
|
|
99
|
+
get: async () => {
|
|
100
|
+
return { passing: this.state.livez };
|
|
101
|
+
},
|
|
102
|
+
response: async () => {
|
|
103
|
+
return this.createProbeResponse(await this.livez.get());
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
startupz = {
|
|
107
|
+
enable: () => {
|
|
108
|
+
this.state.startupz = true;
|
|
109
|
+
},
|
|
110
|
+
disable: () => {
|
|
111
|
+
this.state.startupz = false;
|
|
112
|
+
},
|
|
113
|
+
get: async () => {
|
|
114
|
+
return { passing: this.state.startupz };
|
|
115
|
+
},
|
|
116
|
+
response: async () => {
|
|
117
|
+
return this.createProbeResponse(await this.startupz.get());
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
readyz = {
|
|
121
|
+
enable: () => {
|
|
122
|
+
this.state.readyz = true;
|
|
123
|
+
},
|
|
124
|
+
disable: () => {
|
|
125
|
+
this.state.readyz = false;
|
|
126
|
+
},
|
|
127
|
+
get: async () => {
|
|
128
|
+
return { passing: this.state.readyz ? await checks.runRequired() : false };
|
|
129
|
+
},
|
|
130
|
+
response: async () => {
|
|
131
|
+
return this.createProbeResponse(await this.readyz.get());
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
healthz = {
|
|
135
|
+
get: async () => {
|
|
136
|
+
const checkResults = await checks.runAll();
|
|
137
|
+
const registered = checks.getChecks();
|
|
138
|
+
const hasUnhealthy = Object.values(checkResults).some((c) => c.status === "unhealthy");
|
|
139
|
+
const hasRequiredUnhealthy = registered.filter((check) => !check.optional).some((check) => checkResults[check.name]?.status !== "healthy");
|
|
140
|
+
let status = "healthy";
|
|
141
|
+
if (hasRequiredUnhealthy) {
|
|
142
|
+
status = "unhealthy";
|
|
143
|
+
} else if (hasUnhealthy) {
|
|
144
|
+
status = "degraded";
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
status,
|
|
148
|
+
probes: { livez: this.state.livez, startupz: this.state.startupz, readyz: this.state.readyz },
|
|
149
|
+
checks: checkResults
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
response: async () => {
|
|
153
|
+
const result = await this.healthz.get();
|
|
154
|
+
return new Response(JSON.stringify(result, null, 2), {
|
|
155
|
+
status: 200,
|
|
156
|
+
headers: { "Content-Type": "application/json" }
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
var probes = new HealthProbes();
|
|
162
|
+
|
|
163
|
+
// src/server.ts
|
|
164
|
+
var HealthServer = class {
|
|
165
|
+
instance = null;
|
|
166
|
+
/**
|
|
167
|
+
* Start the health check HTTP server.
|
|
168
|
+
*/
|
|
169
|
+
start(options = {}) {
|
|
170
|
+
if (this.instance) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const host = options.host ?? "localhost";
|
|
174
|
+
const port = options.port ?? 9090;
|
|
175
|
+
const instance = createServer(async (req, res) => {
|
|
176
|
+
try {
|
|
177
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
178
|
+
const response = await this.handleRequest(url.pathname);
|
|
179
|
+
res.statusCode = response.status;
|
|
180
|
+
for (const [key, value] of response.headers.entries()) {
|
|
181
|
+
res.setHeader(key, value);
|
|
182
|
+
}
|
|
183
|
+
const body = await response.text();
|
|
184
|
+
res.end(body);
|
|
185
|
+
} catch {
|
|
186
|
+
res.statusCode = 500;
|
|
187
|
+
res.setHeader("Content-Type", "text/plain");
|
|
188
|
+
res.end("Internal Server Error");
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
instance.listen(port, host);
|
|
192
|
+
this.instance = instance;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Stop the health check HTTP server.
|
|
196
|
+
*/
|
|
197
|
+
stop() {
|
|
198
|
+
return new Promise((resolve, reject) => {
|
|
199
|
+
if (!this.instance) {
|
|
200
|
+
resolve();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
this.instance.close((err) => {
|
|
204
|
+
if (err) {
|
|
205
|
+
reject(err);
|
|
206
|
+
} else {
|
|
207
|
+
this.instance = null;
|
|
208
|
+
resolve();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
async handleRequest(pathname) {
|
|
214
|
+
switch (pathname) {
|
|
215
|
+
case "/livez":
|
|
216
|
+
return probes.livez.response();
|
|
217
|
+
case "/startupz":
|
|
218
|
+
return probes.startupz.response();
|
|
219
|
+
case "/readyz":
|
|
220
|
+
return probes.readyz.response();
|
|
221
|
+
case "/healthz":
|
|
222
|
+
return probes.healthz.response();
|
|
223
|
+
default:
|
|
224
|
+
return new Response("Not Found", { status: 404 });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
var server = new HealthServer();
|
|
229
|
+
export {
|
|
230
|
+
HealthChecks,
|
|
231
|
+
HealthProbes,
|
|
232
|
+
HealthServer,
|
|
233
|
+
checks,
|
|
234
|
+
probes,
|
|
235
|
+
server
|
|
236
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@astroscope/health",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Health check endpoints for Astro — livez, readyz, startupz probes",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/smnbbrv/astroscope.git",
|
|
24
|
+
"directory": "packages/health"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"astro",
|
|
28
|
+
"astro-integration",
|
|
29
|
+
"health",
|
|
30
|
+
"healthcheck",
|
|
31
|
+
"probes",
|
|
32
|
+
"readiness",
|
|
33
|
+
"liveness",
|
|
34
|
+
"kubernetes",
|
|
35
|
+
"k8s"
|
|
36
|
+
],
|
|
37
|
+
"author": "smnbbrv",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/smnbbrv/astroscope/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/smnbbrv/astroscope/tree/main/packages/health#readme",
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"lint": "eslint 'src/**/*.ts'",
|
|
47
|
+
"lint:fix": "eslint 'src/**/*.ts' --fix"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"tsup": "^8.5.1",
|
|
51
|
+
"typescript": "^5.9.3"
|
|
52
|
+
}
|
|
53
|
+
}
|