From bdfe95a0f19957aaf481047a1dd8f2f4f50f7301 Mon Sep 17 00:00:00 2001 From: Stephen Papierski Date: Tue, 28 Nov 2023 10:49:33 -0700 Subject: [PATCH 1/5] Add support for SLOW/NOMINAL notification cards --- server/model/monitor.js | 40 ++++++++++++++----- .../notification-provider.js | 6 +++ server/notification.js | 12 +++++- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/server/model/monitor.js b/server/model/monitor.js index 014b54f12..633b14e22 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -1458,16 +1458,29 @@ class Monitor extends BeanModel { * @param {Monitor} monitor The monitor to send a notificaton about * @param {Bean} bean Status information about monitor * @param {string} msg Notification text to be sent + * @param {object} slowStats Slow response information * @returns {void} */ - static async sendSlowResponseNotification(monitor, bean, msg) { + static async sendSlowResponseNotification(monitor, bean, msg, slowStats) { // Send notification const notificationList = await Monitor.getNotificationList(monitor); for (let notification of notificationList) { try { - log.debug("monitor", `[${this.name}] Sending to ${notification.name}`); - await Notification.send(JSON.parse(notification.config), msg); + const heartbeatJSON = bean.toJSON(); + + // 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) { log.error("monitor", `[${this.name}] Cannot send slow response notification to ${notification.name}`); log.error("monitor", e); @@ -1563,11 +1576,6 @@ class Monitor extends BeanModel { 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 if (actualResponseTime === 0 || !Number.isInteger(actualResponseTime)) { log.debug("monitor", `[${this.name}] Failed to calculate valid response time`); @@ -1580,6 +1588,16 @@ class Monitor extends BeanModel { 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})`; + const slowStats = { + calculatedResponse: `${actualResponseTime}ms (${methodDescription})`, + calculatedThreshold: `${threshold}ms (${thresholdDescription})`, + slowFor: `${bean.slowResponseCount * monitor.interval}s`, + }; + bean.pingThreshold = threshold; // Responding normally @@ -1591,7 +1609,7 @@ class Monitor extends BeanModel { msgStats += ` | Slow for: ${bean.slowResponseCount * monitor.interval}s`; 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); + Monitor.sendSlowResponseNotification(monitor, bean, msg, slowStats); // Mark important (SLOW -> NOMINAL) pingMsg += ` < ${threshold}ms`; @@ -1611,7 +1629,7 @@ class Monitor extends BeanModel { if (bean.slowResponseCount === 1) { log.debug("monitor", `[${this.name}] Responded slowly, sending notification | ${msgStats}`); let msg = `[${this.name}] Responded Slowly \n${msgStats}`; - Monitor.sendSlowResponseNotification(monitor, bean, msg); + Monitor.sendSlowResponseNotification(monitor, bean, msg, slowStats); // Mark important (NOMINAL -> SLOW) pingMsg += ` > ${threshold}ms`; @@ -1625,7 +1643,7 @@ class Monitor extends BeanModel { msgStats += ` | Slow for: ${bean.slowResponseCount * monitor.interval}s`; log.debug("monitor", `[${this.name}] Still responding slowly, sendSlowResponseNotification again | ${msgStats}`); let msg = `[${this.name}] Still Responding Slowly \n${msgStats}`; - Monitor.sendSlowResponseNotification(monitor, bean, msg); + Monitor.sendSlowResponseNotification(monitor, bean, msg, slowStats); } else { log.debug("monitor", `[${this.name}] Still responding slowly, waiting for resend interal | ${msgStats}`); } diff --git a/server/notification-providers/notification-provider.js b/server/notification-providers/notification-provider.js index 9b4f0bb05..59ad3c8a8 100644 --- a/server/notification-providers/notification-provider.js +++ b/server/notification-providers/notification-provider.js @@ -6,6 +6,12 @@ class NotificationProvider { */ name = undefined; + /** + * Does the notification provider support slow response notifications? + * @type {boolean} + */ + supportSlowNotifications = false; + /** * Send a notification * @param {BeanModel} notification Notification to send diff --git a/server/notification.js b/server/notification.js index 5e76d6eb1..bcbf5f5f9 100644 --- a/server/notification.js +++ b/server/notification.js @@ -1,5 +1,5 @@ const { R } = require("redbean-node"); -const { log } = require("../src/util"); +const { log, SLOW, NOMINAL } = require("../src/util"); const Alerta = require("./notification-providers/alerta"); const AlertNow = require("./notification-providers/alertnow"); const AliyunSms = require("./notification-providers/aliyun-sms"); @@ -149,7 +149,15 @@ class Notification { */ static async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { if (this.providerList[notification.type]) { - return this.providerList[notification.type].send(notification, msg, monitorJSON, heartbeatJSON); + 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); + } + } else { throw new Error("Notification type is not supported"); } From 7d5297df9d190976d7d542fce3a577593df022d7 Mon Sep 17 00:00:00 2001 From: Stephen Papierski Date: Tue, 28 Nov 2023 10:50:17 -0700 Subject: [PATCH 2/5] Add discord support for SLOW/NOMINAL notification cards --- server/notification-providers/discord.js | 86 +++++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/server/notification-providers/discord.js b/server/notification-providers/discord.js index f24cd6169..a0b0f8d29 100644 --- a/server/notification-providers/discord.js +++ b/server/notification-providers/discord.js @@ -1,10 +1,11 @@ const NotificationProvider = require("./notification-provider"); const axios = require("axios"); -const { DOWN, UP } = require("../../src/util"); +const { DOWN, UP, SLOW, NOMINAL } = require("../../src/util"); class Discord extends NotificationProvider { name = "discord"; + supportSlowNotifications = true; /** * @inheritdoc @@ -115,12 +116,93 @@ class Discord extends NotificationProvider { await axios.post(notification.discordWebhookUrl, discordupdata); 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) { this.throwGeneralAxiosError(error); } } - } module.exports = Discord; From 19799e5d9d44a89275d3aa5d6cd098ef3aa966da Mon Sep 17 00:00:00 2001 From: Stephen Papierski Date: Tue, 28 Nov 2023 12:46:43 -0700 Subject: [PATCH 3/5] Add slack support for SLOW/NOMINAL notification cards --- server/notification-providers/slack.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/server/notification-providers/slack.js b/server/notification-providers/slack.js index d512a7cd8..f343280d6 100644 --- a/server/notification-providers/slack.js +++ b/server/notification-providers/slack.js @@ -1,11 +1,12 @@ const NotificationProvider = require("./notification-provider"); const axios = require("axios"); 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 { name = "slack"; + supportSlowNotifications = true; /** * Deprecated property notification.slackbutton @@ -50,6 +51,23 @@ class Slack extends NotificationProvider { } 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 = { "text": `${textMsg}\n${msg}`, "channel": notification.slackchannel, @@ -57,7 +75,7 @@ class Slack extends NotificationProvider { "icon_emoji": notification.slackiconemo, "attachments": [ { - "color": (heartbeatJSON["status"] === UP) ? "#2eb886" : "#e01e5a", + "color": color, "blocks": [ { "type": "header", From e4a2f7890be43e84f8af0feea72ea403cff09dc9 Mon Sep 17 00:00:00 2001 From: Stephen Papierski Date: Tue, 28 Nov 2023 12:54:32 -0700 Subject: [PATCH 4/5] Move notification msg generation to send function This better matches the existing model. --- server/model/monitor.js | 42 ++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/server/model/monitor.js b/server/model/monitor.js index 633b14e22..4fa16c67b 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -1457,14 +1457,22 @@ class Monitor extends BeanModel { * Send a slow response notification about a monitor * @param {Monitor} monitor The monitor to send a notificaton about * @param {Bean} bean Status information about monitor - * @param {string} msg Notification text to be sent * @param {object} slowStats Slow response information * @returns {void} */ - static async sendSlowResponseNotification(monitor, bean, msg, slowStats) { + static async sendSlowResponseNotification(monitor, bean, slowStats) { // Send notification 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) { try { const heartbeatJSON = bean.toJSON(); @@ -1591,7 +1599,6 @@ class Monitor extends BeanModel { // 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})`; const slowStats = { calculatedResponse: `${actualResponseTime}ms (${methodDescription})`, calculatedThreshold: `${threshold}ms (${thresholdDescription})`, @@ -1608,13 +1615,12 @@ class Monitor extends BeanModel { } else { msgStats += ` | Slow for: ${bean.slowResponseCount * monitor.interval}s`; 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, slowStats); // Mark important (SLOW -> NOMINAL) - pingMsg += ` < ${threshold}ms`; bean.pingImportant = true; - bean.pingMsg = pingMsg; + bean.pingMsg = `Returned to Normal Response Time \n${msgStats}`; + + Monitor.sendSlowResponseNotification(monitor, bean, slowStats); } // Reset slow response count @@ -1627,28 +1633,30 @@ class Monitor extends BeanModel { // Always send first notification if (bean.slowResponseCount === 1) { - log.debug("monitor", `[${this.name}] Responded slowly, sending notification | ${msgStats}`); - let msg = `[${this.name}] Responded Slowly \n${msgStats}`; - Monitor.sendSlowResponseNotification(monitor, bean, msg, slowStats); + log.debug("monitor", `[${this.name}] Responded slow, sending notification | ${msgStats}`); // Mark important (NOMINAL -> SLOW) - pingMsg += ` > ${threshold}ms`; bean.pingImportant = true; - bean.pingMsg = pingMsg; + bean.pingMsg = `Responded Slow \n${msgStats}`; + + Monitor.sendSlowResponseNotification(monitor, bean, slowStats); + // Send notification every x times } else if (this.slowResponseNotificationResendInterval > 0) { if (((bean.slowResponseCount) % this.slowResponseNotificationResendInterval) === 0) { // Send notification again, because we are still responding slow msgStats += ` | Slow for: ${bean.slowResponseCount * monitor.interval}s`; - log.debug("monitor", `[${this.name}] Still responding slowly, sendSlowResponseNotification again | ${msgStats}`); - let msg = `[${this.name}] Still Responding Slowly \n${msgStats}`; - Monitor.sendSlowResponseNotification(monitor, bean, msg, slowStats); + log.debug("monitor", `[${this.name}] Still responding slow, sendSlowResponseNotification again | ${msgStats}`); + + bean.pingMsg = `Still Responding Slow \n${msgStats}`; + + Monitor.sendSlowResponseNotification(monitor, bean, slowStats); } 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 { - 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}`); } } } From 763925b9b9b76c13d2383b9138787f3968d440fd Mon Sep 17 00:00:00 2001 From: Stephen Papierski Date: Tue, 28 Nov 2023 14:27:46 -0700 Subject: [PATCH 5/5] lint fix --- server/model/monitor.js | 1 - server/notification-providers/slack.js | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/server/model/monitor.js b/server/model/monitor.js index 4fa16c67b..783c42854 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -1641,7 +1641,6 @@ class Monitor extends BeanModel { Monitor.sendSlowResponseNotification(monitor, bean, slowStats); - // Send notification every x times } else if (this.slowResponseNotificationResendInterval > 0) { if (((bean.slowResponseCount) % this.slowResponseNotificationResendInterval) === 0) { diff --git a/server/notification-providers/slack.js b/server/notification-providers/slack.js index f343280d6..cf4810d17 100644 --- a/server/notification-providers/slack.js +++ b/server/notification-providers/slack.js @@ -53,7 +53,7 @@ class Slack extends NotificationProvider { const textMsg = "Uptime Kuma Alert"; let color; - switch(heartbeatJSON["status"]) { + switch (heartbeatJSON["status"]) { case UP: case NOMINAL: color = "#2eb886"; @@ -62,7 +62,7 @@ class Slack extends NotificationProvider { color = "#ffc107"; break; case DOWN: - color = "#e01e5a" + color = "#e01e5a"; break; default: color = "#0dcaf0";