mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-03-04 16:35:57 +00:00
Merge branch 'feature-slow-response-visual-improvements' into feature-slow-response-notification
This commit is contained in:
commit
2755276f22
15 changed files with 194 additions and 18 deletions
|
@ -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
10
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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...",
|
||||
|
|
|
@ -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") }}
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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…
Add table
Reference in a new issue