152 lines
4.7 KiB
TypeScript
152 lines
4.7 KiB
TypeScript
|
|
import type {
|
||
|
|
IAssumptionCheckJob,
|
||
|
|
IHttpCheckJob,
|
||
|
|
ITcpCheckJob,
|
||
|
|
IUptimeCheckResult,
|
||
|
|
TCheckJob,
|
||
|
|
TRunnerCheckResultStatus,
|
||
|
|
} from './interfaces.ts';
|
||
|
|
|
||
|
|
const DEFAULT_TIMEOUT_MS = 10000;
|
||
|
|
|
||
|
|
export class CheckExecutor {
|
||
|
|
constructor(private readonly runnerId: string) {}
|
||
|
|
|
||
|
|
public async execute(checkArg: TCheckJob): Promise<IUptimeCheckResult> {
|
||
|
|
const timeStarted = Date.now();
|
||
|
|
|
||
|
|
try {
|
||
|
|
const partialResult = await this.executeByType(checkArg, timeStarted);
|
||
|
|
const timeEnded = Date.now();
|
||
|
|
return {
|
||
|
|
checkId: checkArg.id,
|
||
|
|
runnerId: this.runnerId,
|
||
|
|
type: checkArg.type,
|
||
|
|
timing: {
|
||
|
|
timeStarted,
|
||
|
|
timeEnded,
|
||
|
|
duration: timeEnded - timeStarted,
|
||
|
|
},
|
||
|
|
metadata: checkArg.metadata,
|
||
|
|
...partialResult,
|
||
|
|
};
|
||
|
|
} catch (error) {
|
||
|
|
const timeEnded = Date.now();
|
||
|
|
const isTimeout = error instanceof Error && /timed out|aborted/i.test(error.message);
|
||
|
|
return {
|
||
|
|
checkId: checkArg.id,
|
||
|
|
runnerId: this.runnerId,
|
||
|
|
type: checkArg.type,
|
||
|
|
status: isTimeout ? 'timed out' : 'not ok',
|
||
|
|
message: error instanceof Error ? error.message : String(error),
|
||
|
|
timing: {
|
||
|
|
timeStarted,
|
||
|
|
timeEnded,
|
||
|
|
duration: timeEnded - timeStarted,
|
||
|
|
},
|
||
|
|
error: error instanceof Error ? error.message : String(error),
|
||
|
|
metadata: checkArg.metadata,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async executeByType(
|
||
|
|
checkArg: TCheckJob,
|
||
|
|
timeStartedArg: number,
|
||
|
|
): Promise<Omit<IUptimeCheckResult, 'checkId' | 'runnerId' | 'type' | 'timing' | 'metadata'>> {
|
||
|
|
switch (checkArg.type) {
|
||
|
|
case 'http':
|
||
|
|
return await this.executeHttpCheck(checkArg, timeStartedArg);
|
||
|
|
case 'tcp':
|
||
|
|
return await this.executeTcpCheck(checkArg, timeStartedArg);
|
||
|
|
case 'assumption':
|
||
|
|
return this.executeAssumptionCheck(checkArg);
|
||
|
|
default: {
|
||
|
|
const neverCheck: never = checkArg;
|
||
|
|
throw new Error(`Unsupported check type: ${JSON.stringify(neverCheck)}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async executeHttpCheck(
|
||
|
|
checkArg: IHttpCheckJob,
|
||
|
|
timeStartedArg: number,
|
||
|
|
): Promise<Omit<IUptimeCheckResult, 'checkId' | 'runnerId' | 'type' | 'timing' | 'metadata'>> {
|
||
|
|
const controller = new AbortController();
|
||
|
|
const timeout = setTimeout(
|
||
|
|
() => controller.abort('HTTP check timed out'),
|
||
|
|
this.getTimeout(checkArg),
|
||
|
|
);
|
||
|
|
const method = checkArg.method ?? (checkArg.expectedBodyIncludes ? 'GET' : 'HEAD');
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch(checkArg.url, {
|
||
|
|
method,
|
||
|
|
headers: checkArg.headers,
|
||
|
|
body: checkArg.body,
|
||
|
|
signal: controller.signal,
|
||
|
|
});
|
||
|
|
const expectedStatusCodes = checkArg.expectedStatusCodes ?? [200];
|
||
|
|
const statusMatches = expectedStatusCodes.includes(response.status);
|
||
|
|
const body = checkArg.expectedBodyIncludes ? await response.text() : '';
|
||
|
|
const bodyMatches = checkArg.expectedBodyIncludes
|
||
|
|
? body.includes(checkArg.expectedBodyIncludes)
|
||
|
|
: true;
|
||
|
|
const status: TRunnerCheckResultStatus = statusMatches && bodyMatches ? 'ok' : 'not ok';
|
||
|
|
|
||
|
|
return {
|
||
|
|
status,
|
||
|
|
statusCode: response.status,
|
||
|
|
responseTime: Date.now() - timeStartedArg,
|
||
|
|
message: status === 'ok'
|
||
|
|
? `HTTP ${response.status} matched expectations`
|
||
|
|
: `HTTP ${response.status} did not match expected status/body`,
|
||
|
|
};
|
||
|
|
} finally {
|
||
|
|
clearTimeout(timeout);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async executeTcpCheck(
|
||
|
|
checkArg: ITcpCheckJob,
|
||
|
|
timeStartedArg: number,
|
||
|
|
): Promise<Omit<IUptimeCheckResult, 'checkId' | 'runnerId' | 'type' | 'timing' | 'metadata'>> {
|
||
|
|
const timeoutMs = this.getTimeout(checkArg);
|
||
|
|
let timeoutId: number | undefined;
|
||
|
|
let connection: Deno.TcpConn | undefined;
|
||
|
|
|
||
|
|
try {
|
||
|
|
connection = await Promise.race([
|
||
|
|
Deno.connect({ hostname: checkArg.host, port: checkArg.port }),
|
||
|
|
new Promise<never>((_resolve, reject) => {
|
||
|
|
timeoutId = setTimeout(() => reject(new Error('TCP check timed out')), timeoutMs);
|
||
|
|
}),
|
||
|
|
]);
|
||
|
|
|
||
|
|
return {
|
||
|
|
status: 'ok',
|
||
|
|
responseTime: Date.now() - timeStartedArg,
|
||
|
|
message: `TCP connection established to ${checkArg.host}:${checkArg.port}`,
|
||
|
|
};
|
||
|
|
} finally {
|
||
|
|
if (timeoutId !== undefined) {
|
||
|
|
clearTimeout(timeoutId);
|
||
|
|
}
|
||
|
|
connection?.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private executeAssumptionCheck(
|
||
|
|
checkArg: IAssumptionCheckJob,
|
||
|
|
): Omit<IUptimeCheckResult, 'checkId' | 'runnerId' | 'type' | 'timing' | 'metadata'> {
|
||
|
|
return {
|
||
|
|
status: checkArg.assumedStatus,
|
||
|
|
message: checkArg.message ?? `Assumed status: ${checkArg.assumedStatus}`,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
private getTimeout(checkArg: TCheckJob): number {
|
||
|
|
return checkArg.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
||
|
|
}
|
||
|
|
}
|