Add moment.js as a dependency, enhance TypeScript configuration to exclude scripts, and introduce new examples for dependency injection and health checks.
This commit is contained in:
143
src/health/HealthCheck.ts
Normal file
143
src/health/HealthCheck.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import moment from 'moment';
|
||||
import type IDisposable from '../lang/IDisposable';
|
||||
import {Events} from '../lang/Events';
|
||||
|
||||
const checkForRecoverabilityTimeout = moment.duration(30, 'seconds');
|
||||
|
||||
interface IHealthCheckable {
|
||||
checkForRecoverability(): Promise<boolean>;
|
||||
}
|
||||
|
||||
export class HealthCheck {
|
||||
private readonly _environment: string;
|
||||
private readonly _app: string;
|
||||
private readonly _version: string;
|
||||
private readonly _regionName: string;
|
||||
private readonly _zone: string;
|
||||
private readonly _checks: IHealthCheckable[];
|
||||
private readonly _since: string;
|
||||
private readonly _events: Events;
|
||||
private _status: string;
|
||||
private _enabled: boolean;
|
||||
|
||||
constructor(environment: string, app: string, version: string, regionName: string, zone: string, ...checkForRecoverability: IHealthCheckable[]) {
|
||||
this._environment = environment;
|
||||
this._app = app;
|
||||
this._version = version;
|
||||
this._regionName = regionName;
|
||||
this._zone = zone;
|
||||
this._checks = checkForRecoverability;
|
||||
this._since = moment.utc().toISOString();
|
||||
this._status = 'starting';
|
||||
this._events = new Events();
|
||||
this._enabled = true;
|
||||
}
|
||||
|
||||
public start() {
|
||||
this._status = 'starting';
|
||||
this._events.emit('starting');
|
||||
this._events.emit('status-changed');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public getInfo() {
|
||||
return {
|
||||
environment: this._environment,
|
||||
app: this._app,
|
||||
version: this._version,
|
||||
region: this._regionName,
|
||||
zone: this._zone,
|
||||
since: this._since
|
||||
};
|
||||
}
|
||||
|
||||
public async checkHealth() {
|
||||
if (this._status !== 'ok') {
|
||||
return {
|
||||
status: this._status,
|
||||
environment: this._environment,
|
||||
app: this._app,
|
||||
version: this._version,
|
||||
zone: this._zone
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
this._checks.map(async check => {
|
||||
const isHealthy = await check.checkForRecoverability();
|
||||
|
||||
if (!isHealthy) {
|
||||
console.warn('Health check failed [%s]', check);
|
||||
} else {
|
||||
console.debug('Health check passed [%s]', check);
|
||||
}
|
||||
|
||||
return isHealthy;
|
||||
})
|
||||
);
|
||||
|
||||
const success = results.every(result => result === true);
|
||||
|
||||
if (success === true) {
|
||||
let status = this._status;
|
||||
|
||||
if (status === 'ok' && !this._enabled) {
|
||||
status = 'disabled';
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
environment: this._environment,
|
||||
app: this._app,
|
||||
version: this._version,
|
||||
zone: this._zone
|
||||
};
|
||||
}
|
||||
|
||||
this._events.emit('health-check-failed');
|
||||
|
||||
return {
|
||||
status: 'health-check-failed',
|
||||
environment: this._environment,
|
||||
app: this._app,
|
||||
version: this._version,
|
||||
zone: this._zone
|
||||
};
|
||||
} catch (e) {
|
||||
this._events.emit('health-check-failed');
|
||||
|
||||
console.warn('Health check failed', e);
|
||||
|
||||
return {
|
||||
status: 'health-check-failed',
|
||||
environment: this._environment,
|
||||
app: this._app,
|
||||
version: this._version,
|
||||
zone: this._zone,
|
||||
reason: (e as Error).message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public on(event: string, listener: (...args: unknown[]) => void | Promise<void>): IDisposable {
|
||||
return this._events.on(event, listener);
|
||||
}
|
||||
|
||||
public markReady() {
|
||||
this._status = 'ok';
|
||||
this._events.emit('ok');
|
||||
this._events.emit('status-changed');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public enable(): void {
|
||||
this._enabled = true;
|
||||
}
|
||||
|
||||
public disable(): void {
|
||||
this._enabled = false;
|
||||
}
|
||||
}
|
||||
137
src/health/HttpHealthCheck.ts
Normal file
137
src/health/HttpHealthCheck.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import type {Duration} from 'moment';
|
||||
import moment from 'moment';
|
||||
import {Agent as HttpAgent, request as httpRequest} from 'http';
|
||||
import {Agent as HttpsAgent, request as httpsRequest} from 'https';
|
||||
|
||||
interface IHttpHealthCheckConfig {
|
||||
protocol: string;
|
||||
hostname: string;
|
||||
port: string;
|
||||
keepalive?: boolean;
|
||||
headers?: Record<string, string>;
|
||||
reconnectTimeout?: string;
|
||||
requestTimeout?: string;
|
||||
}
|
||||
|
||||
export class HttpHealthCheck {
|
||||
private readonly _protocol: string;
|
||||
private readonly _hostname: string;
|
||||
private readonly _port: string;
|
||||
private readonly _keepalive: boolean;
|
||||
private readonly _headers: Record<string, string>;
|
||||
private readonly _connectTimeout: Duration;
|
||||
private readonly _requestTimeout: Duration;
|
||||
|
||||
constructor(config: IHttpHealthCheckConfig) {
|
||||
this._protocol = config.protocol;
|
||||
this._hostname = config.hostname;
|
||||
this._port = config.port;
|
||||
this._keepalive = config.keepalive ?? false;
|
||||
this._headers = config.headers ?? {};
|
||||
this._connectTimeout = moment.duration(config.reconnectTimeout ?? '30s');
|
||||
this._requestTimeout = moment.duration(config.requestTimeout ?? '30s');
|
||||
}
|
||||
|
||||
public checkForRecoverability(): Promise<boolean> {
|
||||
const method = 'GET';
|
||||
const path = '/';
|
||||
const isHttps = this._protocol === 'https';
|
||||
const Agent = isHttps ? HttpsAgent : HttpAgent;
|
||||
const request = isHttps ? httpsRequest : httpRequest;
|
||||
|
||||
const options = {
|
||||
hostname: this._hostname,
|
||||
port: this._port,
|
||||
path,
|
||||
method,
|
||||
headers: {
|
||||
'User-Agent': 'Health-Check/1.0',
|
||||
Connection: 'close'
|
||||
},
|
||||
timeout: this._connectTimeout.asMilliseconds(),
|
||||
agent: new Agent({
|
||||
keepAlive: this._keepalive,
|
||||
maxSockets: 1,
|
||||
maxFreeSockets: 0
|
||||
}),
|
||||
rejectUnauthorized: !isHttps
|
||||
};
|
||||
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
const req = request(options, res => {
|
||||
let isHealthy = false;
|
||||
|
||||
switch (res.statusCode) {
|
||||
case 200:
|
||||
case 301:
|
||||
case 404: {
|
||||
const allHeadersMatch = Object.keys(this._headers).every(headerKey => res.headers[headerKey] === this._headers[headerKey]);
|
||||
|
||||
if (allHeadersMatch) {
|
||||
isHealthy = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Fall through to default case
|
||||
console.warn(
|
||||
'HTTP health check to host [%s]:[%s] for [%s] [%s] failed - headers mismatch. Code [%s] headers [%j]',
|
||||
this._hostname,
|
||||
this._port,
|
||||
method,
|
||||
path,
|
||||
res.statusCode,
|
||||
res.headers
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn(
|
||||
'HTTP health check to host [%s]:[%s] for [%s] [%s] failed with code [%s] and headers [%j]',
|
||||
this._hostname,
|
||||
this._port,
|
||||
method,
|
||||
path,
|
||||
res.statusCode,
|
||||
res.headers
|
||||
);
|
||||
}
|
||||
|
||||
res.on('data', () => {
|
||||
// Ignore response data
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
resolve(isHealthy);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (e: Error & {code?: string}) => {
|
||||
reject(e);
|
||||
|
||||
if (e.code) {
|
||||
console.error('HTTP health check failed: %s', e.code);
|
||||
} else {
|
||||
console.error('HTTP health check failed', e);
|
||||
}
|
||||
});
|
||||
|
||||
req.setTimeout(this._requestTimeout.asMilliseconds(), () => {
|
||||
const err = new Error('HTTP health check timed out') as Error & {code?: string};
|
||||
err.code = 'ETIMEDOUT';
|
||||
|
||||
reject(err);
|
||||
|
||||
console.warn('HTTP health check timed out');
|
||||
|
||||
req.destroy();
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return `HttpHealthCheck[protocol=[${this._protocol}],hostname=[${this._hostname}],port=[${this._port}]]`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user