feat: add notify telegram & bark, and some minor fixes
parent
63be746b4d
commit
0c6556eda9
|
|
@ -22,16 +22,21 @@ jobs:
|
|||
cache: 'npm'
|
||||
|
||||
# Automatically get an account id via the API Token
|
||||
# if secrets.CLOUDFLARE_ACCOUNT_ID is not set.
|
||||
- name: Fetch Account ID
|
||||
id: fetch_account_id
|
||||
run: |
|
||||
ACCOUNT_ID=$(curl -X GET "https://api.cloudflare.com/client/v4/accounts" -H "Authorization: Bearer "$CLOUDFLARE_API_TOKEN -H "Content-Type:application/json" | jq ".result[0].id" -r)
|
||||
if [[ -n "${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" ]]; then
|
||||
ACCOUNT_ID="${{ secrets.CLOUDFLARE_ACCOUNT_ID }}"
|
||||
echo "Using provided CLOUDFLARE_ACCOUNT_ID from secrets."
|
||||
else
|
||||
ACCOUNT_ID=$(curl -X GET "https://api.cloudflare.com/client/v4/accounts" -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" -H "Content-Type:application/json" | jq ".result[0].id" -r)
|
||||
if [[ "$ACCOUNT_ID" == "null" ]]; then
|
||||
echo "Failed to get an account id, please make sure you have set up CLOUDFLARE_API_TOKEN correctly!"
|
||||
exit 1
|
||||
else
|
||||
echo 'account_id='$ACCOUNT_ID >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
echo 'account_id='$ACCOUNT_ID >> $GITHUB_OUTPUT
|
||||
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
|
@ -62,7 +67,7 @@ jobs:
|
|||
terraform apply -auto-approve -input=false
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
TF_VAR_CLOUDFLARE_ACCOUNT_ID: ${{ steps.fetch_account_id.outputs.account_id }}
|
||||
TF_VAR_CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID || steps.fetch_account_id.outputs.account_id }}
|
||||
|
||||
# Still need to upload worker to keep it up-to-date (Terraform will fail after first-time setup)
|
||||
- name: Upload worker
|
||||
|
|
@ -70,7 +75,7 @@ jobs:
|
|||
curl --fail-with-body -X PUT https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/workers/scripts/uptimeflare_worker/content --header 'Authorization: Bearer '$CLOUDFLARE_API_TOKEN -F 'index.js=@worker/dist/index.js;type=application/javascript+module' -F 'metadata={"main_module": "index.js"}'
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ steps.fetch_account_id.outputs.account_id }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID || steps.fetch_account_id.outputs.account_id }}
|
||||
|
||||
# Currently Terraform Cloudflare provider doesn't support direct upload, use wrangler to upload instead.
|
||||
- name: Upload pages
|
||||
|
|
@ -78,3 +83,4 @@ jobs:
|
|||
npx wrangler pages deploy .vercel/output/static --project-name uptimeflare
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID || steps.fetch_account_id.outputs.account_id }}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ Please refer to [Quickstart](https://github.com/lyc8503/UptimeFlare/wiki/Quickst
|
|||
|
||||
- [x] TCP `opened` promise
|
||||
- [x] ~~SSL certificate checks~~
|
||||
- [ ] Slack / Telegram webhook example
|
||||
- [x] Telegram example
|
||||
- [x] [Bark](https://bark.day.app) example
|
||||
- [ ] Slack example
|
||||
- [ ] Incident timeline
|
||||
- [ ] Email notification via Cloudflare Email Workers
|
||||
- [x] Specify region for monitors
|
||||
|
|
|
|||
|
|
@ -40,9 +40,19 @@ export default function MonitorDetail({
|
|||
|
||||
const uptimePercent = (((totalTime - downTime) / totalTime) * 100).toPrecision(4)
|
||||
|
||||
// Conditionally render monitor name with or without hyperlink based on monitor.url presence
|
||||
const monitorNameElement = (
|
||||
<Text mt="sm" fw={700} style={{ display: 'inline-flex', alignItems: 'center' }}>
|
||||
{statusIcon} {monitor.name}
|
||||
{monitor.url ? (
|
||||
<a href={monitor.url} style={{ display: 'inline-flex', alignItems: 'center', color: 'inherit', textDecoration: 'none' }}>
|
||||
{statusIcon} {monitor.name}
|
||||
</a>
|
||||
) : (
|
||||
<>
|
||||
{statusIcon} {monitor.name}
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ resource "cloudflare_worker_cron_trigger" "uptimeflare_worker_cron" {
|
|||
account_id = var.CLOUDFLARE_ACCOUNT_ID
|
||||
script_name = cloudflare_worker_script.uptimeflare.name
|
||||
schedules = [
|
||||
"*/2 * * * *", # every 2 minutes
|
||||
"* * * * *", # every 1 minute, you can reduce the KV write by increase the worker settings of `kvWriteCooldownMinutes`
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
143
uptime.config.ts
143
uptime.config.ts
|
|
@ -10,6 +10,8 @@ const pageConfig = {
|
|||
}
|
||||
|
||||
const workerConfig = {
|
||||
// Write KV at most every 10 minutes unless the status changed.
|
||||
kvWriteCooldownMinutes: 10,
|
||||
// Define all your monitors here
|
||||
monitors: [
|
||||
// Example HTTP Monitor
|
||||
|
|
@ -18,6 +20,8 @@ const workerConfig = {
|
|||
id: 'foo_monitor',
|
||||
// `name` is used at status page and callback message
|
||||
name: 'My API Monitor',
|
||||
// `url` for clickable link at status page
|
||||
url: 'https://example.com',
|
||||
// `method` should be a valid HTTP Method
|
||||
method: 'POST',
|
||||
// `target` is a valid URL
|
||||
|
|
@ -55,35 +59,22 @@ const workerConfig = {
|
|||
],
|
||||
callbacks: {
|
||||
onStatusChange: async (
|
||||
id: string,
|
||||
name: string,
|
||||
env: Env,
|
||||
monitor: any,
|
||||
isUp: boolean,
|
||||
timeIncidentStart: number,
|
||||
timeNow: number,
|
||||
reason: string
|
||||
) => {
|
||||
// This callback will be called when any monitor's status changed
|
||||
// Write any Typescript code here
|
||||
// Example implementation:
|
||||
// const timeString = new Date(timeNow * 1000).toLocaleString('zh-CN', {
|
||||
// timeZone: 'Asia/Shanghai',
|
||||
// })
|
||||
// let statusChangeMsg
|
||||
// if (isUp) {
|
||||
// statusChangeMsg = `✔️${name} came back up at ${timeString} after ${Math.round(
|
||||
// (timeNow - timeIncidentStart) / 60
|
||||
// )} minutes of downtime`
|
||||
// } else {
|
||||
// statusChangeMsg = `❌${name} was down at ${timeString} with error ${reason}`
|
||||
// }
|
||||
// await fetch('https://api.example.com/callback?msg=' + statusChangeMsg)
|
||||
await notify(env, monitor, isUp, timeIncidentStart, timeNow, reason)
|
||||
},
|
||||
onIncident: async (
|
||||
id: string,
|
||||
name: string,
|
||||
env: Env,
|
||||
monitor: any,
|
||||
isUp: boolean,
|
||||
timeIncidentStart: number,
|
||||
timeNow: number,
|
||||
currentError: string
|
||||
reason: string
|
||||
) => {
|
||||
// This callback will be called EVERY 2 MINTUES if there's an on-going incident for any monitor
|
||||
// Write any Typescript code here
|
||||
|
|
@ -91,5 +82,117 @@ const workerConfig = {
|
|||
},
|
||||
}
|
||||
|
||||
|
||||
const escapeMarkdown = (text: string) => {
|
||||
return text.replace(/[_*[\](){}~`>#+\-=|.!\\]/g, '\\$&');
|
||||
};
|
||||
|
||||
async function notify(
|
||||
env: Env,
|
||||
monitor: any,
|
||||
isUp: boolean,
|
||||
timeIncidentStart: number,
|
||||
timeNow: number,
|
||||
reason: string,
|
||||
) {
|
||||
const dateFormatter = new Intl.DateTimeFormat('en-US', {
|
||||
month: 'numeric',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
timeZone: 'Asia/Shanghai',
|
||||
});
|
||||
|
||||
let downtimeDuration = Math.round((timeNow - timeIncidentStart) / 60);
|
||||
const timeIncidentStartFormatted = dateFormatter.format(new Date(timeIncidentStart * 1000));
|
||||
let statusText = isUp
|
||||
? `The service is up again after being down for ${downtimeDuration} minutes.`
|
||||
: `Service became unavailable at ${timeIncidentStartFormatted}. Issue: ${reason || 'unspecified'}`;
|
||||
|
||||
console.log('Notifying: ', monitor.name, statusText);
|
||||
|
||||
if (env.BARK_SERVER && env.BARK_DEVICE_KEY) {
|
||||
try {
|
||||
let title = isUp ? `✅ ${monitor.name} is up again!` : `🔴 ${monitor.name} is currently down.`;
|
||||
await sendBarkNotification(env, monitor, title, statusText);
|
||||
} catch (error) {
|
||||
console.error('Error sending Bark notification:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (env.SECRET_TELEGRAM_CHAT_ID && env.SECRET_TELEGRAM_API_TOKEN) {
|
||||
try {
|
||||
let operationalLabel = isUp ? 'Up' : 'Down';
|
||||
let statusEmoji = isUp ? '✅' : '🔴';
|
||||
let telegramText = `*${escapeMarkdown(
|
||||
monitor.name,
|
||||
)}* is currently *${operationalLabel}*\n${statusEmoji} ${escapeMarkdown(statusText)}`;
|
||||
await notifyTelegram(env, monitor, isUp, telegramText);
|
||||
} catch (error) {
|
||||
console.error('Error sending Telegram notification:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function notifyTelegram(env: Env, monitor: any, operational: boolean, text: string) {
|
||||
const chatId = env.SECRET_TELEGRAM_CHAT_ID;
|
||||
const apiToken = env.SECRET_TELEGRAM_API_TOKEN;
|
||||
|
||||
const payload = new URLSearchParams({
|
||||
chat_id: chatId,
|
||||
parse_mode: 'MarkdownV2',
|
||||
text: text,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://api.telegram.org/bot${apiToken}/sendMessage`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: payload.toString(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(
|
||||
`Failed to send Telegram notification "${text}", ${response.status} ${
|
||||
response.statusText
|
||||
} ${await response.text()}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending Telegram notification:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendBarkNotification(env: Env, monitor: any, title: string, body: string, group: string = '') {
|
||||
const barkServer = env.BARK_SERVER;
|
||||
const barkDeviceKey = env.BARK_DEVICE_KEY;
|
||||
const barkUrl = `${barkServer}/push`;
|
||||
const data = {
|
||||
title: title,
|
||||
body: body,
|
||||
group: group,
|
||||
url: monitor.url,
|
||||
device_key: barkDeviceKey,
|
||||
};
|
||||
|
||||
const response = await fetch(barkUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('Bark notification sent successfully.');
|
||||
} else {
|
||||
const respText = await response.text();
|
||||
console.error('Failed to send Bark notification:', response.status, response.statusText, respText);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget this, otherwise compilation fails.
|
||||
export { pageConfig, workerConfig }
|
||||
|
|
|
|||
|
|
@ -58,8 +58,12 @@ export default {
|
|||
state.overallDown = 0
|
||||
state.overallUp = 0
|
||||
|
||||
let statusChaged = false
|
||||
const currentTimeSecond = Math.round(Date.now() / 1000)
|
||||
|
||||
// Check each monitor
|
||||
// TODO: concurrent status check
|
||||
|
||||
for (const monitor of workerConfig.monitors) {
|
||||
console.log(`[${workerLocation}] Checking ${monitor.name}...`)
|
||||
|
||||
|
|
@ -112,11 +116,12 @@ export default {
|
|||
// close existing incident if any
|
||||
if (lastIncident.end === undefined) {
|
||||
lastIncident.end = currentTimeSecond
|
||||
statusChaged = true
|
||||
|
||||
try {
|
||||
await workerConfig.callbacks.onStatusChange(
|
||||
monitor.id,
|
||||
monitor.name,
|
||||
env,
|
||||
monitor,
|
||||
true,
|
||||
lastIncident.start[0],
|
||||
currentTimeSecond,
|
||||
|
|
@ -136,11 +141,12 @@ export default {
|
|||
end: undefined,
|
||||
error: [status.err],
|
||||
})
|
||||
statusChaged = true
|
||||
|
||||
try {
|
||||
await workerConfig.callbacks.onStatusChange(
|
||||
monitor.id,
|
||||
monitor.name,
|
||||
env,
|
||||
monitor,
|
||||
false,
|
||||
currentTimeSecond,
|
||||
currentTimeSecond,
|
||||
|
|
@ -157,11 +163,12 @@ export default {
|
|||
// append if the error message changes
|
||||
lastIncident.start.push(currentTimeSecond)
|
||||
lastIncident.error.push(status.err)
|
||||
statusChaged = true
|
||||
|
||||
try {
|
||||
await workerConfig.callbacks.onStatusChange(
|
||||
monitor.id,
|
||||
monitor.name,
|
||||
env,
|
||||
monitor,
|
||||
false,
|
||||
lastIncident.start[0],
|
||||
currentTimeSecond,
|
||||
|
|
@ -175,8 +182,8 @@ export default {
|
|||
|
||||
try {
|
||||
await workerConfig.callbacks.onIncident(
|
||||
monitor.id,
|
||||
monitor.name,
|
||||
env,
|
||||
monitor,
|
||||
lastIncident.start[0],
|
||||
currentTimeSecond,
|
||||
status.err
|
||||
|
|
@ -215,6 +222,12 @@ export default {
|
|||
|
||||
// Update state
|
||||
state.lastUpdate = Math.round(Date.now() / 1000)
|
||||
if (
|
||||
statusChanged ||
|
||||
currentTimeSecond - state.lastUpdate >= workerConfig.kvWriteCooldownMinutes * 60
|
||||
) {
|
||||
state.lastUpdate = currentTimeSecond
|
||||
await env.UPTIMEFLARE_STATE.put('state', JSON.stringify(state))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue