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

Group monitors #2693

Merged
merged 23 commits into from
Jun 11, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
645fd94
feat: add ability to group monitors in dashboard
julian-piehl Jan 28, 2023
97bd306
Merge branch 'louislam:master' into group-monitors
julian-piehl Jan 28, 2023
aba515e
feat: disable childs if parent is disabled
julian-piehl Jan 28, 2023
f3ac351
feat: set childs under maintenance if parent is too
julian-piehl Jan 28, 2023
faf3488
fix: unfold tree if monitor is accessed directly
julian-piehl Jan 28, 2023
9a46b50
docs: add comments
julian-piehl Jan 28, 2023
aee4c22
perf: only do one filter instead of 3 in editMonitor
julian-piehl Jan 28, 2023
d48eb24
docs: more comments
julian-piehl Jan 28, 2023
661fa87
feat: make parent link clickable
julian-piehl Jan 28, 2023
c444d78
style: fix linting errors
julian-piehl Jan 28, 2023
6657393
refactor: remove old code
julian-piehl Jan 28, 2023
9286dcb
fix: add serverside check against endless loops
julian-piehl Feb 1, 2023
f286386
fix: add message for empty group pending state
julian-piehl Feb 1, 2023
2c581ad
Merge branch 'louislam:master' into group-monitors
julian-piehl Feb 1, 2023
9446c2d
fix: use active instead of isActive in uploadBackup
julian-piehl Feb 1, 2023
e10ba9e
Merge branch 'master' into group-monitors
julian-piehl Feb 2, 2023
cef0a0f
Merge branch 'master' into group-monitors
julian-piehl Feb 16, 2023
0be8b11
chore: better up message
julian-piehl Feb 20, 2023
9bd76c2
Merge branch 'master' into group-monitors
julian-piehl May 31, 2023
2b46da0
style: fix linting
julian-piehl May 31, 2023
56f448b
fix: maintenance heredity
julian-piehl May 31, 2023
6c2948d
fix: list collapse storage
julian-piehl Jun 3, 2023
57190b5
fix: dont show ping on details page of groups
julian-piehl Jun 3, 2023
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
6 changes: 6 additions & 0 deletions db/patch-add-parent-monitor.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
BEGIN TRANSACTION;

ALTER TABLE monitor
ADD parent INTEGER REFERENCES [monitor] ([id]) ON DELETE SET NULL ON UPDATE CASCADE;

COMMIT
1 change: 1 addition & 0 deletions server/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class Database {
"patch-maintenance-table2.sql": true,
"patch-add-gamedig-monitor.sql": true,
"patch-add-google-analytics-status-page-tag.sql": true,
"patch-add-parent-monitor.sql": true,
};

/**
Expand Down
146 changes: 144 additions & 2 deletions server/model/monitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,17 @@ class Monitor extends BeanModel {
let data = {
id: this.id,
name: this.name,
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,
Expand Down Expand Up @@ -138,6 +142,16 @@ class Monitor extends BeanModel {
return data;
}

/**
* Checks if the monitor is active based on itself and its parents
* @returns {Promise<Boolean>}
*/
async isActive() {
const parentActive = await Monitor.isParentActive(this.id);

return this.active && parentActive;
}

/**
* Get all tags applied to this monitor
* @returns {Promise<LooseObject<any>[]>}
Expand Down Expand Up @@ -253,6 +267,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 childs 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();
Expand Down Expand Up @@ -1279,7 +1323,17 @@ class Monitor extends BeanModel {
ON maintenance_timeslot.maintenance_id = maintenance.id
WHERE ${activeCondition}
LIMIT 1`, [ monitorID ]);
return maintenance.count !== 0;

if (maintenance.count !== 0) {
return true;
}

// Check if parent is under maintenance
const parent = await Monitor.getParent(monitorID);
if (parent === null) {
return false;
}
return await Monitor.isUnderMaintenance(parent.id);
}

/** Make sure monitor interval is between bounds */
Expand All @@ -1291,6 +1345,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<LooseObject<any>>}
*/
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<LooseObject<any>>}
*/
static async getChildren(monitorID) {
return await R.getAll(`
SELECT * FROM monitor
WHERE parent = ?
`, [
monitorID,
]);
}

/**
* Gets Full Path-Name (Groups and Name)
* @returns {Promise<String>}
*/
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<Array>}
*/
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<Boolean>}
*/
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;
11 changes: 10 additions & 1 deletion server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -677,10 +677,19 @@ 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");
}
}

// Reset Prometheus labels
server.monitorList[monitor.id]?.prometheus()?.remove();

bean.name = monitor.name;
bean.parent = monitor.parent;
bean.type = monitor.type;
bean.url = monitor.url;
bean.method = monitor.method;
Expand Down Expand Up @@ -736,7 +745,7 @@ let needSetup = false;

await updateMonitorNotification(bean.id, monitor.notificationIDList);

if (bean.active) {
if (bean.isActive()) {
await restartMonitor(socket.userID, bean.id);
}

Expand Down
2 changes: 1 addition & 1 deletion server/socket-handlers/maintenance-socket-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,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,
]);

Expand Down
56 changes: 17 additions & 39 deletions src/components/MonitorList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,43 +19,18 @@
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
</div>

<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
<div class="row">
<div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
<div class="info">
<Uptime :monitor="item" type="24" :pill="true" />
{{ item.name }}
</div>
<div class="tags">
<Tag v-for="tag in item.tags" :key="tag" :item="tag" :size="'sm'" />
</div>
</div>
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
<HeartbeatBar size="small" :monitor-id="item.id" />
</div>
</div>

<div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
<div class="col-12 bottom-style">
<HeartbeatBar size="small" :monitor-id="item.id" />
</div>
</div>
</router-link>
<MonitorListItem v-for="(item, index) in sortedMonitorList" :key="index" :monitor="item" :isSearch="searchText !== ''" />
</div>
</div>
</template>

<script>
import HeartbeatBar from "../components/HeartbeatBar.vue";
import Tag from "../components/Tag.vue";
import Uptime from "../components/Uptime.vue";
import MonitorListItem from "../components/MonitorListItem.vue";
import { getMonitorRelativeURL } from "../util.ts";

export default {
components: {
Uptime,
HeartbeatBar,
Tag,
MonitorListItem,
},
props: {
/** Should the scrollbar be shown */
Expand Down Expand Up @@ -91,6 +66,20 @@ export default {
sortedMonitorList() {
let result = Object.values(this.$root.monitorList);

// Simple filter by search text
// finds monitor name, tag name or tag value
if (this.searchText !== "") {
const loweredSearchText = this.searchText.toLowerCase();
result = result.filter(monitor => {
return monitor.name.toLowerCase().includes(loweredSearchText)
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|| tag.value?.toLowerCase().includes(loweredSearchText));
});
} else {
result = result.filter(monitor => monitor.parent === null);
}

// Filter result by active state, weight and alphabetical
result.sort((m1, m2) => {

if (m1.active !== m2.active) {
Expand All @@ -116,17 +105,6 @@ export default {
return m1.name.localeCompare(m2.name);
});

// Simple filter by search text
// finds monitor name, tag name or tag value
if (this.searchText !== "") {
const loweredSearchText = this.searchText.toLowerCase();
result = result.filter(monitor => {
return monitor.name.toLowerCase().includes(loweredSearchText)
|| monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
|| tag.value?.toLowerCase().includes(loweredSearchText));
});
}

return result;
},
},
Expand Down
Loading