[WIP] workers works! (lol) pages+migration todo
parent
45d86424b2
commit
e050065a3c
|
|
@ -34,7 +34,7 @@ export default function DetailChart({
|
|||
state: MonitorState
|
||||
}) {
|
||||
const { t } = useTranslation('common')
|
||||
const latencyData = state.latency[monitor.id].recent.map((point) => ({
|
||||
const latencyData = state.latency[monitor.id].map((point) => ({
|
||||
x: point.time * 1000,
|
||||
y: point.ping,
|
||||
loc: point.loc,
|
||||
|
|
|
|||
|
|
@ -70,7 +70,9 @@ export default function MaintenanceAlert({
|
|||
{upcoming ? t('Expected end') : t('To')}
|
||||
</div>
|
||||
<div>
|
||||
{maintenance.end ? new Date(maintenance.end).toLocaleString() : t('Until further notice')}
|
||||
{maintenance.end
|
||||
? new Date(maintenance.end).toLocaleString()
|
||||
: t('Until further notice')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
4
init.sql
4
init.sql
|
|
@ -2,7 +2,3 @@ CREATE TABLE IF NOT EXISTS uptimeflare (
|
|||
key VARCHAR(255) PRIMARY KEY,
|
||||
value BLOB NOT NULL
|
||||
);
|
||||
|
||||
REPLACE INTO uptimeflare (key, value) VALUES ('state', '{"test": 1}');
|
||||
|
||||
INSERT INTO uptimeflare (key, value) VALUES ('state', '{"test": 1}') ON CONFLICT(key) DO UPDATE SET value = excluded.value;
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export default async function handler(req: NextRequest): Promise<Response> {
|
|||
|
||||
const state = JSON.parse(stateStr) as MonitorState
|
||||
const monitorIncidentHistory = state.incident?.[monitorId]
|
||||
const hasLatencyData = Boolean(state.latency?.[monitorId]?.recent?.length)
|
||||
const hasLatencyData = Boolean(state.latency?.[monitorId]?.length)
|
||||
|
||||
if (!monitorIncidentHistory || monitorIncidentHistory.length === 0 || !hasLatencyData) {
|
||||
return new Response(JSON.stringify(errorBadge(label, 'unknown')), {
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ export default async function handler(req: NextRequest): Promise<Response> {
|
|||
const isUp = state.incident[monitor.id].slice(-1)[0].end !== undefined
|
||||
monitors[monitor.id] = {
|
||||
up: isUp,
|
||||
latency: state.latency[monitor.id].recent.slice(-1)[0].ping,
|
||||
location: state.latency[monitor.id].recent.slice(-1)[0].loc,
|
||||
latency: state.latency[monitor.id].slice(-1)[0].ping,
|
||||
location: state.latency[monitor.id].slice(-1)[0].loc,
|
||||
message: isUp ? 'OK' : state.incident[monitor.id].slice(-1)[0].error.slice(-1)[0],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,9 +57,7 @@ export default function Home({
|
|||
|
||||
{state == undefined ? (
|
||||
<Center>
|
||||
<Text fw={700}>
|
||||
{t('Monitor State not defined')}
|
||||
</Text>
|
||||
<Text fw={700}>{t('Monitor State not defined')}</Text>
|
||||
</Center>
|
||||
) : (
|
||||
<div>
|
||||
|
|
@ -76,7 +74,7 @@ export default function Home({
|
|||
|
||||
export async function getServerSideProps() {
|
||||
// Read state as string from KV, to avoid hitting server-side cpu time limit
|
||||
const state = await getFromStore(process.env as any, 'state') as unknown as MonitorState
|
||||
const state = (await getFromStore(process.env as any, 'state')) as unknown as MonitorState
|
||||
|
||||
// Only present these values to client
|
||||
const monitors = workerConfig.monitors.map((monitor) => {
|
||||
|
|
|
|||
|
|
@ -92,32 +92,61 @@ export type Callbacks<TEnv = Env> = {
|
|||
) => Promise<any> | any
|
||||
}
|
||||
|
||||
export type IncidentRecord = {
|
||||
start: number[]
|
||||
end: number | null // null if it's still open
|
||||
error: string[]
|
||||
}
|
||||
|
||||
export type LatencyRecord = {
|
||||
loc: string
|
||||
ping: number
|
||||
time: number
|
||||
}
|
||||
|
||||
export type MonitorState = {
|
||||
lastUpdate: number
|
||||
overallUp: number
|
||||
overallDown: number
|
||||
incident: Record<string, IncidentRecord[]>
|
||||
latency: Record<string, LatencyRecord[]> // recent 12 hour data, N min interval
|
||||
}
|
||||
|
||||
// This is now the actual stored format (after TODO D1 migration) to improve (de)serialization performance
|
||||
// This gives a ~3.5x speedup in computing and a 58% reduction in size
|
||||
// The CPULimitExceeded issue with 10+ monitors on free tier should be largely mitigated by this change
|
||||
// local profiling result (1 op = parse + stringify):
|
||||
// MonitorState (original): 277 ops/s, ±0.51% | slowest, 71.09% slower
|
||||
// MonitorStateCompacted: 958 ops/s, ±1.17% | fastest
|
||||
export type MonitorStateCompacted = {
|
||||
lastUpdate: number
|
||||
overallUp: number
|
||||
overallDown: number
|
||||
|
||||
// incident in stored in columnar format
|
||||
incident: Record<
|
||||
string,
|
||||
string, // monitor id
|
||||
{
|
||||
start: number[]
|
||||
end: number | undefined // undefined if it's still open
|
||||
error: string[]
|
||||
}[]
|
||||
start: number[][]
|
||||
end: (number | null)[]
|
||||
error: string[][]
|
||||
}
|
||||
>
|
||||
|
||||
// latency in stored in columnar format
|
||||
// also uses Run-length encoding for loc & Base64 encoding for number arrays
|
||||
latency: Record<
|
||||
string,
|
||||
string, // monitor id
|
||||
{
|
||||
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
|
||||
loc: {
|
||||
v: string[] // RLE values
|
||||
c: number[] // RLE counts
|
||||
}
|
||||
// Hex results in a larger size and slower encoding/decoding than base64,
|
||||
// but we can pop/append arbitrary number of bytes without decoding then re-encoding the whole string
|
||||
// This is useful in Workers and shows a ~2% speedup comapred to base64, and it also simplifies the code
|
||||
ping: string // Hex encoded Uint16Array
|
||||
time: string // Hex encoded Uint32Array
|
||||
}
|
||||
>
|
||||
}
|
||||
|
|
|
|||
14
util/i18n.ts
14
util/i18n.ts
|
|
@ -1,8 +1,8 @@
|
|||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import en from '../locales/en/common.json';
|
||||
import zhCN from '../locales/zh-CN/common.json';
|
||||
import i18n from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||
import en from '../locales/en/common.json'
|
||||
import zhCN from '../locales/zh-CN/common.json'
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
|
|
@ -20,6 +20,6 @@ i18n
|
|||
detection: {
|
||||
order: ['navigator'],
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
export default i18n;
|
||||
export default i18n
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { DurableObject } from 'cloudflare:workers'
|
||||
import { MonitorState, MonitorTarget } from '../../types/config'
|
||||
import { maintenances, workerConfig } from '../../uptime.config'
|
||||
import { MonitorTarget } from '../../types/config'
|
||||
import { workerConfig } from '../../uptime.config'
|
||||
import { getStatus, getStatusWithGlobalPing } from './monitor'
|
||||
import { formatStatusChangeNotification, getWorkerLocation, webhookNotify } from './util'
|
||||
import { getFromStore, setToStore } from './store'
|
||||
import { formatAndNotify, getWorkerLocation } from './util'
|
||||
import { CompactedMonitorStateWrapper, getFromStore, setToStore } from './store'
|
||||
|
||||
export interface Env {
|
||||
UPTIMEFLARE_STATE: KVNamespace
|
||||
|
|
@ -16,66 +16,12 @@ const Worker = {
|
|||
const workerLocation = (await getWorkerLocation()) || 'ERROR'
|
||||
console.log(`Running scheduled event on ${workerLocation}...`)
|
||||
|
||||
// Auxiliary function to format notification and send it via webhook
|
||||
let formatAndNotify = async (
|
||||
monitor: MonitorTarget,
|
||||
isUp: boolean,
|
||||
timeIncidentStart: number,
|
||||
timeNow: number,
|
||||
reason: string
|
||||
) => {
|
||||
// Skip notification if monitor is in the skip list
|
||||
const skipList = workerConfig.notification?.skipNotificationIds
|
||||
if (skipList && skipList.includes(monitor.id)) {
|
||||
console.log(
|
||||
`Skipping notification for ${monitor.name} (${monitor.id} in skipNotificationIds)`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Skip notification if monitor is in maintenance
|
||||
const maintenanceList = maintenances
|
||||
.filter(
|
||||
(m) =>
|
||||
new Date(timeNow * 1000) >= new Date(m.start) &&
|
||||
(!m.end || new Date(timeNow * 1000) <= new Date(m.end))
|
||||
)
|
||||
.map((e) => e.monitors || [])
|
||||
.flat()
|
||||
|
||||
if (maintenanceList.includes(monitor.id)) {
|
||||
console.log(`Skipping notification for ${monitor.name} (in maintenance)`)
|
||||
return
|
||||
}
|
||||
|
||||
if (workerConfig.notification?.webhook) {
|
||||
const notification = formatStatusChangeNotification(
|
||||
monitor,
|
||||
isUp,
|
||||
timeIncidentStart,
|
||||
timeNow,
|
||||
reason,
|
||||
workerConfig.notification?.timeZone ?? 'Etc/GMT'
|
||||
)
|
||||
await webhookNotify(workerConfig.notification.webhook, notification)
|
||||
} else {
|
||||
console.log(`Webhook not set, skipping notification for ${monitor.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Read state, set init state if it doesn't exist
|
||||
let state = JSON.parse(await getFromStore(env, 'state') || '{}') as unknown as MonitorState
|
||||
if (!state || Object.keys(state).length === 0) {
|
||||
state = {
|
||||
lastUpdate: 0,
|
||||
overallUp: 0,
|
||||
overallDown: 0,
|
||||
incident: {},
|
||||
latency: {},
|
||||
}
|
||||
}
|
||||
state.overallDown = 0
|
||||
state.overallUp = 0
|
||||
// Create a wrapped MonitorState from stored compacted state
|
||||
const state = new CompactedMonitorStateWrapper(
|
||||
await getFromStore(env, 'state')
|
||||
)
|
||||
state.data.overallDown = 0
|
||||
state.data.overallUp = 0
|
||||
|
||||
let statusChanged = false
|
||||
const currentTimeSecond = Math.round(Date.now() / 1000)
|
||||
|
|
@ -126,6 +72,7 @@ const Worker = {
|
|||
console.log('Falling back to local check...')
|
||||
status = await getStatus(monitor)
|
||||
} else {
|
||||
// TODO: more consistent error handling (throw or return?)
|
||||
status = { ping: 0, up: false, err: 'Unknown check proxy error' }
|
||||
}
|
||||
}
|
||||
|
|
@ -134,29 +81,32 @@ const Worker = {
|
|||
status = await getStatus(monitor)
|
||||
}
|
||||
|
||||
// const status = await getStatus(monitor)
|
||||
const currentTimeSecond = Math.round(Date.now() / 1000)
|
||||
|
||||
// Update counters
|
||||
status.up ? state.overallUp++ : state.overallDown++
|
||||
status.up ? state.data.overallUp++ : state.data.overallDown++
|
||||
|
||||
// Update incidents
|
||||
// Create a dummy incident to store the start time of the monitoring and simplify logic
|
||||
state.incident[monitor.id] = state.incident[monitor.id] || [
|
||||
{
|
||||
if (state.incidentLen(monitor.id) === 0) {
|
||||
state.appendIncident(monitor.id, {
|
||||
start: [currentTimeSecond],
|
||||
end: currentTimeSecond,
|
||||
error: ['dummy'],
|
||||
},
|
||||
]
|
||||
// Then lastIncident here must not be undefined
|
||||
let lastIncident = state.incident[monitor.id].slice(-1)[0]
|
||||
})
|
||||
}
|
||||
|
||||
// Then lastIncident here must not be null
|
||||
let lastIncident = state.getIncident(monitor.id, state.incidentLen(monitor.id) - 1)
|
||||
|
||||
if (status.up) {
|
||||
// Current status is up
|
||||
// close existing incident if any
|
||||
if (lastIncident.end === undefined) {
|
||||
if (lastIncident.end === null) {
|
||||
lastIncident.end = currentTimeSecond
|
||||
// write back the modified last incident
|
||||
state.setIncident(monitor.id, state.incidentLen(monitor.id) - 1, lastIncident)
|
||||
|
||||
monitorStatusChanged = true
|
||||
try {
|
||||
if (
|
||||
|
|
@ -190,24 +140,30 @@ const Worker = {
|
|||
} else {
|
||||
// Current status is down
|
||||
// open new incident if not already open
|
||||
if (lastIncident.end !== undefined) {
|
||||
state.incident[monitor.id].push({
|
||||
if (lastIncident.end !== null) {
|
||||
state.appendIncident(monitor.id, {
|
||||
start: [currentTimeSecond],
|
||||
end: undefined,
|
||||
end: null,
|
||||
error: [status.err],
|
||||
})
|
||||
monitorStatusChanged = true
|
||||
} else if (
|
||||
lastIncident.end === undefined &&
|
||||
lastIncident.end === null &&
|
||||
lastIncident.error.slice(-1)[0] !== status.err
|
||||
) {
|
||||
// append if the error message changes
|
||||
lastIncident.start.push(currentTimeSecond)
|
||||
lastIncident.error.push(status.err)
|
||||
|
||||
// write back the modified last incident
|
||||
state.setIncident(monitor.id, state.incidentLen(monitor.id) - 1, lastIncident)
|
||||
monitorStatusChanged = true
|
||||
}
|
||||
|
||||
const currentIncident = state.incident[monitor.id].slice(-1)[0]
|
||||
const currentIncident = state.getIncident(
|
||||
monitor.id,
|
||||
state.incidentLen(monitor.id) - 1
|
||||
)
|
||||
try {
|
||||
if (
|
||||
// monitor status changed AND...
|
||||
|
|
@ -284,63 +240,54 @@ const Worker = {
|
|||
}
|
||||
|
||||
// append to latency data
|
||||
let latencyLists = state.latency[monitor.id] || {
|
||||
recent: [],
|
||||
}
|
||||
latencyLists.all = []
|
||||
|
||||
const record = {
|
||||
state.appendLatency(monitor.id,{
|
||||
loc: checkLocation,
|
||||
ping: status.ping,
|
||||
time: currentTimeSecond,
|
||||
}
|
||||
latencyLists.recent.push(record)
|
||||
})
|
||||
|
||||
// discard old data
|
||||
while (latencyLists.recent[0]?.time < currentTimeSecond - 12 * 60 * 60) {
|
||||
latencyLists.recent.shift()
|
||||
while (state.getFirstLatency(monitor.id).time < currentTimeSecond - 12 * 60 * 60) {
|
||||
state.unshiftLatency(monitor.id)
|
||||
}
|
||||
state.latency[monitor.id] = latencyLists
|
||||
|
||||
// discard old incidents
|
||||
let incidentList = state.incident[monitor.id]
|
||||
while (
|
||||
incidentList.length > 0 &&
|
||||
incidentList[0].end &&
|
||||
incidentList[0].end < currentTimeSecond - 90 * 24 * 60 * 60
|
||||
state.incidentLen(monitor.id) > 0 &&
|
||||
state.getIncident(monitor.id, 0).end &&
|
||||
state.getIncident(monitor.id, 0).end! < currentTimeSecond - 90 * 24 * 60 * 60
|
||||
) {
|
||||
incidentList.shift()
|
||||
state.shiftIncident(monitor.id)
|
||||
}
|
||||
|
||||
if (
|
||||
incidentList.length == 0 ||
|
||||
(incidentList[0].start[0] > currentTimeSecond - 90 * 24 * 60 * 60 &&
|
||||
incidentList[0].error[0] != 'dummy')
|
||||
state.incidentLen(monitor.id) === 0 ||
|
||||
(state.getIncident(monitor.id, 0).start[0] > currentTimeSecond - 90 * 24 * 60 * 60 &&
|
||||
state.getIncident(monitor.id, 0).error[0] != 'dummy')
|
||||
) {
|
||||
// put the dummy incident back
|
||||
incidentList.unshift({
|
||||
state.unshiftIncident(monitor.id, {
|
||||
start: [currentTimeSecond - 90 * 24 * 60 * 60],
|
||||
end: currentTimeSecond - 90 * 24 * 60 * 60,
|
||||
error: ['dummy'],
|
||||
})
|
||||
}
|
||||
state.incident[monitor.id] = incidentList
|
||||
|
||||
statusChanged ||= monitorStatusChanged
|
||||
}
|
||||
|
||||
console.log(
|
||||
`statusChanged: ${statusChanged}, lastUpdate: ${state.lastUpdate}, currentTime: ${currentTimeSecond}`
|
||||
`statusChanged: ${statusChanged}, lastUpdate: ${state.data.lastUpdate}, currentTime: ${currentTimeSecond}`
|
||||
)
|
||||
// Update state
|
||||
// Allow for a cooldown period before writing to KV
|
||||
// Allow for a cooldown period before writing to storage
|
||||
if (
|
||||
statusChanged ||
|
||||
currentTimeSecond - state.lastUpdate >= (workerConfig.kvWriteCooldownMinutes ?? 3) * 60 - 10 // Allow for 10 seconds of clock drift
|
||||
currentTimeSecond - state.data.lastUpdate >= (workerConfig.kvWriteCooldownMinutes ?? 3) * 60 - 10 // Allow for 10 seconds of clock drift
|
||||
) {
|
||||
console.log('Updating state...')
|
||||
state.lastUpdate = currentTimeSecond
|
||||
await setToStore(env, 'state', JSON.stringify(state))
|
||||
state.data.lastUpdate = currentTimeSecond
|
||||
await setToStore(env, 'state', state.getCompactedStateStr())
|
||||
} else {
|
||||
console.log('Skipping state update due to cooldown period.')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -304,7 +304,9 @@ export async function getStatus(
|
|||
response.status,
|
||||
response.text.bind(response)
|
||||
)
|
||||
try { await response.body?.cancel() } catch(e) {} // Always try to cancel body, see issue #166
|
||||
try {
|
||||
await response.body?.cancel()
|
||||
} catch (e) {} // Always try to cancel body, see issue #166
|
||||
|
||||
if (err !== null) {
|
||||
console.log(`${monitor.name} didn't pass response check: ${err}`)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,155 @@
|
|||
import { Env } from ".";
|
||||
import { Env } from '.'
|
||||
import { IncidentRecord, LatencyRecord, MonitorStateCompacted } from '../../types/config'
|
||||
|
||||
export async function getFromStore(env: Env, key: string): Promise<string | null> {
|
||||
const stmt = env.UPTIMEFLARE_D1.prepare("SELECT value FROM uptimeflare WHERE key = ?")
|
||||
const result = await stmt.bind(key).first<{ value: string }>()
|
||||
|
||||
if (!result) {
|
||||
return await env.UPTIMEFLARE_STATE.get(key)
|
||||
}
|
||||
|
||||
return result?.value || null
|
||||
const stmt = env.UPTIMEFLARE_D1.prepare('SELECT value FROM uptimeflare WHERE key = ?')
|
||||
const result = await stmt.bind(key).first<{ value: string }>()
|
||||
return result?.value || null
|
||||
}
|
||||
|
||||
export async function setToStore(env: Env, key: string, value: string): Promise<void> {
|
||||
const stmt = env.UPTIMEFLARE_D1.prepare("INSERT INTO uptimeflare (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value;")
|
||||
await stmt.bind(key, value).run()
|
||||
const stmt = env.UPTIMEFLARE_D1.prepare(
|
||||
'INSERT INTO uptimeflare (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value;'
|
||||
)
|
||||
await stmt.bind(key, value).run()
|
||||
}
|
||||
|
||||
export class CompactedMonitorStateWrapper {
|
||||
data: MonitorStateCompacted
|
||||
|
||||
constructor(compactedStateStr: string | null) {
|
||||
if (!compactedStateStr) {
|
||||
// Initialize empty state
|
||||
this.data = {
|
||||
lastUpdate: 0,
|
||||
overallUp: 0,
|
||||
overallDown: 0,
|
||||
incident: {},
|
||||
latency: {},
|
||||
}
|
||||
return
|
||||
}
|
||||
this.data = JSON.parse(compactedStateStr)
|
||||
}
|
||||
|
||||
getCompactedStateStr(): string {
|
||||
return JSON.stringify(this.data)
|
||||
}
|
||||
|
||||
incidentLen(monitorId: string): number {
|
||||
const incidents = this.data.incident[monitorId]
|
||||
if (!incidents) return 0
|
||||
return incidents.start.length
|
||||
}
|
||||
|
||||
getIncident(monitorId: string, index: number): IncidentRecord {
|
||||
const incidents = this.data.incident[monitorId]
|
||||
if (!incidents || index < 0 || index >= incidents.start.length) {
|
||||
throw new Error('Index out of bounds or monitor not found')
|
||||
}
|
||||
return {
|
||||
start: incidents.start[index],
|
||||
end: incidents.end[index],
|
||||
error: incidents.error[index],
|
||||
}
|
||||
}
|
||||
|
||||
setIncident(monitorId: string, index: number, incident: IncidentRecord) {
|
||||
const incidents = this.data.incident[monitorId]
|
||||
if (!incidents || index < 0 || index >= incidents.start.length) {
|
||||
throw new Error('Index out of bounds or monitor not found')
|
||||
}
|
||||
incidents.start[index] = incident.start
|
||||
incidents.end[index] = incident.end
|
||||
incidents.error[index] = incident.error
|
||||
}
|
||||
|
||||
appendIncident(monitorId: string, incident: IncidentRecord) {
|
||||
let incidents = this.data.incident[monitorId]
|
||||
if (!incidents) {
|
||||
// Initialize incident arrays
|
||||
this.data.incident[monitorId] = {
|
||||
start: [],
|
||||
end: [],
|
||||
error: [],
|
||||
}
|
||||
incidents = this.data.incident[monitorId]
|
||||
}
|
||||
incidents.start.push(incident.start)
|
||||
incidents.end.push(incident.end)
|
||||
incidents.error.push(incident.error)
|
||||
}
|
||||
|
||||
shiftIncident(monitorId: string) {
|
||||
const incidents = this.data.incident[monitorId]
|
||||
incidents.start.shift()
|
||||
incidents.end.shift()
|
||||
incidents.error.shift()
|
||||
}
|
||||
|
||||
unshiftIncident(monitorId: string, incident: IncidentRecord) {
|
||||
const incidents = this.data.incident[monitorId]
|
||||
incidents.start.unshift(incident.start)
|
||||
incidents.end.unshift(incident.end)
|
||||
incidents.error.unshift(incident.error)
|
||||
}
|
||||
|
||||
latencyLen(monitorId: string): number {
|
||||
const latencies = this.data.latency[monitorId]
|
||||
if (!latencies) return 0
|
||||
return latencies.ping.length / 4 // Uint16Array, 4 characters per entry in hex
|
||||
}
|
||||
|
||||
appendLatency(monitorId: string, record: LatencyRecord) {
|
||||
let latencies = this.data.latency[monitorId]
|
||||
if (!latencies) {
|
||||
// Initialize latency arrays
|
||||
this.data.latency[monitorId] = {
|
||||
time: '',
|
||||
ping: '',
|
||||
loc: {
|
||||
c: [],
|
||||
v: [],
|
||||
},
|
||||
}
|
||||
latencies = this.data.latency[monitorId]
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
latencies.time += new Uint8Array(new Uint32Array([record.time]).buffer).toHex()
|
||||
// @ts-expect-error
|
||||
latencies.ping += new Uint8Array(new Uint16Array([record.ping]).buffer).toHex()
|
||||
|
||||
if (latencies.loc.v[latencies.loc.v.length - 1] !== record.loc) {
|
||||
latencies.loc.c.push(1)
|
||||
latencies.loc.v.push(record.loc)
|
||||
} else {
|
||||
latencies.loc.c[latencies.loc.c.length - 1] += 1
|
||||
}
|
||||
}
|
||||
|
||||
getFirstLatency(monitorId: string): LatencyRecord {
|
||||
let latencies = this.data.latency[monitorId]
|
||||
|
||||
return {
|
||||
// @ts-expect-error
|
||||
time: new Uint32Array(Uint8Array.fromHex(latencies.time.slice(0, 8)).buffer)[0],
|
||||
// @ts-expect-error
|
||||
ping: new Uint16Array(Uint8Array.fromHex(latencies.ping.slice(0, 4)).buffer)[0],
|
||||
loc: latencies.loc.v[0],
|
||||
}
|
||||
}
|
||||
|
||||
unshiftLatency(monitorId: string) {
|
||||
let latencies = this.data.latency[monitorId]
|
||||
|
||||
latencies.time = latencies.time.slice(8)
|
||||
latencies.ping = latencies.ping.slice(4)
|
||||
|
||||
latencies.loc.c[0] -= 1
|
||||
if (latencies.loc.c[0] === 0) {
|
||||
latencies.loc.c.shift()
|
||||
latencies.loc.v.shift()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { WebhookConfig } from '../../types/config'
|
||||
import { MonitorTarget, WebhookConfig } from '../../types/config'
|
||||
import { maintenances, workerConfig } from '../../uptime.config'
|
||||
|
||||
async function getWorkerLocation() {
|
||||
const res = await fetch('https://cloudflare.com/cdn-cgi/trace')
|
||||
|
|
@ -145,10 +146,59 @@ async function webhookNotify(webhook: WebhookConfig, message: string) {
|
|||
}
|
||||
}
|
||||
|
||||
// Auxiliary function to format notification and send it via webhook
|
||||
const formatAndNotify = async (
|
||||
monitor: MonitorTarget,
|
||||
isUp: boolean,
|
||||
timeIncidentStart: number,
|
||||
timeNow: number,
|
||||
reason: string
|
||||
) => {
|
||||
// Skip notification if monitor is in the skip list
|
||||
const skipList = workerConfig.notification?.skipNotificationIds
|
||||
if (skipList && skipList.includes(monitor.id)) {
|
||||
console.log(
|
||||
`Skipping notification for ${monitor.name} (${monitor.id} in skipNotificationIds)`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Skip notification if monitor is in maintenance
|
||||
const maintenanceList = maintenances
|
||||
.filter(
|
||||
(m) =>
|
||||
new Date(timeNow * 1000) >= new Date(m.start) &&
|
||||
(!m.end || new Date(timeNow * 1000) <= new Date(m.end))
|
||||
)
|
||||
.map((e) => e.monitors || [])
|
||||
.flat()
|
||||
|
||||
if (maintenanceList.includes(monitor.id)) {
|
||||
console.log(`Skipping notification for ${monitor.name} (in maintenance)`)
|
||||
return
|
||||
}
|
||||
|
||||
if (workerConfig.notification?.webhook) {
|
||||
const notification = formatStatusChangeNotification(
|
||||
monitor,
|
||||
isUp,
|
||||
timeIncidentStart,
|
||||
timeNow,
|
||||
reason,
|
||||
workerConfig.notification?.timeZone ?? 'Etc/GMT'
|
||||
)
|
||||
await webhookNotify(workerConfig.notification.webhook, notification)
|
||||
} else {
|
||||
console.log(`Webhook not set, skipping notification for ${monitor.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
getWorkerLocation,
|
||||
fetchTimeout,
|
||||
withTimeout,
|
||||
webhookNotify,
|
||||
formatStatusChangeNotification,
|
||||
formatAndNotify,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
name = "uptimeflare_worker"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2025-04-02"
|
||||
kv_namespaces = [{ binding = "UPTIMEFLARE_STATE", id = "UPTIMEFLARE_STATE" }]
|
||||
compatibility_flags = [ "nodejs_compat" ]
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "UPTIMEFLARE_D1"
|
||||
database_name = "uptimeflare_d1"
|
||||
database_id = "00000000-0000-0000-0000-000000000000"
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@ name = "uptimeflare_worker"
|
|||
main = "src/index.ts"
|
||||
compatibility_date = "2025-04-02"
|
||||
compatibility_flags = [ "nodejs_compat" ]
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
name = "uptimeflare"
|
||||
compatibility_date = "2025-04-02"
|
||||
compatibility_flags = [ "nodejs_compat" ]
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "UPTIMEFLARE_D1"
|
||||
database_name = "uptimeflare_d1"
|
||||
database_id = "00000000-0000-0000-0000-000000000000"
|
||||
Loading…
Reference in New Issue