format code and no semi

pull/5/head
lyc8503 2023-11-06 00:51:41 +08:00
parent eba65bd654
commit 87611700c3
11 changed files with 135 additions and 157 deletions

View File

@ -2,3 +2,4 @@ trailingComma: 'es5'
tabWidth: 2 tabWidth: 2
semi: false semi: false
singleQuote: true singleQuote: true
printWidth: 100

View File

@ -3,11 +3,7 @@ import classes from '@/styles/Header.module.css'
import config from '@/uptime.config' import config from '@/uptime.config'
export default function Header() { export default function Header() {
const linkToElement = (link: { const linkToElement = (link: { label: string; link: string; highlight?: boolean }) => {
label: string
link: string
highlight?: boolean
}) => {
return ( return (
<a <a
key={link.label} key={link.label}
@ -46,9 +42,7 @@ export default function Header() {
</Group> </Group>
<Group gap={5} hiddenFrom="sm"> <Group gap={5} hiddenFrom="sm">
{config.page.links {config.page.links.filter((link) => link.highlight).map(linkToElement)}
.filter((link) => link.highlight)
.map(linkToElement)}
</Group> </Group>
</Container> </Container>
</header> </header>

View File

@ -31,21 +31,17 @@ export default function MonitorDetail({
{monitor.name} {monitor.name}
</Text> </Text>
<Text mt="sm" fw={700}> <Text mt="sm" fw={700}>
No data available, please make sure you have deployed your workers No data available, please make sure you have deployed your workers with latest config and
with latest config and check your worker status! check your worker status!
</Text> </Text>
</> </>
) )
const statusIcon = const statusIcon =
state.incident[monitor.id].slice(-1)[0].end === undefined ? ( state.incident[monitor.id].slice(-1)[0].end === undefined ? (
<IconAlertCircle <IconAlertCircle style={{ width: '1.25em', height: '1.25em', color: '#b91c1c' }} />
style={{ width: '1.25em', height: '1.25em', color: '#b91c1c' }}
/>
) : ( ) : (
<IconCircleCheck <IconCircleCheck style={{ width: '1.25em', height: '1.25em', color: '#059669' }} />
style={{ width: '1.25em', height: '1.25em', color: '#059669' }}
/>
) )
let totalTime = Date.now() / 1000 - state.incident[monitor.id][0].start[0] 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(totalTime)
console.log(downTime) console.log(downTime)
const uptimePercent = ( const uptimePercent = (((totalTime - downTime) / totalTime) * 100).toPrecision(4)
((totalTime - downTime) / totalTime) *
100
).toPrecision(4)
return ( return (
<> <>
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Text <Text mt="sm" fw={700} style={{ display: 'inline-flex', alignItems: 'center' }}>
mt="sm"
fw={700}
style={{ display: 'inline-flex', alignItems: 'center' }}
>
{statusIcon} {monitor.name} {statusIcon} {monitor.name}
</Text> </Text>
<Text <Text mt="sm" fw={700} style={{ display: 'inline', color: getColor(uptimePercent, true) }}>
mt="sm"
fw={700}
style={{ display: 'inline', color: getColor(uptimePercent, true) }}
>
Overall: {uptimePercent}% Overall: {uptimePercent}%
</Text> </Text>
</div> </div>

View File

@ -2,13 +2,7 @@ import { MonitorState, MonitorTarget } from '@/uptime.types'
import { Card, Center, Divider } from '@mantine/core' import { Card, Center, Divider } from '@mantine/core'
import MonitorDetail from './MonitorDetail' import MonitorDetail from './MonitorDetail'
export default function MonitorList({ export default function MonitorList({ config, state }: { config: any; state: MonitorState }) {
config,
state,
}: {
config: any
state: MonitorState
}) {
return ( return (
<Center> <Center>
<Card <Card

View File

@ -7,18 +7,14 @@ export default function OverallStatus({
state: { overallUp: number; overallDown: number; lastUpdate: number } state: { overallUp: number; overallDown: number; lastUpdate: number }
}) { }) {
let statusString = '' let statusString = ''
let icon = ( let icon = <IconAlertCircle style={{ width: 64, height: 64, color: '#b91c1c' }} />
<IconAlertCircle style={{ width: 64, height: 64, color: '#b91c1c' }} />
)
if (state.overallUp === 0 && state.overallDown === 0) { if (state.overallUp === 0 && state.overallDown === 0) {
statusString = 'No data yet' statusString = 'No data yet'
} else if (state.overallUp === 0) { } else if (state.overallUp === 0) {
statusString = 'All systems not operational' statusString = 'All systems not operational'
} else if (state.overallDown === 0) { } else if (state.overallDown === 0) {
statusString = 'All systems operational' statusString = 'All systems operational'
icon = ( icon = <IconCircleCheck style={{ width: 64, height: 64, color: '#059669' }} />
<IconCircleCheck style={{ width: 64, height: 64, color: '#059669' }} />
)
} else { } else {
statusString = `Some systems not operational (${state.overallDown} out of ${ statusString = `Some systems not operational (${state.overallDown} out of ${
state.overallUp + state.overallDown state.overallUp + state.overallDown
@ -31,11 +27,7 @@ export default function OverallStatus({
<Title mt="sm" style={{ textAlign: 'center' }} order={1}> <Title mt="sm" style={{ textAlign: 'center' }} order={1}>
{statusString} {statusString}
</Title> </Title>
<Title <Title mt="sm" style={{ textAlign: 'center', color: '#70778c' }} order={5}>
mt="sm"
style={{ textAlign: 'center', color: '#70778c' }}
order={5}
>
Last updated on:{' '} Last updated on:{' '}
{`${new Date(state.lastUpdate * 1000).toLocaleString()} (${ {`${new Date(state.lastUpdate * 1000).toLocaleString()} (${
Math.round(Date.now() / 1000) - state.lastUpdate Math.round(Date.now() / 1000) - state.lastUpdate

View File

@ -26,8 +26,8 @@ export default function Home({ state }: { state: MonitorState }) {
{state === undefined ? ( {state === undefined ? (
<Center> <Center>
<Text fw={700}> <Text fw={700}>
Monitor State is not defined now, please check your worker&apos;s Monitor State is not defined now, please check your worker&apos;s status and KV
status and KV binding! binding!
</Text> </Text>
</Center> </Center>
) : ( ) : (
@ -64,10 +64,7 @@ export async function getServerSideProps() {
const { UPTIMEFLARE_STATE } = process.env as unknown as { const { UPTIMEFLARE_STATE } = process.env as unknown as {
UPTIMEFLARE_STATE: KVNamespace UPTIMEFLARE_STATE: KVNamespace
} }
const state = (await UPTIMEFLARE_STATE?.get( const state = (await UPTIMEFLARE_STATE?.get('state', 'json')) as unknown as MonitorState
'state',
'json'
)) as unknown as MonitorState
return { props: { state } } return { props: { state } }
} }

View File

@ -2,8 +2,7 @@
height: rem(56px); height: rem(56px);
margin-bottom: rem(120px); margin-bottom: rem(120px);
background-color: var(--mantine-color-body); background-color: var(--mantine-color-body);
border-bottom: rem(1px) solid border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
} }
.inner { .inner {
@ -24,10 +23,7 @@
font-weight: 500; font-weight: 500;
@mixin hover { @mixin hover {
background-color: light-dark( background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
var(--mantine-color-gray-0),
var(--mantine-color-dark-6)
);
} }
[data-mantine-color-scheme] &[data-active] { [data-mantine-color-scheme] &[data-active] {

View File

@ -10,9 +10,7 @@ const config = {
], ],
}, },
callback: async (statusChangeMsg: string) => { callback: async (statusChangeMsg: string) => {
await fetch( await fetch('https://server.lyc8503.site/wepush?key=wepushkey&msg=' + statusChangeMsg)
'https://server.lyc8503.site/wepush?key=wepushkey&msg=' + statusChangeMsg
)
}, },
monitors: [ monitors: [
{ {

View File

@ -1,45 +1,47 @@
import { connect } from 'cloudflare:sockets'; import { connect } from 'cloudflare:sockets'
import config from '../../uptime.config'; import config from '../../uptime.config'
import { fetchTimeout, getWorkerLocation, withTimeout } from './util'; import { fetchTimeout, getWorkerLocation, withTimeout } from './util'
import { MonitorState, MonitorTarget } from '../../uptime.types'; import { MonitorState, MonitorTarget } from '../../uptime.types'
export interface Env { 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 = { let status = {
ping: 0, ping: 0,
up: false, up: false,
err: 'Unknown', err: 'Unknown',
}; }
const startTime = Date.now(); const startTime = Date.now()
if (monitor.method === 'TCP_PING') { if (monitor.method === 'TCP_PING') {
// TCP port endpoint monitor // TCP port endpoint monitor
// TODO: TCP timeout // TODO: TCP timeout
try { try {
const [hostname, port] = monitor.target.split(':'); const [hostname, port] = monitor.target.split(':')
// Write "PING\n" // Write "PING\n"
const socket = connect({ hostname: hostname, port: Number(port) }); const socket = connect({ hostname: hostname, port: Number(port) })
const writer = socket.writable.getWriter(); const writer = socket.writable.getWriter()
await writer.write(new TextEncoder().encode('PING\n')); await writer.write(new TextEncoder().encode('PING\n'))
// Can't do this: await socket.close() // Can't do this: await socket.close()
// https://github.com/cloudflare/workerd/issues/1305 // 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.ping = Date.now() - startTime
status.up = true; status.up = true
status.err = ''; status.err = ''
} catch (e: Error | any) { } catch (e: Error | any) {
console.log(`${monitor.name} errored with ${e.name}: ${e.message}`); console.log(`${monitor.name} errored with ${e.name}: ${e.message}`)
status.up = false; status.up = false
status.err = e.name + ': ' + e.message; status.err = e.name + ': ' + e.message
} }
} else { } else {
// HTTP endpoint monitor // 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 '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}`); console.log(`${monitor.name} responded with ${response.status}`)
status.ping = Date.now() - startTime; status.ping = Date.now() - startTime
if (monitor.expectedCodes) { if (monitor.expectedCodes) {
if (!monitor.expectedCodes.includes(response.status)) { if (!monitor.expectedCodes.includes(response.status)) {
status.up = false; status.up = false
status.err = `Expected codes: ${JSON.stringify(monitor.expectedCodes)}, Got: ${response.status}`; status.err = `Expected codes: ${JSON.stringify(monitor.expectedCodes)}, Got: ${
return status; response.status
}`
return status
} }
} else { } else {
if (response.status < 200 || response.status > 299) { if (response.status < 200 || response.status > 299) {
status.up = false; status.up = false
status.err = `Expected codes: 2xx, Got: ${response.status}`; status.err = `Expected codes: 2xx, Got: ${response.status}`
return status; return status
} }
} }
if (monitor.responseKeyword) { if (monitor.responseKeyword) {
const responseBody = await response.text(); const responseBody = await response.text()
if (!responseBody.includes(monitor.responseKeyword)) { if (!responseBody.includes(monitor.responseKeyword)) {
status.up = false; status.up = false
status.err = "HTTP response doesn't contain the configured keyword"; status.err = "HTTP response doesn't contain the configured keyword"
return status; return status
} }
} }
status.up = true; status.up = true
status.err = ''; status.err = ''
} catch (e: any) { } 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') { if (e.name === 'AbortError') {
status.ping = monitor.timeout || 10000; status.ping = monitor.timeout || 10000
status.up = false; status.up = false
status.err = `Timeout after ${status.ping}ms`; status.err = `Timeout after ${status.ping}ms`
} else { } else {
status.up = false; status.up = false
status.err = e.name + ': ' + e.message; status.err = e.name + ': ' + e.message
} }
} }
} }
return status; return status
} }
export default { export default {
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> { async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
const workerLocation = (await getWorkerLocation()) || 'ERROR'; const workerLocation = (await getWorkerLocation()) || 'ERROR'
console.log(`Running scheduled event on ${workerLocation}...`); console.log(`Running scheduled event on ${workerLocation}...`)
// Read state, set init state if it doesn't exist // Read state, set init state if it doesn't exist
let state = 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, lastUpdate: 0,
overallUp: 0, overallUp: 0,
overallDown: 0, overallDown: 0,
incident: {}, incident: {},
latency: {}, latency: {},
} as MonitorState); } as MonitorState)
state.overallDown = 0; state.overallDown = 0
state.overallUp = 0; state.overallUp = 0
// Check each monitor // Check each monitor
// TODO: callback exception handler // TODO: callback exception handler
// TODO: advanced status check // TODO: advanced status check
// TODO: concurrent status check // TODO: concurrent status check
for (const monitor of config.monitors) { for (const monitor of config.monitors) {
console.log(`[${workerLocation}] Checking ${monitor.name}...`); console.log(`[${workerLocation}] Checking ${monitor.name}...`)
const status = await getStatus(monitor); const status = await getStatus(monitor)
const currentTimeSecond = Math.round(Date.now() / 1000); const currentTimeSecond = Math.round(Date.now() / 1000)
// Update counters // Update counters
status.up ? state.overallUp++ : state.overallDown++; status.up ? state.overallUp++ : state.overallDown++
// Update incidents // Update incidents
@ -139,21 +145,23 @@ export default {
end: currentTimeSecond, end: currentTimeSecond,
error: ['dummy'], error: ['dummy'],
}, },
]; ]
// Then lastIncident here must not be undefined // Then lastIncident here must not be undefined
const lastIncident = state.incident[monitor.id].slice(-1)[0]; const lastIncident = state.incident[monitor.id].slice(-1)[0]
const timeString = new Date().toLocaleString(config.dateLocale, { timeZone: config.timezone }); const timeString = new Date().toLocaleString(config.dateLocale, {
timeZone: config.timezone,
})
if (status.up) { if (status.up) {
// Current status is up // Current status is up
// close existing incident if any // close existing incident if any
if (lastIncident.end === undefined) { if (lastIncident.end === undefined) {
lastIncident.end = currentTimeSecond; lastIncident.end = currentTimeSecond
await config.callback( await config.callback(
`✔️${monitor.name} came back up at ${timeString} after ${Math.round( `✔️${monitor.name} came back up at ${timeString} after ${Math.round(
(lastIncident.end - lastIncident.start.slice(-1)[0]) / 60, (lastIncident.end - lastIncident.start.slice(-1)[0]) / 60
)} minutes of downtime`, )} minutes of downtime`
); )
} }
} else { } else {
// Current status is down // Current status is down
@ -163,13 +171,20 @@ export default {
start: [currentTimeSecond], start: [currentTimeSecond],
end: undefined, end: undefined,
error: [status.err], error: [status.err],
}); })
await config.callback(`${monitor.name} went down at ${timeString} with error ${status.err}`); await config.callback(
} else if (lastIncident.end === undefined && lastIncident.error.slice(-1)[0] !== status.err) { `${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 // append if the error message changes
lastIncident.start.push(currentTimeSecond); lastIncident.start.push(currentTimeSecond)
lastIncident.error.push(status.err); lastIncident.error.push(status.err)
await config.callback(`${monitor.name} is still down at ${timeString} with error ${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] || { let latencyLists = state.latency[monitor.id] || {
recent: [], recent: [],
all: [], all: [],
}; }
const record = { const record = {
loc: workerLocation, loc: workerLocation,
ping: status.ping, ping: status.ping,
time: currentTimeSecond, time: currentTimeSecond,
}; }
latencyLists.recent.push(record); latencyLists.recent.push(record)
if (latencyLists.all.length === 0 || currentTimeSecond - latencyLists.all[0].time > 60 * 60) { if (latencyLists.all.length === 0 || currentTimeSecond - latencyLists.all[0].time > 60 * 60) {
latencyLists.all.push(record); latencyLists.all.push(record)
} }
// discard old data // discard old data
while (latencyLists.recent[0]?.time < currentTimeSecond - 12 * 60 * 60) { 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) { 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 // Update state
state.lastUpdate = Math.round(Date.now() / 1000); state.lastUpdate = Math.round(Date.now() / 1000)
await env.UPTIMEFLARE_STATE.put('state', JSON.stringify(state)); await env.UPTIMEFLARE_STATE.put('state', JSON.stringify(state))
}, },
}; }

View File

@ -1,27 +1,29 @@
async function getWorkerLocation() { async function getWorkerLocation() {
const res = await fetch('https://cloudflare.com/cdn-cgi/trace'); const res = await fetch('https://cloudflare.com/cdn-cgi/trace')
const text = await res.text(); const text = await res.text()
const colo = /^colo=(.*)$/m.exec(text)?.[1]; const colo = /^colo=(.*)$/m.exec(text)?.[1]
return colo; return colo
} }
const fetchTimeout = ( const fetchTimeout = (
url: string, url: string,
ms: number, ms: number,
{ signal, ...options }: RequestInit<RequestInitCfProperties> | undefined = {}, { signal, ...options }: RequestInit<RequestInitCfProperties> | undefined = {}
): Promise<Response> => { ): Promise<Response> => {
const controller = new AbortController(); const controller = new AbortController()
const promise = fetch(url, { signal: controller.signal, ...options }); const promise = fetch(url, { signal: controller.signal, ...options })
if (signal) signal.addEventListener('abort', () => controller.abort()); if (signal) signal.addEventListener('abort', () => controller.abort())
const timeout = setTimeout(() => controller.abort(), ms); const timeout = setTimeout(() => controller.abort(), ms)
return promise.finally(() => clearTimeout(timeout)); 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]);
} }
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 }

View File

@ -12,7 +12,9 @@
/* Language and Environment */ /* Language and Environment */
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, "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. */, "jsx": "react" /* Specify what JSX code is generated. */,
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "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. */ // "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. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ // "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. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
"resolveJsonModule": true /* Enable importing .json files */, "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. */ // "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */