diff --git a/uptime.config.ts b/uptime.config.ts index 00be288..1b354fd 100644 --- a/uptime.config.ts +++ b/uptime.config.ts @@ -1,4 +1,6 @@ -const pageConfig = { +import type { PageConfig, UptimeConfig } from './uptime.types' + +const pageConfig: PageConfig = { // Title for your status page title: "lyc8503's Status Page", // Links shown at the header of your status page, could set `highlight` to `true` @@ -9,7 +11,7 @@ const pageConfig = { ], } -const workerConfig = { +const workerConfig: UptimeConfig = { // Write KV at most every 3 minutes unless the status changed. kvWriteCooldownMinutes: 3, // Define all your monitors here @@ -61,12 +63,12 @@ const workerConfig = { notification: { // [Optional] apprise API server URL // if not specified, no notification will be sent - appriseApiServer: "https://apprise.example.com/notify", + appriseApiServer: 'https://apprise.example.com/notify', // [Optional] recipient URL for apprise, refer to https://github.com/caronc/apprise // if not specified, no notification will be sent - recipientUrl: "tgram://bottoken/ChatID", + recipientUrl: 'tgram://bottoken/ChatID', // [Optional] timezone used in notification messages, default to "Etc/GMT" - timeZone: "Asia/Shanghai", + timeZone: 'Asia/Shanghai', // [Optional] grace period in minutes before sending a notification // notification will be sent only if the monitor is down for N continuous checks after the initial failure // if not specified, notification will be sent immediately @@ -83,7 +85,6 @@ const workerConfig = { ) => { // This callback will be called when there's a status change for any monitor // Write any Typescript code here - // This will not follow the grace period settings and will be called immediately when the status changes // You need to handle the grace period manually if you want to implement it }, diff --git a/uptime.types.ts b/uptime.types.ts index aa1748b..1e2a734 100644 --- a/uptime.types.ts +++ b/uptime.types.ts @@ -1,4 +1,5 @@ -type MonitorState = { +export type MonitorState = { + version: number lastUpdate: number overallUp: number overallDown: number @@ -28,7 +29,7 @@ type MonitorState = { > } -type MonitorTarget = { +export type MonitorTarget = { id: string name: string method: string // "TCP_PING" or Http Method (e.g. GET, POST, OPTIONS, etc.) @@ -45,4 +46,39 @@ type MonitorTarget = { responseKeyword?: string } -export type { MonitorState, MonitorTarget } +export type UptimeConfig = { + kvWriteCooldownMinutes: number + monitors: MonitorTarget[] + notification?: { + appriseApiServer?: string + recipientUrl?: string + timeZone?: string + gracePeriod?: number + } + callbacks?: { + onStatusChange?: ( + env: any, + monitor: any, + isUp: boolean, + timeIncidentStart: number, + timeNow: number, + reason: string + ) => Promise + onIncident?: ( + env: any, + monitor: any, + timeIncidentStart: number, + timeNow: number, + reason: string + ) => Promise + } +} + +export type PageConfig = { + title: string + links: Array<{ + link: string + label: string + highlight?: boolean + }> +} diff --git a/worker/src/index.ts b/worker/src/index.ts index 3fe7478..a61bedc 100644 --- a/worker/src/index.ts +++ b/worker/src/index.ts @@ -1,41 +1,49 @@ import { workerConfig } from '../../uptime.config' import { formatStatusChangeNotification, getWorkerLocation, notifyWithApprise } from './util' import { MonitorState } from '../../uptime.types' -import { getStatus } from './monitor' +import { getStatus, Status } from './monitor' export interface Env { UPTIMEFLARE_STATE: KVNamespace } +interface CheckLocationWorkerResult { + location: string + status: Record +} + export default { async fetch(request: Request): Promise { - const workerLocation = request.cf?.colo + const workerLocation = request.cf?.colo as string console.log(`Handling request event at ${workerLocation}...`) if (request.method !== 'POST') { return new Response('Remote worker is working...', { status: 405 }) } - const targetId = (await request.json<{ target: string }>())['target'] - const target = workerConfig.monitors.find((m) => m.id === targetId) + const targetIds = new Set((await request.json<{ targets: string[] }>())['targets']) + const targets = workerConfig.monitors.filter(({ id }) => targetIds.has(id)) - if (target === undefined) { + if (!targets.length) { return new Response('Target Not Found', { status: 404 }) } - const status = await getStatus(target) + const status: Record = {} - return new Response( - JSON.stringify({ - location: workerLocation, - status: status, - }), - { - headers: { - 'content-type': 'application/json;charset=UTF-8', - }, - } - ) + for (const target of targets) { + status[target.id] = await getStatus(target) + } + + const result: CheckLocationWorkerResult = { + location: workerLocation, + status, + } + + return new Response(JSON.stringify(result), { + headers: { + 'content-type': 'application/json;charset=UTF-8', + }, + }) }, async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise { @@ -43,14 +51,14 @@ export default { console.log(`Running scheduled event on ${workerLocation}...`) // Auxiliary function to format notification and send it via apprise - let formatAndNotify = async ( + const formatAndNotify = async ( monitor: any, isUp: boolean, timeIncidentStart: number, timeNow: number, reason: string ) => { - if (workerConfig.notification?.appriseApiServer && workerConfig.notification?.recipientUrl) { + if (workerConfig.notification?.appriseApiServer && workerConfig.notification.recipientUrl) { const notification = formatStatusChangeNotification( monitor, isUp, @@ -89,6 +97,33 @@ export default { let statusChanged = false const currentTimeSecond = Math.round(Date.now() / 1000) + // Check monitors with `checkLocationWorkerRoute` + const groupedCheckLocation = new Map() + const groupedCheckLocationResult = new Map() + workerConfig.monitors.forEach(({ id, checkLocationWorkerRoute: url }) => { + if (!url) return + const targets = groupedCheckLocation.get(url) || [] + if (!groupedCheckLocation.has(url)) { + groupedCheckLocation.set(url, targets) + } + targets.push(id) + }) + for (const [url, targets] of groupedCheckLocation) { + // Initiate a check from a different location + console.log('Calling worker: ' + url) + try { + const resp = await ( + await fetch(url, { + method: 'POST', + body: JSON.stringify({ targets }), + }) + ).json() + groupedCheckLocationResult.set(url, resp) + } catch (err) { + console.log('Error calling worker: ' + err) + } + } + // Check each monitor // TODO: concurrent status check for (const monitor of workerConfig.monitors) { @@ -96,24 +131,16 @@ export default { let monitorStatusChanged = false let checkLocation = workerLocation - let status + let status: Status if (monitor.checkLocationWorkerRoute) { - // Initiate a check from a different location - try { - console.log('Calling worker: ' + monitor.checkLocationWorkerRoute) - const resp = await ( - await fetch(monitor.checkLocationWorkerRoute, { - method: 'POST', - body: JSON.stringify({ - target: monitor.id, - }), - }) - ).json<{ location: string; status: { ping: number; up: boolean; err: string } }>() - checkLocation = resp.location - status = resp.status - } catch (err) { - console.log('Error calling worker: ' + err) + // Get check result from a different location + const result = groupedCheckLocationResult.get(monitor.checkLocationWorkerRoute) + const resultStatus = result?.status[monitor.id] + if (resultStatus) { + checkLocation = result.location + status = resultStatus + } else { status = { ping: 0, up: false, err: 'Error initiating check from remote worker' } } } else { @@ -164,7 +191,7 @@ export default { } console.log('Calling config onStatusChange callback...') - await workerConfig.callbacks.onStatusChange( + await workerConfig.callbacks?.onStatusChange?.( env, monitor, true, @@ -230,7 +257,7 @@ export default { if (monitorStatusChanged) { console.log('Calling config onStatusChange callback...') - await workerConfig.callbacks.onStatusChange( + await workerConfig.callbacks?.onStatusChange?.( env, monitor, false, @@ -246,7 +273,7 @@ export default { try { console.log('Calling config onIncident callback...') - await workerConfig.callbacks.onIncident( + await workerConfig.callbacks?.onIncident?.( env, monitor, currentIncident.start[0], diff --git a/worker/src/monitor.ts b/worker/src/monitor.ts index 216c06e..4a4bbc6 100644 --- a/worker/src/monitor.ts +++ b/worker/src/monitor.ts @@ -2,9 +2,13 @@ import { connect } from "cloudflare:sockets"; import { MonitorTarget } from "../../uptime.types"; import { withTimeout, fetchTimeout } from "./util"; -export async function getStatus( - monitor: MonitorTarget -): Promise<{ ping: number; up: boolean; err: string }> { +export interface Status { + ping: number + up: boolean + err: string +} + +export async function getStatus(monitor: MonitorTarget): Promise { let status = { ping: 0, up: false,