From 235491b99a7a10271515cacd5f02bea89591d45e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radu=20Lucu=C8=9B?= Date: Mon, 6 Oct 2025 18:47:23 +0300 Subject: [PATCH 01/22] draft impl --- .../2025-10-06-0000-globalping.js | 17 ++++ package-lock.json | 21 ++++ package.json | 1 + server/model/monitor.js | 3 + server/monitor-types/globalping.js | 96 +++++++++++++++++++ server/server.js | 7 ++ server/uptime-kuma-server.js | 10 +- src/components/settings/General.vue | 18 ++++ src/lang/en.json | 8 +- src/pages/EditMonitor.vue | 56 ++++++++++- 10 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 db/knex_migrations/2025-10-06-0000-globalping.js create mode 100644 server/monitor-types/globalping.js diff --git a/db/knex_migrations/2025-10-06-0000-globalping.js b/db/knex_migrations/2025-10-06-0000-globalping.js new file mode 100644 index 00000000000..59987914549 --- /dev/null +++ b/db/knex_migrations/2025-10-06-0000-globalping.js @@ -0,0 +1,17 @@ +exports.up = function (knex) { + // Add new columns + return knex.schema.alterTable("monitor", function (table) { + table.string("location", 255).nullable(); + table.string("protocol", 255).nullable(); + table.integer("ip_version").nullable(); + }); +}; + +exports.down = function (knex) { + // Drop columns + return knex.schema.alterTable("monitor", function (table) { + table.dropColumn("location"); + table.dropColumn("protocol"); + table.dropColumn("ip_version"); + }); +}; diff --git a/package-lock.json b/package-lock.json index 50221dced98..1a46d973029 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "feed": "^4.2.2", "form-data": "~4.0.0", "gamedig": "^4.2.0", + "globalping": "^0.2.0", "html-escaper": "^3.0.3", "http-cookie-agent": "~5.0.4", "http-graceful-shutdown": "~3.1.7", @@ -2432,6 +2433,18 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@hey-api/client-fetch": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@hey-api/client-fetch/-/client-fetch-0.10.2.tgz", + "integrity": "sha512-AGiFYDx+y8VT1wlQ3EbzzZtfU8EfV+hLLRTtr8Y/tjYZaxIECwJagVZf24YzNbtEBXONFV50bwcU1wLVGXe1ow==", + "deprecated": "Starting with v0.73.0, this package is bundled directly inside @hey-api/openapi-ts.", + "funding": { + "url": "https://github.com/sponsors/hey-api" + }, + "peerDependencies": { + "@hey-api/openapi-ts": "< 2" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", @@ -9979,6 +9992,14 @@ "which": "bin/which" } }, + "node_modules/globalping": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/globalping/-/globalping-0.2.0.tgz", + "integrity": "sha512-rxyvqXF/oz5yBGJHR58TEsCkcbc4WQVmdKC+uZ8LXfIt4KwTdTvbzkfwhhjuyUmuQr8U/b8m/ZicdrA8ZIroKw==", + "dependencies": { + "@hey-api/client-fetch": "^0.10.0" + } + }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", diff --git a/package.json b/package.json index cc276933bf6..dd5d21bb870 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "feed": "^4.2.2", "form-data": "~4.0.0", "gamedig": "^4.2.0", + "globalping": "^0.2.0", "html-escaper": "^3.0.3", "http-cookie-agent": "~5.0.4", "http-graceful-shutdown": "~3.1.7", diff --git a/server/model/monitor.js b/server/model/monitor.js index 178d639cd75..6f612f65465 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -103,6 +103,9 @@ class Monitor extends BeanModel { method: this.method, hostname: this.hostname, port: this.port, + location: this.location, + protocol: this.protocol, + ipVersion: this.ipVersion, maxretries: this.maxretries, weight: this.weight, active: preloadData.activeStatus.get(this.id), diff --git a/server/monitor-types/globalping.js b/server/monitor-types/globalping.js new file mode 100644 index 00000000000..da17d614a05 --- /dev/null +++ b/server/monitor-types/globalping.js @@ -0,0 +1,96 @@ +const { MonitorType } = require("./monitor-type"); +const { Globalping, IpVersion } = require("globalping"); +const { Settings } = require("../settings"); +const { UP, DOWN } = require("../../src/util"); + +class GlobalpingMonitorType extends MonitorType { + name = "globalping"; + agent = ""; + + /** + * @inheritdoc + */ + constructor(agent) { + super(); + this.agent = agent; + } + + /** + * @inheritdoc + */ + async check(monitor, heartbeat, _server) { + const apiKey = await Settings.get("globalpingApiToken"); + const client = new Globalping({ + auth: apiKey, + agent: this.agent, + }); + + if (monitor.type === "globalping-ping") { + const opts = { + type: "ping", + target: monitor.hostname, + inProgressUpdates: false, + limit: 1, + locations: [{ magic: monitor.location }], + measurementOptions: { + protocol: monitor.protocol, + }, + }; + + if (monitor.protocol === "TCP" && monitor.port) { + opts.measurementOptions.port = monitor.port; + } + + if (monitor.ipVersion === 4) { + opts.measurementOptions.ipVersion = IpVersion[4]; + } else if (monitor.ipVersion === 6) { + opts.measurementOptions.ipVersion = IpVersion[6]; + } + + const res = await client.createMeasurement(opts); + + if (!res.ok) { + throw new Error( + `Failed to create measurement: ${this.formatApiError(res.data.error)}`, + ); + } + + const measurement = await client.awaitMeasurement(res.data.id); + + if (!measurement.ok) { + throw new Error( + `Failed to fetch measurement (${res.data.id}): ${this.formatApiError(measurement.data.error)}`, + ); + } + + const result = measurement.data.results[0].result; + heartbeat.ping = result.stats?.avg || 0; + if (!result.timings?.length) { + heartbeat.msg = `Failed: ${result.rawOutput}`; + heartbeat.status = DOWN; + } else { + heartbeat.msg = ""; + heartbeat.status = UP; + } + } + } + + /** + * Format an API error message. + * @param {object} error - The error object. + * @returns {string} The formatted error message. + */ + formatApiError(error) { + let str = `${error.type} ${error.message}.`; + if (error.params) { + for (const key in error.params) { + str += `\n${key}: ${error.params[key]}`; + } + } + return str; + } +} + +module.exports = { + GlobalpingMonitorType, +}; diff --git a/server/server.js b/server/server.js index 55289b55a23..65eca8a069e 100644 --- a/server/server.js +++ b/server/server.js @@ -824,6 +824,13 @@ let needSetup = false; bean.game = monitor.game; bean.maxretries = monitor.maxretries; bean.port = parseInt(monitor.port); + bean.location = monitor.location; + bean.protocol = monitor.protocol; + bean.ipVersion = parseInt(monitor.ipVersion) + + if (isNaN(bean.ipVersion)) { + bean.ipVersion = null; + } if (isNaN(bean.port)) { bean.port = null; diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index a04e6bd4914..c3f60de3d07 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -120,6 +120,13 @@ class UptimeKumaServer { UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType(); UptimeKumaServer.monitorTypeList["manual"] = new ManualMonitorType(); + // Globalping + const globalpingMonitor = new GlobalpingMonitorType( + this.getUserAgent(), + ); + UptimeKumaServer.monitorTypeList["globalping-ping"] = globalpingMonitor; + UptimeKumaServer.monitorTypeList["globalping-http"] = globalpingMonitor; + // Allow all CORS origins (polling) in development let cors = undefined; if (isDev) { @@ -241,7 +248,7 @@ class UptimeKumaServer { async getMonitorJSONList(userID, monitorID = null) { let query = " user_id = ? "; - let queryParams = [ userID ]; + let queryParams = [userID]; if (monitorID) { query += "AND id = ? "; @@ -560,4 +567,5 @@ const { SNMPMonitorType } = require("./monitor-types/snmp"); const { MongodbMonitorType } = require("./monitor-types/mongodb"); const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq"); const { ManualMonitorType } = require("./monitor-types/manual"); +const { GlobalpingMonitorType } = require("./monitor-types/globalping"); const Monitor = require("./model/monitor"); diff --git a/src/components/settings/General.vue b/src/components/settings/General.vue index 487c3ba3a2d..2ddba827ec2 100644 --- a/src/components/settings/General.vue +++ b/src/components/settings/General.vue @@ -150,6 +150,24 @@ + +
+ + +
+ {{ $t("globalpingApiTokenDescription") }} + + https://dash.globalping.io + +
+
+
@@ -298,4 +297,3 @@ export default { }, }; - diff --git a/src/lang/en.json b/src/lang/en.json index 05b52d8e2c6..a958c40656c 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1160,8 +1160,10 @@ "Send DOWN silently": "Send DOWN silently", "Installing a Nextcloud Talk bot requires administrative access to the server.": "Installing a Nextcloud Talk bot requires administrative access to the server.", "Globalping API Token": "Globalping API Token", - "globalpingApiTokenDescription": "Get your Globalping API Token here: ", - "GlobalpingHostname": "A publicly reachable measurement target. Typically a hostname, an IPv4 address, or IPv6 address, depending on the measurement type", - "GlobalpingLocation": "The location option can accept continents, countries, regions, cities, ASNs, ISPs and cloud region names. You can additionally pinpoint a location by combining filters using the + operator. For example, amazon+germany or comcast+california.", - "GlobalpingLocationDocs": "Full location input documentation" + "globalpingApiTokenDescription": "Get your Globalping API Token at {0}", + "GlobalpingHostname": "A publicly reachable measurement target. Typically a hostname or IPv4/IPv6 address, depending on the measurement type", + "GlobalpingLocation": "The location field accepts continents, countries, regions, cities, ASNs, ISPs, or cloud regions. You can combine filters with {plus} (e.g {amazonPlusGermany} or {comcastPlusCalifornia}). {fullDocs}", + "GlobalpingLocationDocs": "Full location input documentation", + "Location": "Location", + "Monitor Subtype": "Monitor Subtype" } diff --git a/src/pages/Details.vue b/src/pages/Details.vue index 0ba0a44ebeb..b232c64ff68 100644 --- a/src/pages/Details.vue +++ b/src/pages/Details.vue @@ -30,14 +30,20 @@ monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'mp-health' || - monitor.type === 'real-browser' + monitor.type === 'real-browser' || + (monitor.type === 'globalping' && monitor.subtype === 'http') " :href="monitor.url" target="_blank" rel="noopener noreferrer" >{{ filterPassword(monitor.url) }} TCP Port {{ monitor.hostname }}:{{ monitor.port }} - Ping: {{ monitor.hostname }} + Ping: {{ monitor.hostname }} + +
+ {{ $t("Location") }}: + {{ monitor.location }} +

{{ $t("Keyword") }}: diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 4c5ca3deecc..5c2b22116fc 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -51,11 +51,6 @@ - - - - - + @@ -117,6 +113,18 @@ {{ $t("tailscalePingWarning") }} +
+ + +
+
@@ -136,7 +144,7 @@
-
+
@@ -305,7 +313,7 @@ -
+
-
+
{{ $t("GlobalpingHostname") }}
- + + + - @@ -1452,6 +1525,7 @@ const monitorDefaults = { parent: null, url: "https://", method: "GET", + protocol: null, ipFamily: null, interval: 60, humanReadableInterval: relativeTimeFormatter.secondsToHumanReadableFormat(60), @@ -1518,8 +1592,8 @@ export default { // Do not add default value here, please check init() method }, acceptedStatusCodeOptions: [], - dnsResolveTypeOptions: [], - globalpingDNSResolveTypeOptions: [], + dnsresolvetypeoptions: [], + globalpingdnsresolvetypeoptions: [], kafkaSaslMechanismOptions: [], ipOrHostnameRegexPattern: hostNameRegexPattern(), mqttIpOrHostnameRegexPattern: hostNameRegexPattern(true), @@ -1809,6 +1883,9 @@ message HealthCheckResponse { }, supportsConditions() { + if (this.monitor.type === "globalping" && this.monitor.subtype !== "dns") { + return false; + } return this.$root.monitorTypeList[this.monitor.type]?.supportsConditions || false; }, @@ -1964,6 +2041,8 @@ message HealthCheckResponse { this.monitor.protocol = "ICMP"; } else if (newSubtype === "dns") { this.monitor.protocol = "UDP"; + } else if (newSubtype === "http") { + this.monitor.protocol = null; } } if (!oldSubtype && this.monitor.port === undefined) { @@ -1978,10 +2057,31 @@ message HealthCheckResponse { } else if (newSubtype === "dns") { this.monitor.protocol = "UDP"; this.monitor.port = "53"; + } else if (newSubtype === "http") { + this.monitor.protocol = null; + } + } + + if (newSubtype === "http") { + if (this.monitor.keyword) { + this.monitor.responsecheck = "keyword"; + } else if (this.monitor.expectedValue) { + this.monitor.responsecheck = "json-query"; + } else { + this.monitor.responsecheck = null; } } }, + "monitor.responsecheck"(newSubtype) { + if (newSubtype !== "keyword") { + this.monitor.keyword = null; + } + if (newSubtype !== "json-query") { + this.monitor.expectedValue = null; + } + }, + currentGameObject(newGameObject, previousGameObject) { if (!this.monitor.port || (previousGameObject && previousGameObject.options.port === this.monitor.port)) { this.monitor.port = newGameObject.options.port; @@ -2006,7 +2106,7 @@ message HealthCheckResponse { "500-599", ]; - const dnsResolveTypeOptions = [ + const dnsresolvetypeoptions = [ "A", "AAAA", "CAA", @@ -2018,7 +2118,7 @@ message HealthCheckResponse { "SRV", "TXT", ]; - const globalpingDNSResolveTypeOptions = [ + const globalpingdnsresolvetypeoptions = [ "A", "AAAA", "ANY", @@ -2050,8 +2150,8 @@ message HealthCheckResponse { } this.acceptedStatusCodeOptions = acceptedStatusCodeOptions; - this.dnsResolveTypeOptions = dnsResolveTypeOptions; - this.globalpingDNSResolveTypeOptions = globalpingDNSResolveTypeOptions; + this.dnsresolvetypeoptions = dnsresolvetypeoptions; + this.globalpingdnsresolvetypeoptions = globalpingdnsresolvetypeoptions; this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions; }, methods: { @@ -2124,6 +2224,16 @@ message HealthCheckResponse { this.monitor.tags = undefined; } + if (this.monitor.type === "globalping" && this.monitor.subtype === "http") { + if (this.monitor.keyword) { + this.monitor.responsecheck = "keyword"; + } else if (this.monitor.expectedValue) { + this.monitor.responsecheck = "json-query"; + } else { + this.monitor.responsecheck = null; + } + } + // Handling for monitors that are created before 1.7.0 if (this.monitor.retryInterval === 0) { this.monitor.retryInterval = this.monitor.interval; From 361ba48bf753a7e93a3ea53e79b1fce0cee6b0ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radu=20Lucu=C8=9B?= Date: Sat, 11 Oct 2025 22:08:39 +0300 Subject: [PATCH 07/22] revert rename --- src/pages/EditMonitor.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 7c660fc1ac0..df27e1903dc 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -598,7 +598,7 @@ Date: Mon, 13 Oct 2025 14:28:37 +0300 Subject: [PATCH 08/22] handle TLS info --- server/model/monitor.js | 50 ++---------------------- server/monitor-types/globalping.js | 62 +++++++++++++++++++++++++++++- server/util-server.js | 48 +++++++++++++++++++++++ src/pages/EditMonitor.vue | 2 +- 4 files changed, 113 insertions(+), 49 deletions(-) diff --git a/server/model/monitor.js b/server/model/monitor.js index e6a86018e3e..36b4d9ad428 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -8,8 +8,8 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MI PING_COUNT_MIN, PING_COUNT_MAX, PING_COUNT_DEFAULT, PING_PER_REQUEST_TIMEOUT_MIN, PING_PER_REQUEST_TIMEOUT_MAX, PING_PER_REQUEST_TIMEOUT_DEFAULT } = require("../../src/util"); -const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery, - redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal, encodeBase64 +const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, httpNtlm, radius, grpcQuery, + redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal, encodeBase64, checkCertExpiryNotifications } = require("../util-server"); const { R } = require("redbean-node"); const { BeanModel } = require("redbean-node/dist/bean-model"); @@ -341,6 +341,7 @@ class Monitor extends BeanModel { let previousBeat = null; let retries = 0; + this.rootCertificates = rootCertificates; this.prometheus = new Prometheus(this); const beat = async () => { @@ -1363,49 +1364,6 @@ class Monitor extends BeanModel { return notificationList; } - /** - * checks certificate chain for expiring certificates - * @param {object} tlsInfoObject Information about certificate - * @returns {void} - */ - async checkCertExpiryNotifications(tlsInfoObject) { - if (tlsInfoObject && tlsInfoObject.certInfo && tlsInfoObject.certInfo.daysRemaining) { - const notificationList = await Monitor.getNotificationList(this); - - if (! notificationList.length > 0) { - // fail fast. If no notification is set, all the following checks can be skipped. - log.debug("monitor", "No notification, no need to send cert notification"); - return; - } - - let notifyDays = await setting("tlsExpiryNotifyDays"); - if (notifyDays == null || !Array.isArray(notifyDays)) { - // Reset Default - await setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general"); - notifyDays = [ 7, 14, 21 ]; - } - - if (Array.isArray(notifyDays)) { - for (const targetDays of notifyDays) { - let certInfo = tlsInfoObject.certInfo; - while (certInfo) { - let subjectCN = certInfo.subject["CN"]; - if (rootCertificates.has(certInfo.fingerprint256)) { - log.debug("monitor", `Known root cert: ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`); - break; - } else if (certInfo.daysRemaining > targetDays) { - log.debug("monitor", `No need to send cert notification for ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`); - } else { - log.debug("monitor", `call sendCertNotificationByTargetDays for ${targetDays} deadline on certificate ${subjectCN}.`); - await this.sendCertNotificationByTargetDays(subjectCN, certInfo.certType, certInfo.daysRemaining, targetDays, notificationList); - } - certInfo = certInfo.issuerCertificate; - } - } - } - } - } - /** * Send a certificate notification when certificate expires in less * than target days @@ -1761,7 +1719,7 @@ class Monitor extends BeanModel { if (!this.getIgnoreTls() && this.isEnabledExpiryNotification()) { log.debug("monitor", `[${this.name}] call checkCertExpiryNotifications`); - await this.checkCertExpiryNotifications(tlsInfo); + await checkCertExpiryNotifications(this, tlsInfo); } } } diff --git a/server/monitor-types/globalping.js b/server/monitor-types/globalping.js index 5e88fea01a7..8644075e986 100644 --- a/server/monitor-types/globalping.js +++ b/server/monitor-types/globalping.js @@ -2,7 +2,7 @@ const { MonitorType } = require("./monitor-type"); const { Globalping, IpVersion } = require("globalping"); const { Settings } = require("../settings"); const { log, UP, DOWN, evaluateJsonQuery } = require("../../src/util"); -const { checkStatusCode, getOidcTokenClientCredentials, encodeBase64 } = require("../util-server"); +const { checkStatusCode, getOidcTokenClientCredentials, encodeBase64, getDaysRemaining, checkCertExpiryNotifications } = require("../util-server"); const { ConditionVariable } = require("../monitor-conditions/variables"); const { defaultStringOperators } = require("../monitor-conditions/operators"); const { ConditionExpressionGroup } = require("../monitor-conditions/expression"); @@ -237,7 +237,7 @@ class GlobalpingMonitorType extends MonitorType { return; } - // TODO: add tls notification + await this.handleTLSInfo(monitor, protocol, result.tls); heartbeat.status = UP; } @@ -389,6 +389,64 @@ class GlobalpingMonitorType extends MonitorType { "Authorization": "Basic " + encodeBase64(monitor.basic_auth_user, monitor.basic_auth_pass), }; } + + /** + * @inheritdoc + */ + async handleTLSInfo(monitor, protocol, tlsInfo) { + if (!tlsInfo) { + return; + } + + if (!monitor.ignoreTls && protocol === "HTTPS" && !tlsInfo.authorized) { + throw new Error(`TLS certificate is not authorized: ${tlsInfo.error}`); + } + + let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [ + monitor.id, + ]); + + if (tlsInfoBean == null) { + tlsInfoBean = R.dispense("monitor_tls_info"); + tlsInfoBean.monitor_id = monitor.id; + } else { + try { + let oldCertInfo = JSON.parse(tlsInfoBean.info_json); + + if (oldCertInfo && oldCertInfo.certInfo && oldCertInfo.certInfo.fingerprint256 !== tlsInfo.fingerprint256) { + log.debug("monitor", "Resetting sent_history"); + await R.exec("DELETE FROM notification_sent_history WHERE type = 'certificate' AND monitor_id = ?", [ + monitor.id + ]); + } + } catch (e) {} + } + + const validTo = new Date(tlsInfo.expiresAt); + const certResult = { + valid: tlsInfo.authorized, + certInfo: { + subject: tlsInfo.subject, + issuer: tlsInfo.issuer, + validTo: validTo, + daysRemaining: getDaysRemaining(new Date(), validTo), + fingerprint: tlsInfo.fingerprint256, + fingerprint256: tlsInfo.fingerprint256, + certType: "", + } + }; + + tlsInfoBean.info_json = JSON.stringify(certResult); + await R.store(tlsInfoBean); + + if (monitor.prometheus) { + monitor.prometheus.update(null, certResult); + } + + if (!monitor.ignoreTls && monitor.expiryNotification) { + await checkCertExpiryNotifications(monitor, certResult); + } + } } module.exports = { diff --git a/server/util-server.js b/server/util-server.js index 89661a36e77..482bda0bc25 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -626,6 +626,7 @@ const getDaysRemaining = (validFrom, validTo) => { } return daysRemaining; }; +module.exports.getDaysRemaining = getDaysRemaining; /** * Fix certificate info for display @@ -1128,3 +1129,50 @@ function encodeBase64(user, pass) { return Buffer.from(`${user || ""}:${pass || ""}`).toString("base64"); } module.exports.encodeBase64 = encodeBase64; + +/** + * checks certificate chain for expiring certificates + * @param {object} monitor - The monitor object + * @param {object} tlsInfoObject Information about certificate + * @returns {Promise} + */ +async function checkCertExpiryNotifications(monitor, tlsInfoObject) { + if (!tlsInfoObject || !tlsInfoObject.certInfo || !tlsInfoObject.certInfo.daysRemaining) { + return; + } + + let notificationList = await R.getAll("SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ", [ + monitor.id, + ]); + + if (! notificationList.length > 0) { + // fail fast. If no notification is set, all the following checks can be skipped. + log.debug("monitor", "No notification, no need to send cert notification"); + return; + } + + let notifyDays = await Settings.get("tlsExpiryNotifyDays"); + if (notifyDays == null || !Array.isArray(notifyDays)) { + // Reset Default + await Settings.setSetting("tlsExpiryNotifyDays", [ 7, 14, 21 ], "general"); + notifyDays = [ 7, 14, 21 ]; + } + + for (const targetDays of notifyDays) { + let certInfo = tlsInfoObject.certInfo; + while (certInfo) { + let subjectCN = certInfo.subject["CN"]; + if (monitor.rootCertificates.has(certInfo.fingerprint256)) { + log.debug("monitor", `Known root cert: ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`); + break; + } else if (certInfo.daysRemaining > targetDays) { + log.debug("monitor", `No need to send cert notification for ${certInfo.certType} certificate "${subjectCN}" (${certInfo.daysRemaining} days valid) on ${targetDays} deadline.`); + } else { + log.debug("monitor", `call sendCertNotificationByTargetDays for ${targetDays} deadline on certificate ${subjectCN}.`); + await monitor.sendCertNotificationByTargetDays(subjectCN, certInfo.certType, certInfo.daysRemaining, targetDays, notificationList); + } + certInfo = certInfo.issuerCertificate; + } + } +} +module.exports.checkCertExpiryNotifications = checkCertExpiryNotifications; diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index df27e1903dc..f65247a1568 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -357,7 +357,7 @@
- +
{{ $t("GlobalpingHostname") }}
From 79989dc1c034fe517cda68575dd85557bbcea89e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radu=20Lucu=C8=9B?= Date: Tue, 14 Oct 2025 10:40:35 +0300 Subject: [PATCH 09/22] update response text, add retries & reorder inputs --- server/monitor-types/globalping.js | 161 +++++++++++++++++++++-------- src/lang/en.json | 12 ++- src/pages/EditMonitor.vue | 107 ++++++++++--------- 3 files changed, 176 insertions(+), 104 deletions(-) diff --git a/server/monitor-types/globalping.js b/server/monitor-types/globalping.js index 8644075e986..613424fa1e6 100644 --- a/server/monitor-types/globalping.js +++ b/server/monitor-types/globalping.js @@ -11,7 +11,11 @@ const { R } = require("redbean-node"); class GlobalpingMonitorType extends MonitorType { name = "globalping"; + agent = ""; + hasAPIToken = false; + creditsHelpLink = "https://dash.globalping.io?view=add-credits"; + supportsConditions = true; conditionVariables = [ new ConditionVariable("record", defaultStringOperators ), @@ -30,10 +34,10 @@ class GlobalpingMonitorType extends MonitorType { */ async check(monitor, heartbeat, _server) { const apiKey = await Settings.get("globalpingApiToken"); + this.hasAPIToken = !!apiKey; const client = new Globalping({ auth: apiKey, agent: this.agent, - timeout: monitor.timeout * 1000, }); switch (monitor.subtype ) { @@ -76,39 +80,51 @@ class GlobalpingMonitorType extends MonitorType { } log.debug("monitor", `Globalping create measurement: ${JSON.stringify(opts)}`); - const res = await client.createMeasurement(opts); + let res = await client.createMeasurement(opts); if (!res.ok) { - throw new Error( - `Failed to create measurement: ${this.formatApiError(res.data.error)}`, - ); + // retry + res = await client.createMeasurement(opts); + if (!res.ok) { + if (Globalping.isHttpStatus(429, res)) { + throw new Error(`Failed to create measurement: ${this.formatTooManyRequestsError()}`); + } + throw new Error( + `Failed to create measurement: ${this.formatApiError(res.data.error)}` + ); + } } - const measurement = await client.awaitMeasurement(res.data.id); + log.debug("monitor", `Globalping fetch measurement: ${res.data.id}`); + let measurement = await client.awaitMeasurement(res.data.id); if (!measurement.ok) { - throw new Error( - `Failed to fetch measurement (${res.data.id}): ${this.formatApiError(measurement.data.error)}`, - ); + // retry + measurement = await client.awaitMeasurement(res.data.id); + if (!measurement.ok) { + throw new Error( + `Failed to fetch measurement (${res.data.id}): ${this.formatApiError(measurement.data.error)}` + ); + } } - log.debug("monitor", `Globalping measurement data: ${JSON.stringify(measurement.data)}`); + const probe = measurement.data.results[0].probe; const result = measurement.data.results[0].result; if (result.status === "failed") { - heartbeat.msg = `Failed: ${result.rawOutput}`; + heartbeat.msg = this.formatResponse(probe, `Failed: ${result.rawOutput}`); heartbeat.status = DOWN; return; } if (!result.timings?.length) { - heartbeat.msg = `Failed: ${result.rawOutput}`; + heartbeat.msg = this.formatResponse(probe, `Failed: ${result.rawOutput}`); heartbeat.status = DOWN; return; } heartbeat.ping = result.stats.avg || 0; - heartbeat.msg = ""; + heartbeat.msg = this.formatResponse(probe, "OK"); heartbeat.status = UP; } @@ -169,27 +185,39 @@ class GlobalpingMonitorType extends MonitorType { } log.debug("monitor", `Globalping create measurement: ${JSON.stringify(opts)}`); - const res = await client.createMeasurement(opts); + let res = await client.createMeasurement(opts); if (!res.ok) { - throw new Error( - `Failed to create measurement: ${this.formatApiError(res.data.error)}`, - ); + // retry + res = await client.createMeasurement(opts); + if (!res.ok) { + if (Globalping.isHttpStatus(429, res)) { + throw new Error(`Failed to create measurement: ${this.formatTooManyRequestsError()}`); + } + throw new Error( + `Failed to create measurement: ${this.formatApiError(res.data.error)}` + ); + } } - const measurement = await client.awaitMeasurement(res.data.id); + log.debug("monitor", `Globalping fetch measurement: ${res.data.id}`); + let measurement = await client.awaitMeasurement(res.data.id); if (!measurement.ok) { - throw new Error( - `Failed to fetch measurement (${res.data.id}): ${this.formatApiError(measurement.data.error)}`, - ); + // retry + measurement = await client.awaitMeasurement(res.data.id); + if (!measurement.ok) { + throw new Error( + `Failed to fetch measurement (${res.data.id}): ${this.formatApiError(measurement.data.error)}` + ); + } } - log.debug("monitor", `Globalping measurement data: ${JSON.stringify(measurement.data)}`); + const probe = measurement.data.results[0].probe; const result = measurement.data.results[0].result; if (result.status === "failed") { - heartbeat.msg = `Failed: ${result.rawOutput}`; + heartbeat.msg = this.formatResponse(probe, `Failed: ${result.rawOutput}`); heartbeat.status = DOWN; return; } @@ -197,12 +225,12 @@ class GlobalpingMonitorType extends MonitorType { heartbeat.ping = result.timings.total || 0; if (!checkStatusCode(result.statusCode, JSON.parse(monitor.accepted_statuscodes_json))) { - heartbeat.msg = `Status code ${result.statusCode} not accepted. Output: ${result.rawOutput}`; + heartbeat.msg = this.formatResponse(probe, `Status code ${result.statusCode} not accepted. Output: ${result.rawOutput}`); heartbeat.status = DOWN; return; } - heartbeat.msg = `${result.statusCode} - ${result.statusCodeName}`; + heartbeat.msg = this.formatResponse(probe, `${result.statusCode} - ${result.statusCodeName}`); // keyword if (monitor.keyword) { @@ -219,7 +247,7 @@ class GlobalpingMonitorType extends MonitorType { } - heartbeat.msg += ", keyword " + (keywordFound ? "is" : "not") + " found"; + heartbeat.msg += this.formatResponse(probe, ", keyword " + (keywordFound ? "is" : "not") + " found"); heartbeat.status = UP; return; } @@ -229,16 +257,17 @@ class GlobalpingMonitorType extends MonitorType { const { status, response } = await evaluateJsonQuery(result.rawOutput, monitor.jsonPath, monitor.jsonPathOperator, monitor.expectedValue); if (!status) { - throw new Error(`JSON query does not pass (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`); + throw new Error(this.formatResponse(probe, `JSON query does not pass (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`)); } - heartbeat.msg = `JSON query passes (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`; + heartbeat.msg = this.formatResponse(probe, `JSON query passes (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`); heartbeat.status = UP; return; } await this.handleTLSInfo(monitor, protocol, result.tls); + heartbeat.msg = this.formatResponse(probe, "OK"); heartbeat.status = UP; } @@ -272,27 +301,39 @@ class GlobalpingMonitorType extends MonitorType { } log.debug("monitor", `Globalping create measurement: ${JSON.stringify(opts)}`); - const res = await client.createMeasurement(opts); - + let res = await client.createMeasurement(opts); + log.debug("monitor", `Globalping ${JSON.stringify(res)}`); if (!res.ok) { - throw new Error( - `Failed to create measurement: ${this.formatApiError(res.data.error)}`, - ); + // retry + res = await client.createMeasurement(opts); + if (!res.ok) { + if (Globalping.isHttpStatus(429, res)) { + throw new Error(`Failed to create measurement: ${this.formatTooManyRequestsError()}`); + } + throw new Error( + `Failed to create measurement: ${this.formatApiError(res.data.error)}` + ); + } } - const measurement = await client.awaitMeasurement(res.data.id); + log.debug("monitor", `Globalping fetch measurement: ${res.data.id}`); + let measurement = await client.awaitMeasurement(res.data.id); if (!measurement.ok) { - throw new Error( - `Failed to fetch measurement (${res.data.id}): ${this.formatApiError(measurement.data.error)}`, - ); + // retry + measurement = await client.awaitMeasurement(res.data.id); + if (!measurement.ok) { + throw new Error( + `Failed to fetch measurement (${res.data.id}): ${this.formatApiError(measurement.data.error)}` + ); + } } - log.debug("monitor", `Globalping measurement data: ${JSON.stringify(measurement.data)}`); + const probe = measurement.data.results[0].probe; const result = measurement.data.results[0].result; if (result.status === "failed") { - heartbeat.msg = `Failed: ${result.rawOutput}`; + heartbeat.msg = this.formatResponse(probe, `Failed: ${result.rawOutput}`); heartbeat.status = DOWN; return; } @@ -330,14 +371,12 @@ class GlobalpingMonitorType extends MonitorType { } heartbeat.ping = result.timings.total || 0; - heartbeat.msg = dnsMessage; + heartbeat.msg = this.formatResponse(probe, dnsMessage); heartbeat.status = conditionsResult ? UP : DOWN; } /** - * Format an API error message. - * @param {object} error - The error object. - * @returns {string} The formatted error message. + * @inheritdoc */ formatApiError(error) { let str = `${error.type} ${error.message}.`; @@ -349,6 +388,40 @@ class GlobalpingMonitorType extends MonitorType { return str; } + /** + * @inheritdoc + */ + formatTooManyRequestsError() { + if (this.hasAPIToken) { + return `You have run out of credits. Get higher limits by sponsoring us or hosting probes. Learn more at ${this.creditsHelpLink}.`; + } + return `You have run out of credits. Get higher limits by creating an account. Sign up at ${this.creditsHelpLink}.`; + } + + /** + * @inheritdoc + */ + formatProbeLocation(probe) { + let tag = ""; + + for (const t of probe.tags) { + if (Number.isInteger(Number(t.slice(-1)))) { + tag = t; + break; + } + } + return `${probe.city}${probe.state ? ` (${probe.state})` : "" + }, ${probe.country}, ${probe.continent}, ${probe.network + } (AS${probe.asn})${tag ? `, (${tag})` : ""}`; + } + + /** + * @inheritdoc + */ + formatResponse(probe, text) { + return `${this.formatProbeLocation(probe)} : ${text}`; + } + /** * @inheritdoc */ @@ -399,7 +472,7 @@ class GlobalpingMonitorType extends MonitorType { } if (!monitor.ignoreTls && protocol === "HTTPS" && !tlsInfo.authorized) { - throw new Error(`TLS certificate is not authorized: ${tlsInfo.error}`); + throw new Error(this.formatResponse(`TLS certificate is not authorized: ${tlsInfo.error}`)); } let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [ diff --git a/src/lang/en.json b/src/lang/en.json index 0107e13e9e6..4f5a22ce24c 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1159,13 +1159,15 @@ "Send UP silently": "Send UP silently", "Send DOWN silently": "Send DOWN silently", "Installing a Nextcloud Talk bot requires administrative access to the server.": "Installing a Nextcloud Talk bot requires administrative access to the server.", + "GlobalpingDescription": "Globalping provides access to thousands of community hosted probes to run network tests and measurements. A limit of 250 tests per hour is set for all anonymous users. To double the limit to 500 per hour please save your token in {accountSettings}.", "Globalping API Token": "Globalping API Token", - "globalpingApiTokenDescription": "Get your Globalping API Token at {0}", - "GlobalpingHostname": "A publicly reachable measurement target. Typically a hostname or IPv4/IPv6 address, depending on the measurement type", - "GlobalpingLocation": "The location field accepts continents, countries, regions, cities, ASNs, ISPs, or cloud regions. You can combine filters with {plus} (e.g {amazonPlusGermany} or {comcastPlusCalifornia}). {fullDocs}", + "globalpingApiTokenDescription": "Get your Globalping API Token at {0}.", + "GlobalpingHostname": "A publicly reachable measurement target. Typically a hostname or IPv4/IPv6 address, depending on the measurement type.", + "GlobalpingLocation": "The location field accepts continents, countries, regions, cities, ASNs, ISPs, or cloud regions. You can combine filters with {plus} (e.g {amazonPlusGermany} or {comcastPlusCalifornia}). {fullDocs}.", "GlobalpingLocationDocs": "Full location input documentation", - "GlobalpingIpFamilyInfo": "The IP version to use. Only allowed if the target is a hostname", - "GlobalpingResolverInfo": "IPv4/IPv6 address or a fully Qualified Domain Name (FQDN). Defaults to the probe system resolver", + "GlobalpingIpFamilyInfo": "The IP version to use. Only allowed if the target is a hostname.", + "GlobalpingResolverInfo": "IPv4/IPv6 address or a fully Qualified Domain Name (FQDN). Defaults to the probe's local network resolver. You can change the resolver server anytime.", + "account settings": "account settings", "Location": "Location", "Monitor Subtype": "Monitor Subtype", "Check for": "Check for" diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index f65247a1568..2a64ae62e56 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -8,6 +8,12 @@

{{ $t("General") }}

+ + + +
- - - - -
-
@@ -415,15 +402,6 @@
- -
- - -
- {{ $t("pingCountDescription") }} -
-
-
@@ -432,29 +410,6 @@
- -
- - - - -
- {{ $t("acceptedStatusCodesDescription") }} -
-
- + + +
+ + +
@@ -823,7 +797,7 @@
-
+
-
+
@@ -973,6 +947,29 @@
+ +
+ + + + +
+ {{ $t("acceptedStatusCodesDescription") }} +
+
+
From a5c544958ead1da739a855c08452daf99241fe73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radu=20Lucu=C8=9B?= Date: Tue, 14 Oct 2025 16:27:07 +0300 Subject: [PATCH 10/22] update text & fix issue when ping is 0 --- server/model/monitor.js | 2 +- src/lang/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/model/monitor.js b/server/model/monitor.js index 36b4d9ad428..8440468fbf8 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -854,7 +854,7 @@ class Monitor extends BeanModel { let startTime = dayjs().valueOf(); const monitorType = UptimeKumaServer.monitorTypeList[this.type]; await monitorType.check(this, bean, UptimeKumaServer.getInstance()); - if (!bean.ping) { + if (bean.ping === undefined || bean.ping === null) { bean.ping = dayjs().valueOf() - startTime; } diff --git a/src/lang/en.json b/src/lang/en.json index 4f5a22ce24c..f1ac0fd7bd9 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1163,7 +1163,7 @@ "Globalping API Token": "Globalping API Token", "globalpingApiTokenDescription": "Get your Globalping API Token at {0}.", "GlobalpingHostname": "A publicly reachable measurement target. Typically a hostname or IPv4/IPv6 address, depending on the measurement type.", - "GlobalpingLocation": "The location field accepts continents, countries, regions, cities, ASNs, ISPs, or cloud regions. You can combine filters with {plus} (e.g {amazonPlusGermany} or {comcastPlusCalifornia}). {fullDocs}.", + "GlobalpingLocation": "The location field accepts continents, countries, regions, cities, ASNs, ISPs, or cloud regions. You can combine filters with {plus} (e.g {amazonPlusGermany} or {comcastPlusCalifornia}). If latency is an important metric, use filters to narrow down the location to a small region to avoid spikes. {fullDocs}.", "GlobalpingLocationDocs": "Full location input documentation", "GlobalpingIpFamilyInfo": "The IP version to use. Only allowed if the target is a hostname.", "GlobalpingResolverInfo": "IPv4/IPv6 address or a fully Qualified Domain Name (FQDN). Defaults to the probe's local network resolver. You can change the resolver server anytime.", From cf916a6abf67217dd4a37a74e9b614fd5cb4e58c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radu=20Lucu=C8=9B?= Date: Tue, 14 Oct 2025 16:46:03 +0300 Subject: [PATCH 11/22] update DNS message --- server/monitor-types/globalping.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/monitor-types/globalping.js b/server/monitor-types/globalping.js index 613424fa1e6..01a255fc809 100644 --- a/server/monitor-types/globalping.js +++ b/server/monitor-types/globalping.js @@ -342,7 +342,7 @@ class GlobalpingMonitorType extends MonitorType { let conditionsResult = true; const handleConditions = (data) => conditions ? evaluateExpressionGroup(conditions, data) : true; - const dnsMessage = (result.answers || []).map(answer => `${answer.name} ${answer.type} ${answer.ttl} ${answer.class} ${answer.value}`).join(" | "); + const dnsMessage = (result.answers || []).map(answer => answer.value).join(" | "); const values = (result.answers || []).map(answer => answer.value); switch (monitor.dns_resolve_type) { @@ -371,6 +371,9 @@ class GlobalpingMonitorType extends MonitorType { } heartbeat.ping = result.timings.total || 0; + if (!dnsMessage) { + this.dnsMessage = "no records found"; + } heartbeat.msg = this.formatResponse(probe, dnsMessage); heartbeat.status = conditionsResult ? UP : DOWN; } From db4d9d8e12e945de68a3fbc2d698f227d5d6d3c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radu=20Lucu=C8=9B?= Date: Tue, 14 Oct 2025 17:12:38 +0300 Subject: [PATCH 12/22] fix DNS message, add location default --- server/monitor-types/globalping.js | 4 ++-- src/pages/EditMonitor.vue | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/server/monitor-types/globalping.js b/server/monitor-types/globalping.js index 01a255fc809..cad6f4762b1 100644 --- a/server/monitor-types/globalping.js +++ b/server/monitor-types/globalping.js @@ -342,7 +342,7 @@ class GlobalpingMonitorType extends MonitorType { let conditionsResult = true; const handleConditions = (data) => conditions ? evaluateExpressionGroup(conditions, data) : true; - const dnsMessage = (result.answers || []).map(answer => answer.value).join(" | "); + let dnsMessage = (result.answers || []).map(answer => answer.value).join(" | "); const values = (result.answers || []).map(answer => answer.value); switch (monitor.dns_resolve_type) { @@ -372,7 +372,7 @@ class GlobalpingMonitorType extends MonitorType { heartbeat.ping = result.timings.total || 0; if (!dnsMessage) { - this.dnsMessage = "no records found"; + dnsMessage = `No records found. ${result.statusCodeName}`; } heartbeat.msg = this.formatResponse(probe, dnsMessage); heartbeat.status = conditionsResult ? UP : DOWN; diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 2a64ae62e56..4cad3a85266 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -1523,6 +1523,7 @@ const monitorDefaults = { url: "https://", method: "GET", protocol: null, + location: "world", ipFamily: null, interval: 60, humanReadableInterval: relativeTimeFormatter.secondsToHumanReadableFormat(60), From afc3d59a505611ca931f439ecde2160704ec72f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radu=20Lucu=C8=9B?= Date: Mon, 20 Oct 2025 10:10:33 +0300 Subject: [PATCH 13/22] review updates --- .../2025-10-06-0000-add-globalping-monitor.js | 4 +- server/monitor-types/globalping.js | 102 +++++++----------- 2 files changed, 42 insertions(+), 64 deletions(-) diff --git a/db/knex_migrations/2025-10-06-0000-add-globalping-monitor.js b/db/knex_migrations/2025-10-06-0000-add-globalping-monitor.js index e63e69cad79..b5ce4014087 100644 --- a/db/knex_migrations/2025-10-06-0000-add-globalping-monitor.js +++ b/db/knex_migrations/2025-10-06-0000-add-globalping-monitor.js @@ -1,9 +1,9 @@ exports.up = function (knex) { // Add new columns return knex.schema.alterTable("monitor", function (table) { - table.string("subtype", 255).nullable(); + table.string("subtype", 10).nullable(); table.string("location", 255).nullable(); - table.string("protocol", 255).nullable(); + table.string("protocol", 20).nullable(); }); }; diff --git a/server/monitor-types/globalping.js b/server/monitor-types/globalping.js index cad6f4762b1..a9230b6d79b 100644 --- a/server/monitor-types/globalping.js +++ b/server/monitor-types/globalping.js @@ -13,8 +13,6 @@ class GlobalpingMonitorType extends MonitorType { name = "globalping"; agent = ""; - hasAPIToken = false; - creditsHelpLink = "https://dash.globalping.io?view=add-credits"; supportsConditions = true; conditionVariables = [ @@ -34,21 +32,21 @@ class GlobalpingMonitorType extends MonitorType { */ async check(monitor, heartbeat, _server) { const apiKey = await Settings.get("globalpingApiToken"); - this.hasAPIToken = !!apiKey; const client = new Globalping({ auth: apiKey, agent: this.agent, }); + const hasAPIToken = !!apiKey; switch (monitor.subtype ) { case "ping": - await this.ping(client, monitor, heartbeat); + await this.ping(client, monitor, heartbeat, hasAPIToken); break; case "http": - await this.http(client, monitor, heartbeat); + await this.http(client, monitor, heartbeat, hasAPIToken); break; case "dns": - await this.dns(client, monitor, heartbeat); + await this.dns(client, monitor, heartbeat, hasAPIToken); break; } } @@ -56,7 +54,7 @@ class GlobalpingMonitorType extends MonitorType { /** * @inheritdoc */ - async ping(client, monitor, heartbeat) { + async ping(client, monitor, heartbeat, hasAPIToken) { const opts = { type: "ping", target: monitor.hostname, @@ -83,29 +81,21 @@ class GlobalpingMonitorType extends MonitorType { let res = await client.createMeasurement(opts); if (!res.ok) { - // retry - res = await client.createMeasurement(opts); - if (!res.ok) { - if (Globalping.isHttpStatus(429, res)) { - throw new Error(`Failed to create measurement: ${this.formatTooManyRequestsError()}`); - } - throw new Error( - `Failed to create measurement: ${this.formatApiError(res.data.error)}` - ); + if (Globalping.isHttpStatus(429, res)) { + throw new Error(`Failed to create measurement: ${this.formatTooManyRequestsError(hasAPIToken)}`); } + throw new Error( + `Failed to create measurement: ${this.formatApiError(res.data.error)}` + ); } log.debug("monitor", `Globalping fetch measurement: ${res.data.id}`); let measurement = await client.awaitMeasurement(res.data.id); if (!measurement.ok) { - // retry - measurement = await client.awaitMeasurement(res.data.id); - if (!measurement.ok) { - throw new Error( - `Failed to fetch measurement (${res.data.id}): ${this.formatApiError(measurement.data.error)}` - ); - } + throw new Error( + `Failed to fetch measurement (${res.data.id}): ${this.formatApiError(measurement.data.error)}` + ); } const probe = measurement.data.results[0].probe; @@ -131,7 +121,7 @@ class GlobalpingMonitorType extends MonitorType { /** * @inheritdoc */ - async http(client, monitor, heartbeat) { + async http(client, monitor, heartbeat, hasAPIToken) { const url = new URL(monitor.url); let protocol = url.protocol.replace(":", "").toUpperCase(); @@ -188,29 +178,21 @@ class GlobalpingMonitorType extends MonitorType { let res = await client.createMeasurement(opts); if (!res.ok) { - // retry - res = await client.createMeasurement(opts); - if (!res.ok) { - if (Globalping.isHttpStatus(429, res)) { - throw new Error(`Failed to create measurement: ${this.formatTooManyRequestsError()}`); - } - throw new Error( - `Failed to create measurement: ${this.formatApiError(res.data.error)}` - ); + if (Globalping.isHttpStatus(429, res)) { + throw new Error(`Failed to create measurement: ${this.formatTooManyRequestsError(hasAPIToken)}`); } + throw new Error( + `Failed to create measurement: ${this.formatApiError(res.data.error)}` + ); } log.debug("monitor", `Globalping fetch measurement: ${res.data.id}`); let measurement = await client.awaitMeasurement(res.data.id); if (!measurement.ok) { - // retry - measurement = await client.awaitMeasurement(res.data.id); - if (!measurement.ok) { - throw new Error( - `Failed to fetch measurement (${res.data.id}): ${this.formatApiError(measurement.data.error)}` - ); - } + throw new Error( + `Failed to fetch measurement (${res.data.id}): ${this.formatApiError(measurement.data.error)}` + ); } const probe = measurement.data.results[0].probe; @@ -274,7 +256,7 @@ class GlobalpingMonitorType extends MonitorType { /** * @inheritdoc */ - async dns(client, monitor, heartbeat) { + async dns(client, monitor, heartbeat, hasAPIToken) { const opts = { type: "dns", target: monitor.hostname, @@ -304,29 +286,21 @@ class GlobalpingMonitorType extends MonitorType { let res = await client.createMeasurement(opts); log.debug("monitor", `Globalping ${JSON.stringify(res)}`); if (!res.ok) { - // retry - res = await client.createMeasurement(opts); - if (!res.ok) { - if (Globalping.isHttpStatus(429, res)) { - throw new Error(`Failed to create measurement: ${this.formatTooManyRequestsError()}`); - } - throw new Error( - `Failed to create measurement: ${this.formatApiError(res.data.error)}` - ); + if (Globalping.isHttpStatus(429, res)) { + throw new Error(`Failed to create measurement: ${this.formatTooManyRequestsError(hasAPIToken)}`); } + throw new Error( + `Failed to create measurement: ${this.formatApiError(res.data.error)}` + ); } log.debug("monitor", `Globalping fetch measurement: ${res.data.id}`); let measurement = await client.awaitMeasurement(res.data.id); if (!measurement.ok) { - // retry - measurement = await client.awaitMeasurement(res.data.id); - if (!measurement.ok) { - throw new Error( - `Failed to fetch measurement (${res.data.id}): ${this.formatApiError(measurement.data.error)}` - ); - } + throw new Error( + `Failed to fetch measurement (${res.data.id}): ${this.formatApiError(measurement.data.error)}` + ); } const probe = measurement.data.results[0].probe; @@ -394,20 +368,24 @@ class GlobalpingMonitorType extends MonitorType { /** * @inheritdoc */ - formatTooManyRequestsError() { - if (this.hasAPIToken) { - return `You have run out of credits. Get higher limits by sponsoring us or hosting probes. Learn more at ${this.creditsHelpLink}.`; + formatTooManyRequestsError(hasAPIToken) { + const creditsHelpLink = "https://dash.globalping.io?view=add-credits"; + if (hasAPIToken) { + return `You have run out of credits. Get higher limits by sponsoring us or hosting probes. Learn more at ${creditsHelpLink}.`; } - return `You have run out of credits. Get higher limits by creating an account. Sign up at ${this.creditsHelpLink}.`; + return `You have run out of credits. Get higher limits by creating an account. Sign up at ${creditsHelpLink}.`; } /** - * @inheritdoc + * Returns the formatted probe location string. e.g "Ashburn (VA), US, NA, Amazon.com (AS14618), (aws-us-east-1)" + * @param {object} probe - The probe object containing location information. + * @returns {string} The formatted probe location string. */ formatProbeLocation(probe) { let tag = ""; for (const t of probe.tags) { + // If tag ends in a number, it's likely a region code and should be displayed if (Number.isInteger(Number(t.slice(-1)))) { tag = t; break; From f9f54bb7423278a705aeeb6b20b627572c19d120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radu=20Lucu=C8=9B?= Date: Wed, 22 Oct 2025 08:48:38 +0300 Subject: [PATCH 14/22] remove dns subtype --- server/monitor-types/globalping.js | 111 ------------------ .../notification-provider.js | 1 - server/server.js | 2 +- src/pages/Details.vue | 6 - src/pages/EditMonitor.vue | 80 +------------ 5 files changed, 3 insertions(+), 197 deletions(-) diff --git a/server/monitor-types/globalping.js b/server/monitor-types/globalping.js index a9230b6d79b..cb201ea0cbc 100644 --- a/server/monitor-types/globalping.js +++ b/server/monitor-types/globalping.js @@ -3,10 +3,6 @@ const { Globalping, IpVersion } = require("globalping"); const { Settings } = require("../settings"); const { log, UP, DOWN, evaluateJsonQuery } = require("../../src/util"); const { checkStatusCode, getOidcTokenClientCredentials, encodeBase64, getDaysRemaining, checkCertExpiryNotifications } = require("../util-server"); -const { ConditionVariable } = require("../monitor-conditions/variables"); -const { defaultStringOperators } = require("../monitor-conditions/operators"); -const { ConditionExpressionGroup } = require("../monitor-conditions/expression"); -const { evaluateExpressionGroup } = require("../monitor-conditions/evaluator"); const { R } = require("redbean-node"); class GlobalpingMonitorType extends MonitorType { @@ -14,11 +10,6 @@ class GlobalpingMonitorType extends MonitorType { agent = ""; - supportsConditions = true; - conditionVariables = [ - new ConditionVariable("record", defaultStringOperators ), - ]; - /** * @inheritdoc */ @@ -45,9 +36,6 @@ class GlobalpingMonitorType extends MonitorType { case "http": await this.http(client, monitor, heartbeat, hasAPIToken); break; - case "dns": - await this.dns(client, monitor, heartbeat, hasAPIToken); - break; } } @@ -253,105 +241,6 @@ class GlobalpingMonitorType extends MonitorType { heartbeat.status = UP; } - /** - * @inheritdoc - */ - async dns(client, monitor, heartbeat, hasAPIToken) { - const opts = { - type: "dns", - target: monitor.hostname, - inProgressUpdates: false, - limit: 1, - locations: [{ magic: monitor.location }], - measurementOptions: { - query: { - type: monitor.dns_resolve_type - }, - port: monitor.port, - protocol: monitor.protocol, - }, - }; - - if (monitor.ipFamily === "ipv4") { - opts.measurementOptions.ipVersion = IpVersion[4]; - } else if (monitor.ipFamily === "ipv6") { - opts.measurementOptions.ipVersion = IpVersion[6]; - } - - if (monitor.dns_resolve_server) { - opts.measurementOptions.resolver = monitor.dns_resolve_server; - } - - log.debug("monitor", `Globalping create measurement: ${JSON.stringify(opts)}`); - let res = await client.createMeasurement(opts); - log.debug("monitor", `Globalping ${JSON.stringify(res)}`); - if (!res.ok) { - if (Globalping.isHttpStatus(429, res)) { - throw new Error(`Failed to create measurement: ${this.formatTooManyRequestsError(hasAPIToken)}`); - } - throw new Error( - `Failed to create measurement: ${this.formatApiError(res.data.error)}` - ); - } - - log.debug("monitor", `Globalping fetch measurement: ${res.data.id}`); - let measurement = await client.awaitMeasurement(res.data.id); - - if (!measurement.ok) { - throw new Error( - `Failed to fetch measurement (${res.data.id}): ${this.formatApiError(measurement.data.error)}` - ); - } - - const probe = measurement.data.results[0].probe; - const result = measurement.data.results[0].result; - - if (result.status === "failed") { - heartbeat.msg = this.formatResponse(probe, `Failed: ${result.rawOutput}`); - heartbeat.status = DOWN; - return; - } - - const conditions = ConditionExpressionGroup.fromMonitor(monitor); - let conditionsResult = true; - const handleConditions = (data) => conditions ? evaluateExpressionGroup(conditions, data) : true; - - let dnsMessage = (result.answers || []).map(answer => answer.value).join(" | "); - const values = (result.answers || []).map(answer => answer.value); - - switch (monitor.dns_resolve_type) { - case "A": - case "AAAA": - case "ANY": - case "CNAME": - case "DNSKEY": - case "DS": - case "HTTPS": - case "MX": - case "NS": - case "NSEC": - case "PTR": - case "RRSIG": - case "SOA": - case "SRV": - case "SVCB": - case "TXT": - conditionsResult = values.some(record => handleConditions({ record })); - break; - } - - if (monitor.dns_last_result !== dnsMessage && dnsMessage !== undefined) { - await R.exec("UPDATE `monitor` SET dns_last_result = ? WHERE id = ? ", [ dnsMessage, monitor.id ]); - } - - heartbeat.ping = result.timings.total || 0; - if (!dnsMessage) { - dnsMessage = `No records found. ${result.statusCodeName}`; - } - heartbeat.msg = this.formatResponse(probe, dnsMessage); - heartbeat.status = conditionsResult ? UP : DOWN; - } - /** * @inheritdoc */ diff --git a/server/notification-providers/notification-provider.js b/server/notification-providers/notification-provider.js index f599472e3b6..e9391b72b48 100644 --- a/server/notification-providers/notification-provider.js +++ b/server/notification-providers/notification-provider.js @@ -49,7 +49,6 @@ class NotificationProvider { case "globalping": switch (monitorJSON["subtype"]) { case "ping": - case "dns": return monitorJSON["hostname"]; case "http": return monitorJSON["url"]; diff --git a/server/server.js b/server/server.js index b344d8f971a..aa32761931f 100644 --- a/server/server.js +++ b/server/server.js @@ -724,7 +724,7 @@ let needSetup = false; * List of frontend-only properties that should not be saved to the database. * Should clean up before saving to the database. */ - const frontendOnlyProperties = [ "humanReadableInterval", "globalpingdnsresolvetypeoptions", "responsecheck" ]; + const frontendOnlyProperties = [ "humanReadableInterval", "responsecheck" ]; for (const prop of frontendOnlyProperties) { if (prop in monitor) { delete monitor[prop]; diff --git a/src/pages/Details.vue b/src/pages/Details.vue index df1444c0fff..58cc5197712 100644 --- a/src/pages/Details.vue +++ b/src/pages/Details.vue @@ -50,12 +50,6 @@ {{ $t("Location") }}: {{ monitor.location }}
- - [{{ monitor.dns_resolve_type }}] -
- {{ $t("Last Result") }}: - {{ monitor.dns_last_result }} -

diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 4cad3a85266..15c99f3f8e5 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -128,9 +128,6 @@ -
@@ -345,7 +342,7 @@ -
@@ -1591,7 +1548,6 @@ export default { }, acceptedStatusCodeOptions: [], dnsresolvetypeOptions: [], - globalpingdnsresolvetypeoptions: [], kafkaSaslMechanismOptions: [], ipOrHostnameRegexPattern: hostNameRegexPattern(), mqttIpOrHostnameRegexPattern: hostNameRegexPattern(true), @@ -1881,9 +1837,6 @@ message HealthCheckResponse { }, supportsConditions() { - if (this.monitor.type === "globalping" && this.monitor.subtype !== "dns") { - return false; - } return this.$root.monitorTypeList[this.monitor.type]?.supportsConditions || false; }, @@ -2037,24 +1990,14 @@ message HealthCheckResponse { if (!oldSubtype && !this.monitor.protocol) { if (newSubtype === "ping") { this.monitor.protocol = "ICMP"; - } else if (newSubtype === "dns") { - this.monitor.protocol = "UDP"; } else if (newSubtype === "http") { this.monitor.protocol = null; } } - if (!oldSubtype && this.monitor.port === undefined) { - if (newSubtype === "dns") { - this.monitor.port = "53"; - } - } if (newSubtype !== oldSubtype) { if (newSubtype === "ping") { this.monitor.protocol = "ICMP"; this.monitor.port = "80"; - } else if (newSubtype === "dns") { - this.monitor.protocol = "UDP"; - this.monitor.port = "53"; } else if (newSubtype === "http") { this.monitor.protocol = null; } @@ -2116,24 +2059,6 @@ message HealthCheckResponse { "SRV", "TXT", ]; - const globalpingdnsresolvetypeoptions = [ - "A", - "AAAA", - "ANY", - "CNAME", - "DNSKEY", - "DS", - "HTTPS", - "MX", - "NS", - "NSEC", - "PTR", - "RRSIG", - "SOA", - "SRV", - "SVCB", - "TXT", - ]; let kafkaSaslMechanismOptions = [ "None", @@ -2149,7 +2074,6 @@ message HealthCheckResponse { this.acceptedStatusCodeOptions = acceptedStatusCodeOptions; this.dnsresolvetypeOptions = dnsresolvetypeOptions; - this.globalpingdnsresolvetypeoptions = globalpingdnsresolvetypeoptions; this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions; }, methods: { From a377cb6a04364179f8708fa414c1db2e9c1246ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radu=20Lucu=C8=9B?= Date: Mon, 27 Oct 2025 08:12:39 +0200 Subject: [PATCH 15/22] implement copilot suggestions --- server/monitor-types/globalping.js | 6 +++--- src/pages/EditMonitor.vue | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/monitor-types/globalping.js b/server/monitor-types/globalping.js index cb201ea0cbc..37aed5d7eb5 100644 --- a/server/monitor-types/globalping.js +++ b/server/monitor-types/globalping.js @@ -235,7 +235,7 @@ class GlobalpingMonitorType extends MonitorType { return; } - await this.handleTLSInfo(monitor, protocol, result.tls); + await this.handleTLSInfo(monitor, protocol, probe, result.tls); heartbeat.msg = this.formatResponse(probe, "OK"); heartbeat.status = UP; @@ -336,13 +336,13 @@ class GlobalpingMonitorType extends MonitorType { /** * @inheritdoc */ - async handleTLSInfo(monitor, protocol, tlsInfo) { + async handleTLSInfo(monitor, protocol, probe, tlsInfo) { if (!tlsInfo) { return; } if (!monitor.ignoreTls && protocol === "HTTPS" && !tlsInfo.authorized) { - throw new Error(this.formatResponse(`TLS certificate is not authorized: ${tlsInfo.error}`)); + throw new Error(this.formatResponse(probe, `TLS certificate is not authorized: ${tlsInfo.error}`)); } let tlsInfoBean = await R.findOne("monitor_tls_info", "monitor_id = ?", [ diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 15c99f3f8e5..72e9f35d530 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -1320,7 +1320,7 @@