server-status-check
Version:
query the server periodically to check whether it is up
175 lines (153 loc) • 6.84 kB
text/typescript
import Dev_ServerBugError from "../errors/Dev_ServerBugError";
import sleep from 'swiss-army-knifey/build/src/utils/sleep';
import { hoursAgo, secondsToYMWDHMSSentence } from "swiss-army-knifey/build/src/utils/getStringDate";
import { CheckStatusFunc, InitializedStatus, MaybeUninitializedStatus, WrappedStatus, WrappedStatusE } from "../requestor";
type GetRequestor = (uri: string) => Promise<string>;
type NotifyMessage = { subject: string; text: string; };
type NotifyAll = (conf: { sms: NotifyMessage, email: NotifyMessage; }) => Promise<[void, any]>;
type NotificationMessage = { subject: string; text: string; };
type NotificationMessageFor = { up: NotificationMessage; down: NotificationMessage; };
type NotificationMessages = { email: NotificationMessageFor; sms: NotificationMessageFor; };
type NotificationMessagesConfig = { all: NotificationMessageFor; } & NotificationMessages;
type StatusName = 'up' | 'down' | 'unknown';
type StatusStats = {
friendlyFullSentence: string;
friendlyStatusPhrase: string;
checkIntervalInMinutes: number;
dateLastChecked: string;
checksSpreeCount: number;
aliveForYMDHMS: string;
statusName: StatusName;
errors: Error[];
}
interface StatusCheckServiceConstructorPropsInterface {
checkStatus: CheckStatusFunc;
checkInterval: number;
getRequestor: GetRequestor;
notifyAll: NotifyAll;
onFailNotifyHoursInterval: number;
serverUri: string;
notificationMessages: NotificationMessagesConfig;
}
function isWrappedStatusWithInitializedErrors<T>(w: WrappedStatus<T>): w is WrappedStatusE<T> {
return w.errors !== undefined && Array.isArray(w.errors);
}
export default class StatusCheckService {
public checkInterval: number;
public userProvidedCheckStatus: CheckStatusFunc;
public lastChecked: number | null;
public lastEmailSentTime: number | null;
public notifyAll: NotifyAll;
public notificationMessages: NotificationMessages;
public onFailNotifyHoursInterval: number;
public wrappedStatus: WrappedStatusE<MaybeUninitializedStatus>;
public statusChecksCount: number;
public serverUri: string;
constructor({ notifyAll, serverUri, checkStatus, checkInterval, onFailNotifyHoursInterval, notificationMessages }: StatusCheckServiceConstructorPropsInterface) {
if (!notifyAll) {
throw new Dev_ServerBugError('StatusCheck service needs notifyAll to operate');
}
this.checkInterval = checkInterval;
this.userProvidedCheckStatus = checkStatus;
this.lastChecked = null;
this.lastEmailSentTime = null;
this.notifyAll = notifyAll;
this.onFailNotifyHoursInterval = onFailNotifyHoursInterval;
this.serverUri = serverUri;
this.wrappedStatus = { status: null, errors: [] };
this.statusChecksCount = 0;
if (notificationMessages.all !== undefined) {
this.notificationMessages = {
email: notificationMessages.all,
sms: notificationMessages.all,
};
} else {
this.notificationMessages = notificationMessages;
}
}
async run() {
while (1) {
this.handleNewStatus(await this.checkStatus());
await sleep(this.checkInterval * 1000);
}
}
statusChanged(status: InitializedStatus) {
return this.wrappedStatus.status !== status;
}
changedStatusOrDownStatusForMoreThanOneHour(status: InitializedStatus) {
const notifyOnlyOnStatusChanges = this.onFailNotifyHoursInterval === 0;
const notifiedTooLongAgo = this.lastEmailSentTime !== null && this.lastEmailSentTime <= hoursAgo(this.onFailNotifyHoursInterval);
return this.statusChanged(status) || (!notifyOnlyOnStatusChanges && (!status && notifiedTooLongAgo));
}
shouldNotifyCurrentStatus(status: InitializedStatus) {
return this.changedStatusOrDownStatusForMoreThanOneHour(status)
}
getStatusFriendlyPhrase(status: MaybeUninitializedStatus) {
return `${status === null
? 'Please wait: system status'
: status
? 'Good news: system'
: 'Alert: system'
} is ${this.getStatusName(status)}!`;
}
async notifyServiceStatus({ status }: WrappedStatus<InitializedStatus>) {
if (this.lastChecked !== null && status !== null) {
await this.notifyAll({
email: { ...this.notificationMessages.email[status ? "up" : "down"] },
sms: { ...this.notificationMessages.sms[status ? "up" : "down"] },
});
}
}
async handleNewStatus(w: WrappedStatus<InitializedStatus>) {
if (this.shouldNotifyCurrentStatus(w.status)) {
this.notifyServiceStatus(w);
this.lastEmailSentTime = Date.now();
}
this.wrappedStatus = isWrappedStatusWithInitializedErrors(w)
? w
: { ...w, errors: [] };
}
async checkStatus(): Promise<WrappedStatus<InitializedStatus>> {
let wrappedStatus = { status: false };
try {
wrappedStatus = await this.userProvidedCheckStatus();
} catch (err) {
this.handleFailedRequest(err as Error);
}
this.statusChecksCount++;
this.lastChecked = Date.now();
return wrappedStatus;
}
handleFailedRequest(err: Error) {
throw err;
}
getCheckIntervalInMinutes() {
return Math.ceil(this.checkInterval / 60);
}
getStatusStats(): StatusStats {
const dateLastChecked = this.lastChecked === null ? 'Never' : `${new Date(this.lastChecked).toLocaleDateString()}, ${new Date(this.lastChecked).toLocaleTimeString()}`;
const checkIntervalInMinutes = this.getCheckIntervalInMinutes();
const checksSpreeCount = this.statusChecksCount;
const friendlyStatusPhrase = this.wrappedStatus.status === null ? 'Unknown' : this.getStatusFriendlyPhrase(this.wrappedStatus.status);
const friendlyFullSentence = (this.lastChecked === null || this.wrappedStatus.status === null)
? `Not checked yet. Please wait ${checkIntervalInMinutes} min. or so and refresh this page.`
: `${friendlyStatusPhrase}. Last time I checked was on ${dateLastChecked.split(', ').join(' at ')} (checked ${checksSpreeCount} time${ checksSpreeCount === 1 ? '' : 's'} in a row without restarting). I check every ${checkIntervalInMinutes} min.`;
const secondsBetweenLastCheckAndNow = Math.floor((Date.now() - (this.lastChecked || Date.now())) / 1000);
const aliveSeconds = this.checkInterval * (this.statusChecksCount - 1) + (secondsBetweenLastCheckAndNow < this.checkInterval ? secondsBetweenLastCheckAndNow : 0);
const aliveForYMDHMS = secondsToYMWDHMSSentence(aliveSeconds);
return {
errors: this.wrappedStatus.errors,
friendlyFullSentence,
friendlyStatusPhrase,
checkIntervalInMinutes,
dateLastChecked,
checksSpreeCount,
statusName: this.getStatusName(this.wrappedStatus.status),
aliveForYMDHMS,
};
}
getStatusName(status: MaybeUninitializedStatus): StatusName {
if (status === null) return 'unknown';
return status ? 'up' : 'down';
}
}