diff --git a/README.md b/README.md index 3f8a054036..c7aa4150f6 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ It is a temporary live demo, all data will be deleted after 10 minutes. Use the ## ⭐ 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 * 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 @@ -49,14 +49,14 @@ Uptime Kuma is now running on http://localhost:3001 ### 💪🏻 Non-Docker -Requirements: +Requirements: - 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 - ❌ Replit / Heroku - [Node.js](https://nodejs.org/en/download/) 14 / 16 / 18 (20 is not supported) - [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 ```bash @@ -71,7 +71,7 @@ npm run setup node server/server.js # (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 # Start Server diff --git a/db/patch-added-json-query.sql b/db/patch-added-json-query.sql new file mode 100644 index 0000000000..d5b7f1a8f4 --- /dev/null +++ b/db/patch-added-json-query.sql @@ -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; diff --git a/package-lock.json b/package-lock.json index 04b4ea15b2..ad26942e28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "https-proxy-agent": "~5.0.1", "iconv-lite": "~0.6.3", "jsesc": "~3.0.2", + "jsonata": "^2.0.3", "jsonwebtoken": "~9.0.0", "jwt-decode": "~3.1.2", "limiter": "~2.1.0", @@ -12921,6 +12922,14 @@ "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": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", diff --git a/package.json b/package.json index ff5c0dc011..fadd0f0dfd 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "https-proxy-agent": "~5.0.1", "iconv-lite": "~0.6.3", "jsesc": "~3.0.2", + "jsonata": "^2.0.3", "jsonwebtoken": "~9.0.0", "jwt-decode": "~3.1.2", "limiter": "~2.1.0", diff --git a/server/database.js b/server/database.js index 1348e10ce1..3e2d0350de 100644 --- a/server/database.js +++ b/server/database.js @@ -72,6 +72,7 @@ class Database { "patch-maintenance-cron.sql": true, "patch-add-parent-monitor.sql": true, "patch-add-invert-keyword.sql": true, + "patch-added-json-query.sql": true, }; /** diff --git a/server/model/monitor.js b/server/model/monitor.js index b3d8bec2a4..fb71f7546e 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -20,6 +20,7 @@ const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent"); const { DockerHost } = require("../docker"); const { UptimeCacheList } = require("../uptime-cache-list"); const Gamedig = require("gamedig"); +const jsonata = require("jsonata"); const jwt = require("jsonwebtoken"); /** @@ -126,6 +127,8 @@ class Monitor extends BeanModel { radiusCallingStationId: this.radiusCallingStationId, game: this.game, httpBodyEncoding: this.httpBodyEncoding, + jsonPath: this.jsonPath, + expectedValue: this.expectedValue, screenshot, }; @@ -320,7 +323,7 @@ class Monitor extends BeanModel { 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" let startTime = dayjs().valueOf(); @@ -448,7 +451,7 @@ class Monitor extends BeanModel { if (this.type === "http") { bean.status = UP; - } else { + } else if (this.type === "keyword") { let data = res.data; @@ -470,6 +473,24 @@ class Monitor extends BeanModel { (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") { diff --git a/server/notification-providers/smtp.js b/server/notification-providers/smtp.js index aae0e46c74..accee9ea7e 100644 --- a/server/notification-providers/smtp.js +++ b/server/notification-providers/smtp.js @@ -67,7 +67,7 @@ class SMTP extends NotificationProvider { if (monitorJSON !== null) { monitorName = monitorJSON["name"]; - if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword") { + if (monitorJSON["type"] === "http" || monitorJSON["type"] === "keyword" || monitorJSON["type"] === "json-query") { monitorHostnameOrURL = monitorJSON["url"]; } else { monitorHostnameOrURL = monitorJSON["hostname"]; diff --git a/server/server.js b/server/server.js index c35af7fc16..b9d618f51f 100644 --- a/server/server.js +++ b/server/server.js @@ -748,6 +748,8 @@ let needSetup = false; bean.radiusCallingStationId = monitor.radiusCallingStationId; bean.radiusSecret = monitor.radiusSecret; bean.httpBodyEncoding = monitor.httpBodyEncoding; + bean.expectedValue = monitor.expectedValue; + bean.jsonPath = monitor.jsonPath; bean.validate(); diff --git a/src/components/MonitorSettingDialog.vue b/src/components/MonitorSettingDialog.vue index a6976853c0..d8e9da1a76 100644 --- a/src/components/MonitorSettingDialog.vue +++ b/src/components/MonitorSettingDialog.vue @@ -104,7 +104,7 @@ export default { // We must check if there are any elements in monitorList to // prevent undefined errors if it hasn't been loaded yet 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; }, diff --git a/src/components/PublicGroupList.vue b/src/components/PublicGroupList.vue index 00f38c5b9e..4bd4a29520 100644 --- a/src/components/PublicGroupList.vue +++ b/src/components/PublicGroupList.vue @@ -150,7 +150,7 @@ export default { // We must check if there are any elements in monitorList to // prevent undefined errors if it hasn't been loaded yet 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; }, diff --git a/src/lang/en.json b/src/lang/en.json index 7a686cd5dd..e656b60fc0 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -52,6 +52,8 @@ "Monitor Type": "Monitor Type", "Keyword": "Keyword", "Invert Keyword": "Invert Keyword", + "Expected Value": "Expected Value", + "Json Query": "Json Query", "Friendly Name": "Friendly Name", "URL": "URL", "Hostname": "Hostname", @@ -523,6 +525,7 @@ "notificationDescription": "Notifications must be assigned to a monitor to function.", "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.", + "jsonQueryDescription": "Do a json Query against the response and check for expected value (Return value will get converted into string for comparison). Check out jsonata.org for the documentation about the query language. A playground can be found here.", "backupDescription": "You can backup all monitors and notifications into a JSON file.", "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.", diff --git a/src/pages/Details.vue b/src/pages/Details.vue index 4a647d32ef..9504cfd81f 100644 --- a/src/pages/Details.vue +++ b/src/pages/Details.vue @@ -8,7 +8,7 @@

- {{ filterPassword(monitor.url) }} + {{ filterPassword(monitor.url) }} TCP Port {{ monitor.hostname }}:{{ monitor.port }} Ping: {{ monitor.hostname }} @@ -17,6 +17,12 @@ {{ monitor.keyword }} + +
+ {{ $t("Json Query") }}: {{ monitor.jsonPath }} +
+ {{ $t("Expected Value") }}: {{ monitor.expectedValue }} +
[{{ monitor.dns_resolve_type }}] {{ monitor.hostname }}
{{ $t("Last Result") }}: {{ monitor.dns_last_result }} @@ -434,7 +440,7 @@ export default { 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"); } diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index de93f043bd..1ce6227932 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -27,6 +27,9 @@ + @@ -97,7 +100,7 @@ -

+
@@ -138,6 +141,20 @@
+ +
+ + + + +
+
+
+ + + +
+
@@ -367,7 +384,7 @@

{{ $t("Advanced") }}

-
+
-
+