Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add RabbitMQ monitor #5199

Merged
merged 23 commits into from
Oct 20, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2879b53
Add Rabbitmq monitor
Suven-p Oct 15, 2024
eeb614e
Merge branch 'master' into 5066_add_rabbitmq_support
Suven-p Oct 15, 2024
4c47d2f
Add translation string
Suven-p Oct 16, 2024
1c02023
Add description and validation for nodes input field
Suven-p Oct 16, 2024
74da7e5
Merge branch '5066_add_rabbitmq_support' of https://github.com/Suven-…
Suven-p Oct 16, 2024
4fa640b
Update messages
Suven-p Oct 16, 2024
44cd675
Replace input type password with HiddenInput
Suven-p Oct 16, 2024
009b004
Make requested changes
Suven-p Oct 18, 2024
fd3dbff
Merge branch 'master' into 5066_add_rabbitmq_support
Suven-p Oct 18, 2024
21a2d3c
Change monitor type to specify management plugin
Suven-p Oct 18, 2024
0ba3773
Bugfix: Correct validation for rabbitmq nodes
Suven-p Oct 19, 2024
685043e
Add test containers
Suven-p Oct 19, 2024
7961ebb
Add translation for RabbitMQ monitor type
Suven-p Oct 19, 2024
bf32550
Fix linting error
Suven-p Oct 20, 2024
73ee628
Bugfix: Add support for base url with path
Suven-p Oct 20, 2024
444661e
Save message for 503 status code
Suven-p Oct 20, 2024
6380df9
Fix relative path in test
Suven-p Oct 20, 2024
fc68042
Removed unnessesary comment
CommanderStorm Oct 20, 2024
7ce46d6
Update src/lang/en.json
Suven-p Oct 20, 2024
2a2b840
Add helptext
Suven-p Oct 20, 2024
85f5552
Remove helptext from monitor type
Suven-p Oct 20, 2024
d1ad668
Apply suggestions from code review
CommanderStorm Oct 20, 2024
9f23b5c
Merge branch 'master' into 5066_add_rabbitmq_support
Suven-p Oct 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions db/knex_migrations/2024-10-1315-rabbitmq-monitor.js
Original file line number Diff line number Diff line change
@@ -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");
});

};
3 changes: 3 additions & 0 deletions server/model/monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};

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

Expand Down
74 changes: 74 additions & 0 deletions server/monitor-types/rabbitmq.js
Original file line number Diff line number Diff line change
@@ -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),
};
Suven-p marked this conversation as resolved.
Show resolved Hide resolved

let status = DOWN;
let msg = "";
Suven-p marked this conversation as resolved.
Show resolved Hide resolved

for (const baseUrl of JSON.parse(monitor.rabbitmqNodes)) {
Suven-p marked this conversation as resolved.
Show resolved Hide resolved
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,
Suven-p marked this conversation as resolved.
Show resolved Hide resolved
};
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) {
Suven-p marked this conversation as resolved.
Show resolved Hide resolved
return Buffer.from(`${user || ""}:${pass || ""}`).toString("base64");
}
}

module.exports = {
RabbitMqMonitorType,
};
5 changes: 5 additions & 0 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions server/uptime-kuma-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
6 changes: 5 additions & 1 deletion src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1051,5 +1051,9 @@
"From":"From",
"Can be found on:": "Can be found on: {0}",
"The phone number of the recipient in E.164 format.": "The phone number of the recipient in E.164 format.",
"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.":"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies."
"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.":"Either a text sender ID or a phone number in E.164 format if you want to be able to receive replies.",
"RabbitMQ Nodes": "RabbitMQ Management Nodes",
"rabbitmqNodesDescription": "Enter the URL for the RabbitMQ management nodes including protocol and port. Example: https://node1.rabbitmq.com:15672",
Suven-p marked this conversation as resolved.
Show resolved Hide resolved
"rabbitmqNodesRequired": "Please set the nodes for this monitor.",
"rabbitmqNodesInvalid": "Please use complete URL for RabbitMQ nodes."
Suven-p marked this conversation as resolved.
Show resolved Hide resolved
}
60 changes: 59 additions & 1 deletion src/pages/EditMonitor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@
<option value="mqtt">
MQTT
</option>
<option value="rabbitmq">
RabbitMQ
</option>
<option value="kafka-producer">
Kafka Producer
</option>
Expand Down Expand Up @@ -233,6 +236,43 @@
</div>
</template>

