diff --git a/db/patch-add-parent-monitor.sql b/db/patch-add-parent-monitor.sql new file mode 100644 index 000000000..756ac5be2 --- /dev/null +++ b/db/patch-add-parent-monitor.sql @@ -0,0 +1,6 @@ +BEGIN TRANSACTION; + +ALTER TABLE monitor + ADD parent INTEGER REFERENCES [monitor] ([id]) ON DELETE SET NULL ON UPDATE CASCADE; + +COMMIT diff --git a/server/database.js b/server/database.js index c31e07c8a..c02c70c69 100644 --- a/server/database.js +++ b/server/database.js @@ -69,6 +69,7 @@ class Database { "patch-api-key-table.sql": true, "patch-monitor-tls.sql": true, "patch-maintenance-cron.sql": true, + "patch-add-parent-monitor.sql": true, }; /** diff --git a/server/model/monitor.js b/server/model/monitor.js index adeb35a04..2dfe2e65f 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -74,13 +74,17 @@ class Monitor extends BeanModel { id: this.id, name: this.name, description: this.description, + pathName: await this.getPathName(), + parent: this.parent, + childrenIDs: await Monitor.getAllChildrenIDs(this.id), url: this.url, method: this.method, hostname: this.hostname, port: this.port, maxretries: this.maxretries, weight: this.weight, - active: this.active, + active: await this.isActive(), + forceInactive: !await Monitor.isParentActive(this.id), type: this.type, interval: this.interval, retryInterval: this.retryInterval, @@ -144,6 +148,16 @@ class Monitor extends BeanModel { return data; } + /** + * Checks if the monitor is active based on itself and its parents + * @returns {Promise} + */ + async isActive() { + const parentActive = await Monitor.isParentActive(this.id); + + return this.active && parentActive; + } + /** * Get all tags applied to this monitor * @returns {Promise[]>} @@ -259,6 +273,36 @@ class Monitor extends BeanModel { if (await Monitor.isUnderMaintenance(this.id)) { bean.msg = "Monitor under maintenance"; bean.status = MAINTENANCE; + } else if (this.type === "group") { + const children = await Monitor.getChildren(this.id); + + if (children.length > 0) { + bean.status = UP; + bean.msg = "All children up and running"; + for (const child of children) { + if (!child.active) { + // Ignore inactive childs + continue; + } + const lastBeat = await Monitor.getPreviousHeartbeat(child.id); + + // Only change state if the monitor is in worse conditions then the ones before + if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) { + bean.status = lastBeat.status; + } else if (bean.status === PENDING && lastBeat.status === DOWN) { + bean.status = lastBeat.status; + } + } + + if (bean.status !== UP) { + bean.msg = "Child inaccessible"; + } + } else { + // Set status pending if group is empty + bean.status = PENDING; + bean.msg = "Group empty"; + } + } else if (this.type === "http" || this.type === "keyword") { // Do not do any queries/high loading things before the "bean.ping" let startTime = dayjs().valueOf(); @@ -1329,6 +1373,11 @@ class Monitor extends BeanModel { } } + const parent = await Monitor.getParent(monitorID); + if (parent != null) { + return await Monitor.isUnderMaintenance(parent.id); + } + return false; } @@ -1341,6 +1390,94 @@ class Monitor extends BeanModel { throw new Error(`Interval cannot be less than ${MIN_INTERVAL_SECOND} seconds`); } } + + /** + * Gets Parent of the monitor + * @param {number} monitorID ID of monitor to get + * @returns {Promise>} + */ + static async getParent(monitorID) { + return await R.getRow(` + SELECT parent.* FROM monitor parent + LEFT JOIN monitor child + ON child.parent = parent.id + WHERE child.id = ? + `, [ + monitorID, + ]); + } + + /** + * Gets all Children of the monitor + * @param {number} monitorID ID of monitor to get + * @returns {Promise>} + */ + static async getChildren(monitorID) { + return await R.getAll(` + SELECT * FROM monitor + WHERE parent = ? + `, [ + monitorID, + ]); + } + + /** + * Gets Full Path-Name (Groups and Name) + * @returns {Promise} + */ + async getPathName() { + let path = this.name; + + if (this.parent === null) { + return path; + } + + let parent = await Monitor.getParent(this.id); + while (parent !== null) { + path = `${parent.name} / ${path}`; + parent = await Monitor.getParent(parent.id); + } + + return path; + } + + /** + * Gets recursive all child ids + * @param {number} monitorID ID of the monitor to get + * @returns {Promise} + */ + static async getAllChildrenIDs(monitorID) { + const childs = await Monitor.getChildren(monitorID); + + if (childs === null) { + return []; + } + + let childrenIDs = []; + + for (const child of childs) { + childrenIDs.push(child.id); + childrenIDs = childrenIDs.concat(await Monitor.getAllChildrenIDs(child.id)); + } + + return childrenIDs; + } + + /** + * Checks recursive if parent (ancestors) are active + * @param {number} monitorID ID of the monitor to get + * @returns {Promise} + */ + static async isParentActive(monitorID) { + const parent = await Monitor.getParent(monitorID); + + if (parent === null) { + return true; + } + + const parentActive = await Monitor.isParentActive(parent.id); + return parent.active && parentActive; + } } module.exports = Monitor; diff --git a/server/server.js b/server/server.js index ca046a017..5e7aaf04f 100644 --- a/server/server.js +++ b/server/server.js @@ -684,8 +684,17 @@ let needSetup = false; throw new Error("Permission denied."); } + // Check if Parent is Decendant (would cause endless loop) + if (monitor.parent !== null) { + const childIDs = await Monitor.getAllChildrenIDs(monitor.id); + if (childIDs.includes(monitor.parent)) { + throw new Error("Invalid Monitor Group"); + } + } + bean.name = monitor.name; bean.description = monitor.description; + bean.parent = monitor.parent; bean.type = monitor.type; bean.url = monitor.url; bean.method = monitor.method; @@ -745,7 +754,7 @@ let needSetup = false; await updateMonitorNotification(bean.id, monitor.notificationIDList); - if (bean.active) { + if (bean.isActive()) { await restartMonitor(socket.userID, bean.id); } diff --git a/server/socket-handlers/maintenance-socket-handler.js b/server/socket-handlers/maintenance-socket-handler.js index 160a62603..ff5bb0fcf 100644 --- a/server/socket-handlers/maintenance-socket-handler.js +++ b/server/socket-handlers/maintenance-socket-handler.js @@ -186,7 +186,7 @@ module.exports.maintenanceSocketHandler = (socket) => { log.debug("maintenance", `Get Monitors for Maintenance: ${maintenanceID} User ID: ${socket.userID}`); - let monitors = await R.getAll("SELECT monitor.id, monitor.name FROM monitor_maintenance mm JOIN monitor ON mm.monitor_id = monitor.id WHERE mm.maintenance_id = ? ", [ + let monitors = await R.getAll("SELECT monitor.id FROM monitor_maintenance mm JOIN monitor ON mm.monitor_id = monitor.id WHERE mm.maintenance_id = ? ", [ maintenanceID, ]); diff --git a/src/components/MonitorList.vue b/src/components/MonitorList.vue index d64b43c18..c69169ccf 100644 --- a/src/components/MonitorList.vue +++ b/src/components/MonitorList.vue @@ -19,43 +19,18 @@ {{ $t("No Monitors, please") }} {{ $t("add one") }} - -
-
-
- - {{ item.name }} -
-
- -
-
-
- -
-
- -
-
- -
-
-
+ + + diff --git a/src/lang/de-DE.json b/src/lang/de-DE.json index 164ac469c..97e9c8e97 100644 --- a/src/lang/de-DE.json +++ b/src/lang/de-DE.json @@ -776,5 +776,7 @@ "Monitor Setting": "{0}'s Monitor Einstellung", "Badge Prefix": "Badge Präfix", "Badge Suffix": "Badge Suffix", - "Badge Warn Days": "Badge Warnung Tage" + "Badge Warn Days": "Badge Warnung Tage", + "Group": "Gruppe", + "Monitor Group": "Monitor Gruppe" } diff --git a/src/lang/en.json b/src/lang/en.json index e1ce954b5..1bb7686e6 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -744,5 +744,7 @@ "Badge Down Days": "Badge Down Days", "Badge Style": "Badge Style", "Badge value (For Testing only.)": "Badge value (For Testing only.)", - "Badge URL": "Badge URL" + "Badge URL": "Badge URL", + "Group": "Group", + "Monitor Group": "Monitor Group" } diff --git a/src/pages/Details.vue b/src/pages/Details.vue index b4f838914..1b37b1866 100644 --- a/src/pages/Details.vue +++ b/src/pages/Details.vue @@ -1,6 +1,7 @@