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.
This commit is contained in:
iotux
2025-12-14 16:39:00 +01:00
parent 883083f5c3
commit 7461bd296f
8 changed files with 225 additions and 39 deletions
+3
View File
@@ -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,
};
}
+62
View File
@@ -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,
};
+11
View File
@@ -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",
+2
View File
@@ -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");