diff --git a/.prettierrc b/.prettierrc.yaml similarity index 79% rename from .prettierrc rename to .prettierrc.yaml index 6b9c164..75d2ffa 100644 --- a/.prettierrc +++ b/.prettierrc.yaml @@ -2,3 +2,4 @@ trailingComma: 'es5' tabWidth: 2 semi: false singleQuote: true +printWidth: 100 diff --git a/components/Header.tsx b/components/Header.tsx index aba0bcf..408a809 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -3,11 +3,7 @@ import classes from '@/styles/Header.module.css' import config from '@/uptime.config' export default function Header() { - const linkToElement = (link: { - label: string - link: string - highlight?: boolean - }) => { + const linkToElement = (link: { label: string; link: string; highlight?: boolean }) => { return ( - {config.page.links - .filter((link) => link.highlight) - .map(linkToElement)} + {config.page.links.filter((link) => link.highlight).map(linkToElement)} diff --git a/components/MonitorDetail.tsx b/components/MonitorDetail.tsx index 7f23165..03166e8 100644 --- a/components/MonitorDetail.tsx +++ b/components/MonitorDetail.tsx @@ -31,21 +31,17 @@ export default function MonitorDetail({ {monitor.name} - No data available, please make sure you have deployed your workers - with latest config and check your worker status! + No data available, please make sure you have deployed your workers with latest config and + check your worker status! ) const statusIcon = state.incident[monitor.id].slice(-1)[0].end === undefined ? ( - + ) : ( - + ) let totalTime = Date.now() / 1000 - state.incident[monitor.id][0].start[0] @@ -56,26 +52,15 @@ export default function MonitorDetail({ console.log(totalTime) console.log(downTime) - const uptimePercent = ( - ((totalTime - downTime) / totalTime) * - 100 - ).toPrecision(4) + const uptimePercent = (((totalTime - downTime) / totalTime) * 100).toPrecision(4) return ( <>
- + {statusIcon} {monitor.name} - + Overall: {uptimePercent}%
diff --git a/components/MonitorList.tsx b/components/MonitorList.tsx index 69548cf..9070886 100644 --- a/components/MonitorList.tsx +++ b/components/MonitorList.tsx @@ -2,13 +2,7 @@ import { MonitorState, MonitorTarget } from '@/uptime.types' import { Card, Center, Divider } from '@mantine/core' import MonitorDetail from './MonitorDetail' -export default function MonitorList({ - config, - state, -}: { - config: any - state: MonitorState -}) { +export default function MonitorList({ config, state }: { config: any; state: MonitorState }) { return (
- ) + let icon = if (state.overallUp === 0 && state.overallDown === 0) { statusString = 'No data yet' } else if (state.overallUp === 0) { statusString = 'All systems not operational' } else if (state.overallDown === 0) { statusString = 'All systems operational' - icon = ( - - ) + icon = } else { statusString = `Some systems not operational (${state.overallDown} out of ${ state.overallUp + state.overallDown @@ -31,11 +27,7 @@ export default function OverallStatus({ {statusString} - + <Title mt="sm" style={{ textAlign: 'center', color: '#70778c' }} order={5}> Last updated on:{' '} {`${new Date(state.lastUpdate * 1000).toLocaleString()} (${ Math.round(Date.now() / 1000) - state.lastUpdate diff --git a/pages/index.tsx b/pages/index.tsx index 995eeac..b2be9b0 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -26,8 +26,8 @@ export default function Home({ state }: { state: MonitorState }) { {state === undefined ? ( <Center> <Text fw={700}> - Monitor State is not defined now, please check your worker's - status and KV binding! + Monitor State is not defined now, please check your worker's status and KV + binding! </Text> </Center> ) : ( @@ -64,10 +64,7 @@ export async function getServerSideProps() { const { UPTIMEFLARE_STATE } = process.env as unknown as { UPTIMEFLARE_STATE: KVNamespace } - const state = (await UPTIMEFLARE_STATE?.get( - 'state', - 'json' - )) as unknown as MonitorState + const state = (await UPTIMEFLARE_STATE?.get('state', 'json')) as unknown as MonitorState return { props: { state } } } diff --git a/styles/Header.module.css b/styles/Header.module.css index 40dfe4c..bd038f4 100644 --- a/styles/Header.module.css +++ b/styles/Header.module.css @@ -2,8 +2,7 @@ height: rem(56px); margin-bottom: rem(120px); background-color: var(--mantine-color-body); - border-bottom: rem(1px) solid - light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); + border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); } .inner { @@ -24,10 +23,7 @@ font-weight: 500; @mixin hover { - background-color: light-dark( - var(--mantine-color-gray-0), - var(--mantine-color-dark-6) - ); + background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6)); } [data-mantine-color-scheme] &[data-active] { diff --git a/uptime.config.ts b/uptime.config.ts index 10c753a..1171884 100644 --- a/uptime.config.ts +++ b/uptime.config.ts @@ -10,9 +10,7 @@ const config = { ], }, callback: async (statusChangeMsg: string) => { - await fetch( - 'https://server.lyc8503.site/wepush?key=wepushkey&msg=' + statusChangeMsg - ) + await fetch('https://server.lyc8503.site/wepush?key=wepushkey&msg=' + statusChangeMsg) }, monitors: [ { diff --git a/worker/src/index.ts b/worker/src/index.ts index 2389f8e..31ec4fe 100644 --- a/worker/src/index.ts +++ b/worker/src/index.ts @@ -1,45 +1,47 @@ -import { connect } from 'cloudflare:sockets'; -import config from '../../uptime.config'; -import { fetchTimeout, getWorkerLocation, withTimeout } from './util'; -import { MonitorState, MonitorTarget } from '../../uptime.types'; +import { connect } from 'cloudflare:sockets' +import config from '../../uptime.config' +import { fetchTimeout, getWorkerLocation, withTimeout } from './util' +import { MonitorState, MonitorTarget } from '../../uptime.types' export interface Env { - UPTIMEFLARE_STATE: KVNamespace; + UPTIMEFLARE_STATE: KVNamespace } -async function getStatus(monitor: MonitorTarget): Promise<{ ping: number; up: boolean; err: string }> { +async function getStatus( + monitor: MonitorTarget +): Promise<{ ping: number; up: boolean; err: string }> { let status = { ping: 0, up: false, err: 'Unknown', - }; + } - const startTime = Date.now(); + const startTime = Date.now() if (monitor.method === 'TCP_PING') { // TCP port endpoint monitor // TODO: TCP timeout try { - const [hostname, port] = monitor.target.split(':'); + const [hostname, port] = monitor.target.split(':') // Write "PING\n" - const socket = connect({ hostname: hostname, port: Number(port) }); - const writer = socket.writable.getWriter(); - await writer.write(new TextEncoder().encode('PING\n')); + const socket = connect({ hostname: hostname, port: Number(port) }) + const writer = socket.writable.getWriter() + await writer.write(new TextEncoder().encode('PING\n')) // Can't do this: await socket.close() // https://github.com/cloudflare/workerd/issues/1305 - await withTimeout(monitor.timeout || 10000, socket.closed); + await withTimeout(monitor.timeout || 10000, socket.closed) - console.log(`${monitor.name} connected to ${monitor.target}`); + console.log(`${monitor.name} connected to ${monitor.target}`) - status.ping = Date.now() - startTime; - status.up = true; - status.err = ''; + status.ping = Date.now() - startTime + status.up = true + status.err = '' } catch (e: Error | any) { - console.log(`${monitor.name} errored with ${e.name}: ${e.message}`); - status.up = false; - status.err = e.name + ': ' + e.message; + console.log(`${monitor.name} errored with ${e.name}: ${e.message}`) + status.up = false + status.err = e.name + ': ' + e.message } } else { // HTTP endpoint monitor @@ -53,82 +55,86 @@ async function getStatus(monitor: MonitorTarget): Promise<{ ping: number; up: bo '100-599': -1, // Don't cache any status code, from https://developers.cloudflare.com/workers/runtime-apis/request/#requestinitcfproperties }, }, - }); + }) - console.log(`${monitor.name} responded with ${response.status}`); - status.ping = Date.now() - startTime; + console.log(`${monitor.name} responded with ${response.status}`) + status.ping = Date.now() - startTime if (monitor.expectedCodes) { if (!monitor.expectedCodes.includes(response.status)) { - status.up = false; - status.err = `Expected codes: ${JSON.stringify(monitor.expectedCodes)}, Got: ${response.status}`; - return status; + status.up = false + status.err = `Expected codes: ${JSON.stringify(monitor.expectedCodes)}, Got: ${ + response.status + }` + return status } } else { if (response.status < 200 || response.status > 299) { - status.up = false; - status.err = `Expected codes: 2xx, Got: ${response.status}`; - return status; + status.up = false + status.err = `Expected codes: 2xx, Got: ${response.status}` + return status } } if (monitor.responseKeyword) { - const responseBody = await response.text(); + const responseBody = await response.text() if (!responseBody.includes(monitor.responseKeyword)) { - status.up = false; - status.err = "HTTP response doesn't contain the configured keyword"; - return status; + status.up = false + status.err = "HTTP response doesn't contain the configured keyword" + return status } } - status.up = true; - status.err = ''; + status.up = true + status.err = '' } catch (e: any) { - console.log(`${monitor.name} errored with ${e.name}: ${e.message}`); + console.log(`${monitor.name} errored with ${e.name}: ${e.message}`) if (e.name === 'AbortError') { - status.ping = monitor.timeout || 10000; - status.up = false; - status.err = `Timeout after ${status.ping}ms`; + status.ping = monitor.timeout || 10000 + status.up = false + status.err = `Timeout after ${status.ping}ms` } else { - status.up = false; - status.err = e.name + ': ' + e.message; + status.up = false + status.err = e.name + ': ' + e.message } } } - return status; + return status } export default { async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> { - const workerLocation = (await getWorkerLocation()) || 'ERROR'; - console.log(`Running scheduled event on ${workerLocation}...`); + 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) || + ((await env.UPTIMEFLARE_STATE.get('state', { + type: 'json', + })) as unknown as MonitorState) || ({ lastUpdate: 0, overallUp: 0, overallDown: 0, incident: {}, latency: {}, - } as MonitorState); - state.overallDown = 0; - state.overallUp = 0; + } as MonitorState) + state.overallDown = 0 + state.overallUp = 0 // Check each monitor // TODO: callback exception handler // TODO: advanced status check // TODO: concurrent status check for (const monitor of config.monitors) { - console.log(`[${workerLocation}] Checking ${monitor.name}...`); + console.log(`[${workerLocation}] Checking ${monitor.name}...`) - const status = await getStatus(monitor); - const currentTimeSecond = Math.round(Date.now() / 1000); + const status = await getStatus(monitor) + const currentTimeSecond = Math.round(Date.now() / 1000) // Update counters - status.up ? state.overallUp++ : state.overallDown++; + status.up ? state.overallUp++ : state.overallDown++ // Update incidents @@ -139,21 +145,23 @@ export default { end: currentTimeSecond, error: ['dummy'], }, - ]; + ] // Then lastIncident here must not be undefined - const lastIncident = state.incident[monitor.id].slice(-1)[0]; - const timeString = new Date().toLocaleString(config.dateLocale, { timeZone: config.timezone }); + const lastIncident = state.incident[monitor.id].slice(-1)[0] + const timeString = new Date().toLocaleString(config.dateLocale, { + timeZone: config.timezone, + }) if (status.up) { // Current status is up // close existing incident if any if (lastIncident.end === undefined) { - lastIncident.end = currentTimeSecond; + lastIncident.end = currentTimeSecond await config.callback( `✔️${monitor.name} came back up at ${timeString} after ${Math.round( - (lastIncident.end - lastIncident.start.slice(-1)[0]) / 60, - )} minutes of downtime`, - ); + (lastIncident.end - lastIncident.start.slice(-1)[0]) / 60 + )} minutes of downtime` + ) } } else { // Current status is down @@ -163,13 +171,20 @@ export default { start: [currentTimeSecond], end: undefined, error: [status.err], - }); - await config.callback(`❌${monitor.name} went down at ${timeString} with error ${status.err}`); - } else if (lastIncident.end === undefined && lastIncident.error.slice(-1)[0] !== status.err) { + }) + await config.callback( + `❌${monitor.name} went down at ${timeString} with error ${status.err}` + ) + } 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); - await config.callback(`❌${monitor.name} is still down at ${timeString} with error ${status.err}`); + lastIncident.start.push(currentTimeSecond) + lastIncident.error.push(status.err) + await config.callback( + `❌${monitor.name} is still down at ${timeString} with error ${status.err}` + ) } } @@ -177,30 +192,30 @@ export default { let latencyLists = state.latency[monitor.id] || { recent: [], all: [], - }; + } const record = { loc: workerLocation, ping: status.ping, time: currentTimeSecond, - }; - latencyLists.recent.push(record); + } + latencyLists.recent.push(record) if (latencyLists.all.length === 0 || currentTimeSecond - latencyLists.all[0].time > 60 * 60) { - latencyLists.all.push(record); + latencyLists.all.push(record) } // discard old data while (latencyLists.recent[0]?.time < currentTimeSecond - 12 * 60 * 60) { - latencyLists.recent.shift(); + latencyLists.recent.shift() } while (latencyLists.all[0]?.time < currentTimeSecond - 90 * 24 * 60 * 60) { - latencyLists.all.shift(); + latencyLists.all.shift() } - state.latency[monitor.id] = latencyLists; + state.latency[monitor.id] = latencyLists } // Update state - state.lastUpdate = Math.round(Date.now() / 1000); - await env.UPTIMEFLARE_STATE.put('state', JSON.stringify(state)); + state.lastUpdate = Math.round(Date.now() / 1000) + await env.UPTIMEFLARE_STATE.put('state', JSON.stringify(state)) }, -}; +} diff --git a/worker/src/util.ts b/worker/src/util.ts index f4c6519..2de3f3b 100644 --- a/worker/src/util.ts +++ b/worker/src/util.ts @@ -1,27 +1,29 @@ async function getWorkerLocation() { - const res = await fetch('https://cloudflare.com/cdn-cgi/trace'); - const text = await res.text(); + const res = await fetch('https://cloudflare.com/cdn-cgi/trace') + const text = await res.text() - const colo = /^colo=(.*)$/m.exec(text)?.[1]; - return colo; + const colo = /^colo=(.*)$/m.exec(text)?.[1] + return colo } const fetchTimeout = ( url: string, ms: number, - { signal, ...options }: RequestInit<RequestInitCfProperties> | undefined = {}, + { signal, ...options }: RequestInit<RequestInitCfProperties> | undefined = {} ): Promise<Response> => { - const controller = new AbortController(); - const promise = fetch(url, { signal: controller.signal, ...options }); - if (signal) signal.addEventListener('abort', () => controller.abort()); - const timeout = setTimeout(() => controller.abort(), ms); - return promise.finally(() => clearTimeout(timeout)); -}; - -function withTimeout<T>(millis: number, promise: Promise<T>): Promise<T> { - const timeout = new Promise<T>((resolve, reject) => setTimeout(() => reject(new Error(`Promise timed out after ${millis}ms`)), millis)); - - return Promise.race([promise, timeout]); + const controller = new AbortController() + const promise = fetch(url, { signal: controller.signal, ...options }) + if (signal) signal.addEventListener('abort', () => controller.abort()) + const timeout = setTimeout(() => controller.abort(), ms) + return promise.finally(() => clearTimeout(timeout)) } -export { getWorkerLocation, fetchTimeout, withTimeout }; +function withTimeout<T>(millis: number, promise: Promise<T>): Promise<T> { + const timeout = new Promise<T>((resolve, reject) => + setTimeout(() => reject(new Error(`Promise timed out after ${millis}ms`)), millis) + ) + + return Promise.race([promise, timeout]) +} + +export { getWorkerLocation, fetchTimeout, withTimeout } diff --git a/worker/tsconfig.json b/worker/tsconfig.json index 2cb9189..cbea026 100644 --- a/worker/tsconfig.json +++ b/worker/tsconfig.json @@ -12,7 +12,9 @@ /* Language and Environment */ "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - "lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, + "lib": [ + "es2021" + ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, "jsx": "react" /* Specify what JSX code is generated. */, // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ @@ -31,7 +33,9 @@ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ - "types": ["@cloudflare/workers-types"] /* Specify type package names to be included without being referenced in a source file. */, + "types": [ + "@cloudflare/workers-types" + ] /* Specify type package names to be included without being referenced in a source file. */, // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ "resolveJsonModule": true /* Enable importing .json files */, // "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */