@astroscope/health 0.1.0 → 0.2.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 +104 -135
- package/dist/chunk-SBLQMZG6.js +23 -0
- package/dist/index.d.ts +20 -145
- package/dist/index.js +68 -228
- package/dist/setup.d.ts +2 -0
- package/dist/setup.js +7 -0
- package/package.json +17 -3
package/README.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
> **Note:** This package is in active development. APIs may change between versions.
|
|
4
4
|
|
|
5
|
-
Kubernetes-style health check endpoints for Astro SSR. Provides `/livez`, `/readyz`, `/startupz`, and `/healthz` probes on a separate HTTP server.
|
|
5
|
+
Kubernetes-style health check endpoints for Astro SSR. Provides `/livez`, `/readyz`, `/startupz`, and `/healthz` probes on a separate HTTP server. Automatically manages probe lifecycle via [@astroscope/boot](../boot).
|
|
6
|
+
|
|
7
|
+
[health-probes](https://github.com/smnbbrv/health-probes) is used under the hood. If you need more control, you can use it directly in your boot file instead of this integration.
|
|
6
8
|
|
|
7
9
|
## Examples
|
|
8
10
|
|
|
@@ -16,78 +18,132 @@ npm install @astroscope/health
|
|
|
16
18
|
|
|
17
19
|
## Usage
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
### 1. Add both integrations to your Astro config
|
|
22
|
+
|
|
23
|
+
**Important:** `health()` must come **after** `boot()` in the integrations array.
|
|
20
24
|
|
|
21
25
|
```ts
|
|
22
|
-
//
|
|
23
|
-
import
|
|
24
|
-
import
|
|
26
|
+
// astro.config.ts
|
|
27
|
+
import { defineConfig } from 'astro/config';
|
|
28
|
+
import node from '@astrojs/node';
|
|
29
|
+
import boot from '@astroscope/boot';
|
|
30
|
+
import health from '@astroscope/health';
|
|
31
|
+
|
|
32
|
+
export default defineConfig({
|
|
33
|
+
adapter: node({ mode: 'standalone' }),
|
|
34
|
+
integrations: [
|
|
35
|
+
boot(),
|
|
36
|
+
// in k8s use '0.0.0.0' to allow kubelet to access probes
|
|
37
|
+
// do not expose the health server publicly unless necessary
|
|
38
|
+
// by default is 127.0.0.1 for security reasons
|
|
39
|
+
health({ host: '0.0.0.0' }),
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
```
|
|
25
43
|
|
|
26
|
-
|
|
27
|
-
// start health server on a separate port
|
|
28
|
-
server.start({ port: 9090 });
|
|
44
|
+
This would set up the health server in production mode, disabled in dev mode by default.
|
|
29
45
|
|
|
30
|
-
|
|
31
|
-
probes.livez.enable();
|
|
46
|
+
### 2. Register health checks in your boot file
|
|
32
47
|
|
|
33
|
-
|
|
48
|
+
The health server and probe lifecycle are managed automatically — you only need to register your health checks:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// src/boot.ts
|
|
52
|
+
import type { BootContext } from '@astroscope/boot';
|
|
53
|
+
import { checks } from '@astroscope/health';
|
|
54
|
+
|
|
55
|
+
export async function onStartup({ dev, host, port }: BootContext) {
|
|
34
56
|
await connectToDatabase();
|
|
35
57
|
|
|
36
58
|
// register health checks
|
|
37
|
-
checks.register(
|
|
59
|
+
checks.register('database', () => ({
|
|
60
|
+
status: db.isConnected() ? 'healthy' : 'unhealthy',
|
|
61
|
+
error: db.isConnected() ? undefined : 'connection lost',
|
|
62
|
+
}));
|
|
38
63
|
|
|
39
|
-
|
|
40
|
-
probes.startupz.enable();
|
|
41
|
-
|
|
42
|
-
// enable readiness probe (ready for traffic)
|
|
43
|
-
probes.readyz.enable();
|
|
64
|
+
console.log(`Server ready at ${host}:${port}`);
|
|
44
65
|
}
|
|
45
66
|
|
|
46
67
|
export async function onShutdown() {
|
|
47
|
-
// disable readiness first (stop receiving traffic)
|
|
48
|
-
probes.readyz.disable();
|
|
49
|
-
|
|
50
68
|
await disconnectFromDatabase();
|
|
51
|
-
|
|
52
|
-
// stop health server
|
|
53
|
-
await server.stop();
|
|
54
69
|
}
|
|
55
70
|
```
|
|
56
71
|
|
|
57
|
-
|
|
72
|
+
### What happens automatically
|
|
58
73
|
|
|
59
|
-
|
|
74
|
+
The integration hooks into boot lifecycle events to manage probes:
|
|
75
|
+
|
|
76
|
+
1. **Before `onStartup`**: health server starts, liveness probe enabled
|
|
77
|
+
2. **After `onStartup`**: startup and readiness probes enabled
|
|
78
|
+
3. **Before `onShutdown`**: readiness probe disabled (stops receiving traffic)
|
|
79
|
+
4. **After `onShutdown`**: health server stopped
|
|
60
80
|
|
|
61
|
-
|
|
81
|
+
This ensures Kubernetes sees the correct state at each phase — liveness is available immediately, readiness only after your app has fully initialized, and traffic stops before shutdown begins.
|
|
82
|
+
|
|
83
|
+
## Options
|
|
62
84
|
|
|
63
85
|
```ts
|
|
64
|
-
|
|
65
|
-
|
|
86
|
+
health({
|
|
87
|
+
host: '0.0.0.0', // default: "127.0.0.1"
|
|
88
|
+
port: 9090, // default: 9090
|
|
89
|
+
paths: SimplePaths, // default: K8sPaths
|
|
90
|
+
dev: true, // default: false
|
|
91
|
+
});
|
|
66
92
|
```
|
|
67
93
|
|
|
68
|
-
###
|
|
94
|
+
### `host`
|
|
69
95
|
|
|
70
|
-
|
|
96
|
+
Host to bind the health server to. Defaults to `127.0.0.1` (localhost only). Set to `0.0.0.0` when probes need to be reachable from outside the host, e.g. in Kubernetes where the kubelet sends requests to the pod IP.
|
|
97
|
+
|
|
98
|
+
- **Type**: `string`
|
|
99
|
+
- **Default**: `"127.0.0.1"`
|
|
100
|
+
|
|
101
|
+
### `port`
|
|
102
|
+
|
|
103
|
+
Port for the health server.
|
|
104
|
+
|
|
105
|
+
- **Type**: `number`
|
|
106
|
+
- **Default**: `9090`
|
|
107
|
+
|
|
108
|
+
### `paths`
|
|
109
|
+
|
|
110
|
+
Probe endpoint paths. Two presets are available:
|
|
111
|
+
|
|
112
|
+
- `K8sPaths` (default): `/livez`, `/readyz`, `/startupz`, `/healthz`
|
|
113
|
+
- `SimplePaths`: `/live`, `/ready`, `/startup`, `/health`
|
|
71
114
|
|
|
72
115
|
```ts
|
|
73
|
-
|
|
74
|
-
|
|
116
|
+
import health, { SimplePaths } from '@astroscope/health';
|
|
117
|
+
|
|
118
|
+
health({ paths: SimplePaths });
|
|
75
119
|
```
|
|
76
120
|
|
|
121
|
+
### `dev`
|
|
122
|
+
|
|
123
|
+
Enable the health server in development mode. By default, health probes only run in production builds.
|
|
124
|
+
|
|
125
|
+
- **Type**: `boolean`
|
|
126
|
+
- **Default**: `false`
|
|
127
|
+
|
|
128
|
+
## Probes
|
|
129
|
+
|
|
130
|
+
### `/livez` — Liveness Probe
|
|
131
|
+
|
|
132
|
+
Indicates if the process is running. If this fails, Kubernetes restarts the container.
|
|
133
|
+
|
|
134
|
+
### `/startupz` — Startup Probe
|
|
135
|
+
|
|
136
|
+
Indicates if the application has finished initializing. Kubernetes waits for this before sending traffic or checking liveness.
|
|
137
|
+
|
|
77
138
|
### `/readyz` — Readiness Probe
|
|
78
139
|
|
|
79
140
|
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
141
|
|
|
81
|
-
```ts
|
|
82
|
-
probes.readyz.enable();
|
|
83
|
-
probes.readyz.disable();
|
|
84
|
-
```
|
|
85
|
-
|
|
86
142
|
The readiness probe automatically runs all non-optional health checks and returns 503 if any fail.
|
|
87
143
|
|
|
88
144
|
### `/healthz` — Health Status
|
|
89
145
|
|
|
90
|
-
Returns detailed JSON status of all probes and health checks
|
|
146
|
+
Returns detailed JSON status of all probes and health checks:
|
|
91
147
|
|
|
92
148
|
```json
|
|
93
149
|
{
|
|
@@ -112,6 +168,7 @@ Returns detailed JSON status of all probes and health checks. Useful for debuggi
|
|
|
112
168
|
```
|
|
113
169
|
|
|
114
170
|
Status values:
|
|
171
|
+
|
|
115
172
|
- `healthy` — all checks pass
|
|
116
173
|
- `degraded` — optional checks failing, required checks pass
|
|
117
174
|
- `unhealthy` — required checks failing
|
|
@@ -121,29 +178,30 @@ Status values:
|
|
|
121
178
|
Register health checks to verify dependencies are working:
|
|
122
179
|
|
|
123
180
|
```ts
|
|
124
|
-
import { checks } from
|
|
181
|
+
import { checks } from '@astroscope/health';
|
|
125
182
|
|
|
126
183
|
// return result (recommended for boolean checks)
|
|
127
|
-
checks.register(
|
|
128
|
-
status: db.isConnected() ?
|
|
129
|
-
error: db.isConnected() ? undefined :
|
|
184
|
+
checks.register('database', () => ({
|
|
185
|
+
status: db.isConnected() ? 'healthy' : 'unhealthy',
|
|
186
|
+
error: db.isConnected() ? undefined : 'connection lost',
|
|
130
187
|
}));
|
|
131
188
|
|
|
132
189
|
// throw-based (classic pattern)
|
|
133
|
-
checks.register(
|
|
190
|
+
checks.register('cache', async () => {
|
|
134
191
|
await redis.ping(); // throws if fails
|
|
135
192
|
});
|
|
136
193
|
|
|
137
194
|
// with options
|
|
138
195
|
checks.register({
|
|
139
|
-
name:
|
|
140
|
-
check: () => fetch(
|
|
196
|
+
name: 'external-api',
|
|
197
|
+
check: () => fetch('https://api.example.com/health').then(() => {}),
|
|
141
198
|
optional: true, // doesn't affect /readyz, only /healthz status
|
|
142
199
|
timeout: 10000, // custom timeout (default: 5000ms)
|
|
143
200
|
});
|
|
144
201
|
```
|
|
145
202
|
|
|
146
203
|
The check function can either:
|
|
204
|
+
|
|
147
205
|
- Return `HealthCheckResult` with status and optional error
|
|
148
206
|
- Return `void` (completing without error = healthy)
|
|
149
207
|
- Throw an error (= unhealthy with error message)
|
|
@@ -153,24 +211,12 @@ The check function can either:
|
|
|
153
211
|
`register()` returns an unregister function:
|
|
154
212
|
|
|
155
213
|
```ts
|
|
156
|
-
const unregister = checks.register(
|
|
157
|
-
name: "database",
|
|
158
|
-
check: () => db.ping(),
|
|
159
|
-
});
|
|
214
|
+
const unregister = checks.register('database', () => db.ping());
|
|
160
215
|
|
|
161
216
|
// later...
|
|
162
217
|
unregister();
|
|
163
218
|
```
|
|
164
219
|
|
|
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
220
|
## Kubernetes Configuration
|
|
175
221
|
|
|
176
222
|
```yaml
|
|
@@ -201,83 +247,6 @@ spec:
|
|
|
201
247
|
periodSeconds: 5
|
|
202
248
|
```
|
|
203
249
|
|
|
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
250
|
## License
|
|
282
251
|
|
|
283
252
|
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/register.ts
|
|
2
|
+
import { on } from "@astroscope/boot/events";
|
|
3
|
+
import { probes, server } from "health-probes";
|
|
4
|
+
function registerHealth(config) {
|
|
5
|
+
on("beforeOnStartup", () => {
|
|
6
|
+
server.start(config);
|
|
7
|
+
probes.live.enable();
|
|
8
|
+
});
|
|
9
|
+
on("afterOnStartup", () => {
|
|
10
|
+
probes.startup.enable();
|
|
11
|
+
probes.ready.enable();
|
|
12
|
+
});
|
|
13
|
+
on("beforeOnShutdown", () => {
|
|
14
|
+
probes.ready.disable();
|
|
15
|
+
});
|
|
16
|
+
on("afterOnShutdown", async () => {
|
|
17
|
+
await server.stop();
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
registerHealth
|
|
23
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { AstroIntegration } from 'astro';
|
|
2
|
+
import { ProbePaths } from 'health-probes';
|
|
3
|
+
export { HealthCheck, HealthCheckResult, K8sPaths, ProbePaths, SimplePaths, checks } from 'health-probes';
|
|
4
|
+
|
|
5
|
+
interface HealthOptions {
|
|
5
6
|
/**
|
|
6
7
|
* Host to bind the health server to.
|
|
7
|
-
* @default '
|
|
8
|
+
* @default '127.0.0.1'
|
|
8
9
|
*/
|
|
9
10
|
host?: string | undefined;
|
|
10
11
|
/**
|
|
@@ -12,152 +13,26 @@ interface HealthServerOptions {
|
|
|
12
13
|
* @default 9090
|
|
13
14
|
*/
|
|
14
15
|
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
16
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* Returning void or completing without error means healthy.
|
|
17
|
+
* Custom paths for probe endpoints.
|
|
18
|
+
* @default K8sPaths (from health-probes)
|
|
28
19
|
*/
|
|
29
|
-
|
|
20
|
+
paths?: ProbePaths | undefined;
|
|
30
21
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
22
|
+
* Enable health probes in dev mode.
|
|
23
|
+
* By default, health probes only run in production.
|
|
33
24
|
* @default false
|
|
34
25
|
*/
|
|
35
|
-
|
|
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;
|
|
26
|
+
dev?: boolean | undefined;
|
|
55
27
|
}
|
|
56
28
|
/**
|
|
57
|
-
*
|
|
29
|
+
* Astro integration for Kubernetes-style health probes.
|
|
30
|
+
*
|
|
31
|
+
* Automatically starts a health probe server after `onStartup` and stops it before `onShutdown`.
|
|
32
|
+
* Uses the `health-probes` package under the hood.
|
|
33
|
+
*
|
|
34
|
+
* Requires `@astroscope/boot` to be configured.
|
|
58
35
|
*/
|
|
59
|
-
|
|
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;
|
|
36
|
+
declare function health(options?: HealthOptions): AstroIntegration;
|
|
162
37
|
|
|
163
|
-
export { type
|
|
38
|
+
export { type HealthOptions, health as default };
|
package/dist/index.js
CHANGED
|
@@ -1,236 +1,76 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
registerHealth
|
|
3
|
+
} from "./chunk-SBLQMZG6.js";
|
|
3
4
|
|
|
4
|
-
// src/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
}
|
|
5
|
+
// src/index.ts
|
|
6
|
+
import MagicString from "magic-string";
|
|
7
|
+
import { checks, K8sPaths, SimplePaths } from "health-probes";
|
|
8
|
+
var VIRTUAL_MODULE_ID = "virtual:@astroscope/health/config";
|
|
9
|
+
var RESOLVED_VIRTUAL_MODULE_ID = `\0${VIRTUAL_MODULE_ID}`;
|
|
10
|
+
function health(options = {}) {
|
|
11
|
+
const enableDev = options.dev ?? false;
|
|
12
|
+
const serverOptions = {
|
|
13
|
+
host: options.host ?? "127.0.0.1",
|
|
14
|
+
port: options.port ?? 9090,
|
|
15
|
+
...options.paths && { paths: options.paths }
|
|
133
16
|
};
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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);
|
|
17
|
+
return {
|
|
18
|
+
name: "@astroscope/health",
|
|
19
|
+
hooks: {
|
|
20
|
+
"astro:config:setup": ({ config, command, updateConfig }) => {
|
|
21
|
+
const bootIndex = config.integrations.findIndex((i) => i.name === "@astroscope/boot");
|
|
22
|
+
const healthIndex = config.integrations.findIndex((i) => i.name === "@astroscope/health");
|
|
23
|
+
if (bootIndex === -1) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
"@astroscope/health requires @astroscope/boot. Add boot() before health() in your integrations array."
|
|
26
|
+
);
|
|
182
27
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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();
|
|
28
|
+
if (healthIndex !== -1 && bootIndex > healthIndex) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
"@astroscope/health must come after @astroscope/boot. Swap the order in your integrations array."
|
|
31
|
+
);
|
|
209
32
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
33
|
+
const isBuild = command === "build";
|
|
34
|
+
updateConfig({
|
|
35
|
+
vite: {
|
|
36
|
+
plugins: [
|
|
37
|
+
{
|
|
38
|
+
name: "@astroscope/health",
|
|
39
|
+
resolveId(id) {
|
|
40
|
+
if (id === VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_MODULE_ID;
|
|
41
|
+
},
|
|
42
|
+
load(id) {
|
|
43
|
+
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
|
|
44
|
+
return `export const config = ${JSON.stringify(serverOptions)};`;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
configureServer() {
|
|
48
|
+
if (!enableDev) return;
|
|
49
|
+
registerHealth(serverOptions);
|
|
50
|
+
},
|
|
51
|
+
generateBundle(_, bundle) {
|
|
52
|
+
if (!isBuild) return;
|
|
53
|
+
const entryChunk = bundle["entry.mjs"];
|
|
54
|
+
if (!entryChunk || entryChunk.type !== "chunk") return;
|
|
55
|
+
const s = new MagicString(entryChunk.code);
|
|
56
|
+
s.prepend(`import '@astroscope/health/setup';
|
|
57
|
+
`);
|
|
58
|
+
entryChunk.code = s.toString();
|
|
59
|
+
if (entryChunk.map) {
|
|
60
|
+
entryChunk.map = s.generateMap({ hires: true });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
225
68
|
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
var server = new HealthServer();
|
|
69
|
+
};
|
|
70
|
+
}
|
|
229
71
|
export {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
HealthServer,
|
|
72
|
+
K8sPaths,
|
|
73
|
+
SimplePaths,
|
|
233
74
|
checks,
|
|
234
|
-
|
|
235
|
-
server
|
|
75
|
+
health as default
|
|
236
76
|
};
|
package/dist/setup.d.ts
ADDED
package/dist/setup.js
ADDED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astroscope/health",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Kubernetes-style health probes integration for Astro",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./setup": {
|
|
14
|
+
"types": "./dist/setup.d.ts",
|
|
15
|
+
"import": "./dist/setup.js"
|
|
12
16
|
}
|
|
13
17
|
},
|
|
14
18
|
"files": [
|
|
@@ -41,13 +45,23 @@
|
|
|
41
45
|
},
|
|
42
46
|
"homepage": "https://github.com/smnbbrv/astroscope/tree/main/packages/health#readme",
|
|
43
47
|
"scripts": {
|
|
44
|
-
"build": "tsup src/index.ts --format esm --dts",
|
|
48
|
+
"build": "tsup src/index.ts src/setup.ts --format esm --dts --external virtual:@astroscope/health/config",
|
|
45
49
|
"typecheck": "tsc --noEmit",
|
|
46
50
|
"lint": "eslint 'src/**/*.ts'",
|
|
47
51
|
"lint:fix": "eslint 'src/**/*.ts' --fix"
|
|
48
52
|
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"health-probes": "^1.0.0",
|
|
55
|
+
"magic-string": "^0.30.21"
|
|
56
|
+
},
|
|
49
57
|
"devDependencies": {
|
|
58
|
+
"@astroscope/boot": "workspace:*",
|
|
59
|
+
"astro": "^5.17.1",
|
|
50
60
|
"tsup": "^8.5.1",
|
|
51
61
|
"typescript": "^5.9.3"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"@astroscope/boot": ">=0.3.0",
|
|
65
|
+
"astro": "^5.0.0"
|
|
52
66
|
}
|
|
53
67
|
}
|