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",
"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",
@ -6062,6 +6063,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-more-types": {
"version": "2.24.0",
"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",
"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",

View file

@ -54,7 +54,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);

View file

@ -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,
};
}

View file

@ -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 {
@ -1565,7 +1567,7 @@ 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})`;
// Add window duration for methods that make sense
let pingMsg = `${actualResponseTime}ms resp. (${methodDescription})`;
// Verify valid response time was calculated
if (actualResponseTime === 0 || !Number.isInteger(actualResponseTime)) {
@ -1579,8 +1581,11 @@ class Monitor extends BeanModel {
return;
}
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 {
@ -1588,6 +1593,11 @@ class Monitor extends BeanModel {
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;
}
// Reset slow response count
@ -1595,6 +1605,7 @@ class Monitor extends BeanModel {
// Responding slowly
} else {
bean.pingStatus = SLOW;
++bean.slowResponseCount;
// Always send first notification
@ -1602,6 +1613,12 @@ class Monitor extends BeanModel {
log.debug("monitor", `[${this.name}] Responded slowly, sending notification | ${msgStats}`);
let msg = `[${this.name}] Responded Slowly \n${msgStats}`;
Monitor.sendSlowResponseNotification(monitor, bean, msg);
// Mark important (NOMINAL -> SLOW)
pingMsg += ` > ${threshold}ms`;
bean.pingImportant = true;
bean.pingMsg = pingMsg;
// Send notification every x times
} else if (this.slowResponseNotificationResendInterval > 0) {
if (((bean.slowResponseCount) % this.slowResponseNotificationResendInterval) === 0) {

View file

@ -1199,9 +1199,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,
]);
}
@ -1225,7 +1225,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 ?
@ -1236,7 +1236,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 ?
@ -1449,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,

View file

@ -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,
}
}
},
},
};
},

View file

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

View file

@ -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...",

View file

@ -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++;
}

View file

@ -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") }}

View file

@ -71,7 +71,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>
@ -219,9 +220,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">
@ -366,6 +373,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')

View file

@ -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;

View file

@ -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;