Simplified and secured Local Service monitor

pull/6488/head
iotux 2 months ago
parent 2ffc06d950
commit d76ce4e28d

@ -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<void>}
*/
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");
});
};

@ -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,
};
}

@ -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<object>} 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);
});
});
}

@ -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;

@ -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",

@ -667,57 +667,10 @@
<template v-if="monitor.type === 'local-service'">
<div class="my-3">
<label for="local-service-command" class="form-label">{{ $t("Command") }}</label>
<input id="local-service-command" v-model="monitor.local_service_command" type="text" class="form-control" required>
<label for="local-service-name" class="form-label">{{ $t("Service Name") }}</label>
<input id="local-service-name" v-model="monitor.local_service_name" type="text" class="form-control" required placeholder="nginx.service">
<div class="form-text">
{{ $t("localServiceCommandDescription") }}
</div>
</div>
<div class="my-3">
<label for="local-service-check-type" class="form-label">{{ $t("Check Type") }}</label>
<select id="local-service-check-type" v-model="monitor.local_service_check_type" class="form-select" required>
<option value="keyword">{{ $t("Keyword") }}</option>
<option value="json-query">{{ $t("Json Query") }}</option>
</select>
</div>
<div v-if="monitor.local_service_check_type === 'keyword'" class="my-3">
<label for="local-service-expected-output" class="form-label">{{ $t("Expected Value") }}</label>
<input id="local-service-expected-output" v-model="monitor.local_service_expected_output" type="text" class="form-control">
<div class="form-text">
{{ $t("localServiceExpectedOutputDescription") }}
</div>
</div>
<div v-if="monitor.local_service_check_type === 'json-query'" class="my-3">
<div class="my-2">
<label for="jsonPath" class="form-label mb-0">{{ $t("Json Query Expression") }}</label>
<i18n-t tag="div" class="form-text mb-2" keypath="jsonQueryDescription">
<a href="https://jsonata.org/" target="_blank" rel="noopener noreferrer">jsonata.org</a>
<a href="https://try.jsonata.org/" target="_blank" rel="noopener noreferrer">{{ $t('playground') }}</a>
</i18n-t>
<input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control" placeholder="$" required>
</div>
<div class="d-flex align-items-start">
<div class="me-2">
<label for="json_path_operator" class="form-label">{{ $t("Condition") }}</label>
<select id="json_path_operator" v-model="monitor.jsonPathOperator" class="form-select me-3" required>
<option value=">">&gt;</option>
<option value=">=">&gt;=</option>
<option value="<">&lt;</option>
<option value="<=">&lt;=</option>
<option value="!=">&#33;=</option>
<option value="==">==</option>
<option value="contains">contains</option>
</select>
</div>
<div class="flex-grow-1">
<label for="expectedValue" class="form-label">{{ $t("Expected Value") }}</label>
<input v-if="monitor.jsonPathOperator !== 'contains' && monitor.jsonPathOperator !== '==' && monitor.jsonPathOperator !== '!='" id="expectedValue" v-model="monitor.expectedValue" type="number" class="form-control" required step=".01">
<input v-else id="expectedValue" v-model="monitor.expectedValue" type="text" class="form-control" required>
</div>
{{ $t("localServiceDescription") }}
</div>
</div>
</template>
@ -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 {

Loading…
Cancel
Save