node: implement live & ready check helpers
This commit is contained in:
@@ -1,30 +1,2 @@
|
|||||||
/**
|
export * from './liveness';
|
||||||
* Simple liveness & readiness helpers
|
export * from './readiness';
|
||||||
*/
|
|
||||||
|
|
||||||
export type ReadinessResult = {
|
|
||||||
ok: boolean;
|
|
||||||
details: { name: string; ok: boolean; error?: string }[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function liveness() {
|
|
||||||
return {
|
|
||||||
status: 'ok',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readiness(
|
|
||||||
checks: Array<{ name: string; fn: () => Promise<boolean> | boolean }>
|
|
||||||
): Promise<ReadinessResult> {
|
|
||||||
const results: ReadinessResult['details'] = [];
|
|
||||||
for (const c of checks) {
|
|
||||||
try {
|
|
||||||
const r = await Promise.resolve(c.fn());
|
|
||||||
results.push({ name: c.name, ok: !!r });
|
|
||||||
} catch (err: any) {
|
|
||||||
results.push({ name: c.name, ok: false, error: err?.message ?? String(err) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { ok: results.every((r) => r.ok), details: results };
|
|
||||||
}
|
|
||||||
|
|||||||
31
node-health/src/liveness.ts
Normal file
31
node-health/src/liveness.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Type representing the result of a liveness check.
|
||||||
|
*/
|
||||||
|
export type LivenessResult = {
|
||||||
|
/** Status of the service (always 'ok'). */
|
||||||
|
status: 'ok';
|
||||||
|
/** Timestamp of the liveness check in milliseconds since the Unix epoch. */
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liveness check - indicates if the service is running.
|
||||||
|
* @returns A LivenessResult object.
|
||||||
|
*/
|
||||||
|
export function liveness() {
|
||||||
|
return {
|
||||||
|
status: 'ok',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
} as LivenessResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for liveness HTTP requests.
|
||||||
|
* @returns A Response object with LivenessResult in JSON format and status 200.
|
||||||
|
*/
|
||||||
|
export const handleLiveness = (): Response => {
|
||||||
|
return new Response(JSON.stringify(liveness()), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
};
|
||||||
199
node-health/src/readiness.ts
Normal file
199
node-health/src/readiness.ts
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
/** Function that performs a readiness check */
|
||||||
|
export type ReadinessFunction = () => Promise<ReadinessDetail> | ReadinessDetail;
|
||||||
|
|
||||||
|
/** Status of a readiness check */
|
||||||
|
export type ReadinessStatus = 'ok' | 'error' | 'degraded';
|
||||||
|
|
||||||
|
const aggregateStatus = (statuses: ReadinessStatus[]): ReadinessStatus => {
|
||||||
|
if (statuses.includes('error')) return 'error';
|
||||||
|
if (statuses.includes('degraded')) return 'degraded';
|
||||||
|
return 'ok';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Result of a system readiness check */
|
||||||
|
export type ReadinessResult = {
|
||||||
|
/**
|
||||||
|
* Status of the system readiness check, aggregated as the worst status
|
||||||
|
* among individual checks. 'unknown' is a special case indicating that
|
||||||
|
* no checks were performed, used by ScheduledReadiness before the first run.
|
||||||
|
* */
|
||||||
|
status: ReadinessStatus | 'unknown';
|
||||||
|
/** Start time of the system readiness check in milliseconds since the Unix epoch */
|
||||||
|
start: number;
|
||||||
|
/** Duration of the system readiness check in milliseconds */
|
||||||
|
duration: number;
|
||||||
|
/** Details of individual readiness checks */
|
||||||
|
details: ReadinessDetail[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Detail of an individual readiness check */
|
||||||
|
export type ReadinessDetail = {
|
||||||
|
name: string;
|
||||||
|
status: ReadinessStatus;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a readiness check by executing the provided readiness functions.
|
||||||
|
* @param checks - An array of readiness functions to execute.
|
||||||
|
* @returns A Promise that resolves to a ReadinessResult object.
|
||||||
|
*/
|
||||||
|
export const readiness = async (checks: ReadinessFunction[]): Promise<ReadinessResult> => {
|
||||||
|
const start = Date.now();
|
||||||
|
const details: ReadinessDetail[] = [];
|
||||||
|
|
||||||
|
for (const check of checks) {
|
||||||
|
try {
|
||||||
|
const result = await Promise.resolve(check());
|
||||||
|
details.push(result);
|
||||||
|
} catch (err) {
|
||||||
|
details.push({
|
||||||
|
name: 'unknown',
|
||||||
|
status: 'error',
|
||||||
|
message: err instanceof Error ? err.message : String(err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: aggregateStatus(details.map((d) => d.status)),
|
||||||
|
start,
|
||||||
|
duration,
|
||||||
|
details,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a handler function for readiness HTTP requests. Warning: this runs all
|
||||||
|
* checks on each request and may be slow.
|
||||||
|
* @param checks - An array of readiness functions to execute.
|
||||||
|
* @returns A function that returns a Response object with ReadinessResult in JSON format.
|
||||||
|
*/
|
||||||
|
export const createReadinessHandler = (checks: ReadinessFunction[]): (() => Promise<Response>) => {
|
||||||
|
return async () => {
|
||||||
|
const result = await readiness(checks);
|
||||||
|
return respondWithResult(result);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that schedules periodic readiness checks.
|
||||||
|
*/
|
||||||
|
export class ScheduledReadiness {
|
||||||
|
private checks: ReadinessFunction[];
|
||||||
|
private interval: number;
|
||||||
|
private started: boolean = false;
|
||||||
|
private timer: NodeJS.Timeout | null = null;
|
||||||
|
private latestResult: ReadinessResult | null = null;
|
||||||
|
private nextResult: Promise<ReadinessResult> | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of ScheduledReadiness.
|
||||||
|
* @param checks - An array of readiness functions to execute.
|
||||||
|
* @param interval - Interval in milliseconds between readiness checks.
|
||||||
|
*/
|
||||||
|
constructor(checks: ReadinessFunction[], interval: number) {
|
||||||
|
this.checks = checks;
|
||||||
|
this.interval = interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Starts the scheduled readiness checks */
|
||||||
|
async start() {
|
||||||
|
if (this.started) return; // Already started
|
||||||
|
this.started = true;
|
||||||
|
|
||||||
|
const runCheck = async () => {
|
||||||
|
// Prevent concurrent runs
|
||||||
|
if (this.nextResult) return;
|
||||||
|
|
||||||
|
this.nextResult = readiness(this.checks);
|
||||||
|
try {
|
||||||
|
this.latestResult = await this.nextResult;
|
||||||
|
} finally {
|
||||||
|
this.nextResult = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await runCheck(); // Initial run
|
||||||
|
this.timer = setInterval(runCheck, this.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stops the scheduled readiness checks */
|
||||||
|
stop() {
|
||||||
|
if (this.timer) {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
|
this.started = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the next readiness result, waiting if a check is in progress */
|
||||||
|
async getNextResult(): Promise<ReadinessResult | null> {
|
||||||
|
if (this.nextResult) {
|
||||||
|
return await this.nextResult;
|
||||||
|
}
|
||||||
|
return this.latestResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the latest readiness result without waiting */
|
||||||
|
getResult(): ReadinessResult | null {
|
||||||
|
return this.latestResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the interval for readiness checks and restarts the timer if already started.
|
||||||
|
* @param ms - Interval in milliseconds.
|
||||||
|
*/
|
||||||
|
setInterval(ms: number) {
|
||||||
|
this.interval = ms;
|
||||||
|
if (this.timer) {
|
||||||
|
this.stop();
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a handler function for readiness HTTP requests using the latest scheduled result.
|
||||||
|
* Scheduled handler always returns the most recent result, or 'unknown' if no checks have run yet.
|
||||||
|
* @returns A function that returns a Response object with ReadinessResult in JSON format.
|
||||||
|
*/
|
||||||
|
createHandler(): () => Promise<Response> {
|
||||||
|
return async () => {
|
||||||
|
const result = await this.getNextResult();
|
||||||
|
if (!result) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
status: 'unknown',
|
||||||
|
start: Date.now(),
|
||||||
|
duration: 0,
|
||||||
|
details: [],
|
||||||
|
} as ReadinessResult),
|
||||||
|
{
|
||||||
|
status: httpStatusFromReadiness('unknown'),
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return respondWithResult(result);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a Response object with the given ReadinessResult in JSON format */
|
||||||
|
const respondWithResult = (result: ReadinessResult) => {
|
||||||
|
return new Response(JSON.stringify(result), {
|
||||||
|
status: httpStatusFromReadiness(result.status),
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Returns the HTTP status code corresponding to a given readiness status */
|
||||||
|
const httpStatusFromReadiness = (status: ReadinessStatus | 'unknown'): number => {
|
||||||
|
if (status === 'ok') return 200;
|
||||||
|
if (status === 'degraded') return 200; // 206 could also be suitable, but let's avoid false alarms
|
||||||
|
if (status === 'error') return 503;
|
||||||
|
return 200; // unknown, treat as ok to avoid false alarms
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user