Compare commits
12 Commits
7375b4d96f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7522e9e0f3 | ||
|
|
0ab0168a8d | ||
|
|
a73105586b | ||
|
|
c6ff80655b | ||
|
|
0956271e29 | ||
|
|
620be9ccda | ||
|
|
061ee7c7fd | ||
|
|
ea3b90aca2 | ||
|
|
a6d9d72322 | ||
|
|
ae664da0e4 | ||
|
|
d88c1c0c49 | ||
|
|
85b698d940 |
@@ -1,17 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
es2021: true,
|
|
||||||
},
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
plugins: ['@typescript-eslint', 'prettier'],
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:prettier/recommended',
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
'prettier/prettier': 'error',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
42
go-health/liveness.go
Normal file
42
go-health/liveness.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Status of the service liveness.
|
||||||
|
type LivenessStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Service is alive and functioning properly.
|
||||||
|
LivenessStatusAlive LivenessStatus = "ok"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Result of a liveness check.
|
||||||
|
type LivenessResult struct {
|
||||||
|
// Status of the service (always LivenessStatusAlive)
|
||||||
|
Status LivenessStatus
|
||||||
|
// Timestamp of the liveness check in milliseconds since Unix epoch
|
||||||
|
Timestamp int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if the service is alive.
|
||||||
|
func Liveness() LivenessResult {
|
||||||
|
return LivenessResult{
|
||||||
|
Status: LivenessStatusAlive,
|
||||||
|
Timestamp: time.Now().UnixMilli(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler for liveness HTTP requests.
|
||||||
|
// Returns a response with status 200 and a JSON body matching LivenessResult.
|
||||||
|
func LivenessHandler() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
result := Liveness()
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
json.NewEncoder(w).Encode(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
221
go-health/readiness.go
Normal file
221
go-health/readiness.go
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Status of the service readiness.
|
||||||
|
type ReadinessStatus string
|
||||||
|
|
||||||
|
// String returns the string representation of the readiness status.
|
||||||
|
func (s ReadinessStatus) String() string {
|
||||||
|
return string(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPStatus returns the corresponding HTTP status code for the readiness status.
|
||||||
|
func (s ReadinessStatus) HTTPStatus() int {
|
||||||
|
switch s {
|
||||||
|
case ReadinessStatusReady:
|
||||||
|
return http.StatusOK
|
||||||
|
case ReadinessStatusDegraded:
|
||||||
|
return http.StatusOK // 206 could also be suitable, but let's avoid false alarms
|
||||||
|
case ReadinessStatusFatal:
|
||||||
|
return http.StatusServiceUnavailable
|
||||||
|
case ReadinessStatusNotReady:
|
||||||
|
return http.StatusOK // treat as ok to avoid false alarms
|
||||||
|
default:
|
||||||
|
return http.StatusOK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Service is ready to handle requests.
|
||||||
|
ReadinessStatusReady ReadinessStatus = "ready"
|
||||||
|
// Service is not ready to handle requests.
|
||||||
|
ReadinessStatusNotReady ReadinessStatus = "not_ready"
|
||||||
|
// Service experienced a fatal error and cannot handle requests.
|
||||||
|
ReadinessStatusFatal ReadinessStatus = "fatal"
|
||||||
|
// Service is degraded but can still handle requests.
|
||||||
|
ReadinessStatusDegraded ReadinessStatus = "degraded"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Returns the worst readiness status from a list of statuses.
|
||||||
|
func worstStatus(statuses []ReadinessStatus) ReadinessStatus {
|
||||||
|
if slices.Contains(statuses, ReadinessStatusFatal) {
|
||||||
|
return ReadinessStatusFatal
|
||||||
|
}
|
||||||
|
if slices.Contains(statuses, ReadinessStatusNotReady) {
|
||||||
|
return ReadinessStatusNotReady
|
||||||
|
}
|
||||||
|
if slices.Contains(statuses, ReadinessStatusDegraded) {
|
||||||
|
return ReadinessStatusDegraded
|
||||||
|
}
|
||||||
|
return ReadinessStatusReady
|
||||||
|
}
|
||||||
|
|
||||||
|
// Readiness check function return type.
|
||||||
|
type ReadinessReturn struct {
|
||||||
|
// Status of the readiness check.
|
||||||
|
Status ReadinessStatus
|
||||||
|
// Optional message providing additional information about the readiness status.
|
||||||
|
Message *string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function that performs a readiness check.
|
||||||
|
type ReadinessCheckFn func() ReadinessReturn
|
||||||
|
|
||||||
|
// Individual readiness check.
|
||||||
|
type ReadinessCheck struct {
|
||||||
|
// Name of the readiness check.
|
||||||
|
Name string
|
||||||
|
// Function that performs the readiness check.
|
||||||
|
Fn ReadinessCheckFn
|
||||||
|
// Timeout for the readiness check (default: 5 seconds)
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// System-generated result from an individual readiness check.
|
||||||
|
type ReadinessCheckResult struct {
|
||||||
|
// Name of the readiness check.
|
||||||
|
Name string
|
||||||
|
// Status of the readiness check.
|
||||||
|
Status ReadinessStatus
|
||||||
|
// Optional message providing additional information about the readiness status.
|
||||||
|
Message *string
|
||||||
|
// Duration of the readiness check in milliseconds.
|
||||||
|
Duration int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// System readiness status.
|
||||||
|
type Readiness struct {
|
||||||
|
// Status of the service readiness. Aggregated as the worst status from
|
||||||
|
// all individual readiness checks. If no checks have been performed
|
||||||
|
// yet, the status is ReadinessStatusNotReady.
|
||||||
|
Status ReadinessStatus
|
||||||
|
// Start time of the readiness check in milliseconds since Unix epoch.
|
||||||
|
Start int64
|
||||||
|
// Duration of the readiness check in milliseconds.
|
||||||
|
Duration int64
|
||||||
|
// Individual readiness check results.
|
||||||
|
Details map[string]ReadinessCheckResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performs a full system readiness check given a list of individual checks.
|
||||||
|
func CheckReadiness(checks []ReadinessCheck) Readiness {
|
||||||
|
start := time.Now()
|
||||||
|
details := make(map[string]ReadinessCheckResult)
|
||||||
|
var statuses []ReadinessStatus
|
||||||
|
|
||||||
|
for _, check := range checks {
|
||||||
|
checkt0 := time.Now()
|
||||||
|
result := checkWithTimeout(check)
|
||||||
|
duration := time.Since(checkt0).Milliseconds()
|
||||||
|
|
||||||
|
details[check.Name] = ReadinessCheckResult{
|
||||||
|
Name: check.Name,
|
||||||
|
Status: result.Status,
|
||||||
|
Message: result.Message,
|
||||||
|
Duration: duration,
|
||||||
|
}
|
||||||
|
statuses = append(statuses, result.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Readiness{
|
||||||
|
Status: worstStatus(statuses),
|
||||||
|
Start: start.UnixMilli(),
|
||||||
|
Duration: time.Since(start).Milliseconds(),
|
||||||
|
Details: details,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a handler function for readiness HTTP requests.
|
||||||
|
func ReadinessHandler(checks []ReadinessCheck) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
respondWithResult(w, CheckReadiness(checks))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs a readiness check with a timeout.
|
||||||
|
func checkWithTimeout(check ReadinessCheck) ReadinessReturn {
|
||||||
|
timeout := check.Timeout
|
||||||
|
if timeout == 0 {
|
||||||
|
timeout = 5 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
resultCh := make(chan ReadinessReturn, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
resultCh <- check.Fn()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case result := <-resultCh:
|
||||||
|
return result
|
||||||
|
case <-time.After(timeout):
|
||||||
|
msg := "readiness check timed out"
|
||||||
|
return ReadinessReturn{
|
||||||
|
Status: ReadinessStatusFatal,
|
||||||
|
Message: &msg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func respondWithResult(w http.ResponseWriter, result Readiness) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(result.Status.HTTPStatus())
|
||||||
|
json.NewEncoder(w).Encode(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScheduledReadiness performs periodic readiness checks and stores the latest result.
|
||||||
|
type ScheduledReadiness struct {
|
||||||
|
checks []ReadinessCheck
|
||||||
|
interval time.Duration
|
||||||
|
currentState Readiness
|
||||||
|
ticker *time.Ticker
|
||||||
|
quit chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewScheduledReadiness creates a new ScheduledReadiness instance.
|
||||||
|
func NewScheduledReadiness(checks []ReadinessCheck, interval time.Duration) *ScheduledReadiness {
|
||||||
|
return &ScheduledReadiness{
|
||||||
|
checks: checks,
|
||||||
|
interval: interval,
|
||||||
|
quit: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start begins the periodic readiness checks.
|
||||||
|
func (sr *ScheduledReadiness) Start() {
|
||||||
|
sr.ticker = time.NewTicker(sr.interval)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-sr.ticker.C:
|
||||||
|
sr.currentState = CheckReadiness(sr.checks)
|
||||||
|
case <-sr.quit:
|
||||||
|
sr.ticker.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop halts the periodic readiness checks.
|
||||||
|
func (sr *ScheduledReadiness) Stop() {
|
||||||
|
close(sr.quit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentState returns the latest readiness state.
|
||||||
|
func (sr *ScheduledReadiness) GetCurrentState() Readiness {
|
||||||
|
return sr.currentState
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a handler function for scheduled readiness HTTP requests.
|
||||||
|
func (sr *ScheduledReadiness) ReadinessHandler() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
respondWithResult(w, sr.GetCurrentState())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
extends: ['../../.eslintrc.cjs'],
|
|
||||||
parserOptions: {
|
|
||||||
project: './tsconfig.json',
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
// package-level overrides
|
|
||||||
},
|
|
||||||
};
|
|
||||||
14
node-health/eslint.config.mts
Normal file
14
node-health/eslint.config.mts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import js from '@eslint/js';
|
||||||
|
import globals from 'globals';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import { defineConfig } from 'eslint/config';
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,mjs,cjs,ts,mts,cts}'],
|
||||||
|
plugins: { js },
|
||||||
|
extends: ['js/recommended'],
|
||||||
|
languageOptions: { globals: globals.browser },
|
||||||
|
},
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
]);
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "@health/node",
|
"name": "@end/node-health",
|
||||||
"version": "0.1.0",
|
"repository": {
|
||||||
"private": true,
|
"type": "git",
|
||||||
|
"url": "https://gitea.auvem.com/end/health.git"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://gitea.auvem.com/api/packages/end/npm/"
|
||||||
|
},
|
||||||
|
"version": "0.1.2",
|
||||||
|
"license": "MIT",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
@@ -14,6 +21,13 @@
|
|||||||
"test": "echo \"No tests configured\" && exit 0",
|
"test": "echo \"No tests configured\" && exit 0",
|
||||||
"check": "npm run lint && npm run build"
|
"check": "npm run lint && npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"devDependencies": {
|
||||||
"devDependencies": {}
|
"@eslint/js": "^9.39.2",
|
||||||
|
"@types/node": "^25.0.3",
|
||||||
|
"eslint": "^9.39.2",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"jiti": "^2.6.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"typescript-eslint": "^8.50.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
40
node-health/src/checks.ts
Normal file
40
node-health/src/checks.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { monitorEventLoopDelay } from 'node:perf_hooks';
|
||||||
|
import { ReadinessCheck, ReadinessFunctionReturn, ReadinessStatus } from './readiness';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a readiness check function that monitors event loop lag.
|
||||||
|
* @param options - Configuration options for the event loop lag check.
|
||||||
|
* @param options.degradedMs - Threshold in milliseconds for degraded status (default: 200).
|
||||||
|
* @param options.failMs - Threshold in milliseconds for error status (default: 1000).
|
||||||
|
* @param options.histResetMs - Interval in milliseconds to reset the histogram (default: 60000).
|
||||||
|
* @param options.percentile - Percentile to monitor (default: 50).
|
||||||
|
* @returns A ReadinessFunction that checks event loop lag.
|
||||||
|
*/
|
||||||
|
export const buildEventLoopLagCheck = (options: {
|
||||||
|
degradedMs?: number;
|
||||||
|
failMs?: number;
|
||||||
|
histResetMs?: number;
|
||||||
|
percentile?: number;
|
||||||
|
}): ReadinessCheck => {
|
||||||
|
const { degradedMs = 200, failMs = 1000, histResetMs = 60000, percentile = 50 } = options;
|
||||||
|
|
||||||
|
const hist = monitorEventLoopDelay({ resolution: 10 });
|
||||||
|
hist.enable();
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
hist.reset();
|
||||||
|
}, histResetMs).unref();
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'event-loop-lag',
|
||||||
|
fn: async (): Promise<ReadinessFunctionReturn> => {
|
||||||
|
const lag = hist.percentile(percentile) / 1e6; // Convert from nanoseconds to milliseconds
|
||||||
|
const status: ReadinessStatus = lag < degradedMs ? 'ok' : lag < failMs ? 'degraded' : 'error';
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
message: `Event loop lag is ${lag.toFixed(2)} ms`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
timeout: 500,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,30 +1,3 @@
|
|||||||
/**
|
export * from './liveness';
|
||||||
* Simple liveness & readiness helpers
|
export * from './readiness';
|
||||||
*/
|
export * from './checks';
|
||||||
|
|
||||||
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' },
|
||||||
|
});
|
||||||
|
};
|
||||||
253
node-health/src/readiness.ts
Normal file
253
node-health/src/readiness.ts
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
/** Readiness check function return type */
|
||||||
|
export type ReadinessReturn = {
|
||||||
|
/** Status of the readiness check */
|
||||||
|
status: ReadinessStatus;
|
||||||
|
/** Optional message providing additional information about the readiness check */
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Function that performs a readiness check */
|
||||||
|
export type ReadinessCheckFn = (check: ReadinessCheck) => Promise<ReadinessReturn>;
|
||||||
|
|
||||||
|
/** Status of the service readiness */
|
||||||
|
export type ReadinessStatus = 'fatal' | 'not_ready' | 'degraded' | 'ready';
|
||||||
|
|
||||||
|
/** Returns the worst readiness status from a list of statuses. */
|
||||||
|
const worstStatus = (statuses: ReadinessStatus[]): ReadinessStatus => {
|
||||||
|
if (statuses.includes('fatal')) return 'fatal';
|
||||||
|
if (statuses.includes('not_ready')) return 'not_ready';
|
||||||
|
if (statuses.includes('degraded')) return 'degraded';
|
||||||
|
return 'ready';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Individual readiness check */
|
||||||
|
export type ReadinessCheck = {
|
||||||
|
/** Name of the readiness check */
|
||||||
|
name: string;
|
||||||
|
/** Function that performs the readiness check */
|
||||||
|
fn: ReadinessCheckFn;
|
||||||
|
/** Timeout in milliseconds for the readiness check (default: 5000) */
|
||||||
|
timeout?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** System readiness status */
|
||||||
|
export type Readiness = {
|
||||||
|
/**
|
||||||
|
* Status of the system readiness check, aggregated as the worst status
|
||||||
|
* among individual checks. If no checks have been performed yet, the status
|
||||||
|
* is 'not_ready'.
|
||||||
|
*/
|
||||||
|
status: ReadinessStatus;
|
||||||
|
/** 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: ReadinessCheckResult[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** System-generated result from an individual readiness check */
|
||||||
|
export type ReadinessCheckResult = {
|
||||||
|
/** Name of the readiness check */
|
||||||
|
name: string;
|
||||||
|
/** Status of the readiness check */
|
||||||
|
status: ReadinessStatus;
|
||||||
|
/** Duration of the readiness check in milliseconds */
|
||||||
|
duration: number;
|
||||||
|
/** Message providing additional information about the readiness check */
|
||||||
|
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 Readiness object.
|
||||||
|
*/
|
||||||
|
export const readiness = async (checks: ReadinessCheck[]): Promise<Readiness> => {
|
||||||
|
const start = Date.now();
|
||||||
|
const t0 = performance.now();
|
||||||
|
const details: ReadinessCheckResult[] = [];
|
||||||
|
|
||||||
|
for (const check of checks) {
|
||||||
|
const checkt0 = performance.now();
|
||||||
|
try {
|
||||||
|
const result = await withTimeout(
|
||||||
|
check.fn(check),
|
||||||
|
check.timeout ?? 5000,
|
||||||
|
`Readiness check '${check.name}' timed out after ${check.timeout ?? 5000} ms`,
|
||||||
|
);
|
||||||
|
details.push({
|
||||||
|
name: check.name,
|
||||||
|
status: result.status,
|
||||||
|
message: result.message,
|
||||||
|
duration: performance.now() - checkt0,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
details.push({
|
||||||
|
name: check.name,
|
||||||
|
status: 'fatal',
|
||||||
|
message: err instanceof Error ? err.message : String(err),
|
||||||
|
duration: performance.now() - checkt0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const duration = performance.now() - t0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: worstStatus(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 Readiness in JSON format.
|
||||||
|
*/
|
||||||
|
export const createReadinessHandler = (checks: ReadinessCheck[]): (() => Promise<Response>) => {
|
||||||
|
return async () => {
|
||||||
|
const result = await readiness(checks);
|
||||||
|
return respondWithResult(result);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that schedules periodic readiness checks.
|
||||||
|
*/
|
||||||
|
export class ScheduledReadiness {
|
||||||
|
private checks: ReadinessCheck[];
|
||||||
|
private interval: number;
|
||||||
|
private started: boolean = false;
|
||||||
|
private timer: NodeJS.Timeout | null = null;
|
||||||
|
private latestResult: Readiness | null = null;
|
||||||
|
private nextResult: Promise<Readiness> | 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: ReadinessCheck[], 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<Readiness | null> {
|
||||||
|
if (this.nextResult) {
|
||||||
|
return await this.nextResult;
|
||||||
|
}
|
||||||
|
return this.latestResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the latest readiness result without waiting */
|
||||||
|
getResult(): Readiness | 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 Readiness in JSON format.
|
||||||
|
*/
|
||||||
|
createHandler(): () => Promise<Response> {
|
||||||
|
return async () => {
|
||||||
|
const result = this.getResult();
|
||||||
|
if (!result) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
status: 'not_ready',
|
||||||
|
start: Date.now(),
|
||||||
|
duration: 0,
|
||||||
|
details: [],
|
||||||
|
} as Readiness),
|
||||||
|
{
|
||||||
|
status: httpStatusFromReadiness('not_ready'),
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return respondWithResult(result);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a Response object with the given Readiness in JSON format */
|
||||||
|
const respondWithResult = (result: Readiness) => {
|
||||||
|
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): number => {
|
||||||
|
if (status === 'ready') return 200;
|
||||||
|
if (status === 'degraded') return 200; // 206 could also be suitable, but let's avoid false alarms
|
||||||
|
if (status === 'fatal') return 503;
|
||||||
|
return 200; // not_ready, treat as ok to avoid false alarms
|
||||||
|
};
|
||||||
|
|
||||||
|
const withTimeout = async <T>(
|
||||||
|
promise: Promise<T>,
|
||||||
|
ms: number,
|
||||||
|
timeoutMessage: string,
|
||||||
|
): Promise<T> => {
|
||||||
|
let timeoutHandle: NodeJS.Timeout;
|
||||||
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||||
|
timeoutHandle = setTimeout(() => {
|
||||||
|
reject(new Error(timeoutMessage));
|
||||||
|
}, ms);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.race([promise, timeoutPromise]).finally(() => {
|
||||||
|
clearTimeout(timeoutHandle);
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,6 +1,16 @@
|
|||||||
{
|
{
|
||||||
"extends": "../tsconfig.base.json",
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"lib": ["es2023"],
|
||||||
|
"target": "es2023",
|
||||||
|
"module": "node20",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"composite": false
|
"composite": false
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2021",
|
|
||||||
"module": "ES2020",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"resolveJsonModule": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user