mirror of
https://github.com/tormachris/cf-workers-status-page.git
synced 2025-07-05 11:32:48 +02:00
feat: collect response metrics from cron locations
This commit is contained in:
@ -16,14 +16,17 @@ export default function MonitorHistogram({ monitorId, kvMonitor }) {
|
||||
let bg = ''
|
||||
let dayInHistogramLabel = config.settings.dayInHistogramNoData
|
||||
|
||||
// filter all dates before first check, check the rest
|
||||
// filter all dates before first check, then check the rest
|
||||
if (kvMonitor && kvMonitor.firstCheck <= dayInHistogram) {
|
||||
if (!kvMonitor.failedDays.includes(dayInHistogram)) {
|
||||
if (
|
||||
kvMonitor.checks.hasOwnProperty(dayInHistogram) &&
|
||||
kvMonitor.checks[dayInHistogram].fails > 0
|
||||
) {
|
||||
bg = 'yellow'
|
||||
dayInHistogramLabel = `${kvMonitor.checks[dayInHistogram].fails} ${config.settings.dayInHistogramNotOperational}`
|
||||
} else {
|
||||
bg = 'green'
|
||||
dayInHistogramLabel = config.settings.dayInHistogramOperational
|
||||
} else {
|
||||
bg = 'yellow'
|
||||
dayInHistogramLabel = config.settings.dayInHistogramNotOperational
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,6 +39,15 @@ export default function MonitorHistogram({ monitorId, kvMonitor }) {
|
||||
<span className="font-semibold text-sm">
|
||||
{dayInHistogramLabel}
|
||||
</span>
|
||||
{kvMonitor.checks.hasOwnProperty(dayInHistogram) &&
|
||||
Object.keys(kvMonitor.checks[dayInHistogram].res).map((key) => {
|
||||
return (
|
||||
<>
|
||||
<br />
|
||||
{key}: {kvMonitor.checks[dayInHistogram].res[key].a}ms
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -7,11 +7,11 @@ const classes = {
|
||||
'bg-yellow-200 text-yellow-700 dark:bg-yellow-700 dark:text-yellow-200 border-yellow-300 dark:border-yellow-600',
|
||||
}
|
||||
|
||||
export default function MonitorStatusHeader({ kvMonitorsMetadata }) {
|
||||
export default function MonitorStatusHeader({ kvMonitorsLastUpdate }) {
|
||||
let color = 'green'
|
||||
let text = config.settings.allmonitorsOperational
|
||||
|
||||
if (!kvMonitorsMetadata.monitorsOperational) {
|
||||
if (!kvMonitorsLastUpdate.allOperational) {
|
||||
color = 'yellow'
|
||||
text = config.settings.notAllmonitorsOperational
|
||||
}
|
||||
@ -20,13 +20,11 @@ export default function MonitorStatusHeader({ kvMonitorsMetadata }) {
|
||||
<div className={`card mb-4 font-semibold ${classes[color]}`}>
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<div>{text}</div>
|
||||
{kvMonitorsMetadata.lastUpdate && typeof window !== 'undefined' && (
|
||||
{kvMonitorsLastUpdate.time && typeof window !== 'undefined' && (
|
||||
<div className="text-xs font-light">
|
||||
checked{' '}
|
||||
{Math.round(
|
||||
(Date.now() - kvMonitorsMetadata.lastUpdate.time) / 1000,
|
||||
)}{' '}
|
||||
sec ago (from {kvMonitorsMetadata.lastUpdate.loc})
|
||||
{Math.round((Date.now() - kvMonitorsLastUpdate.time) / 1000)} sec
|
||||
ago (from {kvMonitorsLastUpdate.loc})
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -12,7 +12,7 @@ export default function MonitorStatusLabel({ kvMonitor }) {
|
||||
let text = 'No data'
|
||||
|
||||
if (typeof kvMonitor !== 'undefined') {
|
||||
if (kvMonitor.operational) {
|
||||
if (kvMonitor.lastCheck.operational) {
|
||||
color = 'green'
|
||||
text = config.settings.monitorLabelOperational
|
||||
} else {
|
||||
|
@ -1,33 +1,40 @@
|
||||
import config from '../../config.yaml'
|
||||
|
||||
import { setKV, getKVWithMetadata, notifySlack } from './helpers'
|
||||
import {
|
||||
notifySlack,
|
||||
getCheckLocation,
|
||||
getKVMonitors,
|
||||
setKVMonitors,
|
||||
} from './helpers'
|
||||
|
||||
function getDate() {
|
||||
return new Date().toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
export async function processCronTrigger(event) {
|
||||
// Get Worker PoP and save it to monitorsStateMetadata
|
||||
const checkLocation = await getCheckLocation()
|
||||
const checkDay = getDate()
|
||||
|
||||
// Get monitors state from KV
|
||||
let {
|
||||
value: monitorsState,
|
||||
metadata: monitorsStateMetadata,
|
||||
} = await getKVWithMetadata('monitors_data', 'json')
|
||||
let monitorsState = await getKVMonitors()
|
||||
|
||||
// Create empty state objects if not exists in KV storage yet
|
||||
if (!monitorsState) {
|
||||
monitorsState = {}
|
||||
}
|
||||
if (!monitorsStateMetadata) {
|
||||
monitorsStateMetadata = {}
|
||||
monitorsState = { lastUpdate: {}, monitors: {} }
|
||||
}
|
||||
|
||||
// Reset default all monitors state to true
|
||||
monitorsStateMetadata.monitorsOperational = true
|
||||
monitorsState.lastUpdate.allOperational = true
|
||||
|
||||
for (const monitor of config.monitors) {
|
||||
// Create default monitor state if does not exist yet
|
||||
if (typeof monitorsState[monitor.id] === 'undefined') {
|
||||
monitorsState[monitor.id] = { failedDays: [] }
|
||||
if (typeof monitorsState.monitors[monitor.id] === 'undefined') {
|
||||
monitorsState.monitors[monitor.id] = {
|
||||
firstCheck: checkDay,
|
||||
lastCheck: {},
|
||||
checks: {},
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Checking ${monitor.name} ...`)
|
||||
@ -41,52 +48,90 @@ export async function processCronTrigger(event) {
|
||||
},
|
||||
}
|
||||
|
||||
// Perform a check and measure time
|
||||
const requestStartTime = Date.now()
|
||||
const checkResponse = await fetch(monitor.url, init)
|
||||
const requestTime = Math.round(Date.now() - requestStartTime)
|
||||
|
||||
// Determine whether operational and status changed
|
||||
const monitorOperational =
|
||||
checkResponse.status === (monitor.expectStatus || 200)
|
||||
const monitorStatusChanged =
|
||||
monitorsState.monitors[monitor.id].lastCheck.operational !==
|
||||
monitorOperational
|
||||
|
||||
// Save monitor's last check response status
|
||||
monitorsState.monitors[monitor.id].lastCheck = {
|
||||
status: checkResponse.status,
|
||||
statusText: checkResponse.statusText,
|
||||
operational: monitorOperational,
|
||||
}
|
||||
|
||||
// Send Slack message on monitor change
|
||||
if (
|
||||
monitorsState[monitor.id].operational !== monitorOperational &&
|
||||
monitorStatusChanged &&
|
||||
typeof SECRET_SLACK_WEBHOOK_URL !== 'undefined' &&
|
||||
SECRET_SLACK_WEBHOOK_URL !== 'default-gh-action-secret'
|
||||
) {
|
||||
event.waitUntil(notifySlack(monitor, monitorOperational))
|
||||
}
|
||||
|
||||
monitorsState[monitor.id].operational =
|
||||
checkResponse.status === (monitor.expectStatus || 200)
|
||||
monitorsState[monitor.id].firstCheck =
|
||||
monitorsState[monitor.id].firstCheck || getDate()
|
||||
// make sure checkDay exists in checks in cases when needed
|
||||
if (
|
||||
(config.settings.collectResponseTimes || !monitorOperational) &&
|
||||
!monitorsState.monitors[monitor.id].checks.hasOwnProperty(checkDay)
|
||||
) {
|
||||
monitorsState.monitors[monitor.id].checks[checkDay] = {
|
||||
fails: 0,
|
||||
res: {},
|
||||
}
|
||||
}
|
||||
|
||||
// Set monitorsOperational and push current day to failedDays
|
||||
if (!monitorOperational) {
|
||||
monitorsStateMetadata.monitorsOperational = false
|
||||
if (config.settings.collectResponseTimes && monitorOperational) {
|
||||
// make sure location exists in current checkDay
|
||||
if (
|
||||
!monitorsState.monitors[monitor.id].checks[checkDay].res.hasOwnProperty(
|
||||
checkLocation,
|
||||
)
|
||||
) {
|
||||
monitorsState.monitors[monitor.id].checks[checkDay].res[
|
||||
checkLocation
|
||||
] = {
|
||||
n: 0,
|
||||
ms: 0,
|
||||
a: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const failedDay = getDate()
|
||||
if (!monitorsState[monitor.id].failedDays.includes(failedDay)) {
|
||||
console.log('Saving new failed daily status ...')
|
||||
monitorsState[monitor.id].failedDays.push(failedDay)
|
||||
// increment number of checks and sum of ms
|
||||
const no = ++monitorsState.monitors[monitor.id].checks[checkDay].res[
|
||||
checkLocation
|
||||
].n
|
||||
const ms = (monitorsState.monitors[monitor.id].checks[checkDay].res[
|
||||
checkLocation
|
||||
].ms += requestTime)
|
||||
|
||||
// save new average ms
|
||||
monitorsState.monitors[monitor.id].checks[checkDay].res[
|
||||
checkLocation
|
||||
].a = Math.round(ms / no)
|
||||
} else if (!monitorOperational) {
|
||||
// Save allOperational to false
|
||||
monitorsState.lastUpdate.allOperational = false
|
||||
|
||||
// Increment failed checks, only on status change (maybe call it .incidents instead?)
|
||||
if (monitorStatusChanged) {
|
||||
monitorsState.monitors[monitor.id].checks[checkDay].fails++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get Worker PoP and save it to monitorsStateMetadata
|
||||
const res = await fetch('https://cloudflare-dns.com/dns-query', {
|
||||
method: 'OPTIONS',
|
||||
})
|
||||
const loc = res.headers.get('cf-ray').split('-')[1]
|
||||
monitorsStateMetadata.lastUpdate = {
|
||||
loc,
|
||||
time: Date.now(),
|
||||
}
|
||||
// Save last update information
|
||||
monitorsState.lastUpdate.time = Date.now()
|
||||
monitorsState.lastUpdate.loc = checkLocation
|
||||
|
||||
// Save monitorsState and monitorsStateMetadata to KV storage
|
||||
await setKV(
|
||||
'monitors_data',
|
||||
JSON.stringify(monitorsState),
|
||||
monitorsStateMetadata,
|
||||
)
|
||||
// Save monitorsState to KV storage
|
||||
await setKVMonitors(monitorsState)
|
||||
|
||||
return new Response('OK')
|
||||
}
|
||||
|
@ -1,18 +1,20 @@
|
||||
import config from '../../config.yaml'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export async function getMonitors() {
|
||||
return await getKVWithMetadata('monitors_data', 'json')
|
||||
export async function getKVMonitors() {
|
||||
// trying both to see performance difference
|
||||
return KV_STATUS_PAGE.get('monitors_data', 'json')
|
||||
//return JSON.parse(await KV_STATUS_PAGE.get('monitors_data', 'text'))
|
||||
}
|
||||
|
||||
export async function setKVMonitors(data) {
|
||||
return setKV('monitors_data', JSON.stringify(data))
|
||||
}
|
||||
|
||||
export async function setKV(key, value, metadata, expirationTtl) {
|
||||
return KV_STATUS_PAGE.put(key, value, { metadata, expirationTtl })
|
||||
}
|
||||
|
||||
export async function getKVWithMetadata(key, type = 'text') {
|
||||
return KV_STATUS_PAGE.getWithMetadata(key, type)
|
||||
}
|
||||
|
||||
export async function notifySlack(monitor, operational) {
|
||||
const payload = {
|
||||
attachments: [
|
||||
@ -23,10 +25,11 @@ export async function notifySlack(monitor, operational) {
|
||||
type: 'section',
|
||||
text: {
|
||||
type: 'mrkdwn',
|
||||
text: `Monitor *${monitor.name}* changed status to *${operational
|
||||
text: `Monitor *${monitor.name}* changed status to *${
|
||||
operational
|
||||
? config.settings.monitorLabelOperational
|
||||
: config.settings.monitorLabelNotOperational
|
||||
}*`,
|
||||
}*`,
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -34,9 +37,11 @@ export async function notifySlack(monitor, operational) {
|
||||
elements: [
|
||||
{
|
||||
type: 'mrkdwn',
|
||||
text: `${operational ? ':white_check_mark:' : ':x:'} \`${monitor.method ? monitor.method : 'GET'
|
||||
} ${monitor.url}\` - :eyes: <${config.settings.url
|
||||
}|Status Page>`,
|
||||
text: `${operational ? ':white_check_mark:' : ':x:'} \`${
|
||||
monitor.method ? monitor.method : 'GET'
|
||||
} ${monitor.url}\` - :eyes: <${
|
||||
config.settings.url
|
||||
}|Status Page>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -78,3 +83,10 @@ export function useKeyPress(targetKey) {
|
||||
|
||||
return keyPressed
|
||||
}
|
||||
|
||||
export async function getCheckLocation() {
|
||||
const res = await fetch('https://cloudflare-dns.com/dns-query', {
|
||||
method: 'OPTIONS',
|
||||
})
|
||||
return res.headers.get('cf-ray').split('-')[1]
|
||||
}
|
||||
|
Reference in New Issue
Block a user