wip: worker & pages
parent
c67ea1bd65
commit
cdd43685f2
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
|
|
@ -9,16 +9,18 @@
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@cloudflare/workers-types": "^4.20231010.0",
|
||||||
|
"next": "13.5.4",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18"
|
||||||
"next": "13.5.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "13.5.4"
|
"eslint-config-next": "13.5.4",
|
||||||
|
"typescript": "^5",
|
||||||
|
"wrangler": "^3.13.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
|
|
||||||
type Data = {
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<Data>
|
|
||||||
) {
|
|
||||||
res.status(200).json({ name: 'John Doe' })
|
|
||||||
}
|
|
||||||
156
pages/index.tsx
156
pages/index.tsx
|
|
@ -1,114 +1,76 @@
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import Image from 'next/image'
|
|
||||||
import { Inter } from 'next/font/google'
|
import { Inter } from 'next/font/google'
|
||||||
import styles from '@/styles/Home.module.css'
|
|
||||||
|
|
||||||
const inter = Inter({ subsets: ['latin'] })
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
export default function Home() {
|
import { MonitorState } from '@/uptime.types'
|
||||||
|
import { KVNamespace } from '@cloudflare/workers-types'
|
||||||
|
import config from '@/uptime.config'
|
||||||
|
|
||||||
|
export const runtime = 'experimental-edge'
|
||||||
|
|
||||||
|
export default function Home({ state }: { state: MonitorState }) {
|
||||||
|
|
||||||
|
|
||||||
|
// Calculate overall status
|
||||||
|
// TODO: Move this to a component
|
||||||
|
let downCount = 0
|
||||||
|
let upCount = 0
|
||||||
|
|
||||||
|
for (const monitor of config.monitors) {
|
||||||
|
if (state.history[monitor.id]?.slice(-1)[0].up) {
|
||||||
|
upCount += 1
|
||||||
|
} else {
|
||||||
|
downCount += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let overallStatus = ""
|
||||||
|
|
||||||
|
if (downCount === 0 && upCount === 0) {
|
||||||
|
overallStatus = "No monitors configured"
|
||||||
|
}
|
||||||
|
if (downCount === 0) {
|
||||||
|
overallStatus = "All systems operational"
|
||||||
|
}
|
||||||
|
if (upCount === 0) {
|
||||||
|
overallStatus = "All systems not operational"
|
||||||
|
}
|
||||||
|
if (upCount > 0 && downCount > 0) {
|
||||||
|
overallStatus = "Some systems not operational"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Create Next App</title>
|
<title>{config.page.title}</title>
|
||||||
<meta name="description" content="Generated by create next app" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<main className={`${styles.main} ${inter.className}`}>
|
|
||||||
<div className={styles.description}>
|
|
||||||
<p>
|
|
||||||
Get started by editing
|
|
||||||
<code className={styles.code}>pages/index.tsx</code>
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
By{' '}
|
|
||||||
<Image
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel Logo"
|
|
||||||
className={styles.vercelLogo}
|
|
||||||
width={100}
|
|
||||||
height={24}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.center}>
|
<main>
|
||||||
<Image
|
<h1>Uptime Flare</h1>
|
||||||
className={styles.logo}
|
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js Logo"
|
|
||||||
width={180}
|
|
||||||
height={37}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.grid}>
|
<h2>Current Status: {overallStatus}</h2>
|
||||||
<a
|
<h2>Last updated on: {state.lastUpdate.toString()}</h2>
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
|
||||||
className={styles.card}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
Docs <span>-></span>
|
|
||||||
</h2>
|
|
||||||
<p>
|
|
||||||
Find in-depth information about Next.js features and API.
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
{
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
config.monitors.map(monitor => (
|
||||||
className={styles.card}
|
<div key={monitor.id}>
|
||||||
target="_blank"
|
<h2>{monitor.name}</h2>
|
||||||
rel="noopener noreferrer"
|
</div>
|
||||||
>
|
))
|
||||||
<h2>
|
}
|
||||||
Learn <span>-></span>
|
<p>{JSON.stringify(state)}</p>
|
||||||
</h2>
|
|
||||||
<p>
|
|
||||||
Learn about Next.js in an interactive course with quizzes!
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
|
||||||
className={styles.card}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
Templates <span>-></span>
|
|
||||||
</h2>
|
|
||||||
<p>
|
|
||||||
Discover and deploy boilerplate example Next.js projects.
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
|
|
||||||
className={styles.card}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2>
|
|
||||||
Deploy <span>-></span>
|
|
||||||
</h2>
|
|
||||||
<p>
|
|
||||||
Instantly deploy your Next.js site to a shareable URL
|
|
||||||
with Vercel.
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
return { props: { state } }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 629 B |
|
|
@ -18,5 +18,5 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules", "worker/**/*.ts"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
dateLocale: "zh-CN",
|
||||||
|
timezone: "Asia/Shanghai",
|
||||||
|
page: {
|
||||||
|
title: "lyc8503's Status Page"
|
||||||
|
},
|
||||||
|
callback: async (statusChangeMsg: string) => {
|
||||||
|
await fetch('https://server.lyc8503.site/wepush?key=wepushkey&msg=' + statusChangeMsg)
|
||||||
|
},
|
||||||
|
monitors: [
|
||||||
|
{
|
||||||
|
id: 'github',
|
||||||
|
name: 'Github Monitor',
|
||||||
|
method: 'GET',
|
||||||
|
target: 'https://github.com',
|
||||||
|
expectedCode: [200],
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "Uptimeflare"
|
||||||
|
},
|
||||||
|
body: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cloudflare',
|
||||||
|
name: 'Cloudflare Monitor',
|
||||||
|
method: 'GET',
|
||||||
|
target: 'https://cloudflare.com',
|
||||||
|
expectedCode: [200],
|
||||||
|
timeout: 10,
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "Uptimeflare"
|
||||||
|
},
|
||||||
|
body: undefined
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'broken-test',
|
||||||
|
name: 'Ping 1.1.1.1',
|
||||||
|
method: 'TCP_PING',
|
||||||
|
target: '1.1.1.1:1234'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
type MonitorState = {
|
||||||
|
lastUpdate: number,
|
||||||
|
overallUp: number,
|
||||||
|
overallDown: number,
|
||||||
|
incident: Record<string, {
|
||||||
|
start: number[],
|
||||||
|
end: number | undefined, // undefined if it's still open
|
||||||
|
error: string[]
|
||||||
|
}[]>,
|
||||||
|
|
||||||
|
latency: Record<string, {
|
||||||
|
recent: {
|
||||||
|
loc: string,
|
||||||
|
ping: number,
|
||||||
|
time: number
|
||||||
|
}[], // recent 12 hour data, 2 min interval
|
||||||
|
all: {
|
||||||
|
loc: string,
|
||||||
|
ping: number,
|
||||||
|
time: number
|
||||||
|
}[] // all data in 90 days, 1 hour interval
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
type MonitorTarget = {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
method: string, // "TCP_PING" or Http Method (e.g. GET, POST, OPTIONS, etc.)
|
||||||
|
target: string, // url for http, hostname:port for tcp
|
||||||
|
|
||||||
|
expectedCode?: number[],
|
||||||
|
timeout?: number,
|
||||||
|
headers?: Record<string, string>,
|
||||||
|
body?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type { MonitorState, MonitorTarget }
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = tab
|
||||||
|
tab_width = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.yml]
|
||||||
|
indent_style = space
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
# Logs
|
||||||
|
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
npm-debug.log_
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
|
||||||
|
pids
|
||||||
|
_.pid
|
||||||
|
_.seed
|
||||||
|
\*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
|
||||||
|
coverage
|
||||||
|
\*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
|
||||||
|
\*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
|
||||||
|
\*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
|
||||||
|
.temp
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.\*
|
||||||
|
|
||||||
|
# wrangler project
|
||||||
|
|
||||||
|
.dev.vars
|
||||||
|
.wrangler/
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"printWidth": 140,
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": true,
|
||||||
|
"useTabs": true
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "uptimeworker",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"vercel-build": "next build",
|
||||||
|
"deploy": "wrangler deploy",
|
||||||
|
"dev": "wrangler dev",
|
||||||
|
"start": "wrangler dev"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@cloudflare/workers-types": "^4.20230419.0",
|
||||||
|
"typescript": "^5.0.4",
|
||||||
|
"wrangler": "^3.13.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
import { connect } from 'cloudflare:sockets'
|
||||||
|
import config from '../../uptime.config'
|
||||||
|
import { fetchTimeout, getWorkerLocation } from './util'
|
||||||
|
import { MonitorState, MonitorTarget } from "../../uptime.types"
|
||||||
|
|
||||||
|
export interface Env {
|
||||||
|
UPTIMEFLARE_STATE: KVNamespace
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStatus(monitor: MonitorTarget): Promise<{ ping: number; up: boolean; err: string }> {
|
||||||
|
|
||||||
|
let status = {
|
||||||
|
ping: 0,
|
||||||
|
up: false,
|
||||||
|
err: "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
if (monitor.method === "TCP_PING") {
|
||||||
|
// TCP port endpoint monitor
|
||||||
|
try {
|
||||||
|
const [hostname, port] = monitor.target.split(":")
|
||||||
|
|
||||||
|
// Write "PING" and read response
|
||||||
|
const socket = connect({ hostname: hostname, port: Number(port) })
|
||||||
|
const reader = socket.readable.getReader()
|
||||||
|
const writer = socket.writable.getWriter()
|
||||||
|
await writer.write(new TextEncoder().encode("PING\n"))
|
||||||
|
await reader.read() // The read here is necessary to actually test the connection, however it may hang if the server doesn't respond
|
||||||
|
await socket.close()
|
||||||
|
|
||||||
|
// TODO: should throw an error here but it doesn't?
|
||||||
|
await socket.closed
|
||||||
|
|
||||||
|
console.log(`${monitor.name} connected to ${monitor.target}`)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// HTTP endpoint monitor
|
||||||
|
try {
|
||||||
|
const response = await fetchTimeout(monitor.target, monitor.timeout || 10000, {
|
||||||
|
method: monitor.method,
|
||||||
|
headers: monitor.headers,
|
||||||
|
body: monitor.body
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`${monitor.name} responded with ${response.status}`)
|
||||||
|
status.ping = Date.now() - startTime
|
||||||
|
status.up = true
|
||||||
|
status.err = ""
|
||||||
|
} catch (e: any) {
|
||||||
|
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`
|
||||||
|
} else {
|
||||||
|
status.up = false
|
||||||
|
status.err = e.name + ": " + e.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
|
||||||
|
const workerLocation = await getWorkerLocation()
|
||||||
|
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 || {
|
||||||
|
lastUpdate: 0,
|
||||||
|
overallUp: 0,
|
||||||
|
overallDown: 0,
|
||||||
|
incident: {},
|
||||||
|
latency: {}
|
||||||
|
} as MonitorState
|
||||||
|
state.overallDown = 0
|
||||||
|
state.overallUp = 0
|
||||||
|
|
||||||
|
// Check each monitor
|
||||||
|
for (const monitor of config.monitors) {
|
||||||
|
console.log(`[${workerLocation}] Checking ${monitor.name}...`)
|
||||||
|
|
||||||
|
const status = await getStatus(monitor)
|
||||||
|
const currentTimeSecond = Math.round(Date.now() / 1000)
|
||||||
|
|
||||||
|
// Update counters
|
||||||
|
status.up ? state.overallUp++ : state.overallDown++
|
||||||
|
|
||||||
|
// Update incidents
|
||||||
|
let incidentList = state.incident[monitor.id] || []
|
||||||
|
const lastIncident = incidentList.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 && lastIncident.end === undefined) {
|
||||||
|
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`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Current status is down
|
||||||
|
// open new incident if not already open
|
||||||
|
if (lastIncident === undefined || lastIncident?.end !== undefined) {
|
||||||
|
incidentList.push({
|
||||||
|
start: [currentTimeSecond],
|
||||||
|
end: undefined,
|
||||||
|
error: [status.err]
|
||||||
|
})
|
||||||
|
await config.callback(`❌${monitor.name} went down at ${timeString} with error ${status.err}`)
|
||||||
|
} else if (lastIncident && 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}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.incident[monitor.id] = incidentList
|
||||||
|
|
||||||
|
// append to latency data
|
||||||
|
let latencyLists = state.latency[monitor.id] || {
|
||||||
|
recent: [],
|
||||||
|
all: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = {
|
||||||
|
loc: workerLocation,
|
||||||
|
ping: status.ping,
|
||||||
|
time: currentTimeSecond
|
||||||
|
}
|
||||||
|
latencyLists.recent.push(record)
|
||||||
|
if (latencyLists.all.length === 0 || currentTimeSecond - latencyLists.all[0].time > 60 * 60) {
|
||||||
|
latencyLists.all.push(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
// discard old data
|
||||||
|
while (latencyLists.recent[0]?.time < currentTimeSecond - 12 * 60 * 60) {
|
||||||
|
latencyLists.recent.shift()
|
||||||
|
}
|
||||||
|
while (latencyLists.all[0]?.time < currentTimeSecond - 90 * 24 * 60 * 60) {
|
||||||
|
latencyLists.all.shift()
|
||||||
|
}
|
||||||
|
state.latency[monitor.id] = latencyLists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
state.lastUpdate = Math.round(Date.now() / 1000)
|
||||||
|
await env.UPTIMEFLARE_STATE.put("state", JSON.stringify(state))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
async function getWorkerLocation() {
|
||||||
|
const res = await fetch('https://cloudflare.com/cdn-cgi/trace')
|
||||||
|
const text = await res.text()
|
||||||
|
|
||||||
|
const loc = /^loc=(.*)$/m.exec(text)?.[1]
|
||||||
|
const colo = /^colo=(.*)$/m.exec(text)?.[1]
|
||||||
|
|
||||||
|
return loc + '/' + colo
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchTimeout = (url: string, ms: number, { 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getWorkerLocation, fetchTimeout }
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||||
|
|
||||||
|
/* Projects */
|
||||||
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
|
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||||
|
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
|
||||||
|
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
|
||||||
|
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||||
|
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||||
|
|
||||||
|
/* 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. */,
|
||||||
|
"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. */
|
||||||
|
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
|
||||||
|
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||||
|
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
|
||||||
|
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
|
||||||
|
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||||
|
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||||
|
|
||||||
|
/* Modules */
|
||||||
|
"module": "es2022" /* Specify what module code is generated. */,
|
||||||
|
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||||
|
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
||||||
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
|
// "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. */,
|
||||||
|
// "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. */
|
||||||
|
|
||||||
|
/* JavaScript Support */
|
||||||
|
"allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */,
|
||||||
|
"checkJs": false /* Enable error reporting in type-checked JavaScript files. */,
|
||||||
|
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||||
|
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||||
|
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||||
|
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||||
|
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
|
||||||
|
// "outDir": "./", /* Specify an output folder for all emitted files. */
|
||||||
|
// "removeComments": true, /* Disable emitting comments. */
|
||||||
|
"noEmit": true /* Disable emitting files from a compilation. */,
|
||||||
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||||
|
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
|
||||||
|
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||||
|
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||||
|
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||||
|
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||||
|
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||||
|
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
|
||||||
|
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
|
||||||
|
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||||
|
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
|
||||||
|
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||||
|
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||||
|
|
||||||
|
/* Interop Constraints */
|
||||||
|
"isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
|
||||||
|
"allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
|
||||||
|
// "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
|
||||||
|
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||||
|
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||||
|
|
||||||
|
/* Type Checking */
|
||||||
|
"strict": true /* Enable all strict type-checking options. */,
|
||||||
|
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
|
||||||
|
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
|
||||||
|
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||||
|
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
|
||||||
|
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||||
|
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
|
||||||
|
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
|
||||||
|
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||||
|
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
|
||||||
|
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
|
||||||
|
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||||
|
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||||
|
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
|
||||||
|
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||||
|
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||||
|
|
||||||
|
/* Completeness */
|
||||||
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
name = "uptimeflare-worker"
|
||||||
|
main = "src/index.ts"
|
||||||
|
compatibility_date = "2023-05-15"
|
||||||
|
|
||||||
|
kv_namespaces = [
|
||||||
|
{ binding = "UPTIMEFLARE_STATE", id = "30a3733511a246e395ab99ee6a8dceb7" }
|
||||||
|
]
|
||||||
|
|
||||||
|
[triggers]
|
||||||
|
crons = ["*/2 * * * *"] # run every 2 minutes
|
||||||
Loading…
Reference in New Issue