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 {