Skip to content

Commit 839ead8

Browse files
Enhanced ping monitor with advanced options (count, timeout, numeric) (#5588)
Co-authored-by: Frank Elsinga <frank@elsinga.de>
1 parent c69fcd4 commit 839ead8

File tree

8 files changed

+332
-41
lines changed

8 files changed

+332
-41
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/* SQL:
2+
ALTER TABLE monitor ADD ping_count INTEGER default 1 not null;
3+
ALTER TABLE monitor ADD ping_numeric BOOLEAN default true not null;
4+
ALTER TABLE monitor ADD ping_per_request_timeout INTEGER default 2 not null;
5+
*/
6+
exports.up = function (knex) {
7+
// Add new columns to table monitor
8+
return knex.schema
9+
.alterTable("monitor", function (table) {
10+
table.integer("ping_count").defaultTo(1).notNullable();
11+
table.boolean("ping_numeric").defaultTo(true).notNullable();
12+
table.integer("ping_per_request_timeout").defaultTo(2).notNullable();
13+
});
14+
15+
};
16+
17+
exports.down = function (knex) {
18+
return knex.schema
19+
.alterTable("monitor", function (table) {
20+
table.dropColumn("ping_count");
21+
table.dropColumn("ping_numeric");
22+
table.dropColumn("ping_per_request_timeout");
23+
});
24+
};

server/model/monitor.js

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ const dayjs = require("dayjs");
22
const axios = require("axios");
33
const { Prometheus } = require("../prometheus");
44
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
5-
SQL_DATETIME_FORMAT, evaluateJsonQuery
5+
SQL_DATETIME_FORMAT, evaluateJsonQuery,
6+
PING_PACKET_SIZE_MIN, PING_PACKET_SIZE_MAX, PING_PACKET_SIZE_DEFAULT,
7+
PING_GLOBAL_TIMEOUT_MIN, PING_GLOBAL_TIMEOUT_MAX, PING_GLOBAL_TIMEOUT_DEFAULT,
8+
PING_COUNT_MIN, PING_COUNT_MAX, PING_COUNT_DEFAULT,
9+
PING_PER_REQUEST_TIMEOUT_MIN, PING_PER_REQUEST_TIMEOUT_MAX, PING_PER_REQUEST_TIMEOUT_DEFAULT
610
} = require("../../src/util");
711
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
812
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
@@ -156,6 +160,11 @@ class Monitor extends BeanModel {
156160
smtpSecurity: this.smtpSecurity,
157161
rabbitmqNodes: JSON.parse(this.rabbitmqNodes),
158162
conditions: JSON.parse(this.conditions),
163+
164+
// ping advanced options
165+
ping_numeric: this.isPingNumeric(),
166+
ping_count: this.ping_count,
167+
ping_per_request_timeout: this.ping_per_request_timeout,
159168
};
160169

