@astroscope/health 0.1.0 → 0.2.1

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 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
- This package is designed to work with [@astroscope/boot](../boot) for lifecycle management.
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
- // src/boot.ts
23
- import type { BootContext } from "@astroscope/boot";
24
- import { checks, probes, server } from "@astroscope/health";
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
- export async function onStartup({ dev, host, port }: BootContext) {
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
- // enable liveness immediately
31
- probes.livez.enable();
46
+ ### 2. Register health checks in your boot file
32
47
 
33
- // initialize your app...
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("database", () => db.ping());
59
+ checks.register('database', () => ({
60
+ status: db.isConnected() ? 'healthy' : 'unhealthy',
61
+ error: db.isConnected() ? undefined : 'connection lost',
62
+ }));
38
63
 
39
- // enable startup probe (initialization complete)
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
- ## Probes
72
+ ### What happens automatically
58
73
 
59
- ### `/livez` Liveness Probe
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
- Indicates if the process is running. If this fails, Kubernetes will restart the container.
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
- probes.livez.enable(); // returns 200 OK
65
- probes.livez.disable(); // returns 503 Service Unavailable
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
- ### `/startupz` — Startup Probe
94
+ ### `host`
69
95
 
70
- Indicates if the application has finished initializing. Kubernetes waits for this before sending traffic or checking liveness.
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
- probes.startupz.enable();
74
- probes.startupz.disable();
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. Useful for debugging and dashboards.
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 "@astroscope/health";
181
+ import { checks } from '@astroscope/health';
125
182
 
126
183
  // return result (recommended for boolean checks)
127
- checks.register("database", () => ({
128
- status: db.isConnected() ? "healthy" : "unhealthy",
129
- error: db.isConnected() ? undefined : "connection lost",
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("cache", async () => {
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: "external-api",
140
- check: () => fetch("https://api.example.com/health").then(() => {}),
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
- * Server configuration options.
3
- */
4
- interface HealthServerOptions {
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 'localhost'
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
- * 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.
17
+ * Custom paths for probe endpoints.
18
+ * @default K8sPaths (from health-probes)
28
19
  */
29
- check: () => Promise<HealthCheckResult | void> | HealthCheckResult | void;
20
+ paths?: ProbePaths | undefined;
30
21
  /**
31
- * Whether this check is optional.
32
- * Optional checks don't affect the /readyz endpoint.
22
+ * Enable health probes in dev mode.
23
+ * By default, health probes only run in production.
33
24
  * @default false
34
25
  */
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;
26
+ dev?: boolean | undefined;
55
27
  }
56
28
  /**
57
- * Full health status including all checks.
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
- 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;
36
+ declare function health(options?: HealthOptions): AstroIntegration;
162
37
 
163
- export { type HealthCheck, type HealthCheckResult, HealthChecks, type HealthProbe, type HealthProbeResult, HealthProbes, HealthServer, type HealthServerOptions, type HealthzProbe, type HealthzResult, checks, probes, server };
38
+ export { type HealthOptions, health as default };
package/dist/index.js CHANGED
@@ -1,236 +1,69 @@
1
- // src/server.ts
2
- import { createServer } from "http";
1
+ import {
2
+ registerHealth
3
+ } from "./chunk-VKEMDBBI.js";
3
4
 
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
- }
5
+ // src/index.ts
6
+ import MagicString from "magic-string";
7
+ import { checks, K8sPaths, SimplePaths } from "health-probes";
8
+ function health(options = {}) {
9
+ const enableDev = options.dev ?? false;
10
+ const serverOptions = {
11
+ host: options.host ?? "127.0.0.1",
12
+ port: options.port ?? 9090,
13
+ ...options.paths && { paths: options.paths }
133
14
  };
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);
15
+ return {
16
+ name: "@astroscope/health",
17
+ hooks: {
18
+ "astro:config:setup": ({ config, command, updateConfig }) => {
19
+ const bootIndex = config.integrations.findIndex((i) => i.name === "@astroscope/boot");
20
+ const healthIndex = config.integrations.findIndex((i) => i.name === "@astroscope/health");
21
+ if (bootIndex === -1) {
22
+ throw new Error(
23
+ "@astroscope/health requires @astroscope/boot. Add boot() before health() in your integrations array."
24
+ );
182
25
  }
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();
26
+ if (healthIndex !== -1 && bootIndex > healthIndex) {
27
+ throw new Error(
28
+ "@astroscope/health must come after @astroscope/boot. Swap the order in your integrations array."
29
+ );
209
30
  }
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 });
31
+ const isBuild = command === "build";
32
+ updateConfig({
33
+ vite: {
34
+ plugins: [
35
+ {
36
+ name: "@astroscope/health",
37
+ configureServer() {
38
+ if (!enableDev) return;
39
+ registerHealth(serverOptions);
40
+ },
41
+ generateBundle(_, bundle) {
42
+ if (!isBuild) return;
43
+ const entryChunk = bundle["entry.mjs"];
44
+ if (!entryChunk || entryChunk.type !== "chunk") return;
45
+ const s = new MagicString(entryChunk.code);
46
+ s.prepend(
47
+ `import { registerHealth as __astroscope_registerHealth } from '@astroscope/health/setup';
48
+ __astroscope_registerHealth(${JSON.stringify(serverOptions)});
49
+ `
50
+ );
51
+ entryChunk.code = s.toString();
52
+ if (entryChunk.map) {
53
+ entryChunk.map = s.generateMap({ hires: true });
54
+ }
55
+ }
56
+ }
57
+ ]
58
+ }
59
+ });
60
+ }
225
61
  }
226
- }
227
- };
228
- var server = new HealthServer();
62
+ };
63
+ }
229
64
  export {
230
- HealthChecks,
231
- HealthProbes,
232
- HealthServer,
65
+ K8sPaths,
66
+ SimplePaths,
233
67
  checks,
234
- probes,
235
- server
68
+ health as default
236
69
  };
@@ -0,0 +1,5 @@
1
+ import { HealthServerOptions } from 'health-probes';
2
+
3
+ declare function registerHealth(config: HealthServerOptions): void;
4
+
5
+ export { registerHealth };
@@ -0,0 +1,6 @@
1
+ import {
2
+ registerHealth
3
+ } from "./chunk-VKEMDBBI.js";
4
+ export {
5
+ registerHealth
6
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@astroscope/health",
3
- "version": "0.1.0",
4
- "description": "Health check endpoints for Astro — livez, readyz, startupz probes",
3
+ "version": "0.2.1",
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/register.d.ts",
15
+ "import": "./dist/register.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/register.ts --format esm --dts",
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.2",
65
+ "astro": "^5.0.0"
52
66
  }
53
67
  }