diff --git a/server/model/monitor.js b/server/model/monitor.js index 52f5ba659..362757333 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -1458,17 +1458,38 @@ 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) { + 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 { - 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); @@ -1564,11 +1585,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`); @@ -1581,6 +1597,15 @@ 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})`; + const slowStats = { + calculatedResponse: `${actualResponseTime}ms (${methodDescription})`, + calculatedThreshold: `${threshold}ms (${thresholdDescription})`, + slowFor: `${bean.slowResponseCount * monitor.interval}s`, + }; + bean.pingThreshold = threshold; // Responding normally @@ -1591,13 +1616,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); // 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 @@ -1610,28 +1634,29 @@ 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); + 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); + 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}`); } } } 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; 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-providers/slack.js b/server/notification-providers/slack.js index d512a7cd8..cf4810d17 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", 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"); }