161170
if (includeSensitiveData) {
@@ -248,6 +257,14 @@ class Monitor extends BeanModel {
248257
return Boolean(this.expiryNotification);
249258
}
250259

260+
/**
261+
* Check if ping should use numeric output only
262+
* @returns {boolean} True if IP addresses will be output instead of symbolic hostnames
263+
*/
264+
isPingNumeric() {
265+
return Boolean(this.ping_numeric);
266+
}
267+
251268
/**
252269
* Parse to boolean
253270
* @returns {boolean} Should TLS errors be ignored?
@@ -585,7 +602,7 @@ class Monitor extends BeanModel {
585602
bean.status = UP;
586603

587604
} else if (this.type === "ping") {
588-
bean.ping = await ping(this.hostname, this.packetSize);
605+
bean.ping = await ping(this.hostname, this.ping_count, "", this.ping_numeric, this.packetSize, this.timeout, this.ping_per_request_timeout);
589606
bean.msg = "";
590607
bean.status = UP;
591608
} else if (this.type === "push") { // Type: Push
@@ -657,7 +674,7 @@ class Monitor extends BeanModel {
657674
bean.msg = res.data.response.servers[0].name;
658675

659676
try {
660-
bean.ping = await ping(this.hostname, this.packetSize);
677+
bean.ping = await ping(this.hostname, PING_COUNT_DEFAULT, "", true, this.packetSize, PING_GLOBAL_TIMEOUT_DEFAULT, PING_PER_REQUEST_TIMEOUT_DEFAULT);
661678
} catch (_) { }
662679
} else {
663680
throw new Error("Server not found on Steam");
@@ -1469,6 +1486,31 @@ class Monitor extends BeanModel {
14691486
if (this.interval < MIN_INTERVAL_SECOND) {
14701487
throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`);
14711488
}
1489+
1490+
if (this.type === "ping") {
1491+
// ping parameters validation
1492+
if (this.packetSize && (this.packetSize < PING_PACKET_SIZE_MIN || this.packetSize > PING_PACKET_SIZE_MAX)) {
1493+
throw new Error(`Packet size must be between ${PING_PACKET_SIZE_MIN} and ${PING_PACKET_SIZE_MAX} (default: ${PING_PACKET_SIZE_DEFAULT})`);
1494+
}
1495+
1496+
if (this.ping_per_request_timeout && (this.ping_per_request_timeout < PING_PER_REQUEST_TIMEOUT_MIN || this.ping_per_request_timeout > PING_PER_REQUEST_TIMEOUT_MAX)) {
1497+
throw new Error(`Per-ping timeout must be between ${PING_PER_REQUEST_TIMEOUT_MIN} and ${PING_PER_REQUEST_TIMEOUT_MAX} seconds (default: ${PING_PER_REQUEST_TIMEOUT_DEFAULT})`);
1498+
}
1499+
1500+
if (this.ping_count && (this.ping_count < PING_COUNT_MIN || this.ping_count > PING_COUNT_MAX)) {
1501+
throw new Error(`Echo requests count must be between ${PING_COUNT_MIN} and ${PING_COUNT_MAX} (default: ${PING_COUNT_DEFAULT})`);
1502+
}
1503+
1504+
if (this.timeout) {
1505+
const pingGlobalTimeout = Math.round(Number(this.timeout));
1506+
1507+
if (pingGlobalTimeout < this.ping_per_request_timeout || pingGlobalTimeout < PING_GLOBAL_TIMEOUT_MIN || pingGlobalTimeout > PING_GLOBAL_TIMEOUT_MAX) {
1508+
throw new Error(`Timeout must be between ${PING_GLOBAL_TIMEOUT_MIN} and ${PING_GLOBAL_TIMEOUT_MAX} seconds (default: ${PING_GLOBAL_TIMEOUT_DEFAULT})`);
1509+
}
1510+
1511+
this.timeout = pingGlobalTimeout;
1512+
}
1513+
}
14721514
}
14731515

14741516
/**

server/server.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,11 @@ let needSetup = false;
876876
bean.rabbitmqPassword = monitor.rabbitmqPassword;
877877
bean.conditions = JSON.stringify(monitor.conditions);
878878

879+
// ping advanced options
880+
bean.ping_numeric = monitor.ping_numeric;
881+
bean.ping_count = monitor.ping_count;
882+
bean.ping_per_request_timeout = monitor.ping_per_request_timeout;
883+
879884
bean.validate();
880885

881886
await R.store(bean);

server/util-server.js

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
const tcpp = require("tcp-ping");
22
const ping = require("@louislam/ping");
33
const { R } = require("redbean-node");
4-
const { log, genSecret, badgeConstants } = require("../src/util");
4+
const {
5+
log, genSecret, badgeConstants,
6+
PING_PACKET_SIZE_DEFAULT, PING_GLOBAL_TIMEOUT_DEFAULT,
7+
PING_COUNT_DEFAULT, PING_PER_REQUEST_TIMEOUT_DEFAULT
8+
} = require("../src/util");
59
const passwordHash = require("./password-hash");
610
const { Resolver } = require("dns");
711
const iconv = require("iconv-lite");
@@ -118,20 +122,33 @@ exports.tcping = function (hostname, port) {
118122

119123
/**
120124
* Ping the specified machine
121-
* @param {string} hostname Hostname / address of machine
122-
* @param {number} size Size of packet to send
125+
* @param {string} destAddr Hostname / IP address of machine to ping
126+
* @param {number} count Number of packets to send before stopping
127+
* @param {string} sourceAddr Source address for sending/receiving echo requests
128+
* @param {boolean} numeric If true, IP addresses will be output instead of symbolic hostnames
129+
* @param {number} size Size (in bytes) of echo request to send
130+
* @param {number} deadline Maximum time in seconds before ping stops, regardless of packets sent
131+
* @param {number} timeout Maximum time in seconds to wait for each response
123132
* @returns {Promise<number>} Time for ping in ms rounded to nearest integer
124133
*/
125-
exports.ping = async (hostname, size = 56) => {
134+
exports.ping = async (
135+
destAddr,
136+
count = PING_COUNT_DEFAULT,
137+
sourceAddr = "",
138+
numeric = true,
139+
size = PING_PACKET_SIZE_DEFAULT,
140+
deadline = PING_GLOBAL_TIMEOUT_DEFAULT,
141+
timeout = PING_PER_REQUEST_TIMEOUT_DEFAULT,
142+
) => {
126143
try {
127-
return await exports.pingAsync(hostname, false, size);
144+
return await exports.pingAsync(destAddr, false, count, sourceAddr, numeric, size, deadline, timeout);
128145
} catch (e) {
129146
// If the host cannot be resolved, try again with ipv6
130147
log.debug("ping", "IPv6 error message: " + e.message);
131148

132149
// As node-ping does not report a specific error for this, try again if it is an empty message with ipv6 no matter what.
133150
if (!e.message) {
134-
return await exports.pingAsync(hostname, true, size);
151+
return await exports.pingAsync(destAddr, true, count, sourceAddr, numeric, size, deadline, timeout);
135152
} else {
136153
throw e;
137154
}
@@ -140,18 +157,35 @@ exports.ping = async (hostname, size = 56) => {
140157

141158
/**
142159
* Ping the specified machine
143-
* @param {string} hostname Hostname / address of machine to ping
160+
* @param {string} destAddr Hostname / IP address of machine to ping
144161
* @param {boolean} ipv6 Should IPv6 be used?
145-
* @param {number} size Size of ping packet to send
162+
* @param {number} count Number of packets to send before stopping
163+
* @param {string} sourceAddr Source address for sending/receiving echo requests
164+
* @param {boolean} numeric If true, IP addresses will be output instead of symbolic hostnames
165+
* @param {number} size Size (in bytes) of echo request to send
166+
* @param {number} deadline Maximum time in seconds before ping stops, regardless of packets sent
167+
* @param {number} timeout Maximum time in seconds to wait for each response
146168
* @returns {Promise<number>} Time for ping in ms rounded to nearest integer
147169
*/
148-
exports.pingAsync = function (hostname, ipv6 = false, size = 56) {
170+
exports.pingAsync = function (
171+
destAddr,
172+
ipv6 = false,
173+
count = PING_COUNT_DEFAULT,
174+
sourceAddr = "",
175+
numeric = true,
176+
size = PING_PACKET_SIZE_DEFAULT,
177+
deadline = PING_GLOBAL_TIMEOUT_DEFAULT,
178+
timeout = PING_PER_REQUEST_TIMEOUT_DEFAULT,
179+
) {
149180
return new Promise((resolve, reject) => {
150-
ping.promise.probe(hostname, {
181+
ping.promise.probe(destAddr, {
151182
v6: ipv6,
152-
min_reply: 1,
153-
deadline: 10,
183+
min_reply: count,
184+
sourceAddr: sourceAddr,
185+
numeric: numeric,
154186
packetSize: size,
187+
deadline: deadline,
188+
timeout: timeout
155189
}).then((res) => {
156190
// If ping failed, it will set field to unknown
157191
if (res.alive) {

src/lang/en.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1074,7 +1074,16 @@
10741074
"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}.",
10751075
"SendGrid API Key": "SendGrid API Key",
10761076
"Separate multiple email addresses with commas": "Separate multiple email addresses with commas",
1077-
"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.",
1077+
"pingCountLabel": "Max Packets",
1078+
"pingCountDescription": "Number of packets to send before stopping",
1079+
"pingNumericLabel": "Numeric Output",
1080+
"pingNumericDescription": "If checked, IP addresses will be output instead of symbolic hostnames",
1081+
"pingGlobalTimeoutLabel": "Global Timeout",
1082+
"pingGlobalTimeoutDescription": "Total time in seconds before ping stops, regardless of packets sent",
1083+
"pingPerRequestTimeoutLabel": "Per-Ping Timeout",
1084+
"pingPerRequestTimeoutDescription": "This is the maximum waiting time (in seconds) before considering a single ping packet lost",
1085+
"pingIntervalAdjustedInfo": "Interval adjusted based on packet count, global timeout and per-ping timeout",
1086+
"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.",
10781087
"Custom URL": "Custom URL",
10791088
"customUrlDescription": "Will be used as the clickable URL instead of the monitor's one.",
10801089
"OneChatAccessToken": "OneChat Access Token",

0 commit comments

Comments
 (0)