Merge branch 'feature-slow-response-notification-cards' into feature-slow-response-notification

This commit is contained in:
Stephen Papierski 2024-01-16 15:05:03 -07:00
commit e4b79139ff
No known key found for this signature in database
5 changed files with 168 additions and 29 deletions

View file

@ -1458,17 +1458,38 @@ class Monitor extends BeanModel {
* Send a slow response notification about a monitor * Send a slow response notification about a monitor
* @param {Monitor} monitor The monitor to send a notificaton about * @param {Monitor} monitor The monitor to send a notificaton about
* @param {Bean} bean Status information about monitor * @param {Bean} bean Status information about monitor
* @param {string} msg Notification text to be sent * @param {object} slowStats Slow response information
* @returns {void} * @returns {void}
*/ */
static async sendSlowResponseNotification(monitor, bean, msg) { static async sendSlowResponseNotification(monitor, bean, slowStats) {
// Send notification // Send notification
const notificationList = await Monitor.getNotificationList(monitor); const notificationList = await Monitor.getNotificationList(monitor);
let text;
if (bean.pingStatus === NOMINAL) {
text = "🚀 Nominal";
} else {
text = "🐌 Slow";
}
let msg = `[${monitor.name}] [${text}] ${bean.pingMsg}`;
for (let notification of notificationList) { for (let notification of notificationList) {
try { try {
log.debug("monitor", `[${this.name}] Sending to ${notification.name}`); const heartbeatJSON = bean.toJSON();
await Notification.send(JSON.parse(notification.config), msg);
// Override status with SLOW/NOMINAL, add slowStats
heartbeatJSON["status"] = bean.pingStatus;
heartbeatJSON["calculatedResponse"] = slowStats.calculatedResponse;
heartbeatJSON["calculatedThreshold"] = slowStats.calculatedThreshold;
heartbeatJSON["slowFor"] = slowStats.slowFor;
// Also provide the time in server timezone
heartbeatJSON["timezone"] = await UptimeKumaServer.getInstance().getTimezone();
heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset();
heartbeatJSON["localDateTime"] = dayjs.utc(heartbeatJSON["time"]).tz(heartbeatJSON["timezone"]).format(SQL_DATETIME_FORMAT);
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), heartbeatJSON);
} catch (e) { } catch (e) {
log.error("monitor", `[${this.name}] Cannot send slow response notification to ${notification.name}`); log.error("monitor", `[${this.name}] Cannot send slow response notification to ${notification.name}`);
log.error("monitor", e); log.error("monitor", e);
@ -1564,11 +1585,6 @@ class Monitor extends BeanModel {
return; return;
} }
// Create stats to append to messages/logs
const methodDescription = [ "average", "max" ].includes(method) ? `${method} of last ${windowDuration}s` : method;
let msgStats = `Response: ${actualResponseTime}ms (${methodDescription}) | Threshold: ${threshold}ms (${thresholdDescription})`;
let pingMsg = `${actualResponseTime}ms resp. (${methodDescription})`;
// Verify valid response time was calculated // Verify valid response time was calculated
if (actualResponseTime === 0 || !Number.isInteger(actualResponseTime)) { if (actualResponseTime === 0 || !Number.isInteger(actualResponseTime)) {
log.debug("monitor", `[${this.name}] Failed to calculate valid response time`); log.debug("monitor", `[${this.name}] Failed to calculate valid response time`);
@ -1581,6 +1597,15 @@ class Monitor extends BeanModel {
return; return;
} }
// Create stats to append to messages/logs
const methodDescription = [ "average", "max" ].includes(method) ? `${method} of last ${windowDuration}s` : method;
let msgStats = `Response: ${actualResponseTime}ms (${methodDescription}) | Threshold: ${threshold}ms (${thresholdDescription})`;
const slowStats = {
calculatedResponse: `${actualResponseTime}ms (${methodDescription})`,
calculatedThreshold: `${threshold}ms (${thresholdDescription})`,
slowFor: `${bean.slowResponseCount * monitor.interval}s`,
};
bean.pingThreshold = threshold; bean.pingThreshold = threshold;
// Responding normally // Responding normally
@ -1591,13 +1616,12 @@ class Monitor extends BeanModel {
} else { } else {
msgStats += ` | Slow for: ${bean.slowResponseCount * monitor.interval}s`; msgStats += ` | Slow for: ${bean.slowResponseCount * monitor.interval}s`;
log.debug("monitor", `[${this.name}] Returned to normal response time | ${msgStats}`); log.debug("monitor", `[${this.name}] Returned to normal response time | ${msgStats}`);
let msg = `[${this.name}] Returned to Normal Response Time \n${msgStats}`;
Monitor.sendSlowResponseNotification(monitor, bean, msg);
// Mark important (SLOW -> NOMINAL) // Mark important (SLOW -> NOMINAL)
pingMsg += ` < ${threshold}ms`;
bean.pingImportant = true; bean.pingImportant = true;
bean.pingMsg = pingMsg; bean.pingMsg = `Returned to Normal Response Time \n${msgStats}`;
Monitor.sendSlowResponseNotification(monitor, bean, slowStats);
} }
// Reset slow response count // Reset slow response count
@ -1610,28 +1634,29 @@ class Monitor extends BeanModel {
// Always send first notification // Always send first notification
if (bean.slowResponseCount === 1) { if (bean.slowResponseCount === 1) {
log.debug("monitor", `[${this.name}] Responded slowly, sending notification | ${msgStats}`); log.debug("monitor", `[${this.name}] Responded slow, sending notification | ${msgStats}`);
let msg = `[${this.name}] Responded Slowly \n${msgStats}`;
Monitor.sendSlowResponseNotification(monitor, bean, msg);
// Mark important (NOMINAL -> SLOW) // Mark important (NOMINAL -> SLOW)
pingMsg += ` > ${threshold}ms`;
bean.pingImportant = true; bean.pingImportant = true;
bean.pingMsg = pingMsg; bean.pingMsg = `Responded Slow \n${msgStats}`;
Monitor.sendSlowResponseNotification(monitor, bean, slowStats);
// Send notification every x times // Send notification every x times
} else if (this.slowResponseNotificationResendInterval > 0) { } else if (this.slowResponseNotificationResendInterval > 0) {
if (((bean.slowResponseCount) % this.slowResponseNotificationResendInterval) === 0) { if (((bean.slowResponseCount) % this.slowResponseNotificationResendInterval) === 0) {
// Send notification again, because we are still responding slow // Send notification again, because we are still responding slow
msgStats += ` | Slow for: ${bean.slowResponseCount * monitor.interval}s`; msgStats += ` | Slow for: ${bean.slowResponseCount * monitor.interval}s`;
log.debug("monitor", `[${this.name}] Still responding slowly, sendSlowResponseNotification again | ${msgStats}`); log.debug("monitor", `[${this.name}] Still responding slow, sendSlowResponseNotification again | ${msgStats}`);
let msg = `[${this.name}] Still Responding Slowly \n${msgStats}`;
Monitor.sendSlowResponseNotification(monitor, bean, msg); bean.pingMsg = `Still Responding Slow \n${msgStats}`;
Monitor.sendSlowResponseNotification(monitor, bean, slowStats);
} else { } else {
log.debug("monitor", `[${this.name}] Still responding slowly, waiting for resend interal | ${msgStats}`); log.debug("monitor", `[${this.name}] Still responding slow, waiting for resend interal | ${msgStats}`);
} }
} else { } else {
log.debug("monitor", `[${this.name}] Still responding slowly, but resend is disabled | ${msgStats}`); log.debug("monitor", `[${this.name}] Still responding slow, but resend is disabled | ${msgStats}`);
} }
} }
} }

View file

@ -1,10 +1,11 @@
const NotificationProvider = require("./notification-provider"); const NotificationProvider = require("./notification-provider");
const axios = require("axios"); const axios = require("axios");
const { DOWN, UP } = require("../../src/util"); const { DOWN, UP, SLOW, NOMINAL } = require("../../src/util");
class Discord extends NotificationProvider { class Discord extends NotificationProvider {
name = "discord"; name = "discord";
supportSlowNotifications = true;
/** /**
* @inheritdoc * @inheritdoc
@ -115,12 +116,93 @@ class Discord extends NotificationProvider {
await axios.post(notification.discordWebhookUrl, discordupdata); await axios.post(notification.discordWebhookUrl, discordupdata);
return okMsg; return okMsg;
} else if (heartbeatJSON["status"] === SLOW) {
let discordslowdata = {
username: discordDisplayName,
embeds: [{
title: "🐌 Your service " + monitorJSON["name"] + " responded slow. 🐌",
color: 16761095,
timestamp: heartbeatJSON["time"],
fields: [
{
name: "Service Name",
value: monitorJSON["name"],
},
{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
},
{
name: `Time (${heartbeatJSON["timezone"]})`,
value: heartbeatJSON["localDateTime"],
},
{
name: "Ping",
value: heartbeatJSON["calculatedResponse"],
},
{
name: "Threshold",
value: heartbeatJSON["calculatedThreshold"],
},
],
}],
};
if (notification.discordPrefixMessage) {
discordslowdata.content = notification.discordPrefixMessage;
}
await axios.post(notification.discordWebhookUrl, discordslowdata);
return okMsg;
} else if (heartbeatJSON["status"] === NOMINAL) {
let discordnominaldata = {
username: discordDisplayName,
embeds: [{
title: "🚀 Your service " + monitorJSON["name"] + " is responding normally! 🚀",
color: 65280,
timestamp: heartbeatJSON["time"],
fields: [
{
name: "Service Name",
value: monitorJSON["name"],
},
{
name: monitorJSON["type"] === "push" ? "Service Type" : "Service URL",
value: monitorJSON["type"] === "push" ? "Heartbeat" : address,
},
{
name: `Time (${heartbeatJSON["timezone"]})`,
value: heartbeatJSON["localDateTime"],
},
{
name: "Ping",
value: heartbeatJSON["calculatedResponse"],
},
{
name: "Threshold",
value: heartbeatJSON["calculatedThreshold"],
},
{
name: "Slow For",
value: heartbeatJSON["slowFor"],
},
],
}],
};
if (notification.discordPrefixMessage) {
discordnominaldata.content = notification.discordPrefixMessage;
}
await axios.post(notification.discordWebhookUrl, discordnominaldata);
return okMsg;
} else {
this.throwGeneralAxiosError("Not sure why we're here");
} }
} catch (error) { } catch (error) {
this.throwGeneralAxiosError(error); this.throwGeneralAxiosError(error);
} }
} }
} }
module.exports = Discord; module.exports = Discord;

View file

@ -6,6 +6,12 @@ class NotificationProvider {
*/ */
name = undefined; name = undefined;
/**
* Does the notification provider support slow response notifications?
* @type {boolean}
*/
supportSlowNotifications = false;
/** /**
* Send a notification * Send a notification
* @param {BeanModel} notification Notification to send * @param {BeanModel} notification Notification to send

View file

@ -1,11 +1,12 @@
const NotificationProvider = require("./notification-provider"); const NotificationProvider = require("./notification-provider");
const axios = require("axios"); const axios = require("axios");
const { setSettings, setting } = require("../util-server"); const { setSettings, setting } = require("../util-server");
const { getMonitorRelativeURL, UP } = require("../../src/util"); const { getMonitorRelativeURL, UP, DOWN, NOMINAL, SLOW } = require("../../src/util");
class Slack extends NotificationProvider { class Slack extends NotificationProvider {
name = "slack"; name = "slack";
supportSlowNotifications = true;
/** /**
* Deprecated property notification.slackbutton * Deprecated property notification.slackbutton
@ -50,6 +51,23 @@ class Slack extends NotificationProvider {
} }
const textMsg = "Uptime Kuma Alert"; const textMsg = "Uptime Kuma Alert";
let color;
switch (heartbeatJSON["status"]) {
case UP:
case NOMINAL:
color = "#2eb886";
break;
case SLOW:
color = "#ffc107";
break;
case DOWN:
color = "#e01e5a";
break;
default:
color = "#0dcaf0";
}
let data = { let data = {
"text": `${textMsg}\n${msg}`, "text": `${textMsg}\n${msg}`,
"channel": notification.slackchannel, "channel": notification.slackchannel,
@ -57,7 +75,7 @@ class Slack extends NotificationProvider {
"icon_emoji": notification.slackiconemo, "icon_emoji": notification.slackiconemo,
"attachments": [ "attachments": [
{ {
"color": (heartbeatJSON["status"] === UP) ? "#2eb886" : "#e01e5a", "color": color,
"blocks": [ "blocks": [
{ {
"type": "header", "type": "header",

View file

@ -1,5 +1,5 @@
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { log } = require("../src/util"); const { log, SLOW, NOMINAL } = require("../src/util");
const Alerta = require("./notification-providers/alerta"); const Alerta = require("./notification-providers/alerta");
const AlertNow = require("./notification-providers/alertnow"); const AlertNow = require("./notification-providers/alertnow");
const AliyunSms = require("./notification-providers/aliyun-sms"); const AliyunSms = require("./notification-providers/aliyun-sms");
@ -149,7 +149,15 @@ class Notification {
*/ */
static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
if (this.providerList[notification.type]) { if (this.providerList[notification.type]) {
if ((heartbeatJSON?.status === SLOW || heartbeatJSON?.status === NOMINAL) && !this.providerList[notification.type].supportSlowNotifications) {
// This is a SLOW/NOMINAL notification where the provider does NOT support card notificatons yet
// TODO Ideally, this goes away once all the notification providers support slow response notification cards
log.debug("notification", `${notification.type} does not support card notifications for SLOW/NOMINAL events yet. Sending plain text message.`);
return this.providerList[notification.type].send(notification, msg);
} else {
return this.providerList[notification.type].send(notification, msg, monitorJSON, heartbeatJSON); return this.providerList[notification.type].send(notification, msg, monitorJSON, heartbeatJSON);
}
} else { } else {
throw new Error("Notification type is not supported"); throw new Error("Notification type is not supported");
} }