<template v-if="monitor.type === 'rabbitmq'">
<!-- RabbitMQ Nodes List -->
<div class="my-3">
<label for="rabbitmqNodes" class="form-label">{{ $t("RabbitMQ Nodes") }}</label>
<VueMultiselect
id="rabbitmqNodes"
v-model="monitor.rabbitmqNodes"
:required="true"
:multiple="true"
:options="[]"
:placeholder="$t('Enter the list of nodes')"
:tag-placeholder="$t('Press Enter to add node')"
:max-height="500"
:taggable="true"
:show-no-options="false"
:close-on-select="false"
:clear-on-select="false"
:preserve-search="false"
:preselect-first="false"
@tag="addRabbitmqNode"
></VueMultiselect>
<div class="form-text">
{{ $t("rabbitmqNodesDescription") }}
</div>
</div>

<div class="my-3">
<label for="rabbitmqUsername" class="form-label">RabbitMQ {{ $t("Username") }}</label>
Suven-p marked this conversation as resolved.
Show resolved Hide resolved
<input id="rabbitmqUsername" v-model="monitor.rabbitmqUsername" type="text" required class="form-control">
</div>

<div class="my-3">
<label for="rabbitmqPassword" class="form-label">RabbitMQ {{ $t("Password") }}</label>
<HiddenInput id="rabbitmqPassword" v-model="monitor.rabbitmqPassword" autocomplete="false" required="true"></HiddenInput>
</div>
</template>

<!-- Hostname -->
<!-- TCP Port / Ping / DNS / Steam / MQTT / Radius / Tailscale Ping / SNMP only -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' || monitor.type === 'steam' || monitor.type === 'gamedig' || monitor.type === 'mqtt' || monitor.type === 'radius' || monitor.type === 'tailscale-ping' || monitor.type === 'snmp'" class="my-3">
Expand Down Expand Up @@ -549,7 +589,7 @@
</div>

<!-- Timeout: HTTP / Keyword / SNMP only -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'snmp'" class="my-3">
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'snmp' || monitor.type === 'rabbitmq'" class="my-3">
<label for="timeout" class="form-label">{{ $t("Request Timeout") }} ({{ $t("timeoutAfter", [ monitor.timeout || clampTimeout(monitor.interval) ]) }})</label>
<input id="timeout" v-model="monitor.timeout" type="number" class="form-control" required min="0" step="0.1">
</div>
Expand Down Expand Up @@ -1122,6 +1162,9 @@ const monitorDefaults = {
kafkaProducerAllowAutoTopicCreation: false,
gamedigGivenPortOnly: true,
remote_browser: null,
rabbitmqNodes: [],
rabbitmqUsername: "",
rabbitmqPassword: "",
conditions: []
};

Expand Down Expand Up @@ -1709,6 +1752,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?
Expand Down Expand Up @@ -1736,6 +1783,17 @@ message HealthCheckResponse {
return false;
}
}

if (this.monitor.type === "rabbitmq") {
if (this.monitor.rabbitmqNodes.length === 0) {
toast.error(this.$t("rabbitmqNodesRequired"));
return false;
}
if (!this.monitor.rabbitmqNodes.every(node => node.startsWith("http://" || node.startsWith("https://")))) {
toast.error(this.$t("rabbitmqNodesInvalid"));
return false;
}
}
return true;
},

Expand Down
Loading