@quanticjs/health 3.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 +389 -0
- package/dist/FileHealthWriter.d.ts +14 -0
- package/dist/FileHealthWriter.js +75 -0
- package/dist/HealthController.d.ts +9 -0
- package/dist/HealthController.js +63 -0
- package/dist/HealthRegistry.d.ts +26 -0
- package/dist/HealthRegistry.js +200 -0
- package/dist/HealthShutdownHook.d.ts +10 -0
- package/dist/HealthShutdownHook.js +46 -0
- package/dist/QuanticHealthModule.d.ts +5 -0
- package/dist/QuanticHealthModule.js +49 -0
- package/dist/StandaloneHealthServer.d.ts +13 -0
- package/dist/StandaloneHealthServer.js +78 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +17 -0
- package/dist/interfaces.d.ts +51 -0
- package/dist/interfaces.js +8 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
# @quanticjs/health
|
|
2
|
+
|
|
3
|
+
Health check module for QuanticJS applications. Provides liveness, readiness, and startup probes with auto-detection, caching, and multiple transports for both HTTP and non-HTTP apps.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Already included in the `@quanticjs/quanticjs` umbrella. For standalone use:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @quanticjs/health
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### HTTP API (most common)
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { Module } from '@nestjs/common';
|
|
19
|
+
import { QuanticCoreModule } from '@quanticjs/core';
|
|
20
|
+
import { QuanticHealthModule } from '@quanticjs/health';
|
|
21
|
+
|
|
22
|
+
@Module({
|
|
23
|
+
imports: [
|
|
24
|
+
QuanticCoreModule.forRoot(),
|
|
25
|
+
QuanticHealthModule.forRoot({
|
|
26
|
+
transport: { type: 'controller' },
|
|
27
|
+
}),
|
|
28
|
+
],
|
|
29
|
+
})
|
|
30
|
+
export class AppModule {}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This registers three endpoints on your existing NestJS server:
|
|
34
|
+
|
|
35
|
+
| Endpoint | Probe | Purpose |
|
|
36
|
+
|----------|-------|---------|
|
|
37
|
+
| `GET /health/live` | Liveness | Is the process alive? Restart if not. |
|
|
38
|
+
| `GET /health/ready` | Readiness | Can this instance serve traffic? Remove from LB if not. |
|
|
39
|
+
| `GET /health/startup` | Startup | Has the app finished initializing? |
|
|
40
|
+
|
|
41
|
+
All endpoints are public (no auth required) and return:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"status": "ok",
|
|
46
|
+
"checks": {
|
|
47
|
+
"database": { "status": "ok", "latency_ms": 2 },
|
|
48
|
+
"redis": { "status": "ok", "latency_ms": 1 }
|
|
49
|
+
},
|
|
50
|
+
"timestamp": "2026-05-18T10:32:01.123Z"
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Status code is `200` when all checks pass, `503` when any check fails.
|
|
55
|
+
|
|
56
|
+
## Auto-Detection
|
|
57
|
+
|
|
58
|
+
When `autoDetect` is enabled (the default), the module scans the NestJS DI container on startup and registers checks automatically:
|
|
59
|
+
|
|
60
|
+
| Dependency | Detection | Probe | Check |
|
|
61
|
+
|------------|-----------|-------|-------|
|
|
62
|
+
| TypeORM `DataSource` | `typeorm` installed + DataSource in DI | readiness | `SELECT 1` |
|
|
63
|
+
| Redis (`REDIS_CLIENT`) | `QuanticRedisModule` imported | readiness | `redis.ping()` |
|
|
64
|
+
| Event loop | Always | liveness | `setImmediate` responsiveness |
|
|
65
|
+
|
|
66
|
+
No configuration needed for framework-managed dependencies. The module logs what it discovers:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
[HealthRegistry] Auto-detected DataSource → database readiness check
|
|
70
|
+
[HealthRegistry] Auto-detected Redis → redis readiness check
|
|
71
|
+
[HealthRegistry] readiness: [database, redis]
|
|
72
|
+
[HealthRegistry] liveness: [event_loop]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Disable with `autoDetect: false`.
|
|
76
|
+
|
|
77
|
+
## Custom Checks
|
|
78
|
+
|
|
79
|
+
Add product-specific checks for dependencies the framework doesn't know about.
|
|
80
|
+
|
|
81
|
+
### Function checks
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
QuanticHealthModule.forRoot({
|
|
85
|
+
transport: { type: 'controller' },
|
|
86
|
+
readiness: [
|
|
87
|
+
{
|
|
88
|
+
name: 'minio',
|
|
89
|
+
check: async () => {
|
|
90
|
+
const exists = await minioClient.bucketExists('my-bucket');
|
|
91
|
+
if (!exists) throw new Error('Bucket not found');
|
|
92
|
+
},
|
|
93
|
+
timeoutMs: 5000, // optional, default 3000
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
startup: [
|
|
97
|
+
{
|
|
98
|
+
name: 'migrations',
|
|
99
|
+
check: async () => {
|
|
100
|
+
if (!migrationService.isComplete()) throw new Error('Migrations pending');
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The check function should **resolve** when healthy and **throw** when unhealthy.
|
|
108
|
+
|
|
109
|
+
### HTTP checks
|
|
110
|
+
|
|
111
|
+
For external services, pass a `url` instead of a `check` function:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
QuanticHealthModule.forRoot({
|
|
115
|
+
transport: { type: 'controller' },
|
|
116
|
+
readiness: [
|
|
117
|
+
{ name: 'ai-gateway', url: 'http://ai-gateway:3005/health' },
|
|
118
|
+
{ name: 'kogito', url: 'http://kogito:8080/q/health/live', timeoutMs: 5000 },
|
|
119
|
+
],
|
|
120
|
+
})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
HTTP checks expect a 2xx response. Any other status or network error marks the check as unhealthy.
|
|
124
|
+
|
|
125
|
+
## Transports
|
|
126
|
+
|
|
127
|
+
The transport determines **how** probes reach the app.
|
|
128
|
+
|
|
129
|
+
### `controller` — HTTP API apps
|
|
130
|
+
|
|
131
|
+
Mounts on the existing NestJS HTTP server. Endpoints: `/health/live`, `/health/ready`, `/health/startup`.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
QuanticHealthModule.forRoot({
|
|
135
|
+
transport: { type: 'controller' },
|
|
136
|
+
})
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### `standalone` — Workers, consumers, queue processors
|
|
140
|
+
|
|
141
|
+
Spins up a minimal HTTP server on a **separate port**. No NestJS overhead — just raw `http.createServer`.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
QuanticHealthModule.forRoot({
|
|
145
|
+
transport: { type: 'standalone', port: 9091 },
|
|
146
|
+
})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Endpoints: `/live`, `/ready`, `/startup` (no `/health` prefix since this is a dedicated health server).
|
|
150
|
+
|
|
151
|
+
Use this for apps that have no HTTP server but still need K8s/Docker health probes.
|
|
152
|
+
|
|
153
|
+
### `file` — Cron jobs, one-shot migration scripts
|
|
154
|
+
|
|
155
|
+
Writes a JSON file when healthy, deletes it when unhealthy. Probes check file existence.
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
QuanticHealthModule.forRoot({
|
|
159
|
+
transport: {
|
|
160
|
+
type: 'file',
|
|
161
|
+
path: '/tmp/.healthy', // optional, default '/tmp/.healthy'
|
|
162
|
+
intervalMs: 10000, // optional, default 10000
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Docker healthcheck: `test: ["CMD", "test", "-f", "/tmp/.healthy"]`
|
|
168
|
+
|
|
169
|
+
### `none` — Programmatic only
|
|
170
|
+
|
|
171
|
+
No endpoints, no files. Inject `HealthRegistry` directly for custom usage:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
@Injectable()
|
|
175
|
+
export class MyService {
|
|
176
|
+
constructor(private readonly health: HealthRegistry) {}
|
|
177
|
+
|
|
178
|
+
async checkDeps() {
|
|
179
|
+
const report = await this.health.evaluate('readiness');
|
|
180
|
+
if (report.status === 'error') {
|
|
181
|
+
// handle unhealthy state
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Result Caching
|
|
188
|
+
|
|
189
|
+
By default, check results are cached for 5 seconds to avoid hammering dependencies on high-frequency probe intervals.
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
QuanticHealthModule.forRoot({
|
|
193
|
+
transport: { type: 'controller' },
|
|
194
|
+
cacheTtlMs: 10000, // cache for 10s
|
|
195
|
+
})
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Set to `0` to disable caching (every probe hit runs all checks).
|
|
199
|
+
|
|
200
|
+
## Shutdown Integration
|
|
201
|
+
|
|
202
|
+
When the app receives SIGTERM, the module:
|
|
203
|
+
|
|
204
|
+
1. Flips readiness to `503` immediately (before `GracefulShutdownService` runs)
|
|
205
|
+
2. Waits `shutdownDelayMs` (default 5s) for the load balancer to observe the change
|
|
206
|
+
3. Then `GracefulShutdownService` proceeds with its drain + cleanup
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"status": "error",
|
|
211
|
+
"reason": "shutting_down",
|
|
212
|
+
"checks": {},
|
|
213
|
+
"timestamp": "2026-05-18T10:35:00.000Z"
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
This ensures no new traffic arrives while the app is draining in-flight work.
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
QuanticHealthModule.forRoot({
|
|
221
|
+
transport: { type: 'controller' },
|
|
222
|
+
shutdownAware: true, // default true
|
|
223
|
+
shutdownDelayMs: 5000, // default 5000
|
|
224
|
+
})
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Disable with `shutdownAware: false` if you don't need this behavior.
|
|
228
|
+
|
|
229
|
+
## Docker Compose
|
|
230
|
+
|
|
231
|
+
```yaml
|
|
232
|
+
backend:
|
|
233
|
+
healthcheck:
|
|
234
|
+
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/health/ready"]
|
|
235
|
+
interval: 10s
|
|
236
|
+
timeout: 5s
|
|
237
|
+
retries: 3
|
|
238
|
+
start_period: 30s
|
|
239
|
+
|
|
240
|
+
worker:
|
|
241
|
+
healthcheck:
|
|
242
|
+
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:9091/ready"]
|
|
243
|
+
interval: 10s
|
|
244
|
+
timeout: 5s
|
|
245
|
+
retries: 3
|
|
246
|
+
start_period: 15s
|
|
247
|
+
|
|
248
|
+
migration-job:
|
|
249
|
+
healthcheck:
|
|
250
|
+
test: ["CMD", "test", "-f", "/tmp/.healthy"]
|
|
251
|
+
interval: 5s
|
|
252
|
+
retries: 3
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Kubernetes
|
|
256
|
+
|
|
257
|
+
```yaml
|
|
258
|
+
# HTTP API
|
|
259
|
+
spec:
|
|
260
|
+
containers:
|
|
261
|
+
- name: backend
|
|
262
|
+
livenessProbe:
|
|
263
|
+
httpGet:
|
|
264
|
+
path: /health/live
|
|
265
|
+
port: 3000
|
|
266
|
+
periodSeconds: 10
|
|
267
|
+
failureThreshold: 3
|
|
268
|
+
readinessProbe:
|
|
269
|
+
httpGet:
|
|
270
|
+
path: /health/ready
|
|
271
|
+
port: 3000
|
|
272
|
+
periodSeconds: 10
|
|
273
|
+
failureThreshold: 3
|
|
274
|
+
startupProbe:
|
|
275
|
+
httpGet:
|
|
276
|
+
path: /health/startup
|
|
277
|
+
port: 3000
|
|
278
|
+
periodSeconds: 5
|
|
279
|
+
failureThreshold: 30
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
# Worker (standalone transport)
|
|
283
|
+
spec:
|
|
284
|
+
containers:
|
|
285
|
+
- name: worker
|
|
286
|
+
livenessProbe:
|
|
287
|
+
httpGet:
|
|
288
|
+
path: /live
|
|
289
|
+
port: 9091
|
|
290
|
+
readinessProbe:
|
|
291
|
+
httpGet:
|
|
292
|
+
path: /ready
|
|
293
|
+
port: 9091
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Registering Checks Programmatically
|
|
297
|
+
|
|
298
|
+
Inject `HealthRegistry` to register checks at runtime:
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
@Injectable()
|
|
302
|
+
export class KafkaConsumer implements OnModuleInit {
|
|
303
|
+
constructor(private readonly health: HealthRegistry) {}
|
|
304
|
+
|
|
305
|
+
onModuleInit() {
|
|
306
|
+
this.health.register('readiness', 'kafka', async () => {
|
|
307
|
+
if (!this.consumer.isConnected()) throw new Error('Kafka disconnected');
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Full Options Reference
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
interface HealthModuleOptions {
|
|
317
|
+
transport: HealthTransport;
|
|
318
|
+
autoDetect?: boolean; // default: true
|
|
319
|
+
cacheTtlMs?: number; // default: 5000
|
|
320
|
+
shutdownAware?: boolean; // default: true
|
|
321
|
+
shutdownDelayMs?: number; // default: 5000
|
|
322
|
+
readiness?: HealthCheckConfig[];
|
|
323
|
+
startup?: HealthCheckConfig[];
|
|
324
|
+
liveness?: HealthCheckConfig[];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Custom function check
|
|
328
|
+
interface CustomHealthCheck {
|
|
329
|
+
name: string;
|
|
330
|
+
check: () => Promise<void>; // resolve = healthy, throw = unhealthy
|
|
331
|
+
timeoutMs?: number; // default: 3000
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// HTTP endpoint check
|
|
335
|
+
interface HttpHealthCheck {
|
|
336
|
+
name: string;
|
|
337
|
+
url: string;
|
|
338
|
+
timeoutMs?: number; // default: 3000
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
type HealthTransport =
|
|
342
|
+
| { type: 'controller' }
|
|
343
|
+
| { type: 'standalone'; port: number }
|
|
344
|
+
| { type: 'file'; path?: string; intervalMs?: number }
|
|
345
|
+
| { type: 'none' };
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Complete Example (AutoFlux-style)
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
import { Module } from '@nestjs/common';
|
|
352
|
+
import { QuanticCoreModule } from '@quanticjs/core';
|
|
353
|
+
import { QuanticRedisModule } from '@quanticjs/redis';
|
|
354
|
+
import { QuanticHealthModule } from '@quanticjs/health';
|
|
355
|
+
|
|
356
|
+
@Module({
|
|
357
|
+
imports: [
|
|
358
|
+
QuanticCoreModule.forRoot(),
|
|
359
|
+
QuanticRedisModule.forRoot(),
|
|
360
|
+
// TypeOrmModule.forRoot({ ... }),
|
|
361
|
+
|
|
362
|
+
QuanticHealthModule.forRoot({
|
|
363
|
+
transport: { type: 'controller' },
|
|
364
|
+
|
|
365
|
+
// Auto-detects: database (TypeORM), redis, event_loop
|
|
366
|
+
// Add product-specific checks below:
|
|
367
|
+
|
|
368
|
+
readiness: [
|
|
369
|
+
{ name: 'minio', check: async () => {
|
|
370
|
+
const exists = await minioClient.bucketExists('autoflux');
|
|
371
|
+
if (!exists) throw new Error('Bucket missing');
|
|
372
|
+
}},
|
|
373
|
+
{ name: 'ai-gateway', url: 'http://ai-gateway:3005/health' },
|
|
374
|
+
{ name: 'kogito', url: 'http://kogito:8080/q/health/live', timeoutMs: 5000 },
|
|
375
|
+
],
|
|
376
|
+
|
|
377
|
+
startup: [
|
|
378
|
+
{ name: 'templates-seeded', check: async () => {
|
|
379
|
+
if (!seederService.isComplete()) throw new Error('Seeding in progress');
|
|
380
|
+
}},
|
|
381
|
+
],
|
|
382
|
+
|
|
383
|
+
cacheTtlMs: 5000,
|
|
384
|
+
shutdownDelayMs: 5000,
|
|
385
|
+
}),
|
|
386
|
+
],
|
|
387
|
+
})
|
|
388
|
+
export class AppModule {}
|
|
389
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
|
2
|
+
import { HealthRegistry } from './HealthRegistry';
|
|
3
|
+
import { type HealthModuleOptions } from './interfaces';
|
|
4
|
+
export declare class FileHealthWriter implements OnModuleInit, OnModuleDestroy {
|
|
5
|
+
private readonly registry;
|
|
6
|
+
private readonly options;
|
|
7
|
+
private readonly logger;
|
|
8
|
+
private timer?;
|
|
9
|
+
constructor(registry: HealthRegistry, options: HealthModuleOptions);
|
|
10
|
+
onModuleInit(): void;
|
|
11
|
+
onModuleDestroy(): void;
|
|
12
|
+
private write;
|
|
13
|
+
private getPath;
|
|
14
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
+
};
|
|
17
|
+
var FileHealthWriter_1;
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.FileHealthWriter = void 0;
|
|
20
|
+
const common_1 = require("@nestjs/common");
|
|
21
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
22
|
+
const HealthRegistry_1 = require("./HealthRegistry");
|
|
23
|
+
const interfaces_1 = require("./interfaces");
|
|
24
|
+
const DEFAULT_PATH = '/tmp/.healthy';
|
|
25
|
+
const DEFAULT_INTERVAL_MS = 10_000;
|
|
26
|
+
let FileHealthWriter = FileHealthWriter_1 = class FileHealthWriter {
|
|
27
|
+
registry;
|
|
28
|
+
options;
|
|
29
|
+
logger = new common_1.Logger(FileHealthWriter_1.name);
|
|
30
|
+
timer;
|
|
31
|
+
constructor(registry, options) {
|
|
32
|
+
this.registry = registry;
|
|
33
|
+
this.options = options;
|
|
34
|
+
}
|
|
35
|
+
onModuleInit() {
|
|
36
|
+
if (this.options.transport.type !== 'file')
|
|
37
|
+
return;
|
|
38
|
+
const intervalMs = this.options.transport.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
39
|
+
this.timer = setInterval(() => this.write(), intervalMs);
|
|
40
|
+
this.write();
|
|
41
|
+
}
|
|
42
|
+
onModuleDestroy() {
|
|
43
|
+
if (this.timer)
|
|
44
|
+
clearInterval(this.timer);
|
|
45
|
+
const path = this.getPath();
|
|
46
|
+
promises_1.default.unlink(path).catch(() => { });
|
|
47
|
+
}
|
|
48
|
+
async write() {
|
|
49
|
+
const path = this.getPath();
|
|
50
|
+
try {
|
|
51
|
+
const report = await this.registry.evaluate('readiness');
|
|
52
|
+
if (report.status === 'ok') {
|
|
53
|
+
await promises_1.default.writeFile(path, JSON.stringify(report), 'utf-8');
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
await promises_1.default.unlink(path).catch(() => { });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
this.logger.warn(`Failed to write health file: ${err.message}`);
|
|
61
|
+
await promises_1.default.unlink(path).catch(() => { });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
getPath() {
|
|
65
|
+
return this.options.transport.type === 'file'
|
|
66
|
+
? (this.options.transport.path ?? DEFAULT_PATH)
|
|
67
|
+
: DEFAULT_PATH;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
exports.FileHealthWriter = FileHealthWriter;
|
|
71
|
+
exports.FileHealthWriter = FileHealthWriter = FileHealthWriter_1 = __decorate([
|
|
72
|
+
(0, common_1.Injectable)(),
|
|
73
|
+
__param(1, (0, common_1.Inject)(interfaces_1.HEALTH_OPTIONS)),
|
|
74
|
+
__metadata("design:paramtypes", [HealthRegistry_1.HealthRegistry, Object])
|
|
75
|
+
], FileHealthWriter);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Response } from 'express';
|
|
2
|
+
import { HealthRegistry } from './HealthRegistry';
|
|
3
|
+
export declare class HealthController {
|
|
4
|
+
private readonly registry;
|
|
5
|
+
constructor(registry: HealthRegistry);
|
|
6
|
+
live(res: Response): Promise<void>;
|
|
7
|
+
ready(res: Response): Promise<void>;
|
|
8
|
+
startup(res: Response): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.HealthController = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const core_1 = require("@quanticjs/core");
|
|
18
|
+
const HealthRegistry_1 = require("./HealthRegistry");
|
|
19
|
+
let HealthController = class HealthController {
|
|
20
|
+
registry;
|
|
21
|
+
constructor(registry) {
|
|
22
|
+
this.registry = registry;
|
|
23
|
+
}
|
|
24
|
+
async live(res) {
|
|
25
|
+
const report = await this.registry.evaluate('liveness');
|
|
26
|
+
res.status(report.status === 'ok' ? common_1.HttpStatus.OK : common_1.HttpStatus.SERVICE_UNAVAILABLE).json(report);
|
|
27
|
+
}
|
|
28
|
+
async ready(res) {
|
|
29
|
+
const report = await this.registry.evaluate('readiness');
|
|
30
|
+
res.status(report.status === 'ok' ? common_1.HttpStatus.OK : common_1.HttpStatus.SERVICE_UNAVAILABLE).json(report);
|
|
31
|
+
}
|
|
32
|
+
async startup(res) {
|
|
33
|
+
const report = await this.registry.evaluate('startup');
|
|
34
|
+
res.status(report.status === 'ok' ? common_1.HttpStatus.OK : common_1.HttpStatus.SERVICE_UNAVAILABLE).json(report);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
exports.HealthController = HealthController;
|
|
38
|
+
__decorate([
|
|
39
|
+
(0, common_1.Get)('live'),
|
|
40
|
+
__param(0, (0, common_1.Res)()),
|
|
41
|
+
__metadata("design:type", Function),
|
|
42
|
+
__metadata("design:paramtypes", [Object]),
|
|
43
|
+
__metadata("design:returntype", Promise)
|
|
44
|
+
], HealthController.prototype, "live", null);
|
|
45
|
+
__decorate([
|
|
46
|
+
(0, common_1.Get)('ready'),
|
|
47
|
+
__param(0, (0, common_1.Res)()),
|
|
48
|
+
__metadata("design:type", Function),
|
|
49
|
+
__metadata("design:paramtypes", [Object]),
|
|
50
|
+
__metadata("design:returntype", Promise)
|
|
51
|
+
], HealthController.prototype, "ready", null);
|
|
52
|
+
__decorate([
|
|
53
|
+
(0, common_1.Get)('startup'),
|
|
54
|
+
__param(0, (0, common_1.Res)()),
|
|
55
|
+
__metadata("design:type", Function),
|
|
56
|
+
__metadata("design:paramtypes", [Object]),
|
|
57
|
+
__metadata("design:returntype", Promise)
|
|
58
|
+
], HealthController.prototype, "startup", null);
|
|
59
|
+
exports.HealthController = HealthController = __decorate([
|
|
60
|
+
(0, core_1.Public)(),
|
|
61
|
+
(0, common_1.Controller)('health'),
|
|
62
|
+
__metadata("design:paramtypes", [HealthRegistry_1.HealthRegistry])
|
|
63
|
+
], HealthController);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { ModuleRef } from '@nestjs/core';
|
|
3
|
+
import { type HealthModuleOptions, type HealthReport } from './interfaces';
|
|
4
|
+
type ProbeType = 'liveness' | 'readiness' | 'startup';
|
|
5
|
+
export declare class HealthRegistry implements OnModuleInit {
|
|
6
|
+
private readonly options;
|
|
7
|
+
private readonly moduleRef;
|
|
8
|
+
private readonly logger;
|
|
9
|
+
private shuttingDown;
|
|
10
|
+
private readonly checks;
|
|
11
|
+
private readonly cache;
|
|
12
|
+
constructor(options: HealthModuleOptions, moduleRef: ModuleRef);
|
|
13
|
+
onModuleInit(): void;
|
|
14
|
+
register(type: ProbeType, name: string, fn: () => Promise<void>, timeoutMs?: number): void;
|
|
15
|
+
setShuttingDown(): void;
|
|
16
|
+
isShuttingDown(): boolean;
|
|
17
|
+
evaluate(type: ProbeType): Promise<HealthReport>;
|
|
18
|
+
private runCheck;
|
|
19
|
+
private autoDetect;
|
|
20
|
+
private autoDetectDatabase;
|
|
21
|
+
private autoDetectRedis;
|
|
22
|
+
private registerConfigs;
|
|
23
|
+
private httpCheck;
|
|
24
|
+
private logRegistered;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
+
};
|
|
17
|
+
var HealthRegistry_1;
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.HealthRegistry = void 0;
|
|
20
|
+
const common_1 = require("@nestjs/common");
|
|
21
|
+
const core_1 = require("@nestjs/core");
|
|
22
|
+
const core_2 = require("@quanticjs/core");
|
|
23
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
24
|
+
const interfaces_1 = require("./interfaces");
|
|
25
|
+
const DEFAULT_TIMEOUT_MS = 3_000;
|
|
26
|
+
const DEFAULT_CACHE_TTL_MS = 5_000;
|
|
27
|
+
let HealthRegistry = HealthRegistry_1 = class HealthRegistry {
|
|
28
|
+
options;
|
|
29
|
+
moduleRef;
|
|
30
|
+
logger = new common_1.Logger(HealthRegistry_1.name);
|
|
31
|
+
shuttingDown = false;
|
|
32
|
+
checks = new Map([
|
|
33
|
+
['liveness', new Map()],
|
|
34
|
+
['readiness', new Map()],
|
|
35
|
+
['startup', new Map()],
|
|
36
|
+
]);
|
|
37
|
+
cache = new Map();
|
|
38
|
+
constructor(options, moduleRef) {
|
|
39
|
+
this.options = options;
|
|
40
|
+
this.moduleRef = moduleRef;
|
|
41
|
+
}
|
|
42
|
+
onModuleInit() {
|
|
43
|
+
if (this.options.autoDetect !== false) {
|
|
44
|
+
this.autoDetect();
|
|
45
|
+
}
|
|
46
|
+
this.registerConfigs('liveness', this.options.liveness);
|
|
47
|
+
this.registerConfigs('readiness', this.options.readiness);
|
|
48
|
+
this.registerConfigs('startup', this.options.startup);
|
|
49
|
+
this.logRegistered();
|
|
50
|
+
}
|
|
51
|
+
register(type, name, fn, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
52
|
+
this.checks.get(type).set(name, { fn, timeoutMs });
|
|
53
|
+
}
|
|
54
|
+
setShuttingDown() {
|
|
55
|
+
this.shuttingDown = true;
|
|
56
|
+
this.cache.clear();
|
|
57
|
+
}
|
|
58
|
+
isShuttingDown() {
|
|
59
|
+
return this.shuttingDown;
|
|
60
|
+
}
|
|
61
|
+
async evaluate(type) {
|
|
62
|
+
if (type === 'readiness' && this.shuttingDown) {
|
|
63
|
+
return {
|
|
64
|
+
status: 'error',
|
|
65
|
+
reason: 'shutting_down',
|
|
66
|
+
checks: {},
|
|
67
|
+
timestamp: new Date().toISOString(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const ttl = this.options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
71
|
+
const cached = this.cache.get(type);
|
|
72
|
+
if (cached && Date.now() < cached.expiry) {
|
|
73
|
+
return cached.report;
|
|
74
|
+
}
|
|
75
|
+
const checkMap = this.checks.get(type);
|
|
76
|
+
const entries = [...checkMap.entries()];
|
|
77
|
+
if (entries.length === 0) {
|
|
78
|
+
const report = { status: 'ok', checks: {}, timestamp: new Date().toISOString() };
|
|
79
|
+
this.cache.set(type, { report, expiry: Date.now() + ttl });
|
|
80
|
+
return report;
|
|
81
|
+
}
|
|
82
|
+
const results = await Promise.allSettled(entries.map(([, { fn, timeoutMs }]) => this.runCheck(fn, timeoutMs)));
|
|
83
|
+
const checks = {};
|
|
84
|
+
let allOk = true;
|
|
85
|
+
for (let i = 0; i < entries.length; i++) {
|
|
86
|
+
const [name] = entries[i];
|
|
87
|
+
const result = results[i];
|
|
88
|
+
checks[name] = result.status === 'fulfilled'
|
|
89
|
+
? result.value
|
|
90
|
+
: { status: 'error', latency_ms: 0, error: String(result.reason) };
|
|
91
|
+
if (checks[name].status === 'error')
|
|
92
|
+
allOk = false;
|
|
93
|
+
}
|
|
94
|
+
const report = {
|
|
95
|
+
status: allOk ? 'ok' : 'error',
|
|
96
|
+
checks,
|
|
97
|
+
timestamp: new Date().toISOString(),
|
|
98
|
+
};
|
|
99
|
+
this.cache.set(type, { report, expiry: Date.now() + ttl });
|
|
100
|
+
return report;
|
|
101
|
+
}
|
|
102
|
+
async runCheck(fn, timeoutMs) {
|
|
103
|
+
const start = performance.now();
|
|
104
|
+
try {
|
|
105
|
+
await Promise.race([
|
|
106
|
+
fn(),
|
|
107
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs)),
|
|
108
|
+
]);
|
|
109
|
+
return { status: 'ok', latency_ms: Math.round(performance.now() - start) };
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
return {
|
|
113
|
+
status: 'error',
|
|
114
|
+
latency_ms: Math.round(performance.now() - start),
|
|
115
|
+
error: err.message,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
autoDetect() {
|
|
120
|
+
this.autoDetectDatabase();
|
|
121
|
+
this.autoDetectRedis();
|
|
122
|
+
this.register('liveness', 'event_loop', async () => {
|
|
123
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
autoDetectDatabase() {
|
|
127
|
+
try {
|
|
128
|
+
// Resolve DataSource without a compile-time dependency on typeorm
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
130
|
+
const { DataSource } = require('typeorm');
|
|
131
|
+
const ds = this.moduleRef.get(DataSource, { strict: false });
|
|
132
|
+
if (ds?.isInitialized) {
|
|
133
|
+
this.register('readiness', 'database', async () => {
|
|
134
|
+
await ds.query('SELECT 1');
|
|
135
|
+
});
|
|
136
|
+
this.logger.log('Auto-detected DataSource → database readiness check');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// typeorm not installed or DataSource not in DI container
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
autoDetectRedis() {
|
|
144
|
+
try {
|
|
145
|
+
const redis = this.moduleRef.get(core_2.REDIS_CLIENT, { strict: false });
|
|
146
|
+
if (redis?.ping) {
|
|
147
|
+
this.register('readiness', 'redis', async () => {
|
|
148
|
+
const result = await redis.ping();
|
|
149
|
+
if (result !== 'PONG')
|
|
150
|
+
throw new Error(`Unexpected ping response: ${result}`);
|
|
151
|
+
});
|
|
152
|
+
this.logger.log('Auto-detected Redis → redis readiness check');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Redis not in DI container
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
registerConfigs(type, configs) {
|
|
160
|
+
if (!configs)
|
|
161
|
+
return;
|
|
162
|
+
for (const config of configs) {
|
|
163
|
+
if ((0, interfaces_1.isHttpHealthCheck)(config)) {
|
|
164
|
+
const { name, url, timeoutMs = DEFAULT_TIMEOUT_MS } = config;
|
|
165
|
+
this.register(type, name, () => this.httpCheck(url, timeoutMs), timeoutMs);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
this.register(type, config.name, config.check, config.timeoutMs);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
httpCheck(url, timeoutMs) {
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
const req = node_http_1.default.get(url, { timeout: timeoutMs }, (res) => {
|
|
175
|
+
res.resume();
|
|
176
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
177
|
+
resolve();
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
req.on('error', (err) => reject(err));
|
|
184
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('HTTP timeout')); });
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
logRegistered() {
|
|
188
|
+
for (const [type, checks] of this.checks) {
|
|
189
|
+
if (checks.size > 0) {
|
|
190
|
+
this.logger.log(`${type}: [${[...checks.keys()].join(', ')}]`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
exports.HealthRegistry = HealthRegistry;
|
|
196
|
+
exports.HealthRegistry = HealthRegistry = HealthRegistry_1 = __decorate([
|
|
197
|
+
(0, common_1.Injectable)(),
|
|
198
|
+
__param(0, (0, common_1.Inject)(interfaces_1.HEALTH_OPTIONS)),
|
|
199
|
+
__metadata("design:paramtypes", [Object, core_1.ModuleRef])
|
|
200
|
+
], HealthRegistry);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BeforeApplicationShutdown } from '@nestjs/common';
|
|
2
|
+
import { HealthRegistry } from './HealthRegistry';
|
|
3
|
+
import { type HealthModuleOptions } from './interfaces';
|
|
4
|
+
export declare class HealthShutdownHook implements BeforeApplicationShutdown {
|
|
5
|
+
private readonly registry;
|
|
6
|
+
private readonly options;
|
|
7
|
+
private readonly logger;
|
|
8
|
+
constructor(registry: HealthRegistry, options: HealthModuleOptions);
|
|
9
|
+
beforeApplicationShutdown(): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var HealthShutdownHook_1;
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.HealthShutdownHook = void 0;
|
|
17
|
+
const common_1 = require("@nestjs/common");
|
|
18
|
+
const HealthRegistry_1 = require("./HealthRegistry");
|
|
19
|
+
const interfaces_1 = require("./interfaces");
|
|
20
|
+
const DEFAULT_SHUTDOWN_DELAY_MS = 5_000;
|
|
21
|
+
let HealthShutdownHook = HealthShutdownHook_1 = class HealthShutdownHook {
|
|
22
|
+
registry;
|
|
23
|
+
options;
|
|
24
|
+
logger = new common_1.Logger(HealthShutdownHook_1.name);
|
|
25
|
+
constructor(registry, options) {
|
|
26
|
+
this.registry = registry;
|
|
27
|
+
this.options = options;
|
|
28
|
+
}
|
|
29
|
+
async beforeApplicationShutdown() {
|
|
30
|
+
if (this.options.shutdownAware === false)
|
|
31
|
+
return;
|
|
32
|
+
this.registry.setShuttingDown();
|
|
33
|
+
this.logger.log('Readiness probe now returns 503');
|
|
34
|
+
const delay = this.options.shutdownDelayMs ?? DEFAULT_SHUTDOWN_DELAY_MS;
|
|
35
|
+
if (delay > 0) {
|
|
36
|
+
this.logger.log(`Waiting ${delay}ms for load balancer to observe readiness change...`);
|
|
37
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
exports.HealthShutdownHook = HealthShutdownHook;
|
|
42
|
+
exports.HealthShutdownHook = HealthShutdownHook = HealthShutdownHook_1 = __decorate([
|
|
43
|
+
(0, common_1.Injectable)(),
|
|
44
|
+
__param(1, (0, common_1.Inject)(interfaces_1.HEALTH_OPTIONS)),
|
|
45
|
+
__metadata("design:paramtypes", [HealthRegistry_1.HealthRegistry, Object])
|
|
46
|
+
], HealthShutdownHook);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var QuanticHealthModule_1;
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.QuanticHealthModule = void 0;
|
|
11
|
+
const common_1 = require("@nestjs/common");
|
|
12
|
+
const HealthRegistry_1 = require("./HealthRegistry");
|
|
13
|
+
const HealthController_1 = require("./HealthController");
|
|
14
|
+
const StandaloneHealthServer_1 = require("./StandaloneHealthServer");
|
|
15
|
+
const FileHealthWriter_1 = require("./FileHealthWriter");
|
|
16
|
+
const HealthShutdownHook_1 = require("./HealthShutdownHook");
|
|
17
|
+
const interfaces_1 = require("./interfaces");
|
|
18
|
+
let QuanticHealthModule = QuanticHealthModule_1 = class QuanticHealthModule {
|
|
19
|
+
static forRoot(options = { transport: { type: 'controller' } }) {
|
|
20
|
+
const providers = [
|
|
21
|
+
{ provide: interfaces_1.HEALTH_OPTIONS, useValue: options },
|
|
22
|
+
HealthRegistry_1.HealthRegistry,
|
|
23
|
+
HealthShutdownHook_1.HealthShutdownHook,
|
|
24
|
+
];
|
|
25
|
+
const controllers = [];
|
|
26
|
+
switch (options.transport.type) {
|
|
27
|
+
case 'controller':
|
|
28
|
+
controllers.push(HealthController_1.HealthController);
|
|
29
|
+
break;
|
|
30
|
+
case 'standalone':
|
|
31
|
+
providers.push(StandaloneHealthServer_1.StandaloneHealthServer);
|
|
32
|
+
break;
|
|
33
|
+
case 'file':
|
|
34
|
+
providers.push(FileHealthWriter_1.FileHealthWriter);
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
module: QuanticHealthModule_1,
|
|
39
|
+
controllers,
|
|
40
|
+
providers,
|
|
41
|
+
exports: [HealthRegistry_1.HealthRegistry],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
exports.QuanticHealthModule = QuanticHealthModule;
|
|
46
|
+
exports.QuanticHealthModule = QuanticHealthModule = QuanticHealthModule_1 = __decorate([
|
|
47
|
+
(0, common_1.Global)(),
|
|
48
|
+
(0, common_1.Module)({})
|
|
49
|
+
], QuanticHealthModule);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
|
2
|
+
import { HealthRegistry } from './HealthRegistry';
|
|
3
|
+
import { type HealthModuleOptions } from './interfaces';
|
|
4
|
+
export declare class StandaloneHealthServer implements OnModuleInit, OnModuleDestroy {
|
|
5
|
+
private readonly registry;
|
|
6
|
+
private readonly options;
|
|
7
|
+
private readonly logger;
|
|
8
|
+
private server?;
|
|
9
|
+
constructor(registry: HealthRegistry, options: HealthModuleOptions);
|
|
10
|
+
onModuleInit(): Promise<void>;
|
|
11
|
+
onModuleDestroy(): Promise<void>;
|
|
12
|
+
private resolveProbe;
|
|
13
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
15
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
|
+
};
|
|
17
|
+
var StandaloneHealthServer_1;
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.StandaloneHealthServer = void 0;
|
|
20
|
+
const common_1 = require("@nestjs/common");
|
|
21
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
22
|
+
const HealthRegistry_1 = require("./HealthRegistry");
|
|
23
|
+
const interfaces_1 = require("./interfaces");
|
|
24
|
+
let StandaloneHealthServer = StandaloneHealthServer_1 = class StandaloneHealthServer {
|
|
25
|
+
registry;
|
|
26
|
+
options;
|
|
27
|
+
logger = new common_1.Logger(StandaloneHealthServer_1.name);
|
|
28
|
+
server;
|
|
29
|
+
constructor(registry, options) {
|
|
30
|
+
this.registry = registry;
|
|
31
|
+
this.options = options;
|
|
32
|
+
}
|
|
33
|
+
async onModuleInit() {
|
|
34
|
+
if (this.options.transport.type !== 'standalone')
|
|
35
|
+
return;
|
|
36
|
+
const { port } = this.options.transport;
|
|
37
|
+
this.server = node_http_1.default.createServer(async (req, res) => {
|
|
38
|
+
const probe = this.resolveProbe(req.url);
|
|
39
|
+
if (!probe) {
|
|
40
|
+
res.writeHead(404).end();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const report = await this.registry.evaluate(probe);
|
|
45
|
+
const status = report.status === 'ok' ? 200 : 503;
|
|
46
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
47
|
+
res.end(JSON.stringify(report));
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
res.writeHead(500).end(JSON.stringify({ status: 'error', error: String(err) }));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
await new Promise((resolve) => this.server.listen(port, resolve));
|
|
54
|
+
this.logger.log(`Health probe server listening on port ${port}`);
|
|
55
|
+
}
|
|
56
|
+
async onModuleDestroy() {
|
|
57
|
+
if (!this.server)
|
|
58
|
+
return;
|
|
59
|
+
await new Promise((resolve, reject) => this.server.close((err) => (err ? reject(err) : resolve())));
|
|
60
|
+
this.logger.log('Health probe server closed');
|
|
61
|
+
}
|
|
62
|
+
resolveProbe(url) {
|
|
63
|
+
const path = url?.split('?')[0];
|
|
64
|
+
if (path === '/live')
|
|
65
|
+
return 'liveness';
|
|
66
|
+
if (path === '/ready')
|
|
67
|
+
return 'readiness';
|
|
68
|
+
if (path === '/startup')
|
|
69
|
+
return 'startup';
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
exports.StandaloneHealthServer = StandaloneHealthServer;
|
|
74
|
+
exports.StandaloneHealthServer = StandaloneHealthServer = StandaloneHealthServer_1 = __decorate([
|
|
75
|
+
(0, common_1.Injectable)(),
|
|
76
|
+
__param(1, (0, common_1.Inject)(interfaces_1.HEALTH_OPTIONS)),
|
|
77
|
+
__metadata("design:paramtypes", [HealthRegistry_1.HealthRegistry, Object])
|
|
78
|
+
], StandaloneHealthServer);
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { QuanticHealthModule } from './QuanticHealthModule';
|
|
2
|
+
export { HealthRegistry } from './HealthRegistry';
|
|
3
|
+
export { HealthController } from './HealthController';
|
|
4
|
+
export { StandaloneHealthServer } from './StandaloneHealthServer';
|
|
5
|
+
export { FileHealthWriter } from './FileHealthWriter';
|
|
6
|
+
export { HealthShutdownHook } from './HealthShutdownHook';
|
|
7
|
+
export { HEALTH_OPTIONS, type HealthModuleOptions, type HealthTransport, type HealthCheckConfig, type CustomHealthCheck, type HttpHealthCheck, type HealthReport, type HealthCheckDetail, } from './interfaces';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HEALTH_OPTIONS = exports.HealthShutdownHook = exports.FileHealthWriter = exports.StandaloneHealthServer = exports.HealthController = exports.HealthRegistry = exports.QuanticHealthModule = void 0;
|
|
4
|
+
var QuanticHealthModule_1 = require("./QuanticHealthModule");
|
|
5
|
+
Object.defineProperty(exports, "QuanticHealthModule", { enumerable: true, get: function () { return QuanticHealthModule_1.QuanticHealthModule; } });
|
|
6
|
+
var HealthRegistry_1 = require("./HealthRegistry");
|
|
7
|
+
Object.defineProperty(exports, "HealthRegistry", { enumerable: true, get: function () { return HealthRegistry_1.HealthRegistry; } });
|
|
8
|
+
var HealthController_1 = require("./HealthController");
|
|
9
|
+
Object.defineProperty(exports, "HealthController", { enumerable: true, get: function () { return HealthController_1.HealthController; } });
|
|
10
|
+
var StandaloneHealthServer_1 = require("./StandaloneHealthServer");
|
|
11
|
+
Object.defineProperty(exports, "StandaloneHealthServer", { enumerable: true, get: function () { return StandaloneHealthServer_1.StandaloneHealthServer; } });
|
|
12
|
+
var FileHealthWriter_1 = require("./FileHealthWriter");
|
|
13
|
+
Object.defineProperty(exports, "FileHealthWriter", { enumerable: true, get: function () { return FileHealthWriter_1.FileHealthWriter; } });
|
|
14
|
+
var HealthShutdownHook_1 = require("./HealthShutdownHook");
|
|
15
|
+
Object.defineProperty(exports, "HealthShutdownHook", { enumerable: true, get: function () { return HealthShutdownHook_1.HealthShutdownHook; } });
|
|
16
|
+
var interfaces_1 = require("./interfaces");
|
|
17
|
+
Object.defineProperty(exports, "HEALTH_OPTIONS", { enumerable: true, get: function () { return interfaces_1.HEALTH_OPTIONS; } });
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export declare const HEALTH_OPTIONS: unique symbol;
|
|
2
|
+
export type HealthTransport = {
|
|
3
|
+
type: 'controller';
|
|
4
|
+
} | {
|
|
5
|
+
type: 'standalone';
|
|
6
|
+
port: number;
|
|
7
|
+
} | {
|
|
8
|
+
type: 'file';
|
|
9
|
+
path?: string;
|
|
10
|
+
intervalMs?: number;
|
|
11
|
+
} | {
|
|
12
|
+
type: 'none';
|
|
13
|
+
};
|
|
14
|
+
export interface HealthModuleOptions {
|
|
15
|
+
transport: HealthTransport;
|
|
16
|
+
/** Scan DI container for DataSource/Redis and register checks automatically (default: true) */
|
|
17
|
+
autoDetect?: boolean;
|
|
18
|
+
/** Cache check results for this many ms (default: 5000) */
|
|
19
|
+
cacheTtlMs?: number;
|
|
20
|
+
/** Flip readiness to 503 on SIGTERM before draining (default: true) */
|
|
21
|
+
shutdownAware?: boolean;
|
|
22
|
+
/** Ms to wait after flipping readiness before shutdown proceeds (default: 5000) */
|
|
23
|
+
shutdownDelayMs?: number;
|
|
24
|
+
readiness?: HealthCheckConfig[];
|
|
25
|
+
startup?: HealthCheckConfig[];
|
|
26
|
+
liveness?: HealthCheckConfig[];
|
|
27
|
+
}
|
|
28
|
+
export type HealthCheckConfig = CustomHealthCheck | HttpHealthCheck;
|
|
29
|
+
export interface CustomHealthCheck {
|
|
30
|
+
name: string;
|
|
31
|
+
/** Resolves = healthy, throws = unhealthy */
|
|
32
|
+
check: () => Promise<void>;
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface HttpHealthCheck {
|
|
36
|
+
name: string;
|
|
37
|
+
url: string;
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface HealthReport {
|
|
41
|
+
status: 'ok' | 'error';
|
|
42
|
+
reason?: string;
|
|
43
|
+
checks: Record<string, HealthCheckDetail>;
|
|
44
|
+
timestamp: string;
|
|
45
|
+
}
|
|
46
|
+
export interface HealthCheckDetail {
|
|
47
|
+
status: 'ok' | 'error';
|
|
48
|
+
latency_ms: number;
|
|
49
|
+
error?: string;
|
|
50
|
+
}
|
|
51
|
+
export declare function isHttpHealthCheck(check: HealthCheckConfig): check is HttpHealthCheck;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HEALTH_OPTIONS = void 0;
|
|
4
|
+
exports.isHttpHealthCheck = isHttpHealthCheck;
|
|
5
|
+
exports.HEALTH_OPTIONS = Symbol('HEALTH_OPTIONS');
|
|
6
|
+
function isHttpHealthCheck(check) {
|
|
7
|
+
return 'url' in check;
|
|
8
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quanticjs/health",
|
|
3
|
+
"version": "3.1.0",
|
|
4
|
+
"description": "Health check module — liveness, readiness, startup probes with auto-detection, caching, and multiple transports",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"registry": "https://registry.npmjs.org",
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"test": "jest --passWithNoTests",
|
|
18
|
+
"clean": "rm -rf dist"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@quanticjs/core": "^3.1.0"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@nestjs/common": "^11.0.0",
|
|
25
|
+
"@nestjs/core": "^11.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|