feat: json-query monitor added (#3253)

*  feat: json-query monitor added

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: import warning error

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: br tag and remove comment

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: supporting compare string with other types

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: switch to a better lib for json query

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: better description on json query and using `v-html` in jsonQueryDescription element to fix `a` tags

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: result variable in error message

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: typos in json query description

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* 📝 docs: `HTTP(s) Json Query` added to monitor list in `README.md`

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* 🐛 fix: needed white space in `README.md`

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* Nostr dm notifications (#3051)

* Add nostr DM notification provider

* require crypto for node 18 compatibility

* remove whitespace

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* move closer to where it is used

* simplify success or failure logic

* don't clobber the non-alert msg

* Update server/notification-providers/nostr.js

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* polyfills required for node <= 18

* resolve linter warnings

* missing comma

---------

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* Drop nostr

* Rebuild package-lock.json

* Lint

---------

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
Co-authored-by: zappityzap <128872140+zappityzap@users.noreply.github.com>
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
This commit is contained in:
Muhammed Hussein karimi 2023-07-13 19:07:26 +03:30 committed by GitHub
parent e7d1b4e14a
commit 6bece8796e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 88 additions and 18 deletions

View File

@ -23,7 +23,7 @@ It is a temporary live demo, all data will be deleted after 10 minutes. Use the
## ⭐ Features ## ⭐ Features
* Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / Ping / DNS Record / Push / Steam Game Server / Docker Containers * Monitoring uptime for HTTP(s) / TCP / HTTP(s) Keyword / HTTP(s) Json Query / Ping / DNS Record / Push / Steam Game Server / Docker Containers
* Fancy, Reactive, Fast UI/UX * Fancy, Reactive, Fast UI/UX
* Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications) * Notifications via Telegram, Discord, Gotify, Slack, Pushover, Email (SMTP), and [90+ notification services, click here for the full list](https://github.com/louislam/uptime-kuma/tree/master/src/components/notifications)
* 20 second intervals * 20 second intervals
@ -49,14 +49,14 @@ Uptime Kuma is now running on http://localhost:3001
### 💪🏻 Non-Docker ### 💪🏻 Non-Docker
Requirements: Requirements:
- Platform - Platform
- ✅ Major Linux distros such as Debian, Ubuntu, CentOS, Fedora and ArchLinux etc. - ✅ Major Linux distros such as Debian, Ubuntu, CentOS, Fedora and ArchLinux etc.
- ✅ Windows 10 (x64), Windows Server 2012 R2 (x64) or higher - ✅ Windows 10 (x64), Windows Server 2012 R2 (x64) or higher
- ❌ Replit / Heroku - ❌ Replit / Heroku
- [Node.js](https://nodejs.org/en/download/) 14 / 16 / 18 (20 is not supported) - [Node.js](https://nodejs.org/en/download/) 14 / 16 / 18 (20 is not supported)
- [npm](https://docs.npmjs.com/cli/) >= 7 - [npm](https://docs.npmjs.com/cli/) >= 7
- [Git](https://git-scm.com/downloads) - [Git](https://git-scm.com/downloads)
- [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background - [pm2](https://pm2.keymetrics.io/) - For running Uptime Kuma in the background
```bash ```bash
@ -71,7 +71,7 @@ npm run setup
node server/server.js node server/server.js
# (Recommended) Option 2. Run in background using PM2 # (Recommended) Option 2. Run in background using PM2
# Install PM2 if you don't have it: # Install PM2 if you don't have it:
npm install pm2 -g && pm2 install pm2-logrotate npm install pm2 -g && pm2 install pm2-logrotate
# Start Server # Start Server

View File

@ -0,0 +1,10 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD json_path TEXT;
ALTER TABLE monitor
ADD expected_value VARCHAR(255);
COMMIT;

9
package-lock.json generated
View File

@ -38,6 +38,7 @@
"https-proxy-agent": "~5.0.1", "https-proxy-agent": "~5.0.1",
"iconv-lite": "~0.6.3", "iconv-lite": "~0.6.3",
"jsesc": "~3.0.2", "jsesc": "~3.0.2",
"jsonata": "^2.0.3",
"jsonwebtoken": "~9.0.0", "jsonwebtoken": "~9.0.0",
"jwt-decode": "~3.1.2", "jwt-decode": "~3.1.2",
"limiter": "~2.1.0", "limiter": "~2.1.0",
@ -12921,6 +12922,14 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/jsonata": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.0.3.tgz",
"integrity": "sha512-Up2H81MUtjqI/dWwWX7p4+bUMfMrQJVMN/jW6clFMTiYP528fBOBNtRu944QhKTs3+IsVWbgMeUTny5fw2VMUA==",
"engines": {
"node": ">= 8"
}
},
"node_modules/jsonfile": { "node_modules/jsonfile": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",

View File

@ -97,6 +97,7 @@
"https-proxy-agent": "~5.0.1", "https-proxy-agent": "~5.0.1",
"iconv-lite": "~0.6.3", "iconv-lite": "~0.6.3",
"jsesc": "~3.0.2", "jsesc": "~3.0.2",
"jsonata": "^2.0.3",
"jsonwebtoken": "~9.0.0", "jsonwebtoken": "~9.0.0",
"jwt-decode": "~3.1.2", "jwt-decode": "~3.1.2",
"limiter": "~2.1.0", "limiter": "~2.1.0",

View File

@ -72,6 +72,7 @@ class Database {
"patch-maintenance-cron.sql": true, "patch-maintenance-cron.sql": true,
"patch-add-parent-monitor.sql": true, "patch-add-parent-monitor.sql": true,
"patch-add-invert-keyword.sql": true, "patch-add-invert-keyword.sql": true,
"patch-added-json-query.sql": true,
}; };
/** /**

View File

@ -20,6 +20,7 @@ const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
const { DockerHost } = require("../docker"); const { DockerHost } = require("../docker");
const { UptimeCacheList } = require("../uptime-cache-list"); const { UptimeCacheList } = require("../uptime-cache-list");
const Gamedig = require("gamedig"); const Gamedig = require("gamedig");
const jsonata = require("jsonata");
const jwt = require("jsonwebtoken"); const jwt = require("jsonwebtoken");
/** /**
@ -126,6 +127,8 @@ class Monitor extends BeanModel {
radiusCallingStationId: this.radiusCallingStationId, radiusCallingStationId: this.radiusCallingStationId,
game: this.game, game: this.game,
httpBodyEncoding: this.httpBodyEncoding, httpBodyEncoding: this.httpBodyEncoding,
jsonPath: this.jsonPath,
expectedValue: this.expectedValue,
screenshot, screenshot,
}; };
@ -320,7 +323,7 @@ class Monitor extends BeanModel {
bean.msg = "Group empty"; bean.msg = "Group empty";
} }
} else if (this.type === "http" || this.type === "keyword") { } else if (this.type === "http" || this.type === "keyword" || this.type === "json-query") {
// Do not do any queries/high loading things before the "bean.ping" // Do not do any queries/high loading things before the "bean.ping"
let startTime = dayjs().valueOf(); let startTime = dayjs().valueOf();
@ -448,7 +451,7 @@ class Monitor extends BeanModel {
if (this.type === "http") { if (this.type === "http") {
bean.status = UP; bean.status = UP;
} else { } else if (this.type === "keyword") {
let data = res.data; let data = res.data;
@ -470,6 +473,24 @@ class Monitor extends BeanModel {
(keywordFound ? "present" : "not") + " in [" + data + "]"); (keywordFound ? "present" : "not") + " in [" + data + "]");
} }
} else if (this.type === "json-query") {
let data = res.data;
// convert data to object
if (typeof data === "string") {
data = JSON.parse(data);
}
let expression = jsonata(this.jsonPath);
let result = await expression.evaluate(data);
if (result.toString() === this.expectedValue) {
bean.msg += ", expected value is found";
bean.status = UP;
} else {
throw new Error(bean.msg + ", but value is not equal to expected value, value was: [" + result + "]");
}
} }
} else if (this.type === "port") { } else if (this.type === "port") {

View File

@ -67,7 +67,7 @@ class SMTP extends NotificationProvider {
if (monitorJSON !== null) { if (monitorJSON !== null) {
monitorName = monitorJSON["name"]; monitorName = monitorJSON["name"];
if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword") { if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") {
monitorHostnameOrURL = monitorJSON["url"]; monitorHostnameOrURL = monitorJSON["url"];
} else { } else {
monitorHostnameOrURL = monitorJSON["hostname"]; monitorHostnameOrURL = monitorJSON["hostname"];

View File

@ -748,6 +748,8 @@ let needSetup = false;
bean.radiusCallingStationId = monitor.radiusCallingStationId; bean.radiusCallingStationId = monitor.radiusCallingStationId;
bean.radiusSecret = monitor.radiusSecret; bean.radiusSecret = monitor.radiusSecret;
bean.httpBodyEncoding = monitor.httpBodyEncoding; bean.httpBodyEncoding = monitor.httpBodyEncoding;
bean.expectedValue = monitor.expectedValue;
bean.jsonPath = monitor.jsonPath;
bean.validate(); bean.validate();

View File

@ -104,7 +104,7 @@ export default {
// We must check if there are any elements in monitorList to // We must check if there are any elements in monitorList to
// prevent undefined errors if it hasn't been loaded yet // prevent undefined errors if it hasn't been loaded yet
if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) { if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) {
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword"; return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword" || this.$root.monitorList[monitor.element.id].type === "json-query";
} }
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode; return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
}, },

View File

@ -150,7 +150,7 @@ export default {
// We must check if there are any elements in monitorList to // We must check if there are any elements in monitorList to
// prevent undefined errors if it hasn't been loaded yet // prevent undefined errors if it hasn't been loaded yet
if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) { if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) {
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword"; return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword" || this.$root.monitorList[monitor.element.id].type === "json-query";
} }
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode; return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://" && !this.editMode;
}, },

View File

@ -52,6 +52,8 @@
"Monitor Type": "Monitor Type", "Monitor Type": "Monitor Type",
"Keyword": "Keyword", "Keyword": "Keyword",
"Invert Keyword": "Invert Keyword", "Invert Keyword": "Invert Keyword",
"Expected Value": "Expected Value",
"Json Query": "Json Query",
"Friendly Name": "Friendly Name", "Friendly Name": "Friendly Name",
"URL": "URL", "URL": "URL",
"Hostname": "Hostname", "Hostname": "Hostname",
@ -523,6 +525,7 @@
"notificationDescription": "Notifications must be assigned to a monitor to function.", "notificationDescription": "Notifications must be assigned to a monitor to function.",
"keywordDescription": "Search keyword in plain HTML or JSON response. The search is case-sensitive.", "keywordDescription": "Search keyword in plain HTML or JSON response. The search is case-sensitive.",
"invertKeywordDescription": "Look for the keyword to be absent rather than present.", "invertKeywordDescription": "Look for the keyword to be absent rather than present.",
"jsonQueryDescription": "Do a json Query against the response and check for expected value (Return value will get converted into string for comparison). Check out <a href='https://jsonata.org/'>jsonata.org</a> for the documentation about the query language. A playground can be found <a href='https://try.jsonata.org/'>here</a>.",
"backupDescription": "You can backup all monitors and notifications into a JSON file.", "backupDescription": "You can backup all monitors and notifications into a JSON file.",
"backupDescription2": "Note: history and event data is not included.", "backupDescription2": "Note: history and event data is not included.",
"backupDescription3": "Sensitive data such as notification tokens are included in the export file; please store export securely.", "backupDescription3": "Sensitive data such as notification tokens are included in the export file; please store export securely.",

View File

@ -8,7 +8,7 @@
<Tag v-for="tag in monitor.tags" :key="tag.id" :item="tag" :size="'sm'" /> <Tag v-for="tag in monitor.tags" :key="tag.id" :item="tag" :size="'sm'" />
</div> </div>
<p class="url"> <p class="url">
<a v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'mp-health' " :href="monitor.url" target="_blank" rel="noopener noreferrer">{{ filterPassword(monitor.url) }}</a> <a v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'mp-health' " :href="monitor.url" target="_blank" rel="noopener noreferrer">{{ filterPassword(monitor.url) }}</a>
<span v-if="monitor.type === 'port'">TCP Port {{ monitor.hostname }}:{{ monitor.port }}</span> <span v-if="monitor.type === 'port'">TCP Port {{ monitor.hostname }}:{{ monitor.port }}</span>
<span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span> <span v-if="monitor.type === 'ping'">Ping: {{ monitor.hostname }}</span>
<span v-if="monitor.type === 'keyword'"> <span v-if="monitor.type === 'keyword'">
@ -17,6 +17,12 @@
<span class="keyword">{{ monitor.keyword }}</span> <span class="keyword">{{ monitor.keyword }}</span>
<span v-if="monitor.invertKeyword" alt="Inverted keyword" class="keyword-inverted"> </span> <span v-if="monitor.invertKeyword" alt="Inverted keyword" class="keyword-inverted"> </span>
</span> </span>
<span v-if="monitor.type === 'json-query'">
<br>
<span>{{ $t("Json Query") }}:</span> <span class="keyword">{{ monitor.jsonPath }}</span>
<br>
<span>{{ $t("Expected Value") }}:</span> <span class="keyword">{{ monitor.expectedValue }}</span>
</span>
<span v-if="monitor.type === 'dns'">[{{ monitor.dns_resolve_type }}] {{ monitor.hostname }} <span v-if="monitor.type === 'dns'">[{{ monitor.dns_resolve_type }}] {{ monitor.hostname }}
<br> <br>
<span>{{ $t("Last Result") }}:</span> <span class="keyword">{{ monitor.dns_last_result }}</span> <span>{{ $t("Last Result") }}:</span> <span class="keyword">{{ monitor.dns_last_result }}</span>
@ -434,7 +440,7 @@ export default {
translationPrefix = "Avg. "; translationPrefix = "Avg. ";
} }
if (this.monitor.type === "http" || this.monitor.type === "keyword") { if (this.monitor.type === "http" || this.monitor.type === "keyword" || this.monitor.type === "json-query") {
return this.$t(translationPrefix + "Response"); return this.$t(translationPrefix + "Response");
} }

View File

@ -27,6 +27,9 @@
<option value="keyword"> <option value="keyword">
HTTP(s) - {{ $t("Keyword") }} HTTP(s) - {{ $t("Keyword") }}
</option> </option>
<option value="json-query">
HTTP(s) - {{ $t("Json Query") }}
</option>
<option value="grpc-keyword"> <option value="grpc-keyword">
gRPC(s) - {{ $t("Keyword") }} gRPC(s) - {{ $t("Keyword") }}
</option> </option>
@ -97,7 +100,7 @@
</div> </div>
<!-- URL --> <!-- URL -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'real-browser' " class="my-3"> <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' || monitor.type === 'real-browser' " class="my-3">
<label for="url" class="form-label">{{ $t("URL") }}</label> <label for="url" class="form-label">{{ $t("URL") }}</label>
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required> <input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
</div> </div>
@ -138,6 +141,20 @@
</div> </div>
</div> </div>
<!-- Json Query -->
<div v-if="monitor.type === 'json-query'" class="my-3">
<label for="jsonPath" class="form-label">{{ $t("Json Query") }}</label>
<input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control" required>
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="form-text" v-html="$t('jsonQueryDescription')">
</div>
<br>
<label for="expectedValue" class="form-label">{{ $t("Expected Value") }}</label>
<input id="expectedValue" v-model="monitor.expectedValue" type="text" class="form-control" required>
</div>
<!-- Game --> <!-- Game -->
<!-- GameDig only --> <!-- GameDig only -->
<div v-if="monitor.type === 'gamedig'" class="my-3"> <div v-if="monitor.type === 'gamedig'" class="my-3">
@ -367,7 +384,7 @@
<h2 v-if="monitor.type !== 'push'" class="mt-5 mb-2">{{ $t("Advanced") }}</h2> <h2 v-if="monitor.type !== 'push'" 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' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check">
<input id="expiry-notification" v-model="monitor.expiryNotification" class="form-check-input" type="checkbox"> <input id="expiry-notification" v-model="monitor.expiryNotification" class="form-check-input" type="checkbox">
<label class="form-check-label" for="expiry-notification"> <label class="form-check-label" for="expiry-notification">
{{ $t("Certificate Expiry Notification") }} {{ $t("Certificate Expiry Notification") }}
@ -376,7 +393,7 @@
</div> </div>
</div> </div>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3 form-check"> <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check">
<input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value=""> <input id="ignore-tls" v-model="monitor.ignoreTls" class="form-check-input" type="checkbox" value="">
<label class="form-check-label" for="ignore-tls"> <label class="form-check-label" for="ignore-tls">
{{ $t("ignoreTLSError") }} {{ $t("ignoreTLSError") }}
@ -468,7 +485,7 @@
</button> </button>
<!-- Proxies --> <!-- Proxies -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword'"> <div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query'">
<h2 class="mt-5 mb-2">{{ $t("Proxy") }}</h2> <h2 class="mt-5 mb-2">{{ $t("Proxy") }}</h2>
<p v-if="$root.proxyList.length === 0"> <p v-if="$root.proxyList.length === 0">
{{ $t("Not available, please setup.") }} {{ $t("Not available, please setup.") }}
@ -496,7 +513,7 @@
</div> </div>
<!-- HTTP Options --> <!-- HTTP Options -->
<template v-if="monitor.type === 'http' || monitor.type === 'keyword' "> <template v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' ">
<h2 class="mt-5 mb-2">{{ $t("HTTP Options") }}</h2> <h2 class="mt-5 mb-2">{{ $t("HTTP Options") }}</h2>
<!-- Method --> <!-- Method -->
@ -1118,7 +1135,7 @@ message HealthCheckResponse {
this.monitor.body = JSON.stringify(JSON.parse(this.monitor.body), null, 4); this.monitor.body = JSON.stringify(JSON.parse(this.monitor.body), null, 4);
} }
if (this.monitor.type && this.monitor.type !== "http" && this.monitor.type !== "keyword") { if (this.monitor.type && this.monitor.type !== "http" && (this.monitor.type !== "keyword" || this.monitor.type !== "json-query")) {
this.monitor.httpBodyEncoding = null; this.monitor.httpBodyEncoding = null;
} }