format code
parent
6157bc0879
commit
eba65bd654
|
|
@ -0,0 +1,4 @@
|
||||||
|
trailingComma: 'es5'
|
||||||
|
tabWidth: 2
|
||||||
|
semi: false
|
||||||
|
singleQuote: true
|
||||||
11
README.md
11
README.md
|
|
@ -1,36 +1,39 @@
|
||||||
# UptimeFlare
|
# UptimeFlare
|
||||||
|
|
||||||
Another **serverless (and free!)** uptime monitoring & status page on Cloudflare Workers with more advanced features and nice UI.
|
Another **serverless (and free!)** uptime monitoring & status page on Cloudflare Workers with more advanced features and nice UI.
|
||||||
|
|
||||||
## ⭐Features
|
## ⭐Features
|
||||||
|
|
||||||
- **Opensource** & Easy to deploy (in 10 minutes without local tools) & Free
|
- **Opensource** & Easy to deploy (in 10 minutes without local tools) & Free
|
||||||
|
|
||||||
#### Monitoring
|
#### Monitoring
|
||||||
|
|
||||||
- Up to 50 **2-minute** interval check on free plan (Up to unlimited 1-min check on paid Workers plan)
|
- Up to 50 **2-minute** interval check on free plan (Up to unlimited 1-min check on paid Workers plan)
|
||||||
- Monitoring for **HTTP/HTTPS/TCP port**
|
- Monitoring for **HTTP/HTTPS/TCP port**
|
||||||
- Up to 90-day uptime history and uptime percentage
|
- Up to 90-day uptime history and uptime percentage
|
||||||
- Custom request method & headers & body for HTTP(s)
|
- Custom request method & headers & body for HTTP(s)
|
||||||
- Custom status code & keyword check for HTTP(s)
|
- Custom status code & keyword check for HTTP(s)
|
||||||
- Email Notifications & Customizable Webhook
|
- Email Notifications & Customizable Webhook
|
||||||
|
|
||||||
#### Status page
|
#### Status page
|
||||||
|
|
||||||
- **Interactive ping(response time) chart** for all type of monitors
|
- **Interactive ping(response time) chart** for all type of monitors
|
||||||
- Responsive UI & Follow system theme
|
- Responsive UI & Follow system theme
|
||||||
- Customizable status page
|
- Customizable status page
|
||||||
- Use your own domain with CNAME
|
- Use your own domain with CNAME
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
My status page (Online demo): https://uptimeflare.pages.dev/
|
My status page (Online demo): https://uptimeflare.pages.dev/
|
||||||
|
|
||||||
Some screenshots: TODO
|
Some screenshots: TODO
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
|
|
||||||
## More documentation
|
## More documentation
|
||||||
|
|
||||||
|
|
||||||
## TODOs
|
## TODOs
|
||||||
|
|
||||||
- SSL?
|
- SSL?
|
||||||
- Slack / Telegram
|
- Slack / Telegram
|
||||||
- Incident timeline
|
- Incident timeline
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
import { MonitorState, MonitorTarget } from "@/uptime.types";
|
import { MonitorState, MonitorTarget } from '@/uptime.types'
|
||||||
import { Box, Tooltip } from "@mantine/core";
|
import { Box, Tooltip } from '@mantine/core'
|
||||||
|
|
||||||
|
export default function DetailBar({
|
||||||
export default function DetailBar({ monitor, state }: { monitor: MonitorTarget, state: MonitorState }) {
|
monitor,
|
||||||
|
state,
|
||||||
|
}: {
|
||||||
|
monitor: MonitorTarget
|
||||||
|
state: MonitorState
|
||||||
|
}) {
|
||||||
// const day = new Date()
|
// const day = new Date()
|
||||||
// day.setHours(0, 0, 0, 0)
|
// day.setHours(0, 0, 0, 0)
|
||||||
// const dayStart = day.getTime()
|
// const dayStart = day.getTime()
|
||||||
|
|
@ -12,9 +17,8 @@ export default function DetailBar({ monitor, state }: { monitor: MonitorTarget,
|
||||||
}
|
}
|
||||||
|
|
||||||
const uptimePercentBars = []
|
const uptimePercentBars = []
|
||||||
|
|
||||||
for (let i = 89; i > 0; i--) {
|
|
||||||
|
|
||||||
|
for (let i = 89; i > 0; i--) {
|
||||||
let dayDownTime = 0
|
let dayDownTime = 0
|
||||||
let dayTotalTime = 0
|
let dayTotalTime = 0
|
||||||
|
|
||||||
|
|
@ -22,14 +26,31 @@ export default function DetailBar({ monitor, state }: { monitor: MonitorTarget,
|
||||||
|
|
||||||
uptimePercentBars.push(
|
uptimePercentBars.push(
|
||||||
<Tooltip key={i} label="Tooltip">
|
<Tooltip key={i} label="Tooltip">
|
||||||
<div style={{ height: '20px', width: '7px', background: 'green', borderRadius: '2px', marginLeft: '1px', marginRight: '1px' }}/>
|
<div
|
||||||
|
style={{
|
||||||
|
height: '20px',
|
||||||
|
width: '7px',
|
||||||
|
background: 'green',
|
||||||
|
borderRadius: '2px',
|
||||||
|
marginLeft: '1px',
|
||||||
|
marginRight: '1px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box style={{ display: 'flex', flexWrap: 'nowrap', marginTop: '10px', marginBottom: '5px' }} visibleFrom='540'>
|
<Box
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'nowrap',
|
||||||
|
marginTop: '10px',
|
||||||
|
marginBottom: '5px',
|
||||||
|
}}
|
||||||
|
visibleFrom="540"
|
||||||
|
>
|
||||||
{uptimePercentBars}
|
{uptimePercentBars}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|
@ -42,4 +63,4 @@ export default function DetailBar({ monitor, state }: { monitor: MonitorTarget,
|
||||||
</Box> */}
|
</Box> */}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,18 @@
|
||||||
import { Line } from "react-chartjs-2"
|
import { Line } from 'react-chartjs-2'
|
||||||
import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip as ChartTooltip, Legend, TimeScale } from 'chart.js'
|
import {
|
||||||
|
Chart as ChartJS,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
PointElement,
|
||||||
|
LineElement,
|
||||||
|
Title,
|
||||||
|
Tooltip as ChartTooltip,
|
||||||
|
Legend,
|
||||||
|
TimeScale,
|
||||||
|
} from 'chart.js'
|
||||||
import 'chartjs-adapter-moment'
|
import 'chartjs-adapter-moment'
|
||||||
import { MonitorState, MonitorTarget } from "@/uptime.types"
|
import { MonitorState, MonitorTarget } from '@/uptime.types'
|
||||||
import { iataToCountry } from "@/util/iata"
|
import { iataToCountry } from '@/util/iata'
|
||||||
|
|
||||||
ChartJS.register(
|
ChartJS.register(
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
|
|
@ -15,10 +25,18 @@ ChartJS.register(
|
||||||
TimeScale
|
TimeScale
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export default function DetailChart({
|
||||||
export default function DetailChart({ monitor, state }: { monitor: MonitorTarget, state: MonitorState }) {
|
monitor,
|
||||||
|
state,
|
||||||
const latencyData = state.latency[monitor.id].recent.map((point) => ({ x: point.time * 1000, y: point.ping, loc: point.loc }))
|
}: {
|
||||||
|
monitor: MonitorTarget
|
||||||
|
state: MonitorState
|
||||||
|
}) {
|
||||||
|
const latencyData = state.latency[monitor.id].recent.map((point) => ({
|
||||||
|
x: point.time * 1000,
|
||||||
|
y: point.ping,
|
||||||
|
loc: point.loc,
|
||||||
|
}))
|
||||||
|
|
||||||
let data = {
|
let data = {
|
||||||
datasets: [
|
datasets: [
|
||||||
|
|
@ -28,11 +46,11 @@ export default function DetailChart({ monitor, state }: { monitor: MonitorTarget
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
radius: 0,
|
radius: 0,
|
||||||
cubicInterpolationMode: 'monotone' as const,
|
cubicInterpolationMode: 'monotone' as const,
|
||||||
tension: 0.4
|
tension: 0.4,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
let options = {
|
let options = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
|
|
@ -41,7 +59,7 @@ export default function DetailChart({ monitor, state }: { monitor: MonitorTarget
|
||||||
intersect: false,
|
intersect: false,
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
duration: 0
|
duration: 0,
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
tooltip: {
|
tooltip: {
|
||||||
|
|
@ -50,16 +68,16 @@ export default function DetailChart({ monitor, state }: { monitor: MonitorTarget
|
||||||
if (item.parsed.y) {
|
if (item.parsed.y) {
|
||||||
return `${item.parsed.y}ms (${iataToCountry(item.raw.loc)})`
|
return `${item.parsed.y}ms (${iataToCountry(item.raw.loc)})`
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
display: false
|
display: false,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: 'Response times(ms)',
|
text: 'Response times(ms)',
|
||||||
align: 'start' as const
|
align: 'start' as const,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
|
|
@ -68,10 +86,10 @@ export default function DetailChart({ monitor, state }: { monitor: MonitorTarget
|
||||||
ticks: {
|
ticks: {
|
||||||
source: 'auto' as const,
|
source: 'auto' as const,
|
||||||
maxRotation: 0,
|
maxRotation: 0,
|
||||||
autoSkip: true
|
autoSkip: true,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -80,4 +98,3 @@ export default function DetailChart({ monitor, state }: { monitor: MonitorTarget
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
import { Container, Group, Text } from '@mantine/core';
|
import { Container, Group, Text } from '@mantine/core'
|
||||||
import classes from '@/styles/Header.module.css';
|
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}
|
||||||
href={link.link}
|
href={link.link}
|
||||||
target='_blank'
|
target="_blank"
|
||||||
className={classes.link}
|
className={classes.link}
|
||||||
data-active={link.highlight}
|
data-active={link.highlight}
|
||||||
>
|
>
|
||||||
|
|
@ -23,21 +25,32 @@ export default function Header() {
|
||||||
<header className={classes.header}>
|
<header className={classes.header}>
|
||||||
<Container size="md" className={classes.inner}>
|
<Container size="md" className={classes.inner}>
|
||||||
<div>
|
<div>
|
||||||
<a href='https://github.com/lyc8503/UptimeFlare' target='_blank'>
|
<a href="https://github.com/lyc8503/UptimeFlare" target="_blank">
|
||||||
<Text size='xl' span>🕒</Text>
|
<Text size="xl" span>
|
||||||
<Text size='xl' span fw={700} variant="gradient" gradient={{ from: 'blue', to: 'cyan', deg: 90 }}>UptimeFlare</Text>
|
🕒
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
size="xl"
|
||||||
|
span
|
||||||
|
fw={700}
|
||||||
|
variant="gradient"
|
||||||
|
gradient={{ from: 'blue', to: 'cyan', deg: 90 }}
|
||||||
|
>
|
||||||
|
UptimeFlare
|
||||||
|
</Text>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Group gap={5} visibleFrom='sm'>
|
<Group gap={5} visibleFrom="sm">
|
||||||
{config.page.links.map(linkToElement)}
|
{config.page.links.map(linkToElement)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Group gap={5} hiddenFrom='sm'>
|
<Group gap={5} hiddenFrom="sm">
|
||||||
{config.page.links.filter(link => link.highlight).map(linkToElement)}
|
{config.page.links
|
||||||
|
.filter((link) => link.highlight)
|
||||||
|
.map(linkToElement)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
</Container>
|
</Container>
|
||||||
</header>
|
</header>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
|
import { Text } from '@mantine/core'
|
||||||
import { Text } from "@mantine/core"
|
import { MonitorState, MonitorTarget } from '@/uptime.types'
|
||||||
import { MonitorState, MonitorTarget } from "@/uptime.types"
|
import { IconAlertCircle, IconCircleCheck } from '@tabler/icons-react'
|
||||||
import { IconAlertCircle, IconCircleCheck } from "@tabler/icons-react"
|
import DetailChart from './DetailChart'
|
||||||
import DetailChart from "./DetailChart"
|
import DetailBar from './DetailBar'
|
||||||
import DetailBar from "./DetailBar"
|
|
||||||
|
|
||||||
function getColor(percent: number | string, darker: boolean): string {
|
function getColor(percent: number | string, darker: boolean): string {
|
||||||
percent = Number(percent)
|
percent = Number(percent)
|
||||||
|
|
@ -18,36 +17,71 @@ function getColor(percent: number | string, darker: boolean): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MonitorDetail({ monitor, state }: { monitor: MonitorTarget, state: MonitorState }) {
|
export default function MonitorDetail({
|
||||||
|
monitor,
|
||||||
|
state,
|
||||||
|
}: {
|
||||||
|
monitor: MonitorTarget
|
||||||
|
state: MonitorState
|
||||||
|
}) {
|
||||||
|
if (!state.latency[monitor.id])
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Text mt="sm" fw={700}>
|
||||||
|
{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!
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
if (!state.latency[monitor.id]) return (
|
const statusIcon =
|
||||||
<>
|
state.incident[monitor.id].slice(-1)[0].end === undefined ? (
|
||||||
<Text mt='sm' fw={700}>{monitor.name}</Text>
|
<IconAlertCircle
|
||||||
<Text mt='sm' fw={700}>No data available, please make sure you have deployed your workers with latest config and check your worker status!</Text>
|
style={{ width: '1.25em', height: '1.25em', color: '#b91c1c' }}
|
||||||
</>
|
/>
|
||||||
)
|
) : (
|
||||||
|
<IconCircleCheck
|
||||||
const statusIcon = state.incident[monitor.id].slice(-1)[0].end === undefined ? <IconAlertCircle style={{ width: '1.25em', height: '1.25em', color: '#b91c1c' }} /> : <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]
|
||||||
let downTime = 0
|
let downTime = 0
|
||||||
for (let incident of state.incident[monitor.id]) {
|
for (let incident of state.incident[monitor.id]) {
|
||||||
downTime += (incident.end ?? (Date.now() / 1000)) - incident.start[0]
|
downTime += (incident.end ?? Date.now() / 1000) - incident.start[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(totalTime)
|
console.log(totalTime)
|
||||||
console.log(downTime)
|
console.log(downTime)
|
||||||
const uptimePercent = ((totalTime - downTime) / totalTime * 100).toPrecision(4)
|
const uptimePercent = (
|
||||||
|
((totalTime - downTime) / totalTime) *
|
||||||
|
100
|
||||||
|
).toPrecision(4)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<Text mt='sm' fw={700} style={{ display: 'inline-flex', alignItems: 'center' }}>{statusIcon} {monitor.name}</Text>
|
<Text
|
||||||
<Text mt='sm' fw={700} style={{ display: 'inline', color: getColor(uptimePercent, true) }}>Overall: {uptimePercent}%</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) }}
|
||||||
|
>
|
||||||
|
Overall: {uptimePercent}%
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DetailBar monitor={monitor} state={state} />
|
<DetailBar monitor={monitor} state={state} />
|
||||||
<DetailChart monitor={monitor} state={state} />
|
<DetailChart monitor={monitor} state={state} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,26 @@
|
||||||
import { MonitorState, MonitorTarget } from "@/uptime.types";
|
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({ config, state }: { config: any, state: MonitorState }) {
|
|
||||||
|
|
||||||
|
export default function MonitorList({
|
||||||
|
config,
|
||||||
|
state,
|
||||||
|
}: {
|
||||||
|
config: any
|
||||||
|
state: MonitorState
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<Center>
|
<Center>
|
||||||
<Card shadow="sm" padding="lg" radius="md" ml="xl" mr="xl" mt="xl" withBorder style={{ width: '865px' }}>
|
<Card
|
||||||
|
shadow="sm"
|
||||||
|
padding="lg"
|
||||||
|
radius="md"
|
||||||
|
ml="xl"
|
||||||
|
mr="xl"
|
||||||
|
mt="xl"
|
||||||
|
withBorder
|
||||||
|
style={{ width: '865px' }}
|
||||||
|
>
|
||||||
{config.monitors.map((monitor: MonitorTarget) => (
|
{config.monitors.map((monitor: MonitorTarget) => (
|
||||||
<div key={monitor.id}>
|
<div key={monitor.id}>
|
||||||
<Card.Section ml="xs" mr="xs">
|
<Card.Section ml="xs" mr="xs">
|
||||||
|
|
@ -17,6 +31,5 @@ export default function MonitorList({ config, state }: { config: any, state: Mon
|
||||||
))}
|
))}
|
||||||
</Card>
|
</Card>
|
||||||
</Center>
|
</Center>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const NoSsr = (props: { children: string | number | boolean | React.ReactElement<any, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | React.PromiseLikeOfReactNode | null | undefined }) => (
|
const NoSsr = (props: {
|
||||||
<React.Fragment>{props.children}</React.Fragment>
|
children:
|
||||||
)
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| React.ReactElement<any, string | React.JSXElementConstructor<any>>
|
||||||
|
| Iterable<React.ReactNode>
|
||||||
|
| React.ReactPortal
|
||||||
|
| React.PromiseLikeOfReactNode
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
}) => <React.Fragment>{props.children}</React.Fragment>
|
||||||
|
|
||||||
export default dynamic(() => Promise.resolve(NoSsr), {
|
export default dynamic(() => Promise.resolve(NoSsr), {
|
||||||
ssr: false
|
ssr: false,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,46 @@
|
||||||
import { Center, Title } from '@mantine/core'
|
import { Center, Title } from '@mantine/core'
|
||||||
import { IconCircleCheck, IconAlertCircle } from '@tabler/icons-react'
|
import { IconCircleCheck, IconAlertCircle } from '@tabler/icons-react'
|
||||||
|
|
||||||
export default function OverallStatus({ state }: { state: { overallUp: number, overallDown: number, lastUpdate: number }}) {
|
export default function OverallStatus({
|
||||||
|
state,
|
||||||
|
}: {
|
||||||
|
state: { overallUp: number; overallDown: number; lastUpdate: number }
|
||||||
|
}) {
|
||||||
let statusString = ''
|
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) {
|
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 = <IconCircleCheck style={{ width: 64, height: 64, color: '#059669' }} />
|
icon = (
|
||||||
|
<IconCircleCheck style={{ width: 64, height: 64, color: '#059669' }} />
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
statusString = `Some systems not operational (${state.overallDown} out of ${state.overallUp + state.overallDown})`
|
statusString = `Some systems not operational (${state.overallDown} out of ${
|
||||||
|
state.overallUp + state.overallDown
|
||||||
|
})`
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<>
|
return (
|
||||||
<Center>
|
<>
|
||||||
{icon}
|
<Center>{icon}</Center>
|
||||||
</Center>
|
<Title mt="sm" style={{ textAlign: 'center' }} order={1}>
|
||||||
<Title mt="sm" style={{ textAlign: 'center' }} order={1}>{statusString}</Title>
|
{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} sec ago)`}</Title>
|
</Title>
|
||||||
</>)
|
<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
|
||||||
|
} sec ago)`}
|
||||||
|
</Title>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"postcss-preset-mantine": "^1.8.0",
|
"postcss-preset-mantine": "^1.8.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
|
"prettier": "3.0.3",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"wrangler": "^3.13.1"
|
"wrangler": "^3.13.1"
|
||||||
}
|
}
|
||||||
|
|
@ -3988,6 +3989,21 @@
|
||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/prettier/-/prettier-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/printable-characters": {
|
"node_modules/printable-characters": {
|
||||||
"version": "1.0.42",
|
"version": "1.0.42",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/printable-characters/-/printable-characters-1.0.42.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/printable-characters/-/printable-characters-1.0.42.tgz",
|
||||||
|
|
@ -7905,6 +7921,12 @@
|
||||||
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"prettier": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/prettier/-/prettier-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"printable-characters": {
|
"printable-characters": {
|
||||||
"version": "1.0.42",
|
"version": "1.0.42",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/printable-characters/-/printable-characters-1.0.42.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/printable-characters/-/printable-characters-1.0.42.tgz",
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"postcss-preset-mantine": "^1.8.0",
|
"postcss-preset-mantine": "^1.8.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
|
"prettier": "3.0.3",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"wrangler": "^3.13.1"
|
"wrangler": "^3.13.1"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import '@mantine/core/styles.css';
|
import '@mantine/core/styles.css'
|
||||||
import type { AppProps } from 'next/app'
|
import type { AppProps } from 'next/app'
|
||||||
import { MantineProvider } from '@mantine/core';
|
import { MantineProvider } from '@mantine/core'
|
||||||
import NoSsr from '@/components/NoSsr';
|
import NoSsr from '@/components/NoSsr'
|
||||||
|
|
||||||
|
|
||||||
export default function App({ Component, pageProps }: AppProps) {
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
return (
|
return (
|
||||||
<NoSsr>
|
<NoSsr>
|
||||||
<MantineProvider defaultColorScheme='auto'>
|
<MantineProvider defaultColorScheme="auto">
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</NoSsr>
|
</NoSsr>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Html, Head, Main, NextScript } from 'next/document'
|
import { Html, Head, Main, NextScript } from 'next/document'
|
||||||
import { ColorSchemeScript } from '@mantine/core';
|
import { ColorSchemeScript } from '@mantine/core'
|
||||||
|
|
||||||
export default function Document() {
|
export default function Document() {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -23,39 +23,51 @@ export default function Home({ state }: { state: MonitorState }) {
|
||||||
<main className={inter.className}>
|
<main className={inter.className}>
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
{
|
{state === undefined ? (
|
||||||
state === undefined ?
|
<Center>
|
||||||
(
|
<Text fw={700}>
|
||||||
<Center>
|
Monitor State is not defined now, please check your worker's
|
||||||
<Text fw={700}>
|
status and KV binding!
|
||||||
Monitor State is not defined now, please check your worker's status and KV binding!
|
</Text>
|
||||||
</Text>
|
</Center>
|
||||||
</Center>
|
) : (
|
||||||
) :
|
<div>
|
||||||
(
|
<OverallStatus state={state} />
|
||||||
<div>
|
<MonitorList config={config} state={state} />
|
||||||
<OverallStatus state={state} />
|
</div>
|
||||||
<MonitorList config={config} state={state} />
|
)}
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
<Divider mt='lg' />
|
<Divider mt="lg" />
|
||||||
<Center>
|
<Center>
|
||||||
<Text size='xs' mt='xs' mb='xs'>
|
<Text size="xs" mt="xs" mb="xs">
|
||||||
Open-source monitoring and status page powered by <a href='https://github.com/lyc8503/UptimeFlare' target='_blank'>Uptimeflare</a> and <a href='https://www.cloudflare.com/' target='_blank'>Cloudflare</a>, made with ❤ by <a href='https://github.com/lyc8503' target='_blank'>lyc8503</a>.
|
Open-source monitoring and status page powered by{' '}
|
||||||
|
<a href="https://github.com/lyc8503/UptimeFlare" target="_blank">
|
||||||
|
Uptimeflare
|
||||||
|
</a>{' '}
|
||||||
|
and{' '}
|
||||||
|
<a href="https://www.cloudflare.com/" target="_blank">
|
||||||
|
Cloudflare
|
||||||
|
</a>
|
||||||
|
, made with ❤ by{' '}
|
||||||
|
<a href="https://github.com/lyc8503" target="_blank">
|
||||||
|
lyc8503
|
||||||
|
</a>
|
||||||
|
.
|
||||||
</Text>
|
</Text>
|
||||||
</Center>
|
</Center>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function getServerSideProps() {
|
export async function getServerSideProps() {
|
||||||
const { UPTIMEFLARE_STATE } = process.env as unknown as { UPTIMEFLARE_STATE: KVNamespace }
|
const { UPTIMEFLARE_STATE } = process.env as unknown as {
|
||||||
const state = await UPTIMEFLARE_STATE?.get('state', 'json') as unknown as MonitorState
|
UPTIMEFLARE_STATE: KVNamespace
|
||||||
|
}
|
||||||
|
const state = (await UPTIMEFLARE_STATE?.get(
|
||||||
|
'state',
|
||||||
|
'json'
|
||||||
|
)) as unknown as MonitorState
|
||||||
|
|
||||||
return { props: { state } }
|
return { props: { state } }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,4 @@ module.exports = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
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 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 {
|
.inner {
|
||||||
|
|
@ -23,11 +24,14 @@
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
@mixin hover {
|
@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] {
|
[data-mantine-color-scheme] &[data-active] {
|
||||||
background-color: var(--mantine-color-blue-filled);
|
background-color: var(--mantine-color-blue-filled);
|
||||||
color: var(--mantine-color-white);
|
color: var(--mantine-color-white);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
|
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
dateLocale: "zh-CN",
|
dateLocale: 'zh-CN',
|
||||||
timezone: "Asia/Shanghai",
|
timezone: 'Asia/Shanghai',
|
||||||
page: {
|
page: {
|
||||||
title: "lyc8503's Status Page",
|
title: "lyc8503's Status Page",
|
||||||
links: [
|
links: [
|
||||||
{ link: 'https://github.com/lyc8503', label: 'GitHub' },
|
{ link: 'https://github.com/lyc8503', label: 'GitHub' },
|
||||||
{ link: 'https://blog.lyc8503.site/', label: 'Blog' },
|
{ link: 'https://blog.lyc8503.site/', label: 'Blog' },
|
||||||
{ link: 'mailto:me@lyc8503.site', label: 'Email Me', highlight: true }
|
{ link: 'mailto:me@lyc8503.site', label: 'Email Me', highlight: true },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
callback: async (statusChangeMsg: string) => {
|
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: [
|
monitors: [
|
||||||
{
|
{
|
||||||
|
|
@ -23,9 +23,9 @@ const config = {
|
||||||
expectedCode: [200],
|
expectedCode: [200],
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
headers: {
|
headers: {
|
||||||
"User-Agent": "Uptimeflare"
|
'User-Agent': 'Uptimeflare',
|
||||||
},
|
},
|
||||||
body: undefined
|
body: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'cloudflare',
|
id: 'cloudflare',
|
||||||
|
|
@ -35,30 +35,30 @@ const config = {
|
||||||
expectedCode: [200],
|
expectedCode: [200],
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
headers: {
|
headers: {
|
||||||
"User-Agent": "Uptimeflare"
|
'User-Agent': 'Uptimeflare',
|
||||||
},
|
},
|
||||||
body: undefined
|
body: undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'blog',
|
id: 'blog',
|
||||||
name: 'My Blog',
|
name: 'My Blog',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
target: 'https://blog.lyc8503.site',
|
target: 'https://blog.lyc8503.site',
|
||||||
timeout: 10000
|
timeout: 10000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'pan',
|
id: 'pan',
|
||||||
name: 'My Fileshare',
|
name: 'My Fileshare',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
target: 'https://pan.lyc8503.site',
|
target: 'https://pan.lyc8503.site',
|
||||||
timeout: 10000
|
timeout: 10000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'server',
|
id: 'server',
|
||||||
name: 'My Aliyun Server',
|
name: 'My Aliyun Server',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
target: 'https://server.lyc8503.site',
|
target: 'https://server.lyc8503.site',
|
||||||
timeout: 10000
|
timeout: 10000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'homelab',
|
id: 'homelab',
|
||||||
|
|
@ -67,15 +67,22 @@ const config = {
|
||||||
target: 'https://lyc8503.cn4.quickconnect.cn/webstation/hello.txt',
|
target: 'https://lyc8503.cn4.quickconnect.cn/webstation/hello.txt',
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
headers: {
|
headers: {
|
||||||
"Cookie": "type=tunnel;"
|
Cookie: 'type=tunnel;',
|
||||||
},
|
},
|
||||||
responseKeyword: 'Hello'
|
responseKeyword: 'Hello',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'homessh',
|
||||||
|
name: 'HomeSSH',
|
||||||
|
method: 'TCP_PING',
|
||||||
|
target: '20.24.87.148:22',
|
||||||
|
timeout: 10000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'broken-test',
|
id: 'broken-test',
|
||||||
name: 'Ping 1.1.1.1',
|
name: 'Ping 1.1.1.1',
|
||||||
method: 'TCP_PING',
|
method: 'TCP_PING',
|
||||||
target: '1.1.1.1:1234'
|
target: '1.1.1.1:1234',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '404test',
|
id: '404test',
|
||||||
|
|
@ -88,7 +95,7 @@ const config = {
|
||||||
name: '404-test2',
|
name: '404-test2',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
target: 'https://www.baidu.com/404',
|
target: 'https://www.baidu.com/404',
|
||||||
expectedCodes: [404, 405]
|
expectedCodes: [404, 405],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '404test3',
|
id: '404test3',
|
||||||
|
|
@ -96,9 +103,9 @@ const config = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
target: 'https://www.baidu.com/404',
|
target: 'https://www.baidu.com/404',
|
||||||
expectedCodes: [404],
|
expectedCodes: [404],
|
||||||
responseKeyword: 'Hello'
|
responseKeyword: 'Hello',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
export default config
|
export default config
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,45 @@
|
||||||
type MonitorState = {
|
type MonitorState = {
|
||||||
lastUpdate: number,
|
lastUpdate: number
|
||||||
overallUp: number,
|
overallUp: number
|
||||||
overallDown: number,
|
overallDown: number
|
||||||
incident: Record<string, {
|
incident: Record<
|
||||||
start: number[],
|
string,
|
||||||
end: number | undefined, // undefined if it's still open
|
{
|
||||||
error: string[]
|
start: number[]
|
||||||
}[]>,
|
end: number | undefined // undefined if it's still open
|
||||||
|
error: string[]
|
||||||
|
}[]
|
||||||
|
>
|
||||||
|
|
||||||
latency: Record<string, {
|
latency: Record<
|
||||||
recent: {
|
string,
|
||||||
loc: string,
|
{
|
||||||
ping: number,
|
recent: {
|
||||||
time: number
|
loc: string
|
||||||
}[], // recent 12 hour data, 2 min interval
|
ping: number
|
||||||
all: {
|
time: number
|
||||||
loc: string,
|
}[] // recent 12 hour data, 2 min interval
|
||||||
ping: number,
|
all: {
|
||||||
time: number
|
loc: string
|
||||||
}[] // all data in 90 days, 1 hour interval
|
ping: number
|
||||||
}>
|
time: number
|
||||||
|
}[] // all data in 90 days, 1 hour interval
|
||||||
|
}
|
||||||
|
>
|
||||||
}
|
}
|
||||||
|
|
||||||
type MonitorTarget = {
|
type MonitorTarget = {
|
||||||
id: string,
|
id: string
|
||||||
name: string,
|
name: string
|
||||||
method: string, // "TCP_PING" or Http Method (e.g. GET, POST, OPTIONS, etc.)
|
method: string // "TCP_PING" or Http Method (e.g. GET, POST, OPTIONS, etc.)
|
||||||
target: string, // url for http, hostname:port for tcp
|
target: string // url for http, hostname:port for tcp
|
||||||
|
|
||||||
// HTTP Code
|
// HTTP Code
|
||||||
expectedCodes?: number[],
|
expectedCodes?: number[]
|
||||||
timeout?: number,
|
timeout?: number
|
||||||
headers?: Record<string, string | undefined>,
|
headers?: Record<string, string | undefined>
|
||||||
body?: BodyInit,
|
body?: BodyInit
|
||||||
responseKeyword?: string
|
responseKeyword?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export type { MonitorState, MonitorTarget }
|
export type { MonitorState, MonitorTarget }
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
|
@ -1,16 +1,16 @@
|
||||||
{
|
{
|
||||||
"name": "uptimeworker",
|
"name": "uptimeworker",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"vercel-build": "next build",
|
"vercel-build": "next build",
|
||||||
"deploy": "wrangler deploy",
|
"deploy": "wrangler deploy",
|
||||||
"dev": "wrangler dev",
|
"dev": "wrangler dev",
|
||||||
"start": "wrangler dev"
|
"start": "wrangler dev"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "^4.20230419.0",
|
"@cloudflare/workers-types": "^4.20230419.0",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
"wrangler": "^3.13.1"
|
"wrangler": "^3.13.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,45 @@
|
||||||
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}`)
|
|
||||||
|
|
||||||
status.ping = Date.now() - startTime
|
console.log(`${monitor.name} connected to ${monitor.target}`);
|
||||||
status.up = true
|
|
||||||
status.err = ""
|
status.ping = Date.now() - startTime;
|
||||||
|
status.up = true;
|
||||||
|
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
|
||||||
|
|
@ -51,84 +50,85 @@ async function getStatus(monitor: MonitorTarget): Promise<{ ping: number; up: bo
|
||||||
body: monitor.body,
|
body: monitor.body,
|
||||||
cf: {
|
cf: {
|
||||||
cacheTtlByStatus: {
|
cacheTtlByStatus: {
|
||||||
'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: ${response.status}`;
|
||||||
return 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 = await env.UPTIMEFLARE_STATE.get("state", { type: 'json' }) as unknown as MonitorState || {
|
let state =
|
||||||
lastUpdate: 0,
|
((await env.UPTIMEFLARE_STATE.get('state', { type: 'json' })) as unknown as MonitorState) ||
|
||||||
overallUp: 0,
|
({
|
||||||
overallDown: 0,
|
lastUpdate: 0,
|
||||||
incident: {},
|
overallUp: 0,
|
||||||
latency: {}
|
overallDown: 0,
|
||||||
} as MonitorState
|
incident: {},
|
||||||
state.overallDown = 0
|
latency: {},
|
||||||
state.overallUp = 0
|
} as MonitorState);
|
||||||
|
state.overallDown = 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
|
||||||
|
|
||||||
|
|
@ -137,19 +137,23 @@ export default {
|
||||||
{
|
{
|
||||||
start: [currentTimeSecond],
|
start: [currentTimeSecond],
|
||||||
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(`✔️${monitor.name} came back up at ${timeString} after ${Math.round((lastIncident.end - lastIncident.start.slice(-1)[0]) / 60)} minutes of downtime`)
|
await config.callback(
|
||||||
|
`✔️${monitor.name} came back up at ${timeString} after ${Math.round(
|
||||||
|
(lastIncident.end - lastIncident.start.slice(-1)[0]) / 60,
|
||||||
|
)} minutes of downtime`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Current status is down
|
// Current status is down
|
||||||
|
|
@ -158,45 +162,45 @@ export default {
|
||||||
state.incident[monitor.id].push({
|
state.incident[monitor.id].push({
|
||||||
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(`❌${monitor.name} went down at ${timeString} with error ${status.err}`);
|
||||||
} else if (lastIncident.end === undefined && lastIncident.error.slice(-1)[0] !== 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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// append to latency data
|
// append to latency data
|
||||||
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));
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,27 @@
|
||||||
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 = (url: string, ms: number, { signal, ...options }: RequestInit<RequestInitCfProperties> | undefined = {}): Promise<Response> => {
|
const fetchTimeout = (
|
||||||
const controller = new AbortController()
|
url: string,
|
||||||
const promise = fetch(url, { signal: controller.signal, ...options })
|
ms: number,
|
||||||
if (signal) signal.addEventListener("abort", () => controller.abort())
|
{ signal, ...options }: RequestInit<RequestInitCfProperties> | undefined = {},
|
||||||
const timeout = setTimeout(() => controller.abort(), ms)
|
): Promise<Response> => {
|
||||||
return promise.finally(() => clearTimeout(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));
|
||||||
|
};
|
||||||
|
|
||||||
|
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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function withTimeout<T> (millis: number, promise: Promise<T>): Promise<T> {
|
export { getWorkerLocation, fetchTimeout, withTimeout };
|
||||||
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 }
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue