Files
uptimerunner/ts/check-executor.ts
2026-04-29 19:48:14 +00:00

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;
}
}