mirror of
https://github.com/louislam/uptime-kuma.git
synced 2026-08-07 10:14:59 +00:00
wip
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.alterTable("monitor", function (table) {
|
||||
table.text("config", "longtext").defaultTo("{}").notNullable();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.alterTable("monitor", function (table) {
|
||||
table.dropColumn("config");
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
exports.up = async function (knex) {
|
||||
// Migrate system_service_name data into config JSON
|
||||
const monitors = await knex("monitor").whereNotNull("system_service_name").select("id", "system_service_name", "config");
|
||||
|
||||
for (const monitor of monitors) {
|
||||
let config = {};
|
||||
|
||||
if (monitor.config) {
|
||||
try {
|
||||
config = JSON.parse(monitor.config);
|
||||
} catch (e) {
|
||||
config = {};
|
||||
}
|
||||
}
|
||||
|
||||
config.system_service_name = monitor.system_service_name;
|
||||
|
||||
await knex("monitor").where("id", monitor.id).update({
|
||||
config: JSON.stringify(config),
|
||||
});
|
||||
}
|
||||
|
||||
// Drop the column
|
||||
await knex.schema.alterTable("monitor", function (table) {
|
||||
table.dropColumn("system_service_name");
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = async function (knex) {
|
||||
// Re-add the column
|
||||
await knex.schema.alterTable("monitor", function (table) {
|
||||
table.string("system_service_name");
|
||||
});
|
||||
|
||||
// Migrate data back from config to column
|
||||
const monitors = await knex("monitor").whereNotNull("config").select("id", "config");
|
||||
|
||||
for (const monitor of monitors) {
|
||||
try {
|
||||
const config = JSON.parse(monitor.config);
|
||||
if (config.system_service_name !== undefined) {
|
||||
await knex("monitor").where("id", monitor.id).update({
|
||||
system_service_name: config.system_service_name,
|
||||
});
|
||||
|
||||
delete config.system_service_name;
|
||||
await knex("monitor").where("id", monitor.id).update({
|
||||
config: JSON.stringify(config),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore parse errors
|
||||
}
|
||||
}
|
||||
};
|
||||
+50
-1
@@ -186,7 +186,6 @@ class Monitor extends BeanModel {
|
||||
httpBodyEncoding: this.httpBodyEncoding,
|
||||
jsonPath: this.jsonPath,
|
||||
expectedValue: this.expectedValue,
|
||||
system_service_name: this.system_service_name,
|
||||
kafkaProducerTopic: this.kafkaProducerTopic,
|
||||
kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers),
|
||||
kafkaProducerSsl: this.getKafkaProducerSsl(),
|
||||
@@ -213,6 +212,9 @@ class Monitor extends BeanModel {
|
||||
saveResponse: this.getSaveResponse(),
|
||||
saveErrorResponse: this.getSaveErrorResponse(),
|
||||
responseMaxLength: this.response_max_length ?? RESPONSE_BODY_LENGTH_DEFAULT,
|
||||
|
||||
// extensible config for new monitor type settings
|
||||
config: this.getConfig(),
|
||||
};
|
||||
|
||||
if (includeSensitiveData) {
|
||||
@@ -403,6 +405,53 @@ class Monitor extends BeanModel {
|
||||
return Boolean(this.save_error_response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full config
|
||||
* @returns {object} Parsed config, empty object if null
|
||||
*/
|
||||
getConfig() {
|
||||
if (this.config) {
|
||||
return JSON.parse(this.config);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the full config object
|
||||
* @param {object} config The config object to store
|
||||
* @returns {void}
|
||||
*/
|
||||
setConfig(config) {
|
||||
this.config = JSON.stringify(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value from the config
|
||||
*
|
||||
* PS: if you want to get multiple fields at the same time, use getConfig() to avoid multiple JSON.parse/stringify calls.
|
||||
* @param {string} key The config key to retrieve
|
||||
* @param {*} defaultValue Default value if key doesn't exist
|
||||
* @returns {*} The config value or defaultValue
|
||||
*/
|
||||
getConfigValue(key, defaultValue = undefined) {
|
||||
const config = this.getConfig();
|
||||
return key in config ? config[key] : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value in the config
|
||||
*
|
||||
* PS: If update multiple fields at the same time, use setConfig() instead to avoid multiple JSON.parse/stringify calls.
|
||||
* @param {string} key The config key to set
|
||||
* @param {*} value The value to store
|
||||
* @returns {void}
|
||||
*/
|
||||
setConfigValue(key, value) {
|
||||
const config = this.getConfig();
|
||||
config[key] = value;
|
||||
this.setConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitor
|
||||
* @param {Server} io Socket server instance
|
||||
|
||||
@@ -15,14 +15,16 @@ class SystemServiceMonitorType extends MonitorType {
|
||||
* @returns {Promise<void>} Resolves when check is complete.
|
||||
*/
|
||||
async check(monitor, heartbeat) {
|
||||
if (!monitor.system_service_name) {
|
||||
const serviceName = monitor.getConfigValue("system_service_name");
|
||||
|
||||
if (!serviceName) {
|
||||
throw new Error("Service Name is required.");
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
return this.checkWindows(monitor.system_service_name, heartbeat);
|
||||
return this.checkWindows(serviceName, heartbeat);
|
||||
} else if (process.platform === "linux") {
|
||||
return this.checkLinux(monitor.system_service_name, heartbeat);
|
||||
return this.checkLinux(serviceName, heartbeat);
|
||||
} else {
|
||||
throw new Error(`System Service monitoring is not supported on ${process.platform}`);
|
||||
}
|
||||
|
||||
+2
-1
@@ -743,6 +743,7 @@ let needSetup = false;
|
||||
monitor.conditions = JSON.stringify(monitor.conditions);
|
||||
|
||||
monitor.rabbitmqNodes = JSON.stringify(monitor.rabbitmqNodes);
|
||||
monitor.config = JSON.stringify(monitor.config || {});
|
||||
|
||||
/*
|
||||
* List of frontend-only properties that should not be saved to the database.
|
||||
@@ -931,7 +932,7 @@ let needSetup = false;
|
||||
bean.rabbitmqPassword = monitor.rabbitmqPassword;
|
||||
bean.conditions = JSON.stringify(monitor.conditions);
|
||||
bean.manual_status = monitor.manual_status;
|
||||
bean.system_service_name = monitor.system_service_name;
|
||||
bean.setConfigValue("system_service_name", monitor.system_service_name);
|
||||
bean.expected_tls_alert = monitor.expectedTlsAlert;
|
||||
|
||||
// ping advanced options
|
||||
|
||||
+11
-11
@@ -1245,7 +1245,7 @@
|
||||
<label for="system-service-name" class="form-label">{{ $t("Service Name") }}</label>
|
||||
<input
|
||||
id="system-service-name"
|
||||
v-model="monitor.system_service_name"
|
||||
v-model="monitor.config.system_service_name"
|
||||
type="text"
|
||||
class="form-control"
|
||||
required
|
||||
@@ -1256,29 +1256,29 @@
|
||||
<template v-if="$root.info.runtime.platform === 'linux'">
|
||||
{{
|
||||
$t("systemServiceDescriptionLinux", {
|
||||
service_name: monitor.system_service_name || "nginx",
|
||||
service_name: monitor.config.system_service_name || "nginx",
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
<template v-else-if="$root.info.runtime.platform === 'win32'">
|
||||
{{
|
||||
$t("systemServiceDescriptionWindows", {
|
||||
service_name: monitor.system_service_name || "Dnscache",
|
||||
service_name: monitor.config.system_service_name || "Dnscache",
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{
|
||||
$t("systemServiceDescription", {
|
||||
service_name: monitor.system_service_name || "nginx",
|
||||
service_name: monitor.config.system_service_name || "nginx",
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="
|
||||
!monitor.system_service_name ||
|
||||
/^[a-zA-Z0-9_\-\.\@\ ]+$/.test(monitor.system_service_name)
|
||||
!monitor.config.system_service_name ||
|
||||
/^[a-zA-Z0-9_\-\.\@\ ]+$/.test(monitor.config.system_service_name)
|
||||
"
|
||||
>
|
||||
<div v-if="$root.info.runtime.platform === 'linux'" class="mt-2">
|
||||
@@ -1287,7 +1287,7 @@
|
||||
<template #command>
|
||||
<code>
|
||||
systemctl is-active
|
||||
{{ monitor.system_service_name || "nginx" }}
|
||||
{{ monitor.config.system_service_name || "nginx" }}
|
||||
</code>
|
||||
</template>
|
||||
</i18n-t>
|
||||
@@ -1303,7 +1303,7 @@
|
||||
<code>
|
||||
(Get-Service -Name '{{
|
||||
(
|
||||
monitor.system_service_name || "Dnscache"
|
||||
monitor.config.system_service_name || "Dnscache"
|
||||
).replaceAll("'", "''")
|
||||
}}').Status
|
||||
</code>
|
||||
@@ -3188,7 +3188,7 @@ const monitorDefaults = {
|
||||
rabbitmqUsername: "",
|
||||
rabbitmqPassword: "",
|
||||
conditions: [],
|
||||
system_service_name: "",
|
||||
config: {},
|
||||
};
|
||||
|
||||
export default {
|
||||
@@ -3259,8 +3259,8 @@ export default {
|
||||
if (this.monitor.hostname) {
|
||||
return this.monitor.hostname;
|
||||
}
|
||||
if (this.monitor.system_service_name) {
|
||||
return this.monitor.system_service_name;
|
||||
if (this.monitor.config?.system_service_name) {
|
||||
return this.monitor.config.system_service_name;
|
||||
}
|
||||
if (this.monitor.url) {
|
||||
if (this.monitor.url !== "http://" && this.monitor.url !== "https://") {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { describe, test, beforeEach, afterEach } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { SystemServiceMonitorType } = require("../../server/monitor-types/system-service");
|
||||
const { Monitor } = require("../../server/model/monitor");
|
||||
const { DOWN, UP } = require("../../src/util");
|
||||
const process = require("process");
|
||||
const { execSync } = require("node:child_process");
|
||||
@@ -27,6 +28,21 @@ function shouldSkip() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock monitor with config support
|
||||
* @param {string} serviceName The system service name
|
||||
* @returns {object} Mock monitor object
|
||||
*/
|
||||
function createMockMonitor(serviceName) {
|
||||
return Object.create(Monitor.prototype, {
|
||||
config: {
|
||||
value: JSON.stringify({ system_service_name: serviceName }),
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("SystemServiceMonitorType", { skip: shouldSkip() }, () => {
|
||||
let monitorType;
|
||||
let heartbeat;
|
||||
@@ -52,9 +68,7 @@ describe("SystemServiceMonitorType", { skip: shouldSkip() }, () => {
|
||||
// Linux: 'dbus' or 'cron' are standard services.
|
||||
const serviceName = process.platform === "win32" ? "Dnscache" : "dbus";
|
||||
|
||||
const monitor = {
|
||||
system_service_name: serviceName,
|
||||
};
|
||||
const monitor = createMockMonitor(serviceName);
|
||||
|
||||
await monitorType.check(monitor, heartbeat);
|
||||
|
||||
@@ -63,9 +77,7 @@ describe("SystemServiceMonitorType", { skip: shouldSkip() }, () => {
|
||||
});
|
||||
|
||||
test("check() returns DOWN for a stopped service", async () => {
|
||||
const monitor = {
|
||||
system_service_name: "non-existent-service-12345",
|
||||
};
|
||||
const monitor = createMockMonitor("non-existent-service-12345");
|
||||
|
||||
// Query a non-existent service to force an error/down state.
|
||||
// We pass the promise directly to assert.rejects, avoiding unnecessary async wrappers.
|
||||
@@ -81,9 +93,7 @@ describe("SystemServiceMonitorType", { skip: shouldSkip() }, () => {
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const monitor = {
|
||||
system_service_name: "invalid&service;name",
|
||||
};
|
||||
const monitor = createMockMonitor("invalid&service;name");
|
||||
|
||||
// Expected validation error
|
||||
await assert.rejects(monitorType.check(monitor, heartbeat));
|
||||
@@ -98,9 +108,7 @@ describe("SystemServiceMonitorType", { skip: shouldSkip() }, () => {
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const monitor = {
|
||||
system_service_name: "test-service",
|
||||
};
|
||||
const monitor = createMockMonitor("test-service");
|
||||
|
||||
await assert.rejects(monitorType.check(monitor, heartbeat), /not supported/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user