pull/4005/merge
Stephen Papierski 1 month ago committed by GitHub
commit 934c0e2b3a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,33 @@
exports.up = function (knex) {
// add various slow_response_notification parameters
return knex.schema
.alterTable("monitor", function (table) {
table.boolean("slow_response_notification").notNullable().defaultTo(false);
table.string("slow_response_notification_method").notNullable().defaultTo("average");
table.integer("slow_response_notification_range").notNullable().defaultTo(300);
table.string("slow_response_notification_threshold_method").notNullable().defaultTo("threshold-relative-24-hour");
table.integer("slow_response_notification_threshold").notNullable().defaultTo(2500);
table.float("slow_response_notification_threshold_multiplier").notNullable().defaultTo(5.0);
table.integer("slow_response_notification_resend_interval").notNullable().defaultTo(0);
})
.alterTable("heartbeat", function (table) {
table.integer("slow_response_count").notNullable().defaultTo(0);
});
};
exports.down = function (knex) {
// remove various slow_response_notification parameters
return knex.schema
.alterTable("monitor", function (table) {
table.dropColumn("slow_response_notification");
table.dropColumn("slow_response_notification_method");
table.dropColumn("slow_response_notification_range");
table.dropColumn("slow_response_notification_threshold_method");
table.dropColumn("slow_response_notification_threshold");
table.dropColumn("slow_response_notification_threshold_multiplier");
table.dropColumn("slow_response_notification_resend_interval");
})
.alterTable("heartbeat", function (table) {
table.dropColumn("slow_response_count");
});
};

@ -0,0 +1,21 @@
exports.up = function (knex) {
// add various slow response parameters
return knex.schema
.alterTable("heartbeat", function (table) {
table.integer("ping_status").nullable().defaultTo(null);
table.integer("ping_threshold").nullable().defaultTo(null);
table.boolean("ping_important").notNullable().defaultTo(0);
table.string("ping_msg").nullable().defaultTo(null);
});
};
exports.down = function (knex) {
// remove various slow response parameters
return knex.schema
.alterTable("heartbeat", function (table) {
table.dropColumn("ping_status");
table.dropColumn("ping_threshold");
table.dropColumn("ping_important");
table.dropColumn("ping_msg");
});
};

10
package-lock.json generated

@ -100,6 +100,7 @@
"bootstrap": "5.1.3",
"chart.js": "~4.2.1",
"chartjs-adapter-dayjs-4": "~1.0.4",
"chartjs-plugin-annotation": "~3.0.1",
"concurrently": "^7.1.0",
"core-js": "~3.26.1",
"cronstrue": "~2.24.0",
@ -5735,6 +5736,15 @@
"dayjs": "^1.9.7"
}
},
"node_modules/chartjs-plugin-annotation": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/chartjs-plugin-annotation/-/chartjs-plugin-annotation-3.0.1.tgz",
"integrity": "sha512-hlIrXXKqSDgb+ZjVYHefmlZUXK8KbkCPiynSVrTb/HjTMkT62cOInaT1NTQCKtxKKOm9oHp958DY3RTAFKtkHg==",
"dev": true,
"peerDependencies": {
"chart.js": ">=4.0.0"
}
},
"node_modules/check-password-strength": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/check-password-strength/-/check-password-strength-2.0.10.tgz",

@ -165,6 +165,7 @@
"bootstrap": "5.1.3",
"chart.js": "~4.2.1",
"chartjs-adapter-dayjs-4": "~1.0.4",
"chartjs-plugin-annotation": "~3.0.1",
"concurrently": "^7.1.0",
"core-js": "~3.26.1",
"cronstrue": "~2.24.0",

@ -55,7 +55,7 @@ async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite =
monitorID,
]);
let result = list.reverse();
let result = R.convertToBeans("heartbeat", list.reverse());
if (toUser) {
io.to(socket.userID).emit("heartbeatList", monitorID, result, overwrite);

@ -6,6 +6,9 @@ const { BeanModel } = require("redbean-node/dist/bean-model");
* 1 = UP
* 2 = PENDING
* 3 = MAINTENANCE
* pingStatus:
* 4 = SLOW
* 5 = NOMINAL
*/
class Heartbeat extends BeanModel {
@ -37,6 +40,10 @@ class Heartbeat extends BeanModel {
important: this._important,
duration: this._duration,
retries: this._retries,
pingThreshold: this._pingThreshold,
pingStatus: this._pingStatus,
pingImportant: this._pingImportant,
pingMsg: this._pingMsg,
};
}

@ -1,8 +1,7 @@
const dayjs = require("dayjs");
const axios = require("axios");
const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
SQL_DATETIME_FORMAT
const { log, UP, DOWN, PENDING, MAINTENANCE, NOMINAL, SLOW, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND, SQL_DATETIME_FORMAT
} = require("../../src/util");
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
@ -34,6 +33,9 @@ const rootCertificates = rootCertificatesFingerprints();
* 1 = UP
* 2 = PENDING
* 3 = MAINTENANCE
* pingStatus:
* 4 = SLOW
* 5 = NOMINAL
*/
class Monitor extends BeanModel {
@ -142,6 +144,13 @@ class Monitor extends BeanModel {
mqttCheckType: this.mqttCheckType,
databaseQuery: this.databaseQuery,
authMethod: this.authMethod,
slowResponseNotification: this.isEnabledSlowResponseNotification(),
slowResponseNotificationMethod: this.slowResponseNotificationMethod,
slowResponseNotificationRange: this.slowResponseNotificationRange,
slowResponseNotificationThresholdMethod: this.slowResponseNotificationThresholdMethod,
slowResponseNotificationThreshold: this.slowResponseNotificationThreshold,
slowResponseNotificationThresholdMultiplier: this.slowResponseNotificationThresholdMultiplier,
slowResponseNotificationResendInterval: this.slowResponseNotificationResendInterval,
grpcUrl: this.grpcUrl,
grpcProtobuf: this.grpcProtobuf,
grpcMethod: this.grpcMethod,
@ -309,6 +318,14 @@ class Monitor extends BeanModel {
return Boolean(this.gamedigGivenPortOnly);
}
/**
* Is the slow response notification enabled?
* @returns {boolean} Slow response notification is enabled?
*/
isEnabledSlowResponseNotification() {
return Boolean(this.slowResponseNotification);
}
/**
* Parse to boolean
* @returns {boolean} Kafka Producer Ssl enabled?
@ -371,6 +388,7 @@ class Monitor extends BeanModel {
bean.time = R.isoDateTimeMillis(dayjs.utc());
bean.status = DOWN;
bean.downCount = previousBeat?.downCount || 0;
bean.slowResponseCount = previousBeat?.slowResponseCount || 0;
if (this.isUpsideDown()) {
bean.status = flipStatus(bean.status);
@ -983,6 +1001,12 @@ class Monitor extends BeanModel {
let endTimeDayjs = await uptimeCalculator.update(bean.status, parseFloat(bean.ping));
bean.end_time = R.isoDateTimeMillis(endTimeDayjs);
// Check if response time is slow
if (this.isEnabledSlowResponseNotification() && !isFirstBeat) {
log.debug("monitor", `[${this.name}] Check if response is slow`);
await this.checkSlowResponseNotification(this, bean);
}
// Send to frontend
log.debug("monitor", `[${this.name}] Send to socket`);
io.to(this.user_id).emit("heartbeat", bean.toJSON());
@ -1449,6 +1473,213 @@ 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 {object} slowStats Slow response information
* @returns {void}
*/
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();
// 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);
}
}
}
/**
* Check if heartbeat response time is slower than threshold.
* @param {Monitor} monitor The monitor to send a notification about
* @param {Bean} bean Status information about monitor
* @returns {Promise<void>}
*/
async checkSlowResponseNotification(monitor, bean) {
if (bean.status !== UP) {
log.debug("monitor", `[${this.name}] Monitor status is not UP, skipping slow response check`);
return;
}
const method = monitor.slowResponseNotificationMethod;
const thresholdMethod = monitor.slowResponseNotificationThresholdMethod;
const thresholdMultipler = monitor.slowResponseNotificationThresholdMultiplier;
const windowDuration = monitor.slowResponseNotificationRange;
let actualResponseTime = 0;
let previousBeats;
if (method !== "last") {
//Get recent heartbeat list with range of time
const afterThisDate = new Date(Date.now() - (1000 * (monitor.slowResponseNotificationRange + 1))); // add 1 second otherwise we grab 0 previous beats when Time Range == Heartbeat Interval
previousBeats = await R.getAll(`
SELECT * FROM heartbeat
WHERE monitor_id = ? AND time > datetime(?) AND status = ?`,
[
monitor.id,
afterThisDate.toISOString(),
UP,
]);
}
switch (method) {
case "average":
previousBeats.forEach(beat => {
actualResponseTime = actualResponseTime + beat.ping;
});
actualResponseTime = Math.round(actualResponseTime / previousBeats.length);
break;
case "max":
previousBeats.forEach(beat => {
actualResponseTime = Math.max(actualResponseTime, beat.ping);
});
break;
case "last":
actualResponseTime = bean.ping;
break;
default:
log.error("monitor", `[${this.name}] Unknown response time calculation method for slow response notification: ${method}`);
return;
}
let threshold;
let thresholdDescription;
let afterThisDate;
let avgPing;
switch (thresholdMethod) {
case "threshold-static":
threshold = monitor.slowResponseNotificationThreshold;
thresholdDescription = "static";
break;
case "threshold-relative-24-hour":
//Get average response time over last 24 hours
afterThisDate = new Date(Date.now() - (1000 * (24 * 60 * 60))); // 24 hours in milliseconds
avgPing = parseInt(await R.getCell(`
SELECT AVG(ping) FROM heartbeat
WHERE time > datetime(?)
AND ping IS NOT NULL
AND monitor_id = ?
AND status = ?
`,
[ afterThisDate.toISOString(), monitor.id, UP ]
));
//calculate threshold
threshold = Math.round(avgPing * thresholdMultipler);
thresholdDescription = `${thresholdMultipler}x 24H Avg`;
break;
default:
log.error("monitor", `[${this.name}] Unknown threshold calculation method for slow response notification: ${thresholdMethod}`);
return;
}
// Verify valid response time was calculated
if (actualResponseTime === 0 || !Number.isInteger(actualResponseTime)) {
log.debug("monitor", `[${this.name}] Failed to calculate valid response time`);
return;
}
// Verify valid threshold was calculated
if (!Number.isInteger(threshold)) {
log.debug("monitor", `[${this.name}] Failed to calculate valid threshold`);
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
if (actualResponseTime < threshold) {
bean.pingStatus = NOMINAL;
if (bean.slowResponseCount === 0) {
log.debug("monitor", `[${this.name}] Responding normally. No need to send slow response notification | ${msgStats}`);
} else {
msgStats += ` | Slow for: ${bean.slowResponseCount * monitor.interval}s`;
log.debug("monitor", `[${this.name}] Returned to normal response time | ${msgStats}`);
// Mark important (SLOW -> NOMINAL)
bean.pingImportant = true;
bean.pingMsg = `Returned to Normal Response Time \n${msgStats}`;
Monitor.sendSlowResponseNotification(monitor, bean, slowStats);
}
// Reset slow response count
bean.slowResponseCount = 0;
// Responding slowly
} else {
bean.pingStatus = SLOW;
++bean.slowResponseCount;
// Always send first notification
if (bean.slowResponseCount === 1) {
log.debug("monitor", `[${this.name}] Responded slow, sending notification | ${msgStats}`);
// Mark important (NOMINAL -> SLOW)
bean.pingImportant = true;
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 slow, sendSlowResponseNotification again | ${msgStats}`);
bean.pingMsg = `Still Responding Slow \n${msgStats}`;
Monitor.sendSlowResponseNotification(monitor, bean, slowStats);
} else {
log.debug("monitor", `[${this.name}] Still responding slow, waiting for resend interal | ${msgStats}`);
}
} else {
log.debug("monitor", `[${this.name}] Still responding slow, but resend is disabled | ${msgStats}`);
}
}
}
/**
* Get the status of the previous heartbeat
* @param {number} monitorID ID of monitor to check

@ -1,9 +1,10 @@
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
@ -114,12 +115,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,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

@ -1,10 +1,11 @@
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
@ -49,6 +50,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,
@ -56,7 +74,7 @@ class Slack extends NotificationProvider {
"icon_emoji": notification.slackiconemo,
"attachments": [
{
"color": (heartbeatJSON["status"] === UP) ? "#2eb886" : "#e01e5a",
"color": color,
"blocks": [
{
"type": "header",

@ -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");
@ -155,7 +155,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");
}

@ -805,6 +805,13 @@ let needSetup = false;
bean.authMethod = monitor.authMethod;
bean.authWorkstation = monitor.authWorkstation;
bean.authDomain = monitor.authDomain;
bean.slowResponseNotification = monitor.slowResponseNotification;
bean.slowResponseNotificationMethod = monitor.slowResponseNotificationMethod;
bean.slowResponseNotificationRange = monitor.slowResponseNotificationRange;
bean.slowResponseNotificationThresholdMethod = monitor.slowResponseNotificationThresholdMethod;
bean.slowResponseNotificationThreshold = monitor.slowResponseNotificationThreshold;
bean.slowResponseNotificationThresholdMultiplier = monitor.slowResponseNotificationThresholdMultiplier;
bean.slowResponseNotificationResendInterval = monitor.slowResponseNotificationResendInterval;
bean.grpcUrl = monitor.grpcUrl;
bean.grpcProtobuf = monitor.grpcProtobuf;
bean.grpcServiceName = monitor.grpcServiceName;
@ -1191,9 +1198,9 @@ let needSetup = false;
let count;
if (monitorID == null) {
count = await R.count("heartbeat", "important = 1");
count = await R.count("heartbeat", "important = 1 OR ping_important = 1");
} else {
count = await R.count("heartbeat", "monitor_id = ? AND important = 1", [
count = await R.count("heartbeat", "monitor_id = ? AND (important = 1 OR ping_important = 1)", [
monitorID,
]);
}
@ -1217,7 +1224,7 @@ let needSetup = false;
let list;
if (monitorID == null) {
list = await R.find("heartbeat", `
important = 1
important = 1 OR ping_important = 1
ORDER BY time DESC
LIMIT ?
OFFSET ?
@ -1228,7 +1235,7 @@ let needSetup = false;
} else {
list = await R.find("heartbeat", `
monitor_id = ?
AND important = 1
AND (important = 1 OR ping_important = 1)
ORDER BY time DESC
LIMIT ?
OFFSET ?
@ -1442,7 +1449,9 @@ let needSetup = false;
log.info("manage", `Clear Events Monitor: ${monitorID} User ID: ${socket.userID}`);
await R.exec("UPDATE heartbeat SET msg = ?, important = ? WHERE monitor_id = ? ", [
await R.exec("UPDATE heartbeat SET msg = ?, important = ?, ping_msg = ?, ping_important = ? WHERE monitor_id = ? ", [
"",
"0",
"",
"0",
monitorID,

@ -19,11 +19,12 @@
<script lang="js">
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
import "chartjs-adapter-dayjs-4";
import annotationPlugin from "chartjs-plugin-annotation";
import dayjs from "dayjs";
import { Line } from "vue-chartjs";
import { DOWN, PENDING, MAINTENANCE, log } from "../util.ts";
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler);
Chart.register(LineController, BarController, LineElement, PointElement, TimeScale, BarElement, LinearScale, Tooltip, Filler, annotationPlugin);
export default {
components: { Line },
@ -56,6 +57,19 @@ export default {
};
},
computed: {
threshold() {
let heartbeatList = this.heartbeatList ||
(this.monitorId in this.$root.heartbeatList && this.$root.heartbeatList[this.monitorId]) ||
[];
let lastBeat = heartbeatList.at(-1);
if (lastBeat) {
return lastBeat.pingThreshold;
} else {
return undefined;
}
},
chartOptions() {
return {
responsive: true,
@ -153,6 +167,22 @@ export default {
legend: {
display: false,
},
annotation: {
annotations: {
line1: {
type: "line",
mode: "horizontal",
scaleID: "y",
value: this.threshold,
endValue: this.threshold,
borderColor: "rgba(248,163,6,1.0)",
borderWith: 2,
borderDash: [ 1, 3 ],
adjustScaleRange: false,
display: this.threshold !== undefined,
}
}
},
},
};
},

@ -30,6 +30,14 @@ export default {
return "maintenance";
}
if (this.status === 4) {
return "warning";
}
if (this.status === 5) {
return "primary";
}
return "secondary";
},
@ -50,6 +58,14 @@ export default {
return this.$t("statusMaintenance");
}
if (this.status === 4) {
return this.$t("Slow");
}
if (this.status === 5) {
return this.$t("Nominal");
}
return this.$t("Unknown");
},
},

@ -28,6 +28,8 @@
"Pending": "Pending",
"statusMaintenance": "Maintenance",
"Maintenance": "Maintenance",
"Slow": "Slow",
"Nominal": "Nominal",
"Unknown": "Unknown",
"Cannot connect to the socket server": "Cannot connect to the socket server",
"Reconnecting...": "Reconnecting...",
@ -494,6 +496,27 @@
"uninstall": "Uninstall",
"uninstalling": "Uninstalling",
"confirmUninstallPlugin": "Are you sure want to uninstall this plugin?",
"slowResponseNotificationEnable": "Slow Response Notification",
"slowResponseNotificationUseDescription": "Send a notification when service response time is slow.",
"slowResponseNotificationThreshold": "Threshold (ms)",
"slowResponseNotificationThresholdDescription": "Send a notification if calculated response time is greater than {0} ms.",
"slowResponseNotificationThresholdMethod": "Threshold Calculation",
"slowResponseNotificationThresholdMethodStatic": "Static Threshold",
"slowResponseNotificationThresholdMethodStaticDescription": "Define a static threshold.",
"slowResponseNotificationThresholdMethodRelative24Hour": "Relative to Avg. Response",
"slowResponseNotificationThresholdMethodRelative24HourDescription": "Calculate the threshold ({0}x the 24-hour average response time).",
"slowResponseNotificationThresholdMultiplier": "Threshold Multiplier",
"slowResponseNotificationThresholdMultiplierDescription": "Send notification if response time is greater than {0}x the 24-hour average.",
"slowResponseNotificationRange": "Window Duration (seconds)",
"slowResponseNotificationRangeDescription": "Window duration for calculating the {0}.",
"slowResponseNotificationMethod": "Response Time Calculation",
"slowResponseNotificationMethodAverage": "Average",
"slowResponseNotificationMethodAverageDescription": "Get the average response time over the last {0} seconds.",
"slowResponseNotificationMethodMax": "Max",
"slowResponseNotificationMethodMaxDescription": "Get the max response time of the last {0} seconds.",
"slowResponseNotificationMethodLast": "Last",
"slowResponseNotificationMethodLastDescription": "Get the response time of the last heartbeat.",
"slowResponseNotificationResendInterval": "Resend Notification if Slow Response X times consecutively",
"notificationRegional": "Regional",
"Clone Monitor": "Clone Monitor",
"Clone": "Clone",

@ -5,7 +5,7 @@ import Favico from "favico.js";
import dayjs from "dayjs";
import mitt from "mitt";
import { DOWN, MAINTENANCE, PENDING, UP } from "../util.ts";
import { DOWN, MAINTENANCE, PENDING, UP, SLOW, NOMINAL } from "../util.ts";
import { getDevContainerServerHostname, isDevContainer, getToastSuccessTimeout, getToastErrorTimeout } from "../util-frontend.js";
const toast = useToast();
@ -190,6 +190,10 @@ export default {
}
// Add to important list if it is important
if (data.important || data.pingImportant) {
this.emitter.emit("newImportantHeartbeat", data);
}
// Also toast
if (data.important) {
@ -206,8 +210,23 @@ export default {
toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`);
}
}
}
this.emitter.emit("newImportantHeartbeat", data);
if (data.pingImportant) {
if (this.monitorList[data.monitorID] !== undefined) {
if (data.pingStatus === SLOW) {
toast.warning(`[${this.monitorList[data.monitorID].name}] [SLOW] ${data.pingMsg}`, {
timeout: getToastErrorTimeout(),
});
} else if (data.pingStatus === NOMINAL) {
toast.success(`[${this.monitorList[data.monitorID].name}] [NOMINAL] ${data.pingMsg}`, {
timeout: getToastSuccessTimeout(),
});
} else {
toast(`[${this.monitorList[data.monitorID].name}] ${data.pingMsg}`);
}
}
}
});
@ -745,11 +764,30 @@ export default {
return result;
},
pingStatusList() {
let result = {};
for (let monitorID in this.lastHeartbeatList) {
let lastHeartBeat = this.lastHeartbeatList[monitorID];
if (lastHeartBeat?.status === UP) {
if (lastHeartBeat.pingStatus === SLOW) {
result[monitorID] = {
text: this.$t("Slow"),
color: "warning",
};
}
}
}
return result;
},
stats() {
let result = {
active: 0,
up: 0,
down: 0,
slow: 0,
maintenance: 0,
pending: 0,
unknown: 0,
@ -775,6 +813,10 @@ export default {
} else {
result.unknown++;
}
if (beat.pingStatus === SLOW) {
result.slow++;
}
} else {
result.unknown++;
}

@ -15,6 +15,10 @@
<h3>{{ $t("Down") }}</h3>
<span class="num text-danger">{{ $root.stats.down }}</span>
</div>
<div class="col">
<h3>{{ $t("Slow") }}</h3>
<span class="num text-warning">{{ $root.stats.slow }}</span>
</div>
<div class="col">
<h3>{{ $t("Maintenance") }}</h3>
<span class="num text-maintenance">{{ $root.stats.maintenance }}</span>
@ -43,11 +47,16 @@
<tbody>
<tr v-for="(beat, index) in displayedRecords" :key="index" :class="{ 'shadow-box': $root.windowWidth <= 550}">
<td><router-link :to="`/dashboard/${beat.monitorID}`">{{ $root.monitorList[beat.monitorID]?.name }}</router-link></td>
<td><Status :status="beat.status" /></td>
<td>
<div v-if="beat.important"><Status :status="beat.status" /></div>
<div v-if="beat.pingImportant"><Status :status="beat.pingStatus" /></div>
</td>
<td :class="{ 'border-0':! beat.msg}"><Datetime :value="beat.time" /></td>
<td class="border-0">{{ beat.msg }}</td>
<td class="border-0">
<div v-if="beat.important">{{ beat.msg }}</div>
<div v-if="beat.pingImportant">{{ beat.pingMsg }}</div>
</td>
</tr>
<tr v-if="importantHeartBeatListLength === 0">
<td colspan="4">
{{ $t("No important events") }}

@ -79,7 +79,8 @@
<span class="word">{{ $t("checkEverySecond", [ monitor.interval ]) }}</span>
</div>
<div class="col-md-4 text-center">
<span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px;">{{ status.text }}</span>
<span class="badge rounded-pill m-1" :class=" 'bg-' + status.color " style="font-size: 30px;">{{ status.text }}</span>
<span v-if="pingStatus" class="badge rounded-pill m-1" :class=" 'bg-' + pingStatus.color " style="font-size: 30px;">{{ pingStatus.text }}</span>
</div>
</div>
</div>
@ -227,9 +228,15 @@
</thead>
<tbody>
<tr v-for="(beat, index) in displayedRecords" :key="index" style="padding: 10px;">
<td><Status :status="beat.status" /></td>
<td>
<div v-if="beat.important"><Status :status="beat.status" /></div>
<div v-if="beat.pingImportant"><Status :status="beat.pingStatus" /></div>
</td>
<td :class="{ 'border-0':! beat.msg}"><Datetime :value="beat.time" /></td>
<td class="border-0">{{ beat.msg }}</td>
<td class="border-0">
<div v-if="beat.important">{{ beat.msg }}</div>
<div v-if="beat.pingImportant">{{ beat.pingMsg }}</div>
</td>
</tr>
<tr v-if="importantHeartBeatListLength === 0">
@ -374,6 +381,14 @@ export default {
return { };
},
pingStatus() {
if (this.$root.pingStatusList[this.monitor.id]) {
return this.$root.pingStatusList[this.monitor.id];
}
return { };
},
tlsInfo() {
// Add: this.$root.tlsInfoList[this.monitor.id].certInfo
// Fix: TypeError: Cannot read properties of undefined (reading 'validTo')

@ -471,6 +471,98 @@
<input id="resend-interval" v-model="monitor.resendInterval" type="number" class="form-control" required min="0" step="1">
</div>
<!-- Slow Response Notification -->
<div class="my-3 form-check">
<input id="slow-response-notification" v-model="monitor.slowResponseNotification" class="form-check-input" type="checkbox">
<label class="form-check-label" for="slow-response-notification">
{{ $t("slowResponseNotificationEnable") }}
</label>
<div class="form-text">
{{ $t("slowResponseNotificationUseDescription") }}
</div>
</div>
<!-- Method -->
<div v-if="monitor.slowResponseNotification" class="my-3">
<label for="slow-response-notification-method" class="form-label">{{ $t("slowResponseNotificationMethod") }}</label>
<select id="slow-response-notification-method" v-model="monitor.slowResponseNotificationMethod" class="form-select">
<option value="average">
{{ $t("slowResponseNotificationMethodAverage") }}
</option>
<option value="max">
{{ $t("slowResponseNotificationMethodMax") }}
</option>
<option value="last">
{{ $t("slowResponseNotificationMethodLast") }}
</option>
</select>
<div v-if="monitor.slowResponseNotificationMethod === 'average'" class="form-text">
{{ $t("slowResponseNotificationMethodAverageDescription", [monitor.slowResponseNotificationRange]) }}
</div>
<div v-if="monitor.slowResponseNotificationMethod === 'max'" class="form-text">
{{ $t("slowResponseNotificationMethodMaxDescription", [monitor.slowResponseNotificationRange]) }}
</div>
<div v-if="monitor.slowResponseNotificationMethod === 'last'" class="form-text">
{{ $t("slowResponseNotificationMethodLastDescription", [monitor.slowResponseNotificationRange]) }}
</div>
</div>
<!-- Window Duration -->
<div v-if="monitor.slowResponseNotification && monitor.slowResponseNotificationMethod !== 'last'" class="my-3">
<label for="slow-response-notification-range" class="form-label">{{ $t("slowResponseNotificationRange") }}</label>
<input id="slow-response-notification-range" v-model="monitor.slowResponseNotificationRange" type="number" class="form-control" required :min="monitor.interval" step="1">
<div class="form-text">
{{ $t("slowResponseNotificationRangeDescription", [monitor.slowResponseNotificationMethod]) }}
</div>
</div>
<!-- Threshold Method -->
<div v-if="monitor.slowResponseNotification" class="my-3">
<label for="slow-response-notification-threshold-method" class="form-label">{{ $t("slowResponseNotificationThresholdMethod") }}</label>
<select id="slow-response-notification-threshold-method" v-model="monitor.slowResponseNotificationThresholdMethod" class="form-select">
<option value="threshold-static">
{{ $t("slowResponseNotificationThresholdMethodStatic") }}
</option>
<option value="threshold-relative-24-hour">
{{ $t("slowResponseNotificationThresholdMethodRelative24Hour") }}
</option>
</select>
<div v-if="monitor.slowResponseNotificationThresholdMethod === 'threshold-static'" class="form-text">
{{ $t("slowResponseNotificationThresholdMethodStaticDescription") }}
</div>
<div v-if="monitor.slowResponseNotificationThresholdMethod === 'threshold-relative-24-hour'" class="form-text">
{{ $t("slowResponseNotificationThresholdMethodRelative24HourDescription", [monitor.slowResponseNotificationThresholdMultiplier]) }}
</div>
</div>
<!-- Threshold -->
<div v-if="monitor.slowResponseNotification && monitor.slowResponseNotificationThresholdMethod == 'threshold-static'" class="my-3">
<label for="slow-response-notification-threshold" class="form-label">{{ $t("slowResponseNotificationThreshold") }}</label>
<input id="slow-response-notification-threshold" v-model="monitor.slowResponseNotificationThreshold" type="number" class="form-control" required min="0" step="1">
<div class="form-text">
{{ $t("slowResponseNotificationThresholdDescription", [monitor.slowResponseNotificationThreshold]) }}
</div>
</div>
<!-- Threshold Multiplier -->
<div v-if="monitor.slowResponseNotification && monitor.slowResponseNotificationThresholdMethod == 'threshold-relative-24-hour'" class="my-3">
<label for="slow-response-notification-threshold-multiplier" class="form-label">{{ $t("slowResponseNotificationThresholdMultiplier") }}</label>
<input id="slow-response-notification-threshold-multiplier" v-model="monitor.slowResponseNotificationThresholdMultiplier" type="number" class="form-control" required min="1" step="0.1">
<div class="form-text">
{{ $t("slowResponseNotificationThresholdMultiplierDescription", [monitor.slowResponseNotificationThresholdMultiplier]) }}
</div>
</div>
<!-- Slow Response Resend Interval -->
<div v-if="monitor.slowResponseNotification" class="my-3">
<label for="slow-response-notification-resend-interval" class="form-label">
{{ $t("slowResponseNotificationResendInterval", [monitor.slowResponseNotificationInterval]) }}
<span v-if="monitor.slowResponseNotificationResendInterval > 0">({{ $t("resendEveryXTimes", [ monitor.slowResponseNotificationResendInterval ]) }})</span>
<span v-else>({{ $t("resendDisabled") }})</span>
</label>
<input id="slow-response-notification-resend-interval" v-model="monitor.slowResponseNotificationResendInterval" type="number" class="form-control" required min="0" step="1">
</div>
<h2 v-if="monitor.type !== 'push'" class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check">
@ -947,7 +1039,14 @@ const monitorDefaults = {
kafkaProducerSsl: false,
kafkaProducerAllowAutoTopicCreation: false,
gamedigGivenPortOnly: true,
remote_browser: null
remote_browser: null,
slowResponseNotification: false,
slowResponseNotificationMethod: "average",
slowResponseNotificationRange: 300,
slowResponseNotificationThresholdMethod: "threshold-relative-24-hour",
slowResponseNotificationThreshold: 2500,
slowResponseNotificationThresholdMultiplier: 5.0,
slowResponseNotificationResendInterval: 0,
};
export default {
@ -1240,6 +1339,10 @@ message HealthCheckResponse {
if (this.monitor.retryInterval === oldValue) {
this.monitor.retryInterval = value;
}
// Always keep slowResponseNotificationRange >= interval
if (this.monitor.slowResponseNotificationRange < value) {
this.monitor.slowResponseNotificationRange = value;
}
},
"monitor.timeout"(value, oldValue) {

@ -20,6 +20,8 @@ exports.DOWN = 0;
exports.UP = 1;
exports.PENDING = 2;
exports.MAINTENANCE = 3;
exports.SLOW = 4;
exports.NOMINAL = 5;
exports.STATUS_PAGE_ALL_DOWN = 0;
exports.STATUS_PAGE_ALL_UP = 1;
exports.STATUS_PAGE_PARTIAL_DOWN = 2;

@ -24,6 +24,8 @@ export const DOWN = 0;
export const UP = 1;
export const PENDING = 2;
export const MAINTENANCE = 3;
export const SLOW = 4;
export const NOMINAL = 5;
export const STATUS_PAGE_ALL_DOWN = 0;
export const STATUS_PAGE_ALL_UP = 1;

Loading…
Cancel
Save