1
0
mirror of https://github.com/tormachris/cf-workers-status-page.git synced 2024-11-23 22:45:43 +01:00

Merge pull request #14 from eidam/e/kv-resources-optimization

feat: optimize KV storage read/write operations
This commit is contained in:
Adam Janiš 2020-11-19 20:35:28 +01:00 committed by GitHub
commit 42f422c455
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 71 additions and 133 deletions

View File

@ -12,7 +12,7 @@ You'll need a [Cloudflare Workers account](https://dash.cloudflare.com/sign-up/w
* A workers domain set up * A workers domain set up
* The Workers Bundled subscription \($5/mo\) * The Workers Bundled subscription \($5/mo\)
* [Try it now with the free tier!](https://blog.cloudflare.com/workers-kv-free-tier/) Check [more info](#workers-kv-free-tier) on how to run on Workers Free. * [It works with Workers Free!](https://blog.cloudflare.com/workers-kv-free-tier/) Check [more info](#workers-kv-free-tier) on how to run on Workers Free.
* Some websites/APIs to watch 🙂 * Some websites/APIs to watch 🙂
Also, prepare the following secrets Also, prepare the following secrets
@ -85,8 +85,8 @@ You can clone the repository yourself and use Wrangler CLI to develop/deploy, ex
* `SECRET_SLACK_WEBHOOK_URL` * `SECRET_SLACK_WEBHOOK_URL`
## Workers KV free tier ## Workers KV free tier
The Workers Free plan includes limited KV usage, in order to not deplete the quota and still have enough room for monitor status changes we recommend the following changes: The Workers Free plan includes limited KV usage, but the quota is sufficient for 2-minute checks only
* Change the CRON trigger to 5 minutes interval (`crons = ["*/5 * * * *"]`) in [wrangler.toml](./wrangler.toml) * Change the CRON trigger to 2 minutes interval (`crons = ["*/2 * * * *"]`) in [wrangler.toml](./wrangler.toml)
## Known issues ## Known issues

View File

@ -2,9 +2,7 @@ import Head from 'flareact/head'
import MonitorHistogram from '../src/components/monitorHistogram' import MonitorHistogram from '../src/components/monitorHistogram'
import { import {
getLastUpdate,
getMonitors, getMonitors,
getMonitorsHistory,
useKeyPress, useKeyPress,
} from '../src/functions/helpers' } from '../src/functions/helpers'
@ -30,30 +28,13 @@ const filterByTerm = (term) => MonitorStore.set(
export async function getEdgeProps() { export async function getEdgeProps() {
// get KV data // get KV data
const kvMonitors = await getMonitors() const {value: kvMonitors, metadata: kvMonitorsMetadata } = await getMonitors()
const kvMonitorsFailedDays = await getMonitorsHistory()
const kvLastUpdate = await getLastUpdate()
// prepare data maps for components
let monitorsOperational = true
let kvMonitorsMap = {}
kvMonitors.forEach(x => {
kvMonitorsMap[x.metadata.id] = x.metadata
if (x.metadata.operational === false) monitorsOperational = false
})
// transform KV list to array of failed days
const kvMonitorsFailedDaysArray = kvMonitorsFailedDays.map(x => {
return x.name
})
return { return {
props: { props: {
config, config,
kvMonitorsMap, kvMonitors: kvMonitors || {},
kvMonitorsFailedDaysArray, kvMonitorsMetadata: kvMonitorsMetadata || {}
monitorsOperational,
kvLastUpdate,
}, },
// Revalidate these props once every x seconds // Revalidate these props once every x seconds
revalidate: 5, revalidate: 5,
@ -62,10 +43,8 @@ export async function getEdgeProps() {
export default function Index({ export default function Index({
config, config,
kvMonitorsMap, kvMonitors,
kvMonitorsFailedDaysArray, kvMonitorsMetadata,
monitorsOperational,
kvLastUpdate,
}) { }) {
const state = useStore(MonitorStore) const state = useStore(MonitorStore)
const slash = useKeyPress('/') const slash = useKeyPress('/')
@ -96,8 +75,7 @@ export default function Index({
/> />
</div> </div>
<MonitorStatusHeader <MonitorStatusHeader
operational={monitorsOperational} kvMonitorsMetadata={kvMonitorsMetadata}
lastUpdate={kvLastUpdate}
/> />
{state.visible.map((monitor, key) => { {state.visible.map((monitor, key) => {
return ( return (
@ -115,15 +93,13 @@ export default function Index({
<div className="content">{monitor.name}</div> <div className="content">{monitor.name}</div>
</div> </div>
<MonitorStatusLabel <MonitorStatusLabel
kvMonitorsMap={kvMonitorsMap} kvMonitor={kvMonitors[monitor.id]}
monitor={monitor}
/> />
</div> </div>
<MonitorHistogram <MonitorHistogram
kvMonitorsFailedDaysArray={kvMonitorsFailedDaysArray} monitorId={monitor.id}
monitor={monitor} kvMonitor={kvMonitors[monitor.id]}
kvMonitor={kvMonitorsMap[monitor.id]}
/> />
<div className="horizontal flex between grey-text"> <div className="horizontal flex between grey-text">

View File

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

View File

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

View File

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

View File

@ -3,7 +3,6 @@ import config from '../../config.yaml'
import { import {
setKV, setKV,
getKVWithMetadata, getKVWithMetadata,
getKV,
notifySlack, notifySlack,
} from './helpers' } from './helpers'
@ -12,9 +11,29 @@ function getDate() {
} }
export async function processCronTrigger(event) { 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) { 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} ...`) console.log(`Checking ${monitor.name} ...`)
// Fetch the monitors URL
const init = { const init = {
method: monitor.method || 'GET', method: monitor.method || 'GET',
redirect: monitor.followRedirect ? 'follow' : 'manual', redirect: monitor.followRedirect ? 'follow' : 'manual',
@ -24,50 +43,40 @@ export async function processCronTrigger(event) {
} }
const checkResponse = await fetch(monitor.url, init) 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 // Send Slack message on monitor change
const newMetadata = { if (monitorsState[monitor.id].operational !== monitorOperational && typeof SECRET_SLACK_WEBHOOK_URL !== 'undefined' && SECRET_SLACK_WEBHOOK_URL !== 'default-gh-action-secret') {
operational: checkResponse.status === (monitor.expectStatus || 200), event.waitUntil(notifySlack(monitor, monitorOperational))
id: monitor.id,
firstCheck: kvState.metadata ? kvState.metadata.firstCheck : getDate(),
} }
// write current status if status changed or for first time monitorsState[monitor.id].operational = checkResponse.status === (monitor.expectStatus || 200)
if ( monitorsState[monitor.id].firstCheck = monitorsState[monitor.id].firstCheck || getDate()
!kvState.metadata ||
kvState.metadata.operational !== newMetadata.operational
) {
console.log('Saving changed state..')
// first try to notify Slack in case fetch() or other limit is reached // Set monitorsOperational and push current day to failedDays
if (typeof SECRET_SLACK_WEBHOOK_URL !== 'undefined' && SECRET_SLACK_WEBHOOK_URL !== 'default-gh-action-secret') { if (!monitorOperational) {
await notifySlack(monitor, newMetadata) monitorsStateMetadata.monitorsOperational = false
}
await setKV('s_' + monitor.id, null, newMetadata) const failedDay = getDate()
} if (!monitorsState[monitor.id].failedDays.includes(failedDay)) {
console.log('Saving new failed daily status ...')
// write daily status if monitor is not operational monitorsState[monitor.id].failedDays.push(failedDay)
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)
} }
} }
} }
// 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', { const res = await fetch('https://cloudflare-dns.com/dns-query', {
method: 'OPTIONS', method: 'OPTIONS',
}) })
const loc = res.headers.get('cf-ray').split('-')[1] 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') return new Response('OK')
} }

View File

@ -2,72 +2,29 @@ import config from '../../config.yaml'
import {useEffect, useState} from 'react' import {useEffect, useState} from 'react'
export async function getMonitors() { export async function getMonitors() {
const monitors = await listKV('s_') return await getKVWithMetadata('monitors_data', "json")
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 }
} }
export async function setKV(key, value, metadata, expirationTtl) { export async function setKV(key, value, metadata, expirationTtl) {
return KV_STATUS_PAGE.put(key, value, { metadata, expirationTtl }) return KV_STATUS_PAGE.put(key, value, { metadata, expirationTtl })
} }
export async function getKV(key, type = 'text') { export async function getKVWithMetadata(key, type = 'text') {
return KV_STATUS_PAGE.get(key, type) return KV_STATUS_PAGE.getWithMetadata(key, type)
} }
export async function getKVWithMetadata(key) { export async function notifySlack(monitor, operational) {
return KV_STATUS_PAGE.getWithMetadata(key)
}
export async function deleteKV(key) {
return KV_STATUS_PAGE.delete(key)
}
export async function notifySlack(monitor, newMetadata) {
const payload = { const payload = {
attachments: [ attachments: [
{ {
color: newMetadata.operational ? '#36a64f' : '#f2c744', color: operational ? '#36a64f' : '#f2c744',
blocks: [ blocks: [
{ {
type: 'section', type: 'section',
text: { text: {
type: 'mrkdwn', type: 'mrkdwn',
text: `Monitor *${monitor.name}* changed status to *${ text: `Monitor *${monitor.name}* changed status to *${
newMetadata.operational operational
? config.settings.monitorLabelOperational ? config.settings.monitorLabelOperational
: config.settings.monitorLabelNotOperational : config.settings.monitorLabelNotOperational
}*`, }*`,
@ -79,7 +36,7 @@ export async function notifySlack(monitor, newMetadata) {
{ {
type: 'mrkdwn', type: 'mrkdwn',
text: `${ text: `${
newMetadata.operational ? ':white_check_mark:' : ':x:' operational ? ':white_check_mark:' : ':x:'
} \`${monitor.method ? monitor.method : "GET"} ${monitor.url}\` - :eyes: <${ } \`${monitor.method ? monitor.method : "GET"} ${monitor.url}\` - :eyes: <${
config.settings.url config.settings.url
}|Status Page>`, }|Status Page>`,