feat: add uptime runner agent
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user