Skip to content

Commit

Permalink
Merge branch 'feature-slow-response-notification-cards' into feature-…
Browse files Browse the repository at this point in the history
…slow-response-notification
  • Loading branch information
stephenpapierski committed Jan 16, 2024
2 parents 2755276 + 64970ed commit e4b7913
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 29 deletions.
71 changes: 48 additions & 23 deletions server/model/monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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`);
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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}`);
}
}
}
Expand Down
86 changes: 84 additions & 2 deletions server/notification-providers/discord.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
6 changes: 6 additions & 0 deletions server/notification-providers/notification-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 20 additions & 2 deletions server/notification-providers/slack.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -50,14 +51,31 @@ 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,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
"attachments": [
{
"color": (heartbeatJSON["status"] === UP) ? "#2eb886" : "#e01e5a",
"color": color,
"blocks": [
{
"type": "header",
Expand Down
12 changes: 10 additions & 2 deletions server/notification.js
Original file line number Diff line number Diff line change
@@ -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");
Expand Down Expand Up @@ -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");
}
Expand Down

0 comments on commit e4b7913

Please sign in to comment.