diff --git a/db/knex_migrations/2024-10-1315-rabbitmq-monitor.js b/db/knex_migrations/2024-10-1315-rabbitmq-monitor.js new file mode 100644 index 000000000..6a17f3366 --- /dev/null +++ b/db/knex_migrations/2024-10-1315-rabbitmq-monitor.js @@ -0,0 +1,17 @@ +exports.up = function (knex) { + return knex.schema.alterTable("monitor", function (table) { + table.text("rabbitmq_nodes"); + table.string("rabbitmq_username"); + table.string("rabbitmq_password"); + }); + +}; + +exports.down = function (knex) { + return knex.schema.alterTable("monitor", function (table) { + table.dropColumn("rabbitmq_nodes"); + table.dropColumn("rabbitmq_username"); + table.dropColumn("rabbitmq_password"); + }); + +}; diff --git a/server/model/monitor.js b/server/model/monitor.js index 5b7e5871a..3031bf701 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -153,6 +153,7 @@ class Monitor extends BeanModel { snmpOid: this.snmpOid, jsonPathOperator: this.jsonPathOperator, snmpVersion: this.snmpVersion, + rabbitmqNodes: JSON.parse(this.rabbitmqNodes), conditions: JSON.parse(this.conditions), }; @@ -183,6 +184,8 @@ class Monitor extends BeanModel { tlsCert: this.tlsCert, tlsKey: this.tlsKey, kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions), + rabbitmqUsername: this.rabbitmqUsername, + rabbitmqPassword: this.rabbitmqPassword, }; } diff --git a/server/monitor-types/rabbitmq.js b/server/monitor-types/rabbitmq.js new file mode 100644 index 000000000..a4a47ce75 --- /dev/null +++ b/server/monitor-types/rabbitmq.js @@ -0,0 +1,74 @@ +const { MonitorType } = require("./monitor-type"); +const { log, UP, DOWN } = require("../../src/util"); +const { axiosAbortSignal } = require("../util-server"); +const axios = require("axios"); + +class RabbitMqMonitorType extends MonitorType { + name = "rabbitmq"; + + /** + * @inheritdoc + */ + async check(monitor, heartbeat, server) { + // HTTP basic auth + let basicAuthHeader = {}; + basicAuthHeader = { + "Authorization": "Basic " + this.encodeBase64(monitor.rabbitmqUsername, monitor.rabbitmqPassword), + }; + + let status = DOWN; + let msg = ""; + + for (const baseUrl of JSON.parse(monitor.rabbitmqNodes)) { + try { + const options = { + url: new URL("/api/health/checks/alarms", baseUrl).href, + method: "get", + timeout: monitor.timeout * 1000, + headers: { + "Accept": "application/json", + ...(basicAuthHeader), + }, + signal: axiosAbortSignal((monitor.timeout + 10) * 1000), + validateStatus: () => true, + }; + log.debug("monitor", `[${monitor.name}] Axios Request: ${JSON.stringify(options)}`); + const res = await axios.request(options); + log.debug("monitor", `[${monitor.name}] Axios Response: status=${res.status} body=${JSON.stringify(res.data)}`); + if (res.status === 200) { + status = UP; + msg = "OK"; + break; + } else { + msg = `${res.status} - ${res.statusText}`; + } + } catch (error) { + if (axios.isCancel(error)) { + msg = "Request timed out"; + log.debug("monitor", `[${monitor.name}] Request timed out`); + } else { + log.debug("monitor", `[${monitor.name}] Axios Error: ${JSON.stringify(error.message)}`); + msg = error.message; + } + } + } + + heartbeat.msg = msg; + heartbeat.status = status; + } + + /** + * Encode user and password to Base64 encoding + * for HTTP "basic" auth, as per RFC-7617 + * @param {string|null} user - The username (nullable if not changed by a user) + * @param {string|null} pass - The password (nullable if not changed by a user) + * @returns {string} Encoded Base64 string + */ + encodeBase64(user, pass) { + return Buffer.from(`${user || ""}:${pass || ""}`).toString("base64"); + } +} + +module.exports = { + RabbitMqMonitorType, +}; diff --git a/server/server.js b/server/server.js index db58ae829..c88daca88 100644 --- a/server/server.js +++ b/server/server.js @@ -718,6 +718,8 @@ let needSetup = false; monitor.conditions = JSON.stringify(monitor.conditions); + monitor.rabbitmqNodes = JSON.stringify(monitor.rabbitmqNodes); + bean.import(monitor); bean.user_id = socket.userID; @@ -868,6 +870,9 @@ let needSetup = false; bean.snmpOid = monitor.snmpOid; bean.jsonPathOperator = monitor.jsonPathOperator; bean.timeout = monitor.timeout; + bean.rabbitmqNodes = JSON.stringify(monitor.rabbitmqNodes); + bean.rabbitmqUsername = monitor.rabbitmqUsername; + bean.rabbitmqPassword = monitor.rabbitmqPassword; bean.conditions = JSON.stringify(monitor.conditions); bean.validate(); diff --git a/server/uptime-kuma-server.js b/server/uptime-kuma-server.js index 76bf42565..062f098d7 100644 --- a/server/uptime-kuma-server.js +++ b/server/uptime-kuma-server.js @@ -115,6 +115,7 @@ class UptimeKumaServer { UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType(); UptimeKumaServer.monitorTypeList["snmp"] = new SNMPMonitorType(); UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType(); + UptimeKumaServer.monitorTypeList["rabbitmq"] = new RabbitMqMonitorType(); // Allow all CORS origins (polling) in development let cors = undefined; @@ -552,4 +553,5 @@ const { DnsMonitorType } = require("./monitor-types/dns"); const { MqttMonitorType } = require("./monitor-types/mqtt"); const { SNMPMonitorType } = require("./monitor-types/snmp"); const { MongodbMonitorType } = require("./monitor-types/mongodb"); +const { RabbitMqMonitorType } = require("./monitor-types/rabbitmq"); const Monitor = require("./model/monitor"); diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 5d999b597..c8a05a82b 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -64,6 +64,9 @@ + @@ -233,6 +236,40 @@ + +
@@ -549,7 +586,7 @@
-
+
@@ -1122,6 +1159,9 @@ const monitorDefaults = { kafkaProducerAllowAutoTopicCreation: false, gamedigGivenPortOnly: true, remote_browser: null, + rabbitmqNodes: [], + rabbitmqUsername: "", + rabbitmqPassword: "", conditions: [] }; @@ -1709,6 +1749,10 @@ message HealthCheckResponse { this.monitor.kafkaProducerBrokers.push(newBroker); }, + addRabbitmqNode(newNode) { + this.monitor.rabbitmqNodes.push(newNode); + }, + /** * Validate form input * @returns {boolean} Is the form input valid?