@mantiq/health 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +61 -0
- package/src/HealthCheck.ts +69 -0
- package/src/HealthHandler.ts +56 -0
- package/src/HealthManager.ts +62 -0
- package/src/checks/AppCheck.ts +36 -0
- package/src/checks/AuthCheck.ts +29 -0
- package/src/checks/CacheCheck.ts +32 -0
- package/src/checks/DatabaseCheck.ts +39 -0
- package/src/checks/EnvironmentCheck.ts +36 -0
- package/src/checks/MailCheck.ts +30 -0
- package/src/checks/MemoryCheck.ts +40 -0
- package/src/checks/QueueCheck.ts +26 -0
- package/src/checks/RouterCheck.ts +41 -0
- package/src/checks/SchedulerCheck.ts +24 -0
- package/src/checks/StorageCheck.ts +39 -0
- package/src/checks/UptimeCheck.ts +24 -0
- package/src/index.ts +24 -0
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mantiq/health",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Application health checks — database, cache, queue, filesystem, mail, and custom checks",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Abdullah Khan",
|
|
8
|
+
"homepage": "https://github.com/mantiqjs/mantiq/tree/main/packages/health",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/mantiqjs/mantiq.git",
|
|
12
|
+
"directory": "packages/health"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/mantiqjs/mantiq/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mantiq",
|
|
19
|
+
"mantiqjs",
|
|
20
|
+
"health",
|
|
21
|
+
"healthcheck",
|
|
22
|
+
"monitoring"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"bun": ">=1.1.0"
|
|
26
|
+
},
|
|
27
|
+
"main": "./src/index.ts",
|
|
28
|
+
"types": "./src/index.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"bun": "./src/index.ts",
|
|
32
|
+
"default": "./src/index.ts"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"src/",
|
|
37
|
+
"package.json",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target bun --packages=external",
|
|
43
|
+
"test": "bun test",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"clean": "rm -rf dist"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"bun-types": "latest",
|
|
49
|
+
"typescript": "^5.7.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@mantiq/core": "^0.1.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"@mantiq/database": { "optional": true },
|
|
56
|
+
"@mantiq/cache": { "optional": true },
|
|
57
|
+
"@mantiq/queue": { "optional": true },
|
|
58
|
+
"@mantiq/filesystem": { "optional": true },
|
|
59
|
+
"@mantiq/mail": { "optional": true }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export type HealthStatus = 'ok' | 'degraded' | 'critical'
|
|
2
|
+
|
|
3
|
+
export interface CheckResult {
|
|
4
|
+
name: string
|
|
5
|
+
status: HealthStatus
|
|
6
|
+
message?: string
|
|
7
|
+
duration: number // ms
|
|
8
|
+
meta?: Record<string, any>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Base class for health checks. Extend this and implement `run()`.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* class CustomCheck extends HealthCheck {
|
|
16
|
+
* name = 'custom'
|
|
17
|
+
* async run(): Promise<void> {
|
|
18
|
+
* // throw to fail, or call this.degrade() for warnings
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
export abstract class HealthCheck {
|
|
23
|
+
abstract readonly name: string
|
|
24
|
+
|
|
25
|
+
private _status: HealthStatus = 'ok'
|
|
26
|
+
private _message?: string
|
|
27
|
+
private _meta: Record<string, any> = {}
|
|
28
|
+
|
|
29
|
+
/** Override to perform the actual check. Throw to fail. */
|
|
30
|
+
abstract run(): Promise<void>
|
|
31
|
+
|
|
32
|
+
/** Mark this check as degraded (warning, not critical). */
|
|
33
|
+
protected degrade(message: string): void {
|
|
34
|
+
this._status = 'degraded'
|
|
35
|
+
this._message = message
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Attach metadata to the result (shown only in debug mode). */
|
|
39
|
+
protected meta(key: string, value: any): void {
|
|
40
|
+
this._meta[key] = value
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @internal Execute the check and return a result. */
|
|
44
|
+
async execute(): Promise<CheckResult> {
|
|
45
|
+
this._status = 'ok'
|
|
46
|
+
this._message = undefined
|
|
47
|
+
this._meta = {}
|
|
48
|
+
|
|
49
|
+
const start = performance.now()
|
|
50
|
+
try {
|
|
51
|
+
await this.run()
|
|
52
|
+
return {
|
|
53
|
+
name: this.name,
|
|
54
|
+
status: this._status,
|
|
55
|
+
message: this._message,
|
|
56
|
+
duration: Math.round((performance.now() - start) * 100) / 100,
|
|
57
|
+
meta: Object.keys(this._meta).length > 0 ? this._meta : undefined,
|
|
58
|
+
}
|
|
59
|
+
} catch (e: any) {
|
|
60
|
+
return {
|
|
61
|
+
name: this.name,
|
|
62
|
+
status: 'critical',
|
|
63
|
+
message: e.message ?? String(e),
|
|
64
|
+
duration: Math.round((performance.now() - start) * 100) / 100,
|
|
65
|
+
meta: Object.keys(this._meta).length > 0 ? this._meta : undefined,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { HealthManager, HealthReport } from './HealthManager.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HTTP request handler for /health endpoint.
|
|
5
|
+
*
|
|
6
|
+
* - Debug mode: full report with all check details, durations, and metadata
|
|
7
|
+
* - Production: minimal response — just status and timestamp
|
|
8
|
+
*
|
|
9
|
+
* Returns 200 for ok/degraded, 503 for critical.
|
|
10
|
+
*/
|
|
11
|
+
export function healthHandler(manager: HealthManager, debug = false) {
|
|
12
|
+
return async (_request: Request): Promise<Response> => {
|
|
13
|
+
const report = await manager.check()
|
|
14
|
+
const status = report.status === 'critical' ? 503 : 200
|
|
15
|
+
|
|
16
|
+
if (debug) {
|
|
17
|
+
return new Response(JSON.stringify(report, null, 2), {
|
|
18
|
+
status,
|
|
19
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Production: strip meta and detailed messages
|
|
24
|
+
const minimal: Record<string, any> = {
|
|
25
|
+
status: report.status,
|
|
26
|
+
timestamp: report.timestamp,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Only include check names + status (no messages, no meta)
|
|
30
|
+
if (report.status !== 'ok') {
|
|
31
|
+
minimal.checks = report.checks
|
|
32
|
+
.filter((c) => c.status !== 'ok')
|
|
33
|
+
.map((c) => ({ name: c.name, status: c.status }))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return new Response(JSON.stringify(minimal), {
|
|
37
|
+
status,
|
|
38
|
+
headers: { 'Content-Type': 'application/json' },
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns the health status as a compact string for Heartbeat headers.
|
|
45
|
+
* Format: "ok" | "degraded:database,memory" | "critical:database"
|
|
46
|
+
*/
|
|
47
|
+
export async function healthHeaderValue(manager: HealthManager): Promise<string> {
|
|
48
|
+
const report = manager.lastReport ?? await manager.check()
|
|
49
|
+
if (report.status === 'ok') return 'ok'
|
|
50
|
+
|
|
51
|
+
const failing = report.checks
|
|
52
|
+
.filter((c) => c.status !== 'ok')
|
|
53
|
+
.map((c) => c.name)
|
|
54
|
+
|
|
55
|
+
return `${report.status}:${failing.join(',')}`
|
|
56
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { HealthCheck, CheckResult, HealthStatus } from './HealthCheck.ts'
|
|
2
|
+
|
|
3
|
+
export interface HealthReport {
|
|
4
|
+
status: HealthStatus
|
|
5
|
+
timestamp: string
|
|
6
|
+
duration: number // total ms
|
|
7
|
+
checks: CheckResult[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class HealthManager {
|
|
11
|
+
private checks: HealthCheck[] = []
|
|
12
|
+
private _lastReport: HealthReport | null = null
|
|
13
|
+
|
|
14
|
+
/** Register a health check. */
|
|
15
|
+
register(check: HealthCheck): this {
|
|
16
|
+
this.checks.push(check)
|
|
17
|
+
return this
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Register multiple checks at once. */
|
|
21
|
+
registerMany(checks: HealthCheck[]): this {
|
|
22
|
+
for (const check of checks) this.register(check)
|
|
23
|
+
return this
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Run all checks and return a full report. */
|
|
27
|
+
async check(): Promise<HealthReport> {
|
|
28
|
+
const start = performance.now()
|
|
29
|
+
const results = await Promise.all(this.checks.map((c) => c.execute()))
|
|
30
|
+
|
|
31
|
+
let overall: HealthStatus = 'ok'
|
|
32
|
+
for (const r of results) {
|
|
33
|
+
if (r.status === 'critical') { overall = 'critical'; break }
|
|
34
|
+
if (r.status === 'degraded') overall = 'degraded'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this._lastReport = {
|
|
38
|
+
status: overall,
|
|
39
|
+
timestamp: new Date().toISOString(),
|
|
40
|
+
duration: Math.round((performance.now() - start) * 100) / 100,
|
|
41
|
+
checks: results,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return this._lastReport
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Returns the last cached report (without re-running checks). */
|
|
48
|
+
get lastReport(): HealthReport | null {
|
|
49
|
+
return this._lastReport
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Quick status check — returns 'ok', 'degraded', or 'critical'. */
|
|
53
|
+
async status(): Promise<HealthStatus> {
|
|
54
|
+
const report = await this.check()
|
|
55
|
+
return report.status
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Returns registered check names. */
|
|
59
|
+
getCheckNames(): string[] {
|
|
60
|
+
return this.checks.map((c) => c.name)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { HealthCheck } from '../HealthCheck.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verifies core application components are booted and functional.
|
|
5
|
+
* Checks: config loaded, encryption key set, app instance exists.
|
|
6
|
+
*/
|
|
7
|
+
export class AppCheck extends HealthCheck {
|
|
8
|
+
readonly name = 'app'
|
|
9
|
+
|
|
10
|
+
constructor(private app: any) {
|
|
11
|
+
super()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override async run(): Promise<void> {
|
|
15
|
+
if (!this.app) throw new Error('Application instance is null')
|
|
16
|
+
|
|
17
|
+
// Check container is functional
|
|
18
|
+
try {
|
|
19
|
+
const name = this.app.config?.('app.name') ?? this.app.make?.('config')?.get?.('app.name')
|
|
20
|
+
this.meta('name', name ?? 'unknown')
|
|
21
|
+
} catch {
|
|
22
|
+
this.degrade('Config not accessible')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check environment
|
|
26
|
+
this.meta('env', process.env['APP_ENV'] ?? process.env['NODE_ENV'] ?? 'unknown')
|
|
27
|
+
this.meta('debug', process.env['APP_DEBUG'] === 'true')
|
|
28
|
+
|
|
29
|
+
// Check APP_KEY is set (critical for encryption/sessions)
|
|
30
|
+
const appKey = process.env['APP_KEY']
|
|
31
|
+
if (!appKey || appKey.length < 10) {
|
|
32
|
+
throw new Error('APP_KEY is missing or too short — run `bun mantiq key:generate`')
|
|
33
|
+
}
|
|
34
|
+
this.meta('key', 'set')
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { HealthCheck } from '../HealthCheck.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verifies the auth system is configured — guards and providers exist.
|
|
5
|
+
*/
|
|
6
|
+
export class AuthCheck extends HealthCheck {
|
|
7
|
+
readonly name = 'auth'
|
|
8
|
+
|
|
9
|
+
constructor(private auth: any) {
|
|
10
|
+
super()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override async run(): Promise<void> {
|
|
14
|
+
if (!this.auth) throw new Error('Auth instance is null')
|
|
15
|
+
|
|
16
|
+
const defaultGuard = this.auth.getDefaultGuard?.() ?? this.auth.defaultGuard ?? 'unknown'
|
|
17
|
+
this.meta('guard', typeof defaultGuard === 'string' ? defaultGuard : 'unknown')
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const guard = this.auth.guard?.() ?? this.auth.guard?.(defaultGuard)
|
|
21
|
+
if (!guard) {
|
|
22
|
+
throw new Error(`Default guard "${defaultGuard}" could not be resolved`)
|
|
23
|
+
}
|
|
24
|
+
this.meta('provider', guard.getProvider?.()?.constructor?.name ?? 'unknown')
|
|
25
|
+
} catch (e: any) {
|
|
26
|
+
throw new Error(`Auth not configured: ${e.message}`)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { HealthCheck } from '../HealthCheck.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verifies the cache driver can read and write.
|
|
5
|
+
* Writes a test key, reads it back, then deletes it.
|
|
6
|
+
*/
|
|
7
|
+
export class CacheCheck extends HealthCheck {
|
|
8
|
+
readonly name = 'cache'
|
|
9
|
+
|
|
10
|
+
constructor(private cache: any) {
|
|
11
|
+
super()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override async run(): Promise<void> {
|
|
15
|
+
if (!this.cache) throw new Error('Cache instance is null')
|
|
16
|
+
|
|
17
|
+
const driver = this.cache.getDefaultDriver?.() ?? this.cache.driver?.() ?? 'unknown'
|
|
18
|
+
this.meta('driver', typeof driver === 'string' ? driver : 'unknown')
|
|
19
|
+
|
|
20
|
+
const key = `__health_check_${Date.now()}`
|
|
21
|
+
const value = 'ok'
|
|
22
|
+
|
|
23
|
+
await this.cache.put(key, value, 10) // 10 seconds TTL
|
|
24
|
+
const read = await this.cache.get(key)
|
|
25
|
+
|
|
26
|
+
if (read !== value) {
|
|
27
|
+
throw new Error(`Cache write/read mismatch: wrote "${value}", got "${read}"`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await this.cache.forget(key)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { HealthCheck } from '../HealthCheck.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verifies the database connection is alive by running a simple query.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* health.register(new DatabaseCheck(db().connection()))
|
|
8
|
+
*/
|
|
9
|
+
export class DatabaseCheck extends HealthCheck {
|
|
10
|
+
readonly name = 'database'
|
|
11
|
+
|
|
12
|
+
constructor(private connection: any) {
|
|
13
|
+
super()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
override async run(): Promise<void> {
|
|
17
|
+
const driver = this.connection.getDriverName?.() ?? 'unknown'
|
|
18
|
+
this.meta('driver', driver)
|
|
19
|
+
|
|
20
|
+
const start = performance.now()
|
|
21
|
+
|
|
22
|
+
if (driver === 'mongodb') {
|
|
23
|
+
// MongoDB: ping the server
|
|
24
|
+
const db = typeof this.connection.native === 'function'
|
|
25
|
+
? await this.connection.native()
|
|
26
|
+
: null
|
|
27
|
+
if (db) {
|
|
28
|
+
await db.command({ ping: 1 })
|
|
29
|
+
} else {
|
|
30
|
+
throw new Error('Cannot access native MongoDB connection')
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
// SQL: run a trivial query
|
|
34
|
+
await this.connection.select('SELECT 1')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.meta('latency', `${Math.round(performance.now() - start)}ms`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { HealthCheck } from '../HealthCheck.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verifies required environment variables are set.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* health.register(new EnvironmentCheck(['APP_KEY', 'DB_DATABASE']))
|
|
8
|
+
*/
|
|
9
|
+
export class EnvironmentCheck extends HealthCheck {
|
|
10
|
+
readonly name = 'environment'
|
|
11
|
+
|
|
12
|
+
constructor(private requiredVars: string[] = ['APP_KEY']) {
|
|
13
|
+
super()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
override async run(): Promise<void> {
|
|
17
|
+
const missing: string[] = []
|
|
18
|
+
const present: string[] = []
|
|
19
|
+
|
|
20
|
+
for (const key of this.requiredVars) {
|
|
21
|
+
if (process.env[key]) {
|
|
22
|
+
present.push(key)
|
|
23
|
+
} else {
|
|
24
|
+
missing.push(key)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
this.meta('checked', this.requiredVars.length)
|
|
29
|
+
this.meta('present', present.length)
|
|
30
|
+
|
|
31
|
+
if (missing.length > 0) {
|
|
32
|
+
this.meta('missing', missing)
|
|
33
|
+
throw new Error(`Missing environment variables: ${missing.join(', ')}`)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { HealthCheck } from '../HealthCheck.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verifies the mail driver is configured and accessible.
|
|
5
|
+
* Does NOT send an actual email — just checks the driver can be resolved.
|
|
6
|
+
*/
|
|
7
|
+
export class MailCheck extends HealthCheck {
|
|
8
|
+
readonly name = 'mail'
|
|
9
|
+
|
|
10
|
+
constructor(private mail: any) {
|
|
11
|
+
super()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override async run(): Promise<void> {
|
|
15
|
+
if (!this.mail) throw new Error('Mail instance is null')
|
|
16
|
+
|
|
17
|
+
const driver = this.mail.getDefaultDriver?.() ?? 'unknown'
|
|
18
|
+
this.meta('driver', typeof driver === 'string' ? driver : 'unknown')
|
|
19
|
+
|
|
20
|
+
// Try to resolve the transport — this validates config without sending
|
|
21
|
+
try {
|
|
22
|
+
const transport = this.mail.driver?.() ?? this.mail.transport?.()
|
|
23
|
+
if (!transport) {
|
|
24
|
+
this.degrade('Mail transport could not be resolved — check config')
|
|
25
|
+
}
|
|
26
|
+
} catch (e: any) {
|
|
27
|
+
throw new Error(`Mail driver not configured: ${e.message}`)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { HealthCheck } from '../HealthCheck.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks memory usage against a threshold.
|
|
5
|
+
* Degrades if usage exceeds the warning threshold, fails if critical.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* health.register(new MemoryCheck({ warnAt: 256, criticalAt: 512 })) // MB
|
|
9
|
+
*/
|
|
10
|
+
export class MemoryCheck extends HealthCheck {
|
|
11
|
+
readonly name = 'memory'
|
|
12
|
+
|
|
13
|
+
private warnMB: number
|
|
14
|
+
private criticalMB: number
|
|
15
|
+
|
|
16
|
+
constructor(opts: { warnAt?: number; criticalAt?: number } = {}) {
|
|
17
|
+
super()
|
|
18
|
+
this.warnMB = opts.warnAt ?? 256
|
|
19
|
+
this.criticalMB = opts.criticalAt ?? 512
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override async run(): Promise<void> {
|
|
23
|
+
const usage = process.memoryUsage()
|
|
24
|
+
const heapMB = Math.round(usage.heapUsed / 1024 / 1024 * 10) / 10
|
|
25
|
+
const rssMB = Math.round(usage.rss / 1024 / 1024 * 10) / 10
|
|
26
|
+
|
|
27
|
+
this.meta('heap', `${heapMB}MB`)
|
|
28
|
+
this.meta('rss', `${rssMB}MB`)
|
|
29
|
+
this.meta('threshold_warn', `${this.warnMB}MB`)
|
|
30
|
+
this.meta('threshold_critical', `${this.criticalMB}MB`)
|
|
31
|
+
|
|
32
|
+
if (heapMB >= this.criticalMB) {
|
|
33
|
+
throw new Error(`Heap usage ${heapMB}MB exceeds critical threshold ${this.criticalMB}MB`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (heapMB >= this.warnMB) {
|
|
37
|
+
this.degrade(`Heap usage ${heapMB}MB exceeds warning threshold ${this.warnMB}MB`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { HealthCheck } from '../HealthCheck.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verifies the queue driver is accessible and can report its size.
|
|
5
|
+
*/
|
|
6
|
+
export class QueueCheck extends HealthCheck {
|
|
7
|
+
readonly name = 'queue'
|
|
8
|
+
|
|
9
|
+
constructor(private queue: any) {
|
|
10
|
+
super()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override async run(): Promise<void> {
|
|
14
|
+
if (!this.queue) throw new Error('Queue instance is null')
|
|
15
|
+
|
|
16
|
+
const driver = this.queue.getDefaultDriver?.() ?? 'unknown'
|
|
17
|
+
this.meta('driver', typeof driver === 'string' ? driver : 'unknown')
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const size = await this.queue.size?.('default')
|
|
21
|
+
this.meta('pending', size ?? 0)
|
|
22
|
+
} catch (e: any) {
|
|
23
|
+
throw new Error(`Queue driver not accessible: ${e.message}`)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { HealthCheck } from '../HealthCheck.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verifies the router has routes registered and key endpoints exist.
|
|
5
|
+
*/
|
|
6
|
+
export class RouterCheck extends HealthCheck {
|
|
7
|
+
readonly name = 'router'
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
private router: any,
|
|
11
|
+
private expectedRoutes: string[] = [],
|
|
12
|
+
) {
|
|
13
|
+
super()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
override async run(): Promise<void> {
|
|
17
|
+
if (!this.router) throw new Error('Router instance is null')
|
|
18
|
+
|
|
19
|
+
const routes = this.router.getRoutes?.() ?? this.router.routes ?? []
|
|
20
|
+
const routeCount = Array.isArray(routes) ? routes.length : Object.keys(routes).length
|
|
21
|
+
this.meta('routes', routeCount)
|
|
22
|
+
|
|
23
|
+
if (routeCount === 0) {
|
|
24
|
+
throw new Error('No routes registered')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Verify expected routes exist
|
|
28
|
+
if (this.expectedRoutes.length > 0) {
|
|
29
|
+
const registeredPaths = Array.isArray(routes)
|
|
30
|
+
? routes.map((r: any) => r.path ?? r.uri ?? '')
|
|
31
|
+
: Object.keys(routes)
|
|
32
|
+
|
|
33
|
+
const missing = this.expectedRoutes.filter((p) => !registeredPaths.some((rp: string) => rp === p || rp.includes(p)))
|
|
34
|
+
|
|
35
|
+
if (missing.length > 0) {
|
|
36
|
+
this.meta('missing', missing)
|
|
37
|
+
this.degrade(`Missing expected routes: ${missing.join(', ')}`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { HealthCheck } from '../HealthCheck.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verifies scheduled tasks are registered and the scheduler is running.
|
|
5
|
+
*/
|
|
6
|
+
export class SchedulerCheck extends HealthCheck {
|
|
7
|
+
readonly name = 'scheduler'
|
|
8
|
+
|
|
9
|
+
constructor(private scheduler: any) {
|
|
10
|
+
super()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
override async run(): Promise<void> {
|
|
14
|
+
if (!this.scheduler) throw new Error('Scheduler instance is null')
|
|
15
|
+
|
|
16
|
+
const tasks = this.scheduler.events?.() ?? this.scheduler.tasks ?? []
|
|
17
|
+
const count = Array.isArray(tasks) ? tasks.length : 0
|
|
18
|
+
this.meta('tasks', count)
|
|
19
|
+
|
|
20
|
+
if (count === 0) {
|
|
21
|
+
this.degrade('No scheduled tasks registered')
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { HealthCheck } from '../HealthCheck.ts'
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Verifies the storage directory is writable.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* health.register(new StorageCheck('./storage'))
|
|
10
|
+
*/
|
|
11
|
+
export class StorageCheck extends HealthCheck {
|
|
12
|
+
readonly name = 'storage'
|
|
13
|
+
|
|
14
|
+
constructor(private storagePath: string = './storage') {
|
|
15
|
+
super()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
override async run(): Promise<void> {
|
|
19
|
+
this.meta('path', this.storagePath)
|
|
20
|
+
|
|
21
|
+
if (!existsSync(this.storagePath)) {
|
|
22
|
+
try {
|
|
23
|
+
mkdirSync(this.storagePath, { recursive: true })
|
|
24
|
+
} catch {
|
|
25
|
+
throw new Error(`Storage directory does not exist and cannot be created: ${this.storagePath}`)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Write and delete a temp file to verify write access
|
|
30
|
+
const testFile = join(this.storagePath, `.health-check-${Date.now()}`)
|
|
31
|
+
try {
|
|
32
|
+
writeFileSync(testFile, 'ok')
|
|
33
|
+
unlinkSync(testFile)
|
|
34
|
+
this.meta('writable', true)
|
|
35
|
+
} catch (e: any) {
|
|
36
|
+
throw new Error(`Storage directory is not writable: ${e.message}`)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { HealthCheck } from '../HealthCheck.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reports process uptime. Always passes.
|
|
5
|
+
*/
|
|
6
|
+
export class UptimeCheck extends HealthCheck {
|
|
7
|
+
readonly name = 'uptime'
|
|
8
|
+
|
|
9
|
+
override async run(): Promise<void> {
|
|
10
|
+
const seconds = Math.floor(process.uptime())
|
|
11
|
+
const hours = Math.floor(seconds / 3600)
|
|
12
|
+
const minutes = Math.floor((seconds % 3600) / 60)
|
|
13
|
+
const secs = seconds % 60
|
|
14
|
+
|
|
15
|
+
const parts: string[] = []
|
|
16
|
+
if (hours > 0) parts.push(`${hours}h`)
|
|
17
|
+
if (minutes > 0) parts.push(`${minutes}m`)
|
|
18
|
+
parts.push(`${secs}s`)
|
|
19
|
+
|
|
20
|
+
this.meta('seconds', seconds)
|
|
21
|
+
this.meta('formatted', parts.join(' '))
|
|
22
|
+
this.meta('pid', process.pid)
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// ── Core ─────────────────────────────────────────────────────────────────────
|
|
2
|
+
export { HealthCheck } from './HealthCheck.ts'
|
|
3
|
+
export type { CheckResult, HealthStatus } from './HealthCheck.ts'
|
|
4
|
+
export { HealthManager } from './HealthManager.ts'
|
|
5
|
+
export type { HealthReport } from './HealthManager.ts'
|
|
6
|
+
|
|
7
|
+
// ── Handler ──────────────────────────────────────────────────────────────────
|
|
8
|
+
export { healthHandler, healthHeaderValue } from './HealthHandler.ts'
|
|
9
|
+
|
|
10
|
+
// ── Infrastructure Checks ────────────────────────────────────────────────────
|
|
11
|
+
export { DatabaseCheck } from './checks/DatabaseCheck.ts'
|
|
12
|
+
export { StorageCheck } from './checks/StorageCheck.ts'
|
|
13
|
+
export { EnvironmentCheck } from './checks/EnvironmentCheck.ts'
|
|
14
|
+
export { MemoryCheck } from './checks/MemoryCheck.ts'
|
|
15
|
+
export { UptimeCheck } from './checks/UptimeCheck.ts'
|
|
16
|
+
|
|
17
|
+
// ── Application Checks ──────────────────────────────────────────────────────
|
|
18
|
+
export { AppCheck } from './checks/AppCheck.ts'
|
|
19
|
+
export { CacheCheck } from './checks/CacheCheck.ts'
|
|
20
|
+
export { QueueCheck } from './checks/QueueCheck.ts'
|
|
21
|
+
export { RouterCheck } from './checks/RouterCheck.ts'
|
|
22
|
+
export { MailCheck } from './checks/MailCheck.ts'
|
|
23
|
+
export { AuthCheck } from './checks/AuthCheck.ts'
|
|
24
|
+
export { SchedulerCheck } from './checks/SchedulerCheck.ts'
|