Merge branch 'feature-slow-response-visual-improvements' into feature-slow-response-notification

This commit is contained in:
Stephen Papierski 2024-01-16 14:40:40 -07:00
commit 2755276f22
No known key found for this signature in database
15 changed files with 194 additions and 18 deletions

View file

@ -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
View file

@ -97,6 +97,7 @@
"bootstrap": "5.1.3", "bootstrap": "5.1.3",
"chart.js": "~4.2.1", "chart.js": "~4.2.1",
"chartjs-adapter-dayjs-4": "~1.0.4", "chartjs-adapter-dayjs-4": "~1.0.4",
"chartjs-plugin-annotation": "~3.0.1",
"concurrently": "^7.1.0", "concurrently": "^7.1.0",
"core-js": "~3.26.1", "core-js": "~3.26.1",
"cronstrue": "~2.24.0", "cronstrue": "~2.24.0",
@ -6062,6 +6063,15 @@
"dayjs": "^1.9.7" "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-more-types": { "node_modules/check-more-types": {
"version": "2.24.0", "version": "2.24.0",
"resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz",

View file

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

View file

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

View file

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

View file

@ -1,8 +1,7 @@
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const axios = require("axios"); const axios = require("axios");
const { Prometheus } = require("../prometheus"); const { Prometheus } = require("../prometheus");
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND, const { log, UP, DOWN, PENDING, MAINTENANCE, NOMINAL, SLOW, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND, SQL_DATETIME_FORMAT
SQL_DATETIME_FORMAT
} = require("../../src/util"); } = require("../../src/util");
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery, const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal redisPingAsync, mongodbPing, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
@ -34,6 +33,9 @@ const rootCertificates = rootCertificatesFingerprints();
* 1 = UP * 1 = UP
* 2 = PENDING * 2 = PENDING
* 3 = MAINTENANCE * 3 = MAINTENANCE
* pingStatus:
* 4 = SLOW
* 5 = NOMINAL
*/ */
class Monitor extends BeanModel { class Monitor extends BeanModel {
@ -1565,7 +1567,7 @@ class Monitor extends BeanModel {
// Create stats to append to messages/logs // Create stats to append to messages/logs
const methodDescription = [ "average", "max" ].includes(method) ? `${method} of last ${windowDuration}s` : method; const methodDescription = [ "average", "max" ].includes(method) ? `${method} of last ${windowDuration}s` : method;
let msgStats = `Response: ${actualResponseTime}ms (${methodDescription}) | Threshold: ${threshold}ms (${thresholdDescription})`; let msgStats = `Response: ${actualResponseTime}ms (${methodDescription}) | Threshold: ${threshold}ms (${thresholdDescription})`;
// Add window duration for methods that make sense 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)) {
@ -1579,8 +1581,11 @@ class Monitor extends BeanModel {
return; return;
} }
bean.pingThreshold = threshold;
// Responding normally // Responding normally
if (actualResponseTime < threshold) { if (actualResponseTime < threshold) {
bean.pingStatus = NOMINAL;
if (bean.slowResponseCount === 0) { if (bean.slowResponseCount === 0) {
log.debug("monitor", `[${this.name}] Responding normally. No need to send slow response notification | ${msgStats}`); log.debug("monitor", `[${this.name}] Responding normally. No need to send slow response notification | ${msgStats}`);
} else { } else {
@ -1588,6 +1593,11 @@ class Monitor extends BeanModel {
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}`; let msg = `[${this.name}] Returned to Normal Response Time \n${msgStats}`;
Monitor.sendSlowResponseNotification(monitor, bean, msg); Monitor.sendSlowResponseNotification(monitor, bean, msg);
// Mark important (SLOW -> NOMINAL)
pingMsg += ` < ${threshold}ms`;
bean.pingImportant = true;
bean.pingMsg = pingMsg;
} }
// Reset slow response count // Reset slow response count
@ -1595,6 +1605,7 @@ class Monitor extends BeanModel {
// Responding slowly // Responding slowly
} else { } else {
bean.pingStatus = SLOW;
++bean.slowResponseCount; ++bean.slowResponseCount;
// Always send first notification // Always send first notification
@ -1602,6 +1613,12 @@ class Monitor extends BeanModel {
log.debug("monitor", `[${this.name}] Responded slowly, sending notification | ${msgStats}`); log.debug("monitor", `[${this.name}] Responded slowly, sending notification | ${msgStats}`);
let msg = `[${this.name}] Responded Slowly \n${msgStats}`; let msg = `[${this.name}] Responded Slowly \n${msgStats}`;
Monitor.sendSlowResponseNotification(monitor, bean, msg); Monitor.sendSlowResponseNotification(monitor, bean, msg);
// Mark important (NOMINAL -> SLOW)
pingMsg += ` > ${threshold}ms`;
bean.pingImportant = true;
bean.pingMsg = pingMsg;
// 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) {

View file

@ -1199,9 +1199,9 @@ let needSetup = false;
let count; let count;
if (monitorID == null) { if (monitorID == null) {
count = await R.count("heartbeat", "important = 1"); count = await R.count("heartbeat", "important = 1 OR ping_important = 1");
} else { } 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, monitorID,
]); ]);
} }
@ -1225,7 +1225,7 @@ let needSetup = false;
let list; let list;
if (monitorID == null) { if (monitorID == null) {
list = await R.find("heartbeat", ` list = await R.find("heartbeat", `
important = 1 important = 1 OR ping_important = 1
ORDER BY time DESC ORDER BY time DESC
LIMIT ? LIMIT ?
OFFSET ? OFFSET ?
@ -1236,7 +1236,7 @@ let needSetup = false;
} else { } else {
list = await R.find("heartbeat", ` list = await R.find("heartbeat", `
monitor_id = ? monitor_id = ?
AND important = 1 AND (important = 1 OR ping_important = 1)
ORDER BY time DESC ORDER BY time DESC
LIMIT ? LIMIT ?
OFFSET ? OFFSET ?
@ -1449,7 +1449,9 @@ let needSetup = false;
log.info("manage", `Clear Events Monitor: ${monitorID} User ID: ${socket.userID}`); 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", "0",
monitorID, monitorID,

View file

@ -19,11 +19,12 @@
<script lang="js"> <script lang="js">
import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js"; import { BarController, BarElement, Chart, Filler, LinearScale, LineController, LineElement, PointElement, TimeScale, Tooltip } from "chart.js";
import "chartjs-adapter-dayjs-4"; import "chartjs-adapter-dayjs-4";
import annotationPlugin from "chartjs-plugin-annotation";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { Line } from "vue-chartjs"; import { Line } from "vue-chartjs";
import { DOWN, PENDING, MAINTENANCE, log } from "../util.ts"; 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 { export default {
components: { Line }, components: { Line },
@ -56,6 +57,19 @@ export default {
}; };
}, },
computed: { 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() { chartOptions() {
return { return {
responsive: true, responsive: true,
@ -153,6 +167,22 @@ export default {
legend: { legend: {
display: false, 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,
}
}
},
}, },
}; };
}, },

View file

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

View file

@ -28,6 +28,8 @@
"Pending": "Pending", "Pending": "Pending",
"statusMaintenance": "Maintenance", "statusMaintenance": "Maintenance",
"Maintenance": "Maintenance", "Maintenance": "Maintenance",
"Slow": "Slow",
"Nominal": "Nominal",
"Unknown": "Unknown", "Unknown": "Unknown",
"Cannot connect to the socket server": "Cannot connect to the socket server", "Cannot connect to the socket server": "Cannot connect to the socket server",
"Reconnecting...": "Reconnecting...", "Reconnecting...": "Reconnecting...",

View file

@ -5,7 +5,7 @@ import Favico from "favico.js";
import dayjs from "dayjs"; import dayjs from "dayjs";
import mitt from "mitt"; 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"; import { getDevContainerServerHostname, isDevContainer, getToastSuccessTimeout, getToastErrorTimeout } from "../util-frontend.js";
const toast = useToast(); const toast = useToast();
@ -190,6 +190,10 @@ export default {
} }
// Add to important list if it is important // Add to important list if it is important
if (data.important || data.pingImportant) {
this.emitter.emit("newImportantHeartbeat", data);
}
// Also toast // Also toast
if (data.important) { if (data.important) {
@ -206,8 +210,23 @@ export default {
toast(`[${this.monitorList[data.monitorID].name}] ${data.msg}`); 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; 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() { stats() {
let result = { let result = {
active: 0, active: 0,
up: 0, up: 0,
down: 0, down: 0,
slow: 0,
maintenance: 0, maintenance: 0,
pending: 0, pending: 0,
unknown: 0, unknown: 0,
@ -775,6 +813,10 @@ export default {
} else { } else {
result.unknown++; result.unknown++;
} }
if (beat.pingStatus === SLOW) {
result.slow++;
}
} else { } else {
result.unknown++; result.unknown++;
} }

View file

@ -15,6 +15,10 @@
<h3>{{ $t("Down") }}</h3> <h3>{{ $t("Down") }}</h3>
<span class="num text-danger">{{ $root.stats.down }}</span> <span class="num text-danger">{{ $root.stats.down }}</span>
</div> </div>
<div class="col">
<h3>{{ $t("Slow") }}</h3>
<span class="num text-warning">{{ $root.stats.slow }}</span>
</div>
<div class="col"> <div class="col">
<h3>{{ $t("Maintenance") }}</h3> <h3>{{ $t("Maintenance") }}</h3>
<span class="num text-maintenance">{{ $root.stats.maintenance }}</span> <span class="num text-maintenance">{{ $root.stats.maintenance }}</span>
@ -43,11 +47,16 @@
<tbody> <tbody>
<tr v-for="(beat, index) in displayedRecords" :key="index" :class="{ 'shadow-box': $root.windowWidth <= 550}"> <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><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}"><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>
<tr v-if="importantHeartBeatListLength === 0"> <tr v-if="importantHeartBeatListLength === 0">
<td colspan="4"> <td colspan="4">
{{ $t("No important events") }} {{ $t("No important events") }}

View file

@ -71,7 +71,8 @@
<span class="word">{{ $t("checkEverySecond", [ monitor.interval ]) }}</span> <span class="word">{{ $t("checkEverySecond", [ monitor.interval ]) }}</span>
</div> </div>
<div class="col-md-4 text-center"> <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> </div>
</div> </div>
@ -219,9 +220,15 @@
</thead> </thead>
<tbody> <tbody>
<tr v-for="(beat, index) in displayedRecords" :key="index" style="padding: 10px;"> <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}"><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>
<tr v-if="importantHeartBeatListLength === 0"> <tr v-if="importantHeartBeatListLength === 0">
@ -366,6 +373,14 @@ export default {
return { }; return { };
}, },
pingStatus() {
if (this.$root.pingStatusList[this.monitor.id]) {
return this.$root.pingStatusList[this.monitor.id];
}
return { };
},
tlsInfo() { tlsInfo() {
// Add: this.$root.tlsInfoList[this.monitor.id].certInfo // Add: this.$root.tlsInfoList[this.monitor.id].certInfo
// Fix: TypeError: Cannot read properties of undefined (reading 'validTo') // Fix: TypeError: Cannot read properties of undefined (reading 'validTo')

View file

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

View file

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