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
semi: false
singleQuote: true
printWidth: 100

View File

@ -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 (
<a
key={link.label}
@ -46,9 +42,7 @@ export default function Header() {
</Group>
<Group gap={5} hiddenFrom="sm">
{config.page.links
.filter((link) => link.highlight)
.map(linkToElement)}
{config.page.links.filter((link) => link.highlight).map(linkToElement)}
</Group>
</Container>
</header>

View File

@ -31,21 +31,17 @@ export default function MonitorDetail({
{monitor.name}
</Text>
<Text mt="sm" fw={700}>
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!
</Text>
</>
)
const statusIcon =
state.incident[monitor.id].slice(-1)[0].end === undefined ? (
<IconAlertCircle
style={{ width: '1.25em', height: '1.25em', color: '#b91c1c' }}
/>
<IconAlertCircle style={{ width: '1.25em', height: '1.25em', color: '#b91c1c' }} />
) : (
<IconCircleCheck
style={{ width: '1.25em', height: '1.25em', color: '#059669' }}
/>
<IconCircleCheck style={{ width: '1.25em', height: '1.25em', color: '#059669' }} />
)
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 (
<>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Text
mt="sm"
fw={700}
style={{ display: 'inline-flex', alignItems: 'center' }}
>
<Text mt="sm" fw={700} style={{ display: 'inline-flex', alignItems: 'center' }}>
{statusIcon} {monitor.name}
</Text>
<Text
mt="sm"
fw={700}
style={{ display: 'inline', color: getColor(uptimePercent, true) }}
>
<Text mt="sm" fw={700} style={{ display: 'inline', color: getColor(uptimePercent, true) }}>
Overall: {uptimePercent}%
</Text>
</div>

View File

@ -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 (
<Center>
<Card

View File

@ -7,18 +7,14 @@ export default function OverallStatus({
state: { overallUp: number; overallDown: number; lastUpdate: number }
}) {
let statusString = ''
let icon = (
<IconAlertCircle style={{ width: 64, height: 64, color: '#b91c1c' }} />
)
let icon = <IconAlertCircle style={{ width: 64, height: 64, color: '#b91c1c' }} />
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 = (
<IconCircleCheck style={{ width: 64, height: 64, color: '#059669' }} />
)
icon = <IconCircleCheck style={{ width: 64, height: 64, color: '#059669' }} />
} else {
statusString = `Some systems not operational (${state.overallDown} out of ${
state.overallUp + state.overallDown
@ -31,11 +27,7 @@ export default function OverallStatus({
<Title mt="sm" style={{ textAlign: 'center' }} order={1}>
{statusString}
</Title>
<Title
mt="sm"
style={{ textAlign: 'center', color: '#70778c' }}
order={5}
>
<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

View File

@ -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&apos;s
status and KV binding!
Monitor State is not defined now, please check your worker&apos;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 } }
}

View File

@ -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] {

View File

@ -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: [
{

View File

@ -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))
},
};
}

View File

@ -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 }

View File

@ -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. */