1
0
mirror of https://github.com/tormachris/cf-workers-status-page.git synced 2025-07-04 11:02:48 +02:00

feat: optimize KV storage read/write operations

- the state is now stored in a single KV key
- there is one write for cron and one read for render
This commit is contained in:
Adam Janis
2020-11-19 18:43:33 +01:00
parent 293dff9425
commit c5b9232eb2
7 changed files with 71 additions and 133 deletions

View File

@ -1,8 +1,7 @@
import config from '../../config.yaml'
export default function MonitorHistogram({
kvMonitorsFailedDaysArray,
monitor,
monitorId,
kvMonitor,
}) {
// create date and set date - daysInHistogram for the first day of the histogram
@ -12,20 +11,19 @@ export default function MonitorHistogram({
if (typeof window !== 'undefined') {
return (
<div
key={`${monitor.id}-histogram`}
key={`${monitorId}-histogram`}
className="horizontal flex histogram"
>
{Array.from(Array(config.settings.daysInHistogram).keys()).map(key => {
date.setDate(date.getDate() + 1)
const dayInHistogram = date.toISOString().split('T')[0]
const dayInHistogramKey = 'h_' + monitor.id + '_' + dayInHistogram
let bg = ''
let dayInHistogramLabel = config.settings.dayInHistogramNoData
// filter all dates before first check, check the rest
if (kvMonitor && kvMonitor.firstCheck <= dayInHistogram) {
if (!kvMonitorsFailedDaysArray.includes(dayInHistogramKey)) {
if (!kvMonitor.failedDays.includes(dayInHistogram)) {
bg = 'green'
dayInHistogramLabel = config.settings.dayInHistogramOperational
} else {
@ -48,7 +46,7 @@ export default function MonitorHistogram({
} else {
return (
<div
key={`${monitor.id}-histogram`}
key={`${monitorId}-histogram`}
className="horizontal flex histogram"
>
<div className="grey-text">Loading histogram ...</div>

View File

@ -1,17 +1,15 @@
import config from '../../config.yaml'
export default function MonitorStatusHeader({ operational, lastUpdate }) {
export default function MonitorStatusHeader({kvMonitorsMetadata}) {
let backgroundColor = 'green'
let headerText = config.settings.allmonitorsOperational
let textColor = 'black'
if (!operational) {
if (!kvMonitorsMetadata.monitorsOperational) {
backgroundColor = 'yellow'
headerText = config.settings.notAllmonitorsOperational
}
const lastCheckAgo = Math.round((Date.now() - lastUpdate.value) / 1000)
return (
<div className={`ui inverted segment ${backgroundColor}`}>
<div className="horizontal flex between">
@ -19,9 +17,9 @@ export default function MonitorStatusHeader({ operational, lastUpdate }) {
{headerText}
</div>
{
lastUpdate.metadata && typeof window !== 'undefined' && (
kvMonitorsMetadata.lastUpdate && typeof window !== 'undefined' && (
<div className={`${textColor}-text`}>
checked {lastCheckAgo} sec ago (from {lastUpdate.metadata.loc})
checked {Math.round((Date.now() - kvMonitorsMetadata.lastUpdate.time) / 1000)} sec ago (from {kvMonitorsMetadata.lastUpdate.loc})
</div>
)
}

View File

@ -1,11 +1,11 @@
import config from '../../config.yaml'
export default function MonitorStatusLabel({ kvMonitorsMap, monitor }) {
export default function MonitorStatusLabel({ kvMonitor }) {
let labelColor = 'grey'
let labelText = 'No data'
if (typeof kvMonitorsMap[monitor.id] !== 'undefined') {
if (kvMonitorsMap[monitor.id].operational) {
if (typeof kvMonitor !== 'undefined') {
if (kvMonitor.operational) {
labelColor = 'green'
labelText = config.settings.monitorLabelOperational
} else {

View File

@ -3,7 +3,6 @@ import config from '../../config.yaml'
import {
setKV,
getKVWithMetadata,
getKV,
notifySlack,
} from './helpers'
@ -12,9 +11,29 @@ function getDate() {
}
export async function processCronTrigger(event) {
// Get monitors state from KV
let {value: monitorsState, metadata: monitorsStateMetadata} = await getKVWithMetadata('monitors_data', 'json')
// Create empty state objects if not exists in KV storage yet
if (!monitorsState) {
monitorsState = {}
}
if (!monitorsStateMetadata) {
monitorsStateMetadata = {}
}
// Reset default all monitors state to true
monitorsStateMetadata.monitorsOperational = 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: []}
}
console.log(`Checking ${monitor.name} ...`)
// Fetch the monitors URL
const init = {
method: monitor.method || 'GET',
redirect: monitor.followRedirect ? 'follow' : 'manual',
@ -24,50 +43,40 @@ export async function processCronTrigger(event) {
}
const checkResponse = await fetch(monitor.url, init)
const kvState = await getKVWithMetadata('s_' + monitor.id)
const monitorOperational = checkResponse.status === (monitor.expectStatus || 200)
// metadata from monitor settings
const newMetadata = {
operational: checkResponse.status === (monitor.expectStatus || 200),
id: monitor.id,
firstCheck: kvState.metadata ? kvState.metadata.firstCheck : getDate(),
// Send Slack message on monitor change
if (monitorsState[monitor.id].operational !== monitorOperational && typeof SECRET_SLACK_WEBHOOK_URL !== 'undefined' && SECRET_SLACK_WEBHOOK_URL !== 'default-gh-action-secret') {
event.waitUntil(notifySlack(monitor, monitorOperational))
}
// write current status if status changed or for first time
if (
!kvState.metadata ||
kvState.metadata.operational !== newMetadata.operational
) {
console.log('Saving changed state..')
monitorsState[monitor.id].operational = checkResponse.status === (monitor.expectStatus || 200)
monitorsState[monitor.id].firstCheck = monitorsState[monitor.id].firstCheck || getDate()
// first try to notify Slack in case fetch() or other limit is reached
if (typeof SECRET_SLACK_WEBHOOK_URL !== 'undefined' && SECRET_SLACK_WEBHOOK_URL !== 'default-gh-action-secret') {
await notifySlack(monitor, newMetadata)
}
// Set monitorsOperational and push current day to failedDays
if (!monitorOperational) {
monitorsStateMetadata.monitorsOperational = false
await setKV('s_' + monitor.id, null, newMetadata)
}
// write daily status if monitor is not operational
if (!newMetadata.operational) {
// try to get failed daily status first as KV read is cheaper than write
const kvFailedDayStatusKey = 'h_' + monitor.id + '_' + getDate()
const kvFailedDayStatus = await getKV(kvFailedDayStatusKey)
// write if not found
if (!kvFailedDayStatus) {
console.log('Saving new failed daily status..')
await setKV(kvFailedDayStatusKey, null)
const failedDay = getDate()
if (!monitorsState[monitor.id].failedDays.includes(failedDay)) {
console.log('Saving new failed daily status ...')
monitorsState[monitor.id].failedDays.push(failedDay)
}
}
}
// save last check timestamp including PoP location
// 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]
await setKV('lastUpdate', Date.now(), { loc })
monitorsStateMetadata.lastUpdate = {
loc,
time: Date.now()
}
// Save monitorsState and monitorsStateMetadata to KV storage
await setKV('monitors_data', JSON.stringify(monitorsState), monitorsStateMetadata)
return new Response('OK')
}

View File

@ -2,72 +2,29 @@ import config from '../../config.yaml'
import {useEffect, useState} from 'react'
export async function getMonitors() {
const monitors = await listKV('s_')
return monitors.keys
}
export async function getMonitorsHistory() {
const monitorsHistory = await listKV('h_', 300)
return monitorsHistory.keys
}
export async function getLastUpdate() {
return await getKVWithMetadata('lastUpdate')
}
export async function listKV(prefix = '', cacheTtl = false) {
const cacheKey = 'list_' + prefix + '_' + process.env.BUILD_ID
if (cacheTtl) {
const cachedResponse = await getKV(cacheKey)
if (cachedResponse) {
return JSON.parse(cachedResponse)
}
}
let list = []
let cursor = null
let res = {}
do {
res = await KV_STATUS_PAGE.list({ prefix: prefix, cursor })
list = list.concat(res.keys)
cursor = res.cursor
} while (!res.list_complete)
if (cacheTtl) {
await setKV(cacheKey, JSON.stringify({ keys: list }), null, cacheTtl)
}
return { keys: list }
return await getKVWithMetadata('monitors_data', "json")
}
export async function setKV(key, value, metadata, expirationTtl) {
return KV_STATUS_PAGE.put(key, value, { metadata, expirationTtl })
}
export async function getKV(key, type = 'text') {
return KV_STATUS_PAGE.get(key, type)
export async function getKVWithMetadata(key, type = 'text') {
return KV_STATUS_PAGE.getWithMetadata(key, type)
}
export async function getKVWithMetadata(key) {
return KV_STATUS_PAGE.getWithMetadata(key)
}
export async function deleteKV(key) {
return KV_STATUS_PAGE.delete(key)
}
export async function notifySlack(monitor, newMetadata) {
export async function notifySlack(monitor, operational) {
const payload = {
attachments: [
{
color: newMetadata.operational ? '#36a64f' : '#f2c744',
color: operational ? '#36a64f' : '#f2c744',
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `Monitor *${monitor.name}* changed status to *${
newMetadata.operational
operational
? config.settings.monitorLabelOperational
: config.settings.monitorLabelNotOperational
}*`,
@ -79,7 +36,7 @@ export async function notifySlack(monitor, newMetadata) {
{
type: 'mrkdwn',
text: `${
newMetadata.operational ? ':white_check_mark:' : ':x:'
operational ? ':white_check_mark:' : ':x:'
} \`${monitor.method ? monitor.method : "GET"} ${monitor.url}\` - :eyes: <${
config.settings.url
}|Status Page>`,