Enhanced ping monitor with advanced options (count, timeout, numeric) (#5588)

Co-authored-by: Frank Elsinga <frank@elsinga.de>
This commit is contained in:
Filippo
2025-05-24 02:57:39 +02:00
committed by GitHub
parent c69fcd4093
commit 839ead80cc
8 changed files with 332 additions and 41 deletions

View File

@@ -1074,7 +1074,16 @@
"rabbitmqHelpText": "To use the monitor, you will need to enable the Management Plugin in your RabbitMQ setup. For more information, please consult the {rabitmq_documentation}.",
"SendGrid API Key": "SendGrid API Key",
"Separate multiple email addresses with commas": "Separate multiple email addresses with commas",
"smtpHelpText": "“SMTPS” tests that SMTP/TLS is working; “Ignore TLS” connects over plaintext; “STARTTLS” connects, issues a STARTTLS command and verifies the server certificate. None of these send an email.",
"pingCountLabel": "Max Packets",
"pingCountDescription": "Number of packets to send before stopping",
"pingNumericLabel": "Numeric Output",
"pingNumericDescription": "If checked, IP addresses will be output instead of symbolic hostnames",
"pingGlobalTimeoutLabel": "Global Timeout",
"pingGlobalTimeoutDescription": "Total time in seconds before ping stops, regardless of packets sent",
"pingPerRequestTimeoutLabel": "Per-Ping Timeout",
"pingPerRequestTimeoutDescription": "This is the maximum waiting time (in seconds) before considering a single ping packet lost",
"pingIntervalAdjustedInfo": "Interval adjusted based on packet count, global timeout and per-ping timeout",
"smtpHelpText": "'SMTPS' tests that SMTP/TLS is working; 'Ignore TLS' connects over plaintext; 'STARTTLS' connects, issues a STARTTLS command and verifies the server certificate. None of these send an email.",
"Custom URL": "Custom URL",
"customUrlDescription": "Will be used as the clickable URL instead of the monitor's one.",
"OneChatAccessToken": "OneChat Access Token",

View File

@@ -610,10 +610,14 @@
<input id="retry-interval" v-model="monitor.retryInterval" type="number" class="form-control" required :min="minInterval" step="1">
</div>
<!-- Timeout: HTTP / Keyword / SNMP only -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'snmp' || monitor.type === 'rabbitmq'" class="my-3">
<label for="timeout" class="form-label">{{ $t("Request Timeout") }} ({{ $t("timeoutAfter", [ monitor.timeout || clampTimeout(monitor.interval) ]) }})</label>
<input id="timeout" v-model="monitor.timeout" type="number" class="form-control" required min="0" step="0.1">
<!-- Timeout: HTTP / JSON query / Keyword / Ping / RabbitMQ / SNMP only -->
<div v-if="monitor.type === 'http' || monitor.type === 'json-query' || monitor.type === 'keyword' || monitor.type === 'ping' || monitor.type === 'rabbitmq' || monitor.type === 'snmp'" class="my-3">
<label for="timeout" class="form-label">
{{ monitor.type === 'ping' ? $t("pingGlobalTimeoutLabel") : $t("Request Timeout") }}
<span v-if="monitor.type !== 'ping'">({{ $t("timeoutAfter", [monitor.timeout || clampTimeout(monitor.interval)]) }})</span>
</label>
<input id="timeout" v-model="monitor.timeout" type="number" class="form-control" :min="timeoutMin" :max="timeoutMax" :step="timeoutStep" required>
<div v-if="monitor.type === 'ping'" class="form-text">{{ $t("pingGlobalTimeoutDescription") }}</div>
</div>
<div class="my-3">
@@ -675,10 +679,39 @@
</div>
</div>
<!-- Ping packet size -->
<!-- Max Packets / Count -->
<div v-if="monitor.type === 'ping'" class="my-3">
<label for="ping-count" class="form-label">{{ $t("pingCountLabel") }}</label>
<input id="ping-count" v-model="monitor.ping_count" type="number" class="form-control" required min="1" max="100" step="1">
<div class="form-text">
{{ $t("pingCountDescription") }}
</div>
</div>
<!-- Numeric Output -->
<div v-if="monitor.type === 'ping'" class="my-3 form-check">
<input id="ping_numeric" v-model="monitor.ping_numeric" type="checkbox" class="form-check-input" :checked="monitor.ping_numeric">
<label class="form-check-label" for="ping_numeric">
{{ $t("pingNumericLabel") }}
</label>
<div class="form-text">
{{ $t("pingNumericDescription") }}
</div>
</div>
<!-- Packet size -->
<div v-if="monitor.type === 'ping'" class="my-3">
<label for="packet-size" class="form-label">{{ $t("Packet Size") }}</label>
<input id="packet-size" v-model="monitor.packetSize" type="number" class="form-control" required min="1" max="65500" step="1">
<input id="packet-size" v-model="monitor.packetSize" type="number" class="form-control" required min="1" :max="65500" step="1">
</div>
<!-- per-request timeout -->
<div v-if="monitor.type === 'ping'" class="my-3">
<label for="ping_per_request_timeout" class="form-label">{{ $t("pingPerRequestTimeoutLabel") }}</label>
<input id="ping_per_request_timeout" v-model="monitor.ping_per_request_timeout" type="number" class="form-control" required min="0" max="300" step="1">
<div class="form-text">
{{ $t("pingPerRequestTimeoutDescription") }}
</div>
</div>
<!-- HTTP / Keyword only -->
@@ -1075,7 +1108,13 @@ import DockerHostDialog from "../components/DockerHostDialog.vue";
import RemoteBrowserDialog from "../components/RemoteBrowserDialog.vue";
import ProxyDialog from "../components/ProxyDialog.vue";
import TagsManager from "../components/TagsManager.vue";
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND, sleep } from "../util.ts";
import {
genSecret,
isDev,
MAX_INTERVAL_SECOND,
MIN_INTERVAL_SECOND,
sleep,
} from "../util.ts";
import { hostNameRegexPattern } from "../util-frontend";
import HiddenInput from "../components/HiddenInput.vue";
import EditMonitorConditions from "../components/EditMonitorConditions.vue";
@@ -1097,7 +1136,6 @@ const monitorDefaults = {
notificationIDList: {},
ignoreTls: false,
upsideDown: false,
packetSize: 56,
expiryNotification: false,
maxredirects: 10,
accepted_statuscodes: [ "200-299" ],
@@ -1172,6 +1210,29 @@ export default {
},
computed: {
timeoutStep() {
return this.monitor.type === "ping" ? 1 : 0.1;
},
timeoutMin() {
return this.monitor.type === "ping" ? 1 : 0;
},
timeoutMax() {
return this.monitor.type === "ping" ? 60 : undefined;
},
timeoutLabel() {
return this.monitor.type === "ping" ? this.$t("pingTimeoutLabel") : this.$t("Request Timeout");
},
timeoutDescription() {
if (this.monitor.type === "ping") {
return this.$t("pingTimeoutDescription");
}
return "";
},
defaultFriendlyName() {
if (this.monitor.hostname) {
return this.monitor.hostname;
@@ -1209,6 +1270,7 @@ export default {
}
return this.$t(name);
},
remoteBrowsersOptions() {
return this.$root.remoteBrowserList.map(browser => {
return {
@@ -1217,6 +1279,7 @@ export default {
};
});
},
remoteBrowsersToggle: {
get() {
return this.remoteBrowsersEnabled || this.monitor.remote_browser != null;
@@ -1234,6 +1297,7 @@ export default {
}
}
},
isAdd() {
return this.$route.path === "/add";
},
@@ -1284,6 +1348,7 @@ message HealthCheckResponse {
}
` ]);
},
bodyPlaceholder() {
if (this.monitor && this.monitor.httpBodyEncoding && this.monitor.httpBodyEncoding === "xml") {
return this.$t("Example:", [ `
@@ -1449,9 +1514,25 @@ message HealthCheckResponse {
},
"monitor.timeout"(value, oldValue) {
// keep timeout within 80% range
if (value && value !== oldValue) {
this.monitor.timeout = this.clampTimeout(value);
if (this.monitor.type === "ping") {
this.finishUpdateInterval();
} else {
// keep timeout within 80% range
if (value && value !== oldValue) {
this.monitor.timeout = this.clampTimeout(value);
}
}
},
"monitor.ping_count"() {
if (this.monitor.type === "ping") {
this.finishUpdateInterval();
}
},
"monitor.ping_per_request_timeout"() {
if (this.monitor.type === "ping") {
this.finishUpdateInterval();
}
},
@@ -1480,8 +1561,10 @@ message HealthCheckResponse {
// Set a default timeout if the monitor type has changed or if it's a new monitor
if (oldType || this.isAdd) {
if (this.monitor.type === "snmp") {
// snmp is not expected to be executed via the internet => we can choose a lower default timeout
// snmp is not expected to be executed via the internet => we can choose a lower default timeout
this.monitor.timeout = 5;
} else if (this.monitor.type === "ping") {
this.monitor.timeout = 10;
} else {
this.monitor.timeout = 48;
}
@@ -1598,7 +1681,11 @@ message HealthCheckResponse {
if (this.isAdd) {
this.monitor = {
...monitorDefaults
...monitorDefaults,
ping_count: 3,
ping_numeric: true,
packetSize: 56,
ping_per_request_timeout: 2,
};
if (this.$root.proxyList && !this.monitor.proxyId) {
@@ -1661,7 +1748,12 @@ message HealthCheckResponse {
}
// Handling for monitors that are missing/zeroed timeout
if (!this.monitor.timeout) {
this.monitor.timeout = ~~(this.monitor.interval * 8) / 10;
if (this.monitor.type === "ping") {
// set to default
this.monitor.timeout = 10;
} else {
this.monitor.timeout = ~~(this.monitor.interval * 8) / 10;
}
}
} else {
this.$root.toastError(res.msg);
@@ -1878,11 +1970,48 @@ message HealthCheckResponse {
return Number.isFinite(clamped) ? clamped : maxTimeout;
},
calculatePingInterval() {
// If monitor.type is not "ping", simply return the configured interval
if (this.monitor.type !== "ping") {
return this.monitor.interval;
}
// Calculate the maximum theoretical time needed if every ping request times out
const theoreticalTotal = this.monitor.ping_count * this.monitor.ping_per_request_timeout;
// The global timeout (aka deadline) forces ping to terminate, so the effective limit
// is the smaller value between deadline and theoreticalTotal
const effectiveLimit = Math.min(this.monitor.timeout, theoreticalTotal);
// Add a 10% margin to the effective limit to ensure proper handling
const adjustedLimit = Math.ceil(effectiveLimit * 1.1);
// If the calculated limit is lower than the minimum allowed interval, use the minimum interval
if (adjustedLimit < this.minInterval) {
return this.minInterval;
}
return adjustedLimit;
},
finishUpdateInterval() {
// Update timeout if it is greater than the clamp timeout
let clampedValue = this.clampTimeout(this.monitor.interval);
if (this.monitor.timeout > clampedValue) {
this.monitor.timeout = clampedValue;
if (this.monitor.type === "ping") {
// Calculate the minimum required interval based on ping configuration
const calculatedPingInterval = this.calculatePingInterval();
// If the configured interval is too small, adjust it to the minimum required value
if (this.monitor.interval < calculatedPingInterval) {
this.monitor.interval = calculatedPingInterval;
// Notify the user that the interval has been automatically adjusted
toast.info(this.$t("pingIntervalAdjustedInfo"));
}
} else {
// Update timeout if it is greater than the clamp timeout
let clampedValue = this.clampTimeout(this.monitor.interval);
if (this.monitor.timeout > clampedValue) {
this.monitor.timeout = clampedValue;
}
}
},

View File

@@ -8,17 +8,34 @@
// Backend uses the compiled file util.js
// Frontend uses util.ts
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.sleep = exports.flipStatus = exports.badgeConstants = exports.CONSOLE_STYLE_BgGray = exports.CONSOLE_STYLE_BgWhite = exports.CONSOLE_STYLE_BgCyan = exports.CONSOLE_STYLE_BgMagenta = exports.CONSOLE_STYLE_BgBlue = exports.CONSOLE_STYLE_BgYellow = exports.CONSOLE_STYLE_BgGreen = exports.CONSOLE_STYLE_BgRed = exports.CONSOLE_STYLE_BgBlack = exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = exports.CONSOLE_STYLE_FgViolet = exports.CONSOLE_STYLE_FgLightBlue = exports.CONSOLE_STYLE_FgLightGreen = exports.CONSOLE_STYLE_FgOrange = exports.CONSOLE_STYLE_FgGray = exports.CONSOLE_STYLE_FgWhite = exports.CONSOLE_STYLE_FgCyan = exports.CONSOLE_STYLE_FgMagenta = exports.CONSOLE_STYLE_FgBlue = exports.CONSOLE_STYLE_FgYellow = exports.CONSOLE_STYLE_FgGreen = exports.CONSOLE_STYLE_FgRed = exports.CONSOLE_STYLE_FgBlack = exports.CONSOLE_STYLE_Hidden = exports.CONSOLE_STYLE_Reverse = exports.CONSOLE_STYLE_Blink = exports.CONSOLE_STYLE_Underscore = exports.CONSOLE_STYLE_Dim = exports.CONSOLE_STYLE_Bright = exports.CONSOLE_STYLE_Reset = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isNode = exports.isDev = void 0;
exports.evaluateJsonQuery = exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = void 0;
exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = void 0;
exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = exports.CONSOLE_STYLE_FgViolet = exports.CONSOLE_STYLE_FgLightBlue = exports.CONSOLE_STYLE_FgLightGreen = exports.CONSOLE_STYLE_FgOrange = exports.CONSOLE_STYLE_FgGray = exports.CONSOLE_STYLE_FgWhite = exports.CONSOLE_STYLE_FgCyan = exports.CONSOLE_STYLE_FgMagenta = exports.CONSOLE_STYLE_FgBlue = exports.CONSOLE_STYLE_FgYellow = exports.CONSOLE_STYLE_FgGreen = exports.CONSOLE_STYLE_FgRed = exports.CONSOLE_STYLE_FgBlack = exports.CONSOLE_STYLE_Hidden = exports.CONSOLE_STYLE_Reverse = exports.CONSOLE_STYLE_Blink = exports.CONSOLE_STYLE_Underscore = exports.CONSOLE_STYLE_Dim = exports.CONSOLE_STYLE_Bright = exports.CONSOLE_STYLE_Reset = exports.PING_PER_REQUEST_TIMEOUT_DEFAULT = exports.PING_PER_REQUEST_TIMEOUT_MAX = exports.PING_PER_REQUEST_TIMEOUT_MIN = exports.PING_COUNT_DEFAULT = exports.PING_COUNT_MAX = exports.PING_COUNT_MIN = exports.PING_GLOBAL_TIMEOUT_DEFAULT = exports.PING_GLOBAL_TIMEOUT_MAX = exports.PING_GLOBAL_TIMEOUT_MIN = exports.PING_PACKET_SIZE_DEFAULT = exports.PING_PACKET_SIZE_MAX = exports.PING_PACKET_SIZE_MIN = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isNode = exports.isDev = void 0;
exports.evaluateJsonQuery = exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMaintenanceRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.badgeConstants = exports.CONSOLE_STYLE_BgGray = exports.CONSOLE_STYLE_BgWhite = exports.CONSOLE_STYLE_BgCyan = exports.CONSOLE_STYLE_BgMagenta = exports.CONSOLE_STYLE_BgBlue = exports.CONSOLE_STYLE_BgYellow = exports.CONSOLE_STYLE_BgGreen = exports.CONSOLE_STYLE_BgRed = exports.CONSOLE_STYLE_BgBlack = void 0;
const dayjs_1 = __importDefault(require("dayjs"));
const dayjs = require("dayjs");
const jsonata = require("jsonata");
const jsonata = __importStar(require("jsonata"));
exports.isDev = process.env.NODE_ENV === "development";
exports.isNode = typeof process !== "undefined" && ((_a = process === null || process === void 0 ? void 0 : process.versions) === null || _a === void 0 ? void 0 : _a.node);
exports.appName = "Uptime Kuma";
@@ -35,6 +52,18 @@ exports.SQL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
exports.MAX_INTERVAL_SECOND = 2073600;
exports.MIN_INTERVAL_SECOND = 20;
exports.PING_PACKET_SIZE_MIN = 1;
exports.PING_PACKET_SIZE_MAX = 65500;
exports.PING_PACKET_SIZE_DEFAULT = 56;
exports.PING_GLOBAL_TIMEOUT_MIN = 1;
exports.PING_GLOBAL_TIMEOUT_MAX = 300;
exports.PING_GLOBAL_TIMEOUT_DEFAULT = 10;
exports.PING_COUNT_MIN = 1;
exports.PING_COUNT_MAX = 100;
exports.PING_COUNT_DEFAULT = 1;
exports.PING_PER_REQUEST_TIMEOUT_MIN = 1;
exports.PING_PER_REQUEST_TIMEOUT_MAX = 60;
exports.PING_PER_REQUEST_TIMEOUT_DEFAULT = 2;
exports.CONSOLE_STYLE_Reset = "\x1b[0m";
exports.CONSOLE_STYLE_Bright = "\x1b[1m";
exports.CONSOLE_STYLE_Dim = "\x1b[2m";
@@ -66,7 +95,6 @@ exports.CONSOLE_STYLE_BgMagenta = "\x1b[45m";
exports.CONSOLE_STYLE_BgCyan = "\x1b[46m";
exports.CONSOLE_STYLE_BgWhite = "\x1b[47m";
exports.CONSOLE_STYLE_BgGray = "\x1b[100m";
const consoleModuleColors = [
exports.CONSOLE_STYLE_FgCyan,
exports.CONSOLE_STYLE_FgGreen,
@@ -458,4 +486,4 @@ async function evaluateJsonQuery(data, jsonPath, jsonPathOperator, expectedValue
throw new Error(`Error evaluating JSON query: ${err.message}. Response from server was: ${response}`);
}
}
exports.evaluateJsonQuery = evaluateJsonQuery;
exports.evaluateJsonQuery = evaluateJsonQuery;

View File

@@ -39,6 +39,26 @@ export const SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
export const MAX_INTERVAL_SECOND = 2073600; // 24 days
export const MIN_INTERVAL_SECOND = 20; // 20 seconds
// Packet Size limits
export const PING_PACKET_SIZE_MIN = 1;
export const PING_PACKET_SIZE_MAX = 65500;
export const PING_PACKET_SIZE_DEFAULT = 56;
// Global timeout (aka deadline) limits in seconds
export const PING_GLOBAL_TIMEOUT_MIN = 1;
export const PING_GLOBAL_TIMEOUT_MAX = 300;
export const PING_GLOBAL_TIMEOUT_DEFAULT = 10;
// Ping count limits
export const PING_COUNT_MIN = 1;
export const PING_COUNT_MAX = 100;
export const PING_COUNT_DEFAULT = 1;
// per-request timeout (aka timeout) limits in seconds
export const PING_PER_REQUEST_TIMEOUT_MIN = 1;
export const PING_PER_REQUEST_TIMEOUT_MAX = 60;
export const PING_PER_REQUEST_TIMEOUT_DEFAULT = 2;
// Console colors
// https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color
export const CONSOLE_STYLE_Reset = "\x1b[0m";