234 lines
6.8 KiB
TypeScript
234 lines
6.8 KiB
TypeScript
import { workerConfig } from '../../uptime.config'
|
|
import { getWorkerLocation } from './util'
|
|
import { MonitorState } from '../../uptime.types'
|
|
import { getStatus } from './monitor'
|
|
|
|
export interface Env {
|
|
UPTIMEFLARE_STATE: KVNamespace
|
|
}
|
|
|
|
export default {
|
|
async fetch(request: Request): Promise<Response> {
|
|
const workerLocation = request.cf?.colo
|
|
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)
|
|
|
|
if (target === undefined) {
|
|
return new Response('Target Not Found', { status: 404 })
|
|
}
|
|
|
|
const status = await getStatus(target)
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
location: workerLocation,
|
|
status: status,
|
|
}),
|
|
{
|
|
headers: {
|
|
'content-type': 'application/json;charset=UTF-8',
|
|
},
|
|
}
|
|
)
|
|
},
|
|
|
|
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
|
|
const workerLocation = (await getWorkerLocation()) || 'ERROR'
|
|
console.log(`Running scheduled event on ${workerLocation}...`)
|
|
|
|
// Read state, set init state if it doesn't exist
|
|
let state =
|
|
((await env.UPTIMEFLARE_STATE.get('state', {
|
|
type: 'json',
|
|
})) as unknown as MonitorState) ||
|
|
({
|
|
version: 1,
|
|
lastUpdate: 0,
|
|
overallUp: 0,
|
|
overallDown: 0,
|
|
incident: {},
|
|
latency: {},
|
|
} as MonitorState)
|
|
state.overallDown = 0
|
|
state.overallUp = 0
|
|
|
|
let statusChanged = false
|
|
const currentTimeSecond = Math.round(Date.now() / 1000)
|
|
|
|
// Check each monitor
|
|
// TODO: concurrent status check
|
|
|
|
for (const monitor of workerConfig.monitors) {
|
|
console.log(`[${workerLocation}] Checking ${monitor.name}...`)
|
|
|
|
let checkLocation = workerLocation
|
|
let 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)
|
|
status = { ping: 0, up: false, err: 'Error initiating check from remote worker' }
|
|
}
|
|
} else {
|
|
// Initiate a check from the current location
|
|
status = await getStatus(monitor)
|
|
}
|
|
|
|
// const status = await getStatus(monitor)
|
|
const currentTimeSecond = Math.round(Date.now() / 1000)
|
|
|
|
// Update counters
|
|
status.up ? state.overallUp++ : state.overallDown++
|
|
|
|
// Update incidents
|
|
// Create a dummy incident to store the start time of the monitoring and simplify logic
|
|
state.incident[monitor.id] = state.incident[monitor.id] || [
|
|
{
|
|
start: [currentTimeSecond],
|
|
end: currentTimeSecond,
|
|
error: ['dummy'],
|
|
},
|
|
]
|
|
// Then lastIncident here must not be undefined
|
|
const lastIncident = state.incident[monitor.id].slice(-1)[0]
|
|
|
|
if (status.up) {
|
|
// Current status is up
|
|
// close existing incident if any
|
|
if (lastIncident.end === undefined) {
|
|
lastIncident.end = currentTimeSecond
|
|
statusChanged = true
|
|
|
|
try {
|
|
await workerConfig.callbacks.onStatusChange(
|
|
env,
|
|
monitor,
|
|
true,
|
|
lastIncident.start[0],
|
|
currentTimeSecond,
|
|
'OK'
|
|
)
|
|
} catch (e) {
|
|
console.log('Error calling callback: ')
|
|
console.log(e)
|
|
}
|
|
}
|
|
} else {
|
|
// Current status is down
|
|
// open new incident if not already open
|
|
if (lastIncident.end !== undefined) {
|
|
state.incident[monitor.id].push({
|
|
start: [currentTimeSecond],
|
|
end: undefined,
|
|
error: [status.err],
|
|
})
|
|
statusChanged = true
|
|
|
|
try {
|
|
await workerConfig.callbacks.onStatusChange(
|
|
env,
|
|
monitor,
|
|
false,
|
|
currentTimeSecond,
|
|
currentTimeSecond,
|
|
status.err
|
|
)
|
|
} catch (e) {
|
|
console.log('Error calling callback: ')
|
|
console.log(e)
|
|
}
|
|
} else if (
|
|
lastIncident.end === undefined &&
|
|
lastIncident.error.slice(-1)[0] !== status.err
|
|
) {
|
|
// append if the error message changes
|
|
lastIncident.start.push(currentTimeSecond)
|
|
lastIncident.error.push(status.err)
|
|
statusChanged = true
|
|
|
|
try {
|
|
await workerConfig.callbacks.onStatusChange(
|
|
env,
|
|
monitor,
|
|
false,
|
|
lastIncident.start[0],
|
|
currentTimeSecond,
|
|
status.err
|
|
)
|
|
} catch (e) {
|
|
console.log('Error calling callback: ')
|
|
console.log(e)
|
|
}
|
|
}
|
|
|
|
try {
|
|
await workerConfig.callbacks.onIncident(
|
|
env,
|
|
monitor,
|
|
lastIncident.start[0],
|
|
currentTimeSecond,
|
|
status.err
|
|
)
|
|
} catch (e) {
|
|
console.log('Error calling callback: ')
|
|
console.log(e)
|
|
}
|
|
}
|
|
|
|
// append to latency data
|
|
let latencyLists = state.latency[monitor.id] || {
|
|
recent: [],
|
|
all: [],
|
|
}
|
|
|
|
const record = {
|
|
loc: checkLocation,
|
|
ping: status.ping,
|
|
time: currentTimeSecond,
|
|
}
|
|
latencyLists.recent.push(record)
|
|
if (latencyLists.all.length === 0 || currentTimeSecond - latencyLists.all.slice(-1)[0].time > 60 * 60) {
|
|
latencyLists.all.push(record)
|
|
}
|
|
|
|
// discard old data
|
|
while (latencyLists.recent[0]?.time < currentTimeSecond - 12 * 60 * 60) {
|
|
latencyLists.recent.shift()
|
|
}
|
|
while (latencyLists.all[0]?.time < currentTimeSecond - 90 * 24 * 60 * 60) {
|
|
latencyLists.all.shift()
|
|
}
|
|
state.latency[monitor.id] = latencyLists
|
|
}
|
|
|
|
// Update state
|
|
state.lastUpdate = Math.round(Date.now() / 1000)
|
|
if (
|
|
statusChanged ||
|
|
currentTimeSecond - state.lastUpdate >= workerConfig.kvWriteCooldownMinutes * 60
|
|
) {
|
|
state.lastUpdate = currentTimeSecond
|
|
await env.UPTIMEFLARE_STATE.put('state', JSON.stringify(state))
|
|
}
|
|
},
|
|
}
|