From d6c4eeaa1d7971724cd761aabf4f0a41c62f7daf Mon Sep 17 00:00:00 2001 From: iotux Date: Sat, 13 Dec 2025 10:16:14 +0100 Subject: [PATCH 01/80] Fixed Promotheus issue related to adding tags (#6476) --- server/prometheus.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/prometheus.js b/server/prometheus.js index 70daf8ce7..19d4ac197 100644 --- a/server/prometheus.js +++ b/server/prometheus.js @@ -34,6 +34,7 @@ class Prometheus { * @returns {Promise} */ static async init() { + PrometheusClient.register.clear(); // Add all available tags as possible labels, // and use Set to remove possible duplicates (for when multiple tags contain non-ascii characters, and thus are sanitized to the same label) const tags = new Set((await R.findAll("tag")).map((tag) => { From 14f9f1260a623eadd06be689c59d7d887cefaa33 Mon Sep 17 00:00:00 2001 From: iotux Date: Sat, 13 Dec 2025 10:53:39 +0100 Subject: [PATCH 02/80] Fix for issue #6476 --- server/server.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server/server.js b/server/server.js index d8088bc90..8f4da1ddf 100644 --- a/server/server.js +++ b/server/server.js @@ -900,6 +900,9 @@ let needSetup = false; bean.rabbitmqPassword = monitor.rabbitmqPassword; bean.conditions = JSON.stringify(monitor.conditions); bean.manual_status = monitor.manual_status; + bean.local_service_command = monitor.local_service_command; + bean.local_service_expected_output = monitor.local_service_expected_output; + bean.local_service_check_type = monitor.local_service_check_type; // ping advanced options bean.ping_numeric = monitor.ping_numeric; @@ -1173,6 +1176,8 @@ let needSetup = false; bean.color = tag.color; await R.store(bean); + await Prometheus.init(); + callback({ ok: true, tag: await bean.toJSON(), @@ -1248,6 +1253,8 @@ let needSetup = false; value, ]); + await server.sendUpdateMonitorIntoList(socket, monitorID); + callback({ ok: true, msg: "successAdded", @@ -1272,6 +1279,8 @@ let needSetup = false; monitorID, ]); + await server.sendUpdateMonitorIntoList(socket, monitorID); + callback({ ok: true, msg: "successEdited", @@ -1296,6 +1305,8 @@ let needSetup = false; value, ]); + await server.sendUpdateMonitorIntoList(socket, monitorID); + callback({ ok: true, msg: "successDeleted", @@ -1979,3 +1990,4 @@ let unexpectedErrorHandler = (error, promise) => { }; process.addListener("unhandledRejection", unexpectedErrorHandler); process.addListener("uncaughtException", unexpectedErrorHandler); + From 883083f5c35709ad1bc9141bb10d4a72952d8e65 Mon Sep 17 00:00:00 2001 From: iotux Date: Sun, 14 Dec 2025 04:59:02 +0100 Subject: [PATCH 03/80] Reverted previous change --- server/server.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/server/server.js b/server/server.js index 8f4da1ddf..d8088bc90 100644 --- a/server/server.js +++ b/server/server.js @@ -900,9 +900,6 @@ let needSetup = false; bean.rabbitmqPassword = monitor.rabbitmqPassword; bean.conditions = JSON.stringify(monitor.conditions); bean.manual_status = monitor.manual_status; - bean.local_service_command = monitor.local_service_command; - bean.local_service_expected_output = monitor.local_service_expected_output; - bean.local_service_check_type = monitor.local_service_check_type; // ping advanced options bean.ping_numeric = monitor.ping_numeric; @@ -1176,8 +1173,6 @@ let needSetup = false; bean.color = tag.color; await R.store(bean); - await Prometheus.init(); - callback({ ok: true, tag: await bean.toJSON(), @@ -1253,8 +1248,6 @@ let needSetup = false; value, ]); - await server.sendUpdateMonitorIntoList(socket, monitorID); - callback({ ok: true, msg: "successAdded", @@ -1279,8 +1272,6 @@ let needSetup = false; monitorID, ]); - await server.sendUpdateMonitorIntoList(socket, monitorID); - callback({ ok: true, msg: "successEdited", @@ -1305,8 +1296,6 @@ let needSetup = false; value, ]); - await server.sendUpdateMonitorIntoList(socket, monitorID); - callback({ ok: true, msg: "successDeleted", @@ -1990,4 +1979,3 @@ let unexpectedErrorHandler = (error, promise) => { }; process.addListener("unhandledRejection", unexpectedErrorHandler); process.addListener("uncaughtException", unexpectedErrorHandler); - From 7461bd296fd2e65ad7f5e508580c2c16573ea072 Mon Sep 17 00:00:00 2001 From: iotux Date: Tue, 9 Dec 2025 14:19:38 +0100 Subject: [PATCH 04/80] feat: Add 'local service' monitor type This adds a new monitor type to check local services by executing a shell command. It also includes fixes for Prometheus errors when adding new tags and for the UI not updating when tags are changed. --- config/vite.config.js | 7 + ...25-12-09-0000-add-local-service-monitor.js | 33 ++++ server/model/monitor.js | 3 + server/monitor-types/local-service.js | 62 ++++++++ server/server.js | 11 ++ server/uptime-kuma-server.js | 2 + src/lang/en.json | 4 + src/pages/EditMonitor.vue | 142 +++++++++++++----- 8 files changed, 225 insertions(+), 39 deletions(-) create mode 100644 db/knex_migrations/2025-12-09-0000-add-local-service-monitor.js create mode 100644 server/monitor-types/local-service.js diff --git a/config/vite.config.js b/config/vite.config.js index f4a60da9d..62963519a 100644 --- a/config/vite.config.js +++ b/config/vite.config.js @@ -13,6 +13,13 @@ const viteCompressionFilter = /\.(js|mjs|json|css|html|svg)$/i; export default defineConfig({ server: { port: 3000, + watch: { + ignored: [ + "**/node_modules/**", + "**/dist/**", + "**/data/**", + ], + }, }, define: { "FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version), diff --git a/db/knex_migrations/2025-12-09-0000-add-local-service-monitor.js b/db/knex_migrations/2025-12-09-0000-add-local-service-monitor.js new file mode 100644 index 000000000..1522312fd --- /dev/null +++ b/db/knex_migrations/2025-12-09-0000-add-local-service-monitor.js @@ -0,0 +1,33 @@ +const { settings } = require("../../server/util-server"); + +/** + * @param {import("knex").Knex} knex + */ +exports.up = async (knex) => { + await knex.schema.alterTable("monitor", (table) => { + table.string("local_service_command"); + table.string("local_service_expected_output"); + table.string("local_service_check_type").notNullable().defaultTo("keyword"); + }); +}; + +/** + * @param {import("knex").Knex} knex + */ +exports.down = async (knex) => { + if (await knex.schema.hasColumn("monitor", "local_service_command")) { + await knex.schema.alterTable("monitor", (table) => { + table.dropColumn("local_service_command"); + }); + } + if (await knex.schema.hasColumn("monitor", "local_service_expected_output")) { + await knex.schema.alterTable("monitor", (table) => { + table.dropColumn("local_service_expected_output"); + }); + } + if (await knex.schema.hasColumn("monitor", "local_service_check_type")) { + await knex.schema.alterTable("monitor", (table) => { + table.dropColumn("local_service_check_type"); + }); + } +}; diff --git a/server/model/monitor.js b/server/model/monitor.js index eff8add94..08c83f4ee 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -148,6 +148,7 @@ class Monitor extends BeanModel { httpBodyEncoding: this.httpBodyEncoding, jsonPath: this.jsonPath, expectedValue: this.expectedValue, + local_service_check_type: this.local_service_check_type, kafkaProducerTopic: this.kafkaProducerTopic, kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers), kafkaProducerSsl: this.getKafkaProducerSsl(), @@ -201,6 +202,8 @@ class Monitor extends BeanModel { kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions), rabbitmqUsername: this.rabbitmqUsername, rabbitmqPassword: this.rabbitmqPassword, + local_service_command: this.local_service_command, + local_service_expected_output: this.local_service_expected_output, }; } diff --git a/server/monitor-types/local-service.js b/server/monitor-types/local-service.js new file mode 100644 index 000000000..70866839a --- /dev/null +++ b/server/monitor-types/local-service.js @@ -0,0 +1,62 @@ +const { MonitorType } = require("./monitor-type"); +const { exec } = require("child_process"); +const { DOWN, UP, log, evaluateJsonQuery } = require("../../src/util"); + +class LocalServiceMonitorType extends MonitorType { + name = "local-service"; + description = "Checks if a local service is running by executing a command."; + + async check(monitor, heartbeat, server) { + return new Promise((resolve, reject) => { + exec(monitor.local_service_command, async (error, stdout, stderr) => { + if (error) { + heartbeat.status = DOWN; + heartbeat.msg = `Error executing command: ${error.message}`; + reject(new Error(heartbeat.msg)); + return; + } + + const output = stdout.trim(); + + if (monitor.local_service_check_type === "keyword") { + if (monitor.local_service_expected_output) { + if (output.includes(monitor.local_service_expected_output)) { + heartbeat.status = UP; + heartbeat.msg = `OK - Output contains "${monitor.local_service_expected_output}"`; + resolve(); + } else { + heartbeat.status = DOWN; + heartbeat.msg = `Output did not contain "${monitor.local_service_expected_output}"`; + reject(new Error(heartbeat.msg)); + } + } else { + heartbeat.status = UP; + heartbeat.msg = "OK - Command executed successfully"; + resolve(); + } + } else if (monitor.local_service_check_type === "json-query") { + try { + const data = JSON.parse(output); + const { status, response } = await evaluateJsonQuery(data, monitor.jsonPath, monitor.jsonPathOperator, monitor.expectedValue); + + if (status) { + heartbeat.status = UP; + heartbeat.msg = `JSON query passes (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`; + resolve(); + } else { + throw new Error(`JSON query does not pass (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`); + } + } catch (e) { + heartbeat.status = DOWN; + heartbeat.msg = e.message; + reject(e); + } + } + }); + }); + } +} + +module.exports = { + LocalServiceMonitorType, +}; \ No newline at end of file diff --git a/server/server.js b/server/server.js index d8088bc90..104093201 100644 --- a/server/server.js +++ b/server/server.js @@ -900,6 +900,9 @@ let needSetup = false; bean.rabbitmqPassword = monitor.rabbitmqPassword; bean.conditions = JSON.stringify(monitor.conditions); bean.manual_status = monitor.manual_status; + bean.local_service_command = monitor.local_service_command; + bean.local_service_expected_output = monitor.local_service_expected_output; + bean.local_service_check_type = monitor.local_service_check_type; // ping advanced options bean.ping_numeric = monitor.ping_numeric; @@ -1173,6 +1176,8 @@ let needSetup = false; bean.color = tag.color; await R.store(bean); + await Prometheus.init(); + callback({ ok: true, tag: await bean.toJSON(), @@ -1248,6 +1253,8 @@ let needSetup = false; value, ]); + await server.sendUpdateMonitorIntoList(socket, monitorID); + callback({ ok: true, msg: "successAdded", @@ -1272,6 +1279,8 @@ let needSetup = false; monitorID, ]); + await server.sendUpdateMonitorIntoList(socket, monitorID); + callback({ ok: true, msg: "successEdited", @@ -1296,6 +1305,8 @@ let needSetup = false; value, ]); + await server.sendUpdateMonitorIntoList(socket, monitorID); + callback({ ok: true, msg: "successDeleted", diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index 982af6cf8..33bdb099e 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -124,6 +124,7 @@ class UptimeKumaServer { UptimeKumaServer.monitorTypeList["port"] = new TCPMonitorType(); UptimeKumaServer.monitorTypeList["manual"] = new ManualMonitorType(); UptimeKumaServer.monitorTypeList["redis"] = new RedisMonitorType(); + UptimeKumaServer.monitorTypeList["local-service"] = new LocalServiceMonitorType(); // Allow all CORS origins (polling) in development let cors = undefined; @@ -570,5 +571,6 @@ const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq"); const { TCPMonitorType } = require("./monitor-types/tcp.js"); const { ManualMonitorType } = require("./monitor-types/manual"); const { RedisMonitorType } = require("./monitor-types/redis"); +const { LocalServiceMonitorType } = require("./monitor-types/local-service"); const Monitor = require("./model/monitor"); diff --git a/src/lang/en.json b/src/lang/en.json index c8d69973a..a4c745803 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1024,8 +1024,12 @@ "useRemoteBrowser": "Use a Remote Browser", "deleteRemoteBrowserMessage": "Are you sure want to delete this Remote Browser for all monitors?", "GrafanaOncallUrl": "Grafana Oncall URL", + "Local Service": "Local Service", "Browser Screenshot": "Browser Screenshot", "Command": "Command", + "localServiceCommandDescription": "The command to execute. For example: `systemctl is-active mosquitto`", + "localServiceExpectedOutputDescription": "The expected output of the command. If the output contains this string, the monitor will be considered UP. Leave empty to only check the exit code.", + "Expected Output": "Expected Output", "mongodbCommandDescription": "Run a MongoDB command against the database. For information about the available commands check out the {documentation}", "wayToGetSevenIOApiKey": "Visit the dashboard under app.seven.io > developer > api key > the green add button", "senderSevenIO": "Sending number or name", diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 824b5be2e..637c2adf1 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -98,6 +98,9 @@ + @@ -656,6 +659,7 @@ + + + - - + + - - + + - - + + - -
+ +
@@ -766,8 +827,8 @@ {{ $t("minimumIntervalWarning") }}
- - + +
- -

{{ $t("Advanced") }}

+ +

{{ $t("Advanced") }}

@@ -796,8 +857,8 @@
- -
+ +
Sec-Websocket-Accept @@ -813,8 +874,8 @@ {{ monitor.type === "redis" ? $t("ignoreTLSErrorGeneral") : $t("ignoreTLSError") }}
- -
+ +
- -
+ +
- - + +
- - + +
@@ -912,8 +973,8 @@ {{ $t("acceptedStatusCodesDescription") }}
- -
+ +
@@ -956,8 +1017,8 @@
- - + +

{{ $t("Notifications") }}

{{ $t("Not available, please setup.") }} @@ -977,8 +1038,8 @@ - - + +

{{ $t("Proxy") }}

@@ -997,8 +1058,8 @@ {{ proxy.host }}:{{ proxy.port }} ({{ proxy.protocol }}) {{ $t("Edit") }} - - {{ $t("default") }} + + {{ $t("default") }}

- - + +

{{ $t("Authentication") }}

@@ -1189,8 +1250,8 @@
- -
+ +
@@ -1236,8 +1297,8 @@ {{ $t("grpcMethodDescription") }}
- - + +
@@ -1359,7 +1420,10 @@ const monitorDefaults = { rabbitmqNodes: [], rabbitmqUsername: "", rabbitmqPassword: "", - conditions: [] + conditions: [], + local_service_command: "", + local_service_expected_output: "", + local_service_check_type: "keyword", }; export default { From 3b9c5530cccb5f511986432be4c72bbae01320e3 Mon Sep 17 00:00:00 2001 From: iotux Date: Thu, 11 Dec 2025 05:04:17 +0100 Subject: [PATCH 05/80] Replaced some tabs --- ...25-12-09-0000-add-local-service-monitor.js | 8 +- server/monitor-types/local-service.js | 7 +- src/pages/EditMonitor.vue | 79 +++++++++---------- 3 files changed, 48 insertions(+), 46 deletions(-) diff --git a/db/knex_migrations/2025-12-09-0000-add-local-service-monitor.js b/db/knex_migrations/2025-12-09-0000-add-local-service-monitor.js index 1522312fd..deb411040 100644 --- a/db/knex_migrations/2025-12-09-0000-add-local-service-monitor.js +++ b/db/knex_migrations/2025-12-09-0000-add-local-service-monitor.js @@ -1,7 +1,6 @@ -const { settings } = require("../../server/util-server"); - /** - * @param {import("knex").Knex} knex + * @param {import("knex").Knex} knex Database connection + * @returns {Promise} */ exports.up = async (knex) => { await knex.schema.alterTable("monitor", (table) => { @@ -12,7 +11,8 @@ exports.up = async (knex) => { }; /** - * @param {import("knex").Knex} knex + * @param {import("knex").Knex} knex Database connection + * @returns {Promise} */ exports.down = async (knex) => { if (await knex.schema.hasColumn("monitor", "local_service_command")) { diff --git a/server/monitor-types/local-service.js b/server/monitor-types/local-service.js index 70866839a..d5eaeb849 100644 --- a/server/monitor-types/local-service.js +++ b/server/monitor-types/local-service.js @@ -1,11 +1,14 @@ const { MonitorType } = require("./monitor-type"); const { exec } = require("child_process"); -const { DOWN, UP, log, evaluateJsonQuery } = require("../../src/util"); +const { DOWN, UP, evaluateJsonQuery } = require("../../src/util"); class LocalServiceMonitorType extends MonitorType { name = "local-service"; description = "Checks if a local service is running by executing a command."; + /** + * @inheritdoc + */ async check(monitor, heartbeat, server) { return new Promise((resolve, reject) => { exec(monitor.local_service_command, async (error, stdout, stderr) => { @@ -59,4 +62,4 @@ class LocalServiceMonitorType extends MonitorType { module.exports = { LocalServiceMonitorType, -}; \ No newline at end of file +}; diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 637c2adf1..e45740b13 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -659,7 +659,6 @@
- - - + + - - + + - - + +
- -
+ +
@@ -827,8 +826,8 @@ {{ $t("minimumIntervalWarning") }}
- - + +
- -

{{ $t("Advanced") }}

+ +

{{ $t("Advanced") }}

@@ -857,8 +856,8 @@
- -
+ +
Sec-Websocket-Accept @@ -874,8 +873,8 @@ {{ monitor.type === "redis" ? $t("ignoreTLSErrorGeneral") : $t("ignoreTLSError") }}
- -
+ +
- -
+ +
- - + +
- - + +
@@ -973,8 +972,8 @@ {{ $t("acceptedStatusCodesDescription") }}
- -
+ +
@@ -1017,8 +1016,8 @@
- - + +

{{ $t("Notifications") }}

{{ $t("Not available, please setup.") }} @@ -1038,8 +1037,8 @@ - - + +

{{ $t("Proxy") }}

@@ -1058,8 +1057,8 @@ {{ proxy.host }}:{{ proxy.port }} ({{ proxy.protocol }}) {{ $t("Edit") }} - - {{ $t("default") }} + + {{ $t("default") }}

-
+
@@ -1170,8 +1169,8 @@
- - + +

{{ $t("Authentication") }}

@@ -1250,8 +1249,8 @@
- -
+ +
@@ -1297,8 +1296,8 @@ {{ $t("grpcMethodDescription") }}
- - + +
From 9a2b058b50a870248f4df7570fda762d645d0f5c Mon Sep 17 00:00:00 2001 From: iotux Date: Thu, 11 Dec 2025 08:07:03 +0100 Subject: [PATCH 06/80] Fix: Linting errors and typo --- src/pages/EditMonitor.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index e45740b13..99865f827 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -1101,7 +1101,7 @@
-
+
From 8e93a1d4da39a7d392f2064ebb2f90b520afad95 Mon Sep 17 00:00:00 2001 From: iotux Date: Fri, 12 Dec 2025 09:31:12 +0100 Subject: [PATCH 07/80] Removed temporary block from vite.config.js --- config/vite.config.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/config/vite.config.js b/config/vite.config.js index 62963519a..f4a60da9d 100644 --- a/config/vite.config.js +++ b/config/vite.config.js @@ -13,13 +13,6 @@ const viteCompressionFilter = /\.(js|mjs|json|css|html|svg)$/i; export default defineConfig({ server: { port: 3000, - watch: { - ignored: [ - "**/node_modules/**", - "**/dist/**", - "**/data/**", - ], - }, }, define: { "FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version), From 1220160038a301b0213d447e4179dae245193815 Mon Sep 17 00:00:00 2001 From: iotux Date: Fri, 12 Dec 2025 11:09:55 +0100 Subject: [PATCH 08/80] Moved Local Service to General Monitor Type --- src/pages/EditMonitor.vue | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 99865f827..a0a4d934b 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -42,13 +42,15 @@ - - - + + + - @@ -1422,7 +1421,7 @@ const monitorDefaults = { conditions: [], local_service_command: "", local_service_expected_output: "", - local_service_check_type: "keyword", + local_service_check_type: "", }; export default { From 2ffc06d950209a995badf98051783231802d50b3 Mon Sep 17 00:00:00 2001 From: iotux Date: Sat, 13 Dec 2025 07:15:42 +0100 Subject: [PATCH 09/80] Fixed indent issues --- src/pages/EditMonitor.vue | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index a0a4d934b..bf5cb422a 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -42,16 +42,15 @@ - - - - + + + From d76ce4e28df2a50fe878cac6935d010c8ba467cc Mon Sep 17 00:00:00 2001 From: iotux Date: Sat, 13 Dec 2025 15:25:14 +0100 Subject: [PATCH 10/80] Simplified and secured Local Service monitor --- ...25-12-09-0000-add-local-service-monitor.js | 22 ++---- server/model/monitor.js | 4 +- server/monitor-types/local-service.js | 70 ++++++++----------- server/server.js | 4 +- src/lang/en.json | 4 +- src/pages/EditMonitor.vue | 57 ++------------- 6 files changed, 41 insertions(+), 120 deletions(-) diff --git a/db/knex_migrations/2025-12-09-0000-add-local-service-monitor.js b/db/knex_migrations/2025-12-09-0000-add-local-service-monitor.js index deb411040..ef627a324 100644 --- a/db/knex_migrations/2025-12-09-0000-add-local-service-monitor.js +++ b/db/knex_migrations/2025-12-09-0000-add-local-service-monitor.js @@ -4,9 +4,7 @@ */ exports.up = async (knex) => { await knex.schema.alterTable("monitor", (table) => { - table.string("local_service_command"); - table.string("local_service_expected_output"); - table.string("local_service_check_type").notNullable().defaultTo("keyword"); + table.string("local_service_name"); }); }; @@ -15,19 +13,7 @@ exports.up = async (knex) => { * @returns {Promise} */ exports.down = async (knex) => { - if (await knex.schema.hasColumn("monitor", "local_service_command")) { - await knex.schema.alterTable("monitor", (table) => { - table.dropColumn("local_service_command"); - }); - } - if (await knex.schema.hasColumn("monitor", "local_service_expected_output")) { - await knex.schema.alterTable("monitor", (table) => { - table.dropColumn("local_service_expected_output"); - }); - } - if (await knex.schema.hasColumn("monitor", "local_service_check_type")) { - await knex.schema.alterTable("monitor", (table) => { - table.dropColumn("local_service_check_type"); - }); - } + await knex.schema.alterTable("monitor", (table) => { + table.dropColumn("local_service_name"); + }); }; diff --git a/server/model/monitor.js b/server/model/monitor.js index 08c83f4ee..c9ba0b140 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -148,7 +148,7 @@ class Monitor extends BeanModel { httpBodyEncoding: this.httpBodyEncoding, jsonPath: this.jsonPath, expectedValue: this.expectedValue, - local_service_check_type: this.local_service_check_type, + local_service_name: this.local_service_name, kafkaProducerTopic: this.kafkaProducerTopic, kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers), kafkaProducerSsl: this.getKafkaProducerSsl(), @@ -202,8 +202,6 @@ class Monitor extends BeanModel { kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions), rabbitmqUsername: this.rabbitmqUsername, rabbitmqPassword: this.rabbitmqPassword, - local_service_command: this.local_service_command, - local_service_expected_output: this.local_service_expected_output, }; } diff --git a/server/monitor-types/local-service.js b/server/monitor-types/local-service.js index d5eaeb849..65183fda0 100644 --- a/server/monitor-types/local-service.js +++ b/server/monitor-types/local-service.js @@ -1,60 +1,48 @@ const { MonitorType } = require("./monitor-type"); -const { exec } = require("child_process"); -const { DOWN, UP, evaluateJsonQuery } = require("../../src/util"); +const { execFile } = require("child_process"); +const { DOWN, UP } = require("../../src/util"); class LocalServiceMonitorType extends MonitorType { name = "local-service"; description = "Checks if a local service is running by executing a command."; /** - * @inheritdoc + * Check a local systemd service status. + * Uses `systemctl is-running` to determine if the service is active. + * @param {object} monitor The monitor object containing serviceName. + * @param {object} heartbeat The heartbeat object to update. + * @param {object} server The server object (unused in this specific check). + * @returns {Promise} A promise that resolves with the updated heartbeat. + * @throws {Error} If the serviceName is invalid or the command execution fails. */ async check(monitor, heartbeat, server) { + // This is the name of the service to check e.g. "nginx.service" + const serviceName = monitor.local_service_name; + + // Basic sanitization to prevent argument injection. + // This regex allows for standard service names, including those with instances like "sshd@.service". + if (!serviceName || !/^[a-zA-Z0-9._\-@]+$/.test(serviceName)) { + heartbeat.status = DOWN; + heartbeat.msg = "Invalid service name provided."; + throw new Error(heartbeat.msg); + } + return new Promise((resolve, reject) => { - exec(monitor.local_service_command, async (error, stdout, stderr) => { + execFile("systemctl", [ "is-active", serviceName ], (error, stdout, stderr) => { + // systemctl is-active exits with 0 if the service is active, + // and a non-zero code if it is inactive, failed, or not found. if (error) { heartbeat.status = DOWN; - heartbeat.msg = `Error executing command: ${error.message}`; + // stderr often contains useful info like "service not found" + heartbeat.msg = stderr || stdout || `Service '${serviceName}' is not running.`; reject(new Error(heartbeat.msg)); return; } - const output = stdout.trim(); - - if (monitor.local_service_check_type === "keyword") { - if (monitor.local_service_expected_output) { - if (output.includes(monitor.local_service_expected_output)) { - heartbeat.status = UP; - heartbeat.msg = `OK - Output contains "${monitor.local_service_expected_output}"`; - resolve(); - } else { - heartbeat.status = DOWN; - heartbeat.msg = `Output did not contain "${monitor.local_service_expected_output}"`; - reject(new Error(heartbeat.msg)); - } - } else { - heartbeat.status = UP; - heartbeat.msg = "OK - Command executed successfully"; - resolve(); - } - } else if (monitor.local_service_check_type === "json-query") { - try { - const data = JSON.parse(output); - const { status, response } = await evaluateJsonQuery(data, monitor.jsonPath, monitor.jsonPathOperator, monitor.expectedValue); - - if (status) { - heartbeat.status = UP; - heartbeat.msg = `JSON query passes (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`; - resolve(); - } else { - throw new Error(`JSON query does not pass (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`); - } - } catch (e) { - heartbeat.status = DOWN; - heartbeat.msg = e.message; - reject(e); - } - } + // If there's no error, the service is running. + heartbeat.status = UP; + heartbeat.msg = `Service '${serviceName}' is running.`; + resolve(heartbeat); }); }); } diff --git a/server/server.js b/server/server.js index 104093201..27bf0b388 100644 --- a/server/server.js +++ b/server/server.js @@ -900,9 +900,7 @@ let needSetup = false; bean.rabbitmqPassword = monitor.rabbitmqPassword; bean.conditions = JSON.stringify(monitor.conditions); bean.manual_status = monitor.manual_status; - bean.local_service_command = monitor.local_service_command; - bean.local_service_expected_output = monitor.local_service_expected_output; - bean.local_service_check_type = monitor.local_service_check_type; + bean.local_service_name = monitor.local_service_name; // ping advanced options bean.ping_numeric = monitor.ping_numeric; diff --git a/src/lang/en.json b/src/lang/en.json index a4c745803..d981175c5 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1025,10 +1025,10 @@ "deleteRemoteBrowserMessage": "Are you sure want to delete this Remote Browser for all monitors?", "GrafanaOncallUrl": "Grafana Oncall URL", "Local Service": "Local Service", + "Service Name": "Service Name", + "localServiceDescription": "The name of the systemd service to check. Example: `nginx.service`", "Browser Screenshot": "Browser Screenshot", "Command": "Command", - "localServiceCommandDescription": "The command to execute. For example: `systemctl is-active mosquitto`", - "localServiceExpectedOutputDescription": "The expected output of the command. If the output contains this string, the monitor will be considered UP. Leave empty to only check the exit code.", "Expected Output": "Expected Output", "mongodbCommandDescription": "Run a MongoDB command against the database. For information about the available commands check out the {documentation}", "wayToGetSevenIOApiKey": "Visit the dashboard under app.seven.io > developer > api key > the green add button", diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index bf5cb422a..3c1a209f1 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -667,57 +667,10 @@ @@ -1418,9 +1371,7 @@ const monitorDefaults = { rabbitmqUsername: "", rabbitmqPassword: "", conditions: [], - local_service_command: "", - local_service_expected_output: "", - local_service_check_type: "", + local_service_name: "", }; export default { From 96e82689862bf356f366cc296e701c59719eea2a Mon Sep 17 00:00:00 2001 From: iotux Date: Mon, 15 Dec 2025 13:58:02 +0100 Subject: [PATCH 11/80] Minor local-service cleanup --- server/monitor-types/local-service.js | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/server/monitor-types/local-service.js b/server/monitor-types/local-service.js index 65183fda0..b15891ac0 100644 --- a/server/monitor-types/local-service.js +++ b/server/monitor-types/local-service.js @@ -9,40 +9,45 @@ class LocalServiceMonitorType extends MonitorType { /** * Check a local systemd service status. * Uses `systemctl is-running` to determine if the service is active. - * @param {object} monitor The monitor object containing serviceName. + * @param {object} monitor The monitor object containing monitor.local_service_name. * @param {object} heartbeat The heartbeat object to update. * @param {object} server The server object (unused in this specific check). * @returns {Promise} A promise that resolves with the updated heartbeat. - * @throws {Error} If the serviceName is invalid or the command execution fails. + * @throws {Error} If the monitor.local_service_name is invalid or the command execution fails. */ async check(monitor, heartbeat, server) { - // This is the name of the service to check e.g. "nginx.service" - const serviceName = monitor.local_service_name; - // Basic sanitization to prevent argument injection. // This regex allows for standard service names, including those with instances like "sshd@.service". - if (!serviceName || !/^[a-zA-Z0-9._\-@]+$/.test(serviceName)) { + if (!monitor.local_service_name || !/^[a-zA-Z0-9._\-@]+$/.test(monitor.local_service_name)) { heartbeat.status = DOWN; heartbeat.msg = "Invalid service name provided."; throw new Error(heartbeat.msg); } return new Promise((resolve, reject) => { - execFile("systemctl", [ "is-active", serviceName ], (error, stdout, stderr) => { + execFile("systemctl", [ "is-active", monitor.local_service_name ], (error, stdout, stderr) => { // systemctl is-active exits with 0 if the service is active, // and a non-zero code if it is inactive, failed, or not found. + // 1. Capture the raw output (prioritize stderr for errors) + let output = (stderr || stdout || "").toString().trim(); + + // 2. Truncate if too long to ~200 chars + if (output.length > 200) { + output = output.substring(0, 200) + "..."; + } + if (error) { - heartbeat.status = DOWN; // stderr often contains useful info like "service not found" - heartbeat.msg = stderr || stdout || `Service '${serviceName}' is not running.`; + // Use the truncated output, or a default fallback if empty + heartbeat.msg = stderr || stdout || `Service '${monitor.local_service_name}' is not running.`; reject(new Error(heartbeat.msg)); return; } // If there's no error, the service is running. heartbeat.status = UP; - heartbeat.msg = `Service '${serviceName}' is running.`; - resolve(heartbeat); + heartbeat.msg = `Service '${monitor.local_service_name}' is running.`; + resolve(); }); }); } From a2930204690ec71a20625d0302a5414acf797900 Mon Sep 17 00:00:00 2001 From: iotux Date: Mon, 15 Dec 2025 14:08:14 +0100 Subject: [PATCH 12/80] Removed leftover from debugging --- server/prometheus.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/prometheus.js b/server/prometheus.js index 19d4ac197..70daf8ce7 100644 --- a/server/prometheus.js +++ b/server/prometheus.js @@ -34,7 +34,6 @@ class Prometheus { * @returns {Promise} */ static async init() { - PrometheusClient.register.clear(); // Add all available tags as possible labels, // and use Set to remove possible duplicates (for when multiple tags contain non-ascii characters, and thus are sanitized to the same label) const tags = new Set((await R.findAll("tag")).map((tag) => { From 9a0b6a56dc1824953ec024e7282e78f3ae459966 Mon Sep 17 00:00:00 2001 From: iotux Date: Mon, 15 Dec 2025 14:26:15 +0100 Subject: [PATCH 13/80] Removed leftovers from testing/debugging --- server/server.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/server/server.js b/server/server.js index 27bf0b388..f68932322 100644 --- a/server/server.js +++ b/server/server.js @@ -1174,8 +1174,6 @@ let needSetup = false; bean.color = tag.color; await R.store(bean); - await Prometheus.init(); - callback({ ok: true, tag: await bean.toJSON(), @@ -1250,9 +1248,6 @@ let needSetup = false; monitorID, value, ]); - - await server.sendUpdateMonitorIntoList(socket, monitorID); - callback({ ok: true, msg: "successAdded", @@ -1276,9 +1271,6 @@ let needSetup = false; tagID, monitorID, ]); - - await server.sendUpdateMonitorIntoList(socket, monitorID); - callback({ ok: true, msg: "successEdited", @@ -1302,9 +1294,6 @@ let needSetup = false; monitorID, value, ]); - - await server.sendUpdateMonitorIntoList(socket, monitorID); - callback({ ok: true, msg: "successDeleted", From 7a0a85c1e2a109dbc7ee90aeaffb28b4e8c1e6e3 Mon Sep 17 00:00:00 2001 From: iotux Date: Mon, 15 Dec 2025 15:09:42 +0100 Subject: [PATCH 14/80] Minor i18n changes --- src/lang/en.json | 3 ++- src/pages/EditMonitor.vue | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/lang/en.json b/src/lang/en.json index d981175c5..c2d80a3e6 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1026,7 +1026,8 @@ "GrafanaOncallUrl": "Grafana Oncall URL", "Local Service": "Local Service", "Service Name": "Service Name", - "localServiceDescription": "The name of the systemd service to check. Example: `nginx.service`", + "localServiceDescription": "The internal service name used by the OS (systemd for Linux, Service Name for Windows).", + "localServiceDebugHelp": "To debug, run this on your server: {linuxCommand} (Linux) or {windowsCommand} (Windows PowerShell). The command must return 'active' or 'Running'.", "Browser Screenshot": "Browser Screenshot", "Command": "Command", "Expected Output": "Expected Output", diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 3c1a209f1..b078a95da 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -671,6 +671,18 @@
{{ $t("localServiceDescription") }} + +
+ + + + + +
From 9befa8d4b62a26b33178dc07e4857dc18aa14b7c Mon Sep 17 00:00:00 2001 From: iotux Date: Mon, 15 Dec 2025 15:14:31 +0100 Subject: [PATCH 15/80] Minor i18n changes --- src/pages/EditMonitor.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index b078a95da..b5459ae78 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -671,7 +671,7 @@
{{ $t("localServiceDescription") }} - +
-