Browse Source

feat(monitor-checks): add checks with specific types

bertyhell/feature/monitor-checks
Bert Verhelst 4 years ago
parent
commit
20e3c5061e
  1. 2
      db/patch-add-monitor-checks-table.sql
  2. 8126
      package-lock.json
  3. 1
      package.json
  4. 17
      server/model/monitor.js
  5. 102
      server/model/validate-monitor-checks.js
  6. 14
      server/server.js
  7. 141
      src/components/MonitorCheckEditor.vue
  8. 4
      src/icon.js
  9. 6
      src/pages/Details.vue
  10. 62
      src/pages/EditMonitor.vue

2
db/patch-add-monitor-checks-table.sql

@ -8,7 +8,7 @@ create table monitor_checks
constraint monitor_checks_pk
primary key autoincrement,
type VARCHAR(50) not null,
value TEXTt,
value TEXT,
monitor_id INTEGER not null
constraint monitor_checks_monitor_id_fk
references monitor

8126
package-lock.json

File diff suppressed because it is too large

1
package.json

@ -57,6 +57,7 @@
"form-data": "^4.0.0",
"http-graceful-shutdown": "^3.1.4",
"jsonwebtoken": "^8.5.1",
"lodash.get": "^4.4.2",
"nodemailer": "^6.6.3",
"notp": "^2.0.3",
"password-hash": "^1.2.2",

17
server/model/monitor.js

@ -7,10 +7,11 @@ dayjs.extend(timezone)
const axios = require("axios");
const { Prometheus } = require("../prometheus");
const { debug, UP, DOWN, PENDING, flipStatus, TimeLogger } = require("../../src/util");
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom } = require("../util-server");
const { tcping, ping, dnsResolve, checkCertificate, getTotalClientInRoom } = require("../util-server");
const { R } = require("redbean-node");
const { BeanModel } = require("redbean-node/dist/bean-model");
const { Notification } = require("../notification")
const validateMonitorChecks = require("./validate-monitor-checks");
const version = require("../../package.json").version;
/**
@ -43,15 +44,14 @@ class Monitor extends BeanModel {
active: this.active,
type: this.type,
interval: this.interval,
keyword: this.keyword,
ignoreTls: this.getIgnoreTls(),
upsideDown: this.isUpsideDown(),
maxredirects: this.maxredirects,
accepted_statuscodes: this.getAcceptedStatuscodes(),
dns_resolve_type: this.dns_resolve_type,
dns_resolve_server: this.dns_resolve_server,
dns_last_result: this.dns_last_result,
notificationIDList,
checks: this.checks,
};
}
@ -71,10 +71,6 @@ class Monitor extends BeanModel {
return Boolean(this.upsideDown);
}
getAcceptedStatuscodes() {
return JSON.parse(this.accepted_statuscodes_json);
}
start(io) {
let previousBeat = null;
let retries = 0;
@ -112,7 +108,7 @@ class Monitor extends BeanModel {
}
try {
if (this.type === "http" || this.type === "keyword") {
if (this.type === "http") {
// Do not do any queries/high loading things before the "bean.ping"
let startTime = dayjs().valueOf();
@ -127,9 +123,6 @@ class Monitor extends BeanModel {
rejectUnauthorized: ! this.getIgnoreTls(),
}),
maxRedirects: this.maxredirects,
validateStatus: (status) => {
return checkStatusCode(status, this.getAcceptedStatuscodes());
},
});
bean.msg = `${res.status} - ${res.statusText}`
bean.ping = dayjs().valueOf() - startTime;
@ -148,6 +141,8 @@ class Monitor extends BeanModel {
debug("Cert Info Query Time: " + (dayjs().valueOf() - certInfoStartTime) + "ms")
validateMonitorChecks(res, this.checks, bean);
if (this.type === "http") {
bean.status = UP;
} else {

102
server/model/validate-monitor-checks.js

@ -0,0 +1,102 @@
const { checkStatusCode } = require("../util-server");
const { UP } = require("../../src/util");
const get = require("lodash.get");
function validateMonitorChecks(res, checks, bean) {
const responseText = typeof data === "string" ? res.data : JSON.stringify(res.data);
let checkObj;
this.checks.forEach(check => {
switch (check.type) {
case "HTTP_STATUS_CODE_SHOULD_EQUAL":
if (checkStatusCode(res.status, check.value)) {
bean.msg += `, status matches '${check.value}'`
bean.status = UP;
} else {
throw new Error(bean.msg + ", but status code dit not match " + check.value)
}
break;
case "RESPONSE_SHOULD_CONTAIN_TEXT":
if (responseText.includes(check.value)) {
bean.msg += `, response contains '${check.value}'`
bean.status = UP;
} else {
throw new Error(bean.msg + ", but response does not contain '" + check.value + "'");
}
break;
case "RESPONSE_SHOULD_NOT_CONTAIN_TEXT":
if (!responseText.includes(check.value)) {
bean.msg += `, response does not contain '${check.value}'`
bean.status = UP;
} else {
throw new Error(bean.msg + ", but response does contain '" + check.value + "'");
}
break;
case "RESPONSE_SHOULD_MATCH_REGEX":
if (responseText.test(new RegExp(check.value))) {
bean.msg += `, regex '${check.value}' matches`
bean.status = UP;
} else {
throw new Error(bean.msg + ", but response does not match regex: '" + check.value + "'");
}
break;
case "RESPONSE_SHOULD_NOT_MATCH_REGEX":
if (!responseText.test(new RegExp(check.value))) {
bean.msg += `, regex '${check.value}' does not matches`
bean.status = UP;
} else {
throw new Error(bean.msg + ", but response does match regex: '" + check.value + "'");
}
break;
case "RESPONSE_SELECTOR_SHOULD_EQUAL":
checkObj = JSON.parse(check.value);
if (get(res, checkObj.selector) === checkObj.value) {
bean.msg += `, response selector equals '${checkObj.value}'`
bean.status = UP;
} else {
throw new Error(`${bean.msg}, but response selector '${checkObj.selector}' does not equal '${checkObj.value}'`);
}
break;
case "RESPONSE_SELECTOR_SHOULD_NOT_EQUAL":
checkObj = JSON.parse(check.value);
if (get(res, checkObj.selector) !== checkObj.value) {
bean.msg += `, response selector does not equal '${checkObj.value}'`
bean.status = UP;
} else {
throw new Error(`${bean.msg}, but response selector '${checkObj.selector}' does equal '${checkObj.value}'`);
}
break;
case "RESPONSE_SELECTOR_SHOULD_MATCH_REGEX":
checkObj = JSON.parse(check.value);
if (get(res, checkObj.selector).test(new RegExp(checkObj.value))) {
bean.msg += `, response selector matches regex '${checkObj.value}'`
bean.status = UP;
} else {
throw new Error(`${bean.msg}, but response selector '${checkObj.selector}' does not match regex '${checkObj.value}'`);
}
break;
case "RESPONSE_SELECTOR_SHOULD_NOT_MATCH_REGEX":
checkObj = JSON.parse(check.value);
if (!get(res, checkObj.selector).test(new RegExp(checkObj.value))) {
bean.msg += `, response selector does not match regex '${checkObj.value}'`
bean.status = UP;
} else {
throw new Error(`${bean.msg}, but response selector '${checkObj.selector}' does match regex '${checkObj.value}'`);
}
break;
default:
throw new Error(`${bean.msg}, encountered unknown monitor_check.type`);
}
});
}
module.exports = validateMonitorChecks;

14
server/server.js

@ -1,7 +1,7 @@
console.log("Welcome to Uptime Kuma");
console.log("Node Env: " + process.env.NODE_ENV);
const { sleep, debug, TimeLogger, getRandomInt } = require("../src/util");
const { sleep, debug, getRandomInt } = require("../src/util");
console.log("Importing Node libraries")
const fs = require("fs");
@ -437,8 +437,8 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
let notificationIDList = monitor.notificationIDList;
delete monitor.notificationIDList;
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
delete monitor.accepted_statuscodes;
monitor.checks_json = JSON.stringify(monitor.checks);
delete monitor.checks;
bean.import(monitor)
bean.user_id = socket.userID
@ -481,13 +481,12 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
bean.hostname = monitor.hostname;
bean.maxretries = monitor.maxretries;
bean.port = monitor.port;
bean.keyword = monitor.keyword;
bean.ignoreTls = monitor.ignoreTls;
bean.upsideDown = monitor.upsideDown;
bean.maxredirects = monitor.maxredirects;
bean.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
bean.dns_resolve_type = monitor.dns_resolve_type;
bean.dns_resolve_server = monitor.dns_resolve_server;
bean.checks_json = JSON.stringify(monitor.checks);
await R.store(bean)
@ -776,11 +775,10 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
hostname: monitorList[i].hostname,
maxretries: monitorList[i].maxretries,
port: monitorList[i].port,
keyword: monitorList[i].keyword,
ignoreTls: monitorList[i].ignoreTls,
upsideDown: monitorList[i].upsideDown,
maxredirects: monitorList[i].maxredirects,
accepted_statuscodes: monitorList[i].accepted_statuscodes,
checks: monitorList[i].checks,
dns_resolve_type: monitorList[i].dns_resolve_type,
dns_resolve_server: monitorList[i].dns_resolve_server,
notificationIDList: {},
@ -791,7 +789,7 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString();
let notificationIDList = monitor.notificationIDList;
delete monitor.notificationIDList;
monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes);
monitor.checks_json = JSON.stringify(monitor.checks);
delete monitor.accepted_statuscodes;
bean.import(monitor)

141
src/components/MonitorCheckEditor.vue

@ -0,0 +1,141 @@
<template>
<div class="monitor-check mb-4">
<div>
<select id="type" v-model="monitorCheck.type" :class="{'form-select': true, 'mb-1': !!monitorCheck.type}">
<option value="HTTP_STATUS_CODE_SHOULD_EQUAL">
{{ $t("HTTP status code should equal") }}
</option>
<option value="RESPONSE_SHOULD_CONTAIN_TEXT">
{{ $t("Response should contain text") }}
</option>
<option value="RESPONSE_SHOULD_NOT_CONTAIN_TEXT">
{{ $t("Response should not contain text") }}
</option>
<option value="RESPONSE_SHOULD_MATCH_REGEX">
{{ $t("Response should match regex") }}
</option>
<option value="RESPONSE_SHOULD_NOT_MATCH_REGEX">
{{ $t("Response should not match regex") }}
</option>
<option value="RESPONSE_SELECTOR_SHOULD_EQUAL">
{{ $t("Response selector should equal") }}
</option>
<option value="RESPONSE_SELECTOR_SHOULD_NOT_EQUAL">
{{ $t("Response selector should not equal") }}
</option>
<option value="RESPONSE_SELECTOR_SHOULD_MATCH_REGEX">
{{ $t("Response selector should match regex") }}
</option>
<option value="RESPONSE_SELECTOR_SHOULD_NOT_MATCH_REGEX">
{{ $t("Response selector should not match regex") }}
</option>
</select>
<div v-if="monitorCheck.type === 'HTTP_STATUS_CODE_SHOULD_EQUAL'">
<VueMultiselect
id="acceptedStatusCodes"
v-model="monitorCheck.value"
:options="acceptedStatusCodeOptions"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
:preserve-search="true"
placeholder="Pick Accepted Status Codes..."
:preselect-first="false"
:max-height="600"
:taggable="true"
></VueMultiselect>
</div>
<div v-if="monitorCheck.type === 'RESPONSE_SHOULD_CONTAIN_TEXT' || monitorCheck.type === 'RESPONSE_SHOULD_NOT_CONTAIN_TEXT'">
<input v-model="monitorCheck.value" type="text" class="form-control" required :placeholder="$t('Value')">
</div>
<div v-if="monitorCheck.type === 'RESPONSE_SHOULD_MATCH_REGEX' || monitorCheck.type === 'RESPONSE_SHOULD_NOT_MATCH_REGEX'">
<input v-model="monitorCheck.value" type="text" class="form-control" required
:placeholder="$t('Regexp, Example: [a-z0-9.]+@gmail\.com')">
</div>
<div
v-if="monitorCheck.type === 'RESPONSE_SELECTOR_SHOULD_EQUAL' || monitorCheck.type === 'RESPONSE_SELECTOR_SHOULD_NOT_EQUAL'">
<input v-model="monitorCheck.value.selector" type="text" class="form-control mb-1" required
:placeholder="$t('Selector, Example: customer.address.street')">
<input v-model="monitorCheck.value.value" type="text" class="form-control" required :placeholder="$t('Value, Example: First street')">
</div>
<div
v-if="monitorCheck.type === 'RESPONSE_SELECTOR_SHOULD_MATCH_REGEX' || monitorCheck.type === 'RESPONSE_SELECTOR_SHOULD_NOT_MATCH_REGEX'">
<input v-model="monitorCheck.value.selector" type="text" class="form-control mb-1" required
:placeholder="$t('Selector, Example: customer.contactInfo.email')">
<input v-model="monitorCheck.value.value" type="text" class="form-control" required
:placeholder="$t('Regexp, Example: [a-z0-9.]+@gmail\.com')">
</div>
</div>
<button class="btn btn-outline-danger" type="button" @click="deleteMonitorCheck">
<font-awesome-icon icon="times" />
</button>
</div>
</template>
<script>
import VueMultiselect from "vue-multiselect";
export default {
components: {
VueMultiselect,
},
props: {
monitorCheck: {
type: Object,
},
},
data() {
return {
acceptedStatusCodeOptions: [],
}
},
mounted() {
let acceptedStatusCodeOptions = [
"100-199",
"200-299",
"300-399",
"400-499",
"500-599",
];
for (let i = 100; i <= 999; i++) {
acceptedStatusCodeOptions.push(i.toString());
}
this.acceptedStatusCodeOptions = acceptedStatusCodeOptions;
},
methods: {
deleteMonitorCheck() {
this.$emit('delete');
},
},
}
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.monitor-check {
display: flex;
input,
select {
border-radius: 19px 0 0 19px;
}
button {
margin-left: 0.25rem;
padding-left: 15px;
padding-right: 15px;
border-radius: 0 19px 19px 0;
}
}
</style>
<style lang="scss">
.monitor-check {
.multiselect__tags {
border-radius: 19px 0 0 19px;
}
}
</style>

4
src/icon.js

@ -1,10 +1,10 @@
import { library } from "@fortawesome/fontawesome-svg-core"
import { faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons"
import { faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp, faEye, faEyeSlash, faTimes } from "@fortawesome/free-solid-svg-icons"
//import { fa } from '@fortawesome/free-regular-svg-icons'
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"
// Add Free Font Awesome Icons here
// https://fontawesome.com/v5.15/icons?d=gallery&p=2&s=solid&m=free
library.add(faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp, faEye, faEyeSlash);
library.add(faCog, faEdit, faPlus, faPause, faPlay, faTachometerAlt, faTrash, faList, faArrowAltCircleUp, faEye, faEyeSlash, faTimes);
export { FontAwesomeIcon }

6
src/pages/Details.vue

@ -3,13 +3,9 @@
<div v-if="monitor">
<h1> {{ monitor.name }}</h1>
<p class="url">
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' " :href="monitor.url" target="_blank">{{ monitor.url }}</a>
<a v-if="monitor.type === 'http'" :href="monitor.url" target="_blank">{{ monitor.url }}</a>
<span v-if="monitor.type === 'port'">TCP Ping {{ monitor.hostname }}:{{ monitor.port }}</span>
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
<span v-if="monitor.type === 'keyword'">
<br>
<span>{{ $t("Keyword") }}:</span> <span class="keyword">{{ monitor.keyword }}</span>
</span>
<span v-if="monitor.type === 'dns'">[{{ monitor.dns_resolve_type }}] {{ monitor.hostname }}
<br>
<span>{{ $t("Last Result") }}:</span> <span class="keyword">{{ monitor.dns_last_result }}</span>

62
src/pages/EditMonitor.vue

@ -12,19 +12,16 @@
<label for="type" class="form-label">{{ $t("Monitor Type") }}</label>
<select id="type" v-model="monitor.type" class="form-select">
<option value="http">
HTTP(s)
{{ $t("HTTP(s)") }}
</option>
<option value="port">
TCP Port
{{ $t("TCP Port") }}
</option>
<option value="ping">
Ping
</option>
<option value="keyword">
HTTP(s) - {{ $t("Keyword") }}
{{ $t("Ping") }}
</option>
<option value="dns">
DNS
{{ $t("DNS") }}
</option>
</select>
</div>
@ -34,19 +31,11 @@
<input id="name" v-model="monitor.name" type="text" class="form-control" required>
</div>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
<div v-if="monitor.type === 'http'" class="my-3">
<label for="url" class="form-label">{{ $t("URL") }}</label>
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
</div>
<div v-if="monitor.type === 'keyword' " class="my-3">
<label for="keyword" class="form-label">{{ $t("Keyword") }}</label>
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
<div class="form-text">
{{ $t("keywordDescription") }}
</div>
</div>
<!-- TCP Port / Ping / DNS only -->
<div v-if="monitor.type === 'port' || monitor.type === 'ping' || monitor.type === 'dns' " class="my-3">
<label for="hostname" class="form-label">{{ $t("Hostname") }}</label>
@ -108,7 +97,7 @@
<h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check">
<div v-if="monitor.type === 'http'" class="my-3 form-check">
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="ignore-tls">
{{ $t("ignoreTLSError") }}
@ -125,8 +114,8 @@
</div>
</div>
<!-- HTTP / Keyword only -->
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' ">
<!-- HTTP only -->
<template v-if="monitor.type === 'http'">
<div class="my-3">
<label for="maxRedirects" class="form-label">{{ $t("Max. Redirects") }}</label>
<input id="maxRedirects" v-model="monitor.maxredirects" type="number" class="form-control" required min="0" step="1">
@ -135,26 +124,13 @@
</div>
</div>
<div class="my-3">
<label for="acceptedStatusCodes" class="form-label">{{ $t("Accepted Status Codes") }}</label>
<VueMultiselect
id="acceptedStatusCodes"
v-model="monitor.accepted_statuscodes"
:options="acceptedStatusCodeOptions"
:multiple="true"
:close-on-select="false"
:clear-on-select="false"
:preserve-search="true"
placeholder="Pick Accepted Status Codes..."
:preselect-first="false"
:max-height="600"
:taggable="true"
></VueMultiselect>
<h2 class="mt-5 mb-2">{{ $t("Checks") }}</h2>
<div class="form-text">
{{ $t("acceptedStatusCodesDescription") }}
<div class="my-3">
<div v-for="(monitorCheck, index) in monitor.checks" :key="index" class="mb-3">
<MonitorCheckEditor :monitorCheck="monitorCheck" :index="index" @delete="deleteMonitorCheck(index)"></MonitorCheckEditor>
</div>
<button class="btn btn-light" type="button" @click="addMonitorCheck()">{{ $t("Add check") }}</button>
</div>
</template>
@ -197,6 +173,7 @@
<script>
import NotificationDialog from "../components/NotificationDialog.vue";
import MonitorCheckEditor from "../components/MonitorCheckEditor.vue";
import { useToast } from "vue-toastification"
import VueMultiselect from "vue-multiselect"
import { isDev } from "../util.ts";
@ -205,6 +182,7 @@ const toast = useToast()
export default {
components: {
NotificationDialog,
MonitorCheckEditor,
VueMultiselect,
},
@ -314,7 +292,17 @@ export default {
}
})
}
},
addMonitorCheck() {
this.monitor.checks = [...(this.monitor.checks || []), {
type: null,
value: '',
}];
},
deleteMonitorCheck(index) {
this.monitor.checks = this.monitor.checks.splice(index, 1);
},
submit() {

Loading…
Cancel
Save