From fdc145bffd8009e9e0fab72fdf24c208ad503564 Mon Sep 17 00:00:00 2001 From: Matt Visnovsky Date: Thu, 6 Jun 2024 18:52:33 -0600 Subject: [PATCH] Added Robustness There are a lot of changes here: -Fixed a lot of issues encountered during my testing -JSON path is evaluated BEFORE making comparisons (this was the true intended behavior by @chakflying) -Variable name changes (cosmetic) -Added != operator -Changed jsonQueryDescription (again) --- server/model/monitor.js | 19 +++----------- server/monitor-types/snmp.js | 6 ++--- src/lang/en.json | 4 +-- src/pages/EditMonitor.vue | 19 ++++++++------ src/util.js | 40 +++++++++++++--------------- src/util.ts | 51 +++++++++++++++++------------------- 6 files changed, 61 insertions(+), 78 deletions(-) diff --git a/server/model/monitor.js b/server/model/monitor.js index bd155128f..2ef7aae9e 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -600,23 +600,12 @@ class Monitor extends BeanModel { } else if (this.type === "json-query") { let data = res.data; - // convert data to object - if (typeof data === "string" && res.headers["content-type"] !== "application/json") { - try { - data = JSON.parse(data); - } catch (_) { - // Failed to parse as JSON, just process it as a string - } - } + const { status, response } = await evaluateJsonQuery(data, this.jsonPath, this.jsonPathOperator, this.expectedValue); - const { status, evaluation } = await evaluateJsonQuery(data, this.jsonPath, this.jsonPathOperator, this.expectedValue); + bean.status = status ? UP : DOWN; + bean.msg = `JSON query ${status ? "passes" : "does not pass"} `; + bean.msg += `comparison: ${response} ${this.jsonPathOperator} ${this.expectedValue}.`; - if (status) { - 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: [${evaluation}]`); - } } } else if (this.type === "port") { diff --git a/server/monitor-types/snmp.js b/server/monitor-types/snmp.js index f3f67bc46..5168cc9ad 100644 --- a/server/monitor-types/snmp.js +++ b/server/monitor-types/snmp.js @@ -44,13 +44,11 @@ class SNMPMonitorType extends MonitorType { // We restrict querying to one OID per monitor, therefore `varbinds[0]` will always contain the value we're interested in. const value = varbinds[0].value; - const { status, evaluation } = await evaluateJsonQuery(value, monitor.jsonPath, monitor.jsonPathOperator, monitor.expectedValue); + const { status, response } = await evaluateJsonQuery(value, monitor.jsonPath, monitor.jsonPathOperator, monitor.expectedValue); heartbeat.status = status ? UP : DOWN; heartbeat.msg = `SNMP value ${status ? "passes" : "does not pass"} `; - heartbeat.msg += (monitor.jsonPathOperator === "custom") - ? `custom query. Query result: ${evaluation}. Expected Value: ${monitor.expectedValue}.` - : `comparison: ${value.toString()} ${monitor.jsonPathOperator} ${monitor.expectedValue}.`; + heartbeat.msg += `comparison: ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue}.`; } catch (err) { heartbeat.status = DOWN; diff --git a/src/lang/en.json b/src/lang/en.json index d1ed2f1b0..1b2de3146 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -59,7 +59,7 @@ "Keyword": "Keyword", "Invert Keyword": "Invert Keyword", "Expected Value": "Expected Value", - "Custom Json Query Expression": "Custom Json Query Expression", + "Json Query Expression": "Json Query Expression", "Friendly Name": "Friendly Name", "URL": "URL", "Hostname or IP Address": "Hostname or IP Address", @@ -577,7 +577,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": "Use JSON query to parse and extract specific data from the server's JSON response. Compare the evaluated query against the expected value after converting it into a string. Refer to {0} for detailed documentation on the query language or experiment with queries using the {1}.", + "jsonQueryDescription": "Parse and extract specific data from the server's JSON response using JSON query or use \"$\" for the raw response, if not expecting JSON. The result is then compared to the expected value, as strings. See {0} for documentation and use {1} to experiment with queries.", "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/EditMonitor.vue b/src/pages/EditMonitor.vue index 6db318bd7..5f64c4c96 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -277,13 +277,13 @@
-
+
jsonata.org {{ $t('playground') }} - +
@@ -294,14 +294,14 @@ + -
- - + +
@@ -1330,10 +1330,13 @@ message HealthCheckResponse { this.monitor.snmpVersion = "1"; } + // Set default jsonPath + if (!this.monitor.jsonPath) { + this.monitor.jsonPath = "$"; + } + // Set default condition for for jsonPathOperator - if (this.monitor.type === "json-query") { - this.monitor.jsonPathOperator = "custom"; - } else { + if (!this.monitor.jsonPathOperator) { this.monitor.jsonPathOperator = "=="; } diff --git a/src/util.js b/src/util.js index 5340ff7c5..0a576a9a4 100644 --- a/src/util.js +++ b/src/util.js @@ -398,7 +398,6 @@ function intHash(str, length = 10) { } exports.intHash = intHash; async function evaluateJsonQuery(data, jsonPath, jsonPathOperator, expectedValue) { - const expected = isNaN(expectedValue) ? expectedValue.toString() : parseFloat(expectedValue); let response; try { response = JSON.parse(data); @@ -406,46 +405,43 @@ async function evaluateJsonQuery(data, jsonPath, jsonPathOperator, expectedValue catch (_a) { response = typeof data === "number" || typeof data === "object" ? data : data.toString(); } + if (jsonPath && typeof data === "object") { + try { + response = await jsonata(jsonPath).evaluate(response); + } + catch (err) { + throw new Error(`Error evaluating JSON query: ${err.message}`); + } + } let jsonQueryExpression; switch (jsonPathOperator) { case ">": case ">=": case "<": case "<=": - jsonQueryExpression = `$.value ${jsonPathOperator} $.control`; + case "!=": + jsonQueryExpression = `$.value ${jsonPathOperator} $.expected`; break; case "==": - jsonQueryExpression = "$string($.value) = $string($.control)"; + jsonQueryExpression = "$string($.value) = $string($.expected)"; break; case "contains": - jsonQueryExpression = "$contains($string($.value), $string($.control))"; - break; - case "custom": - jsonQueryExpression = jsonPath; + jsonQueryExpression = "$contains($string($.value), $string($.expected))"; break; default: throw new Error(`Invalid condition ${jsonPathOperator}`); } const expression = jsonata(jsonQueryExpression); - let evaluation; - if (jsonPathOperator === "custom") { - evaluation = await expression.evaluate(response); - } - else { - evaluation = await expression.evaluate({ - value: response, - control: expectedValue - }); - } - if (evaluation === undefined) { + const status = await expression.evaluate({ + value: response.toString(), + expected: expectedValue.toString() + }); + if (response === undefined || status === undefined) { throw new Error("Query evaluation returned undefined. Check your query syntax and the structure of the response data."); } - const status = (jsonPathOperator === "custom") - ? evaluation.toString() === expected.toString() - : evaluation; return { status, - evaluation + response }; } exports.evaluateJsonQuery = evaluateJsonQuery; diff --git a/src/util.ts b/src/util.ts index 5af0088e4..94765f187 100644 --- a/src/util.ts +++ b/src/util.ts @@ -654,10 +654,8 @@ export function intHash(str : string, length = 10) : number { * @returns An object containing the status and the evaluation result. * @throws Error if the evaluation returns undefined. */ -export async function evaluateJsonQuery(data: any, jsonPath: string, jsonPathOperator: string, expectedValue: any): Promise<{ status: boolean; evaluation: any }> { - // Check if inputs are numeric. If not, re-parse as strings. This ensures comparisons are handled correctly. - const expected = isNaN(expectedValue) ? expectedValue.toString() : parseFloat(expectedValue); - +export async function evaluateJsonQuery(data: any, jsonPath: string, jsonPathOperator: string, expectedValue: any): Promise<{ status: boolean; response: any }> { + // Attempt to parse data as JSON; if unsuccessful, handle based on data type. let response: any; try { response = JSON.parse(data); @@ -665,22 +663,31 @@ export async function evaluateJsonQuery(data: any, jsonPath: string, jsonPathOpe response = typeof data === "number" || typeof data === "object" ? data : data.toString(); } + // If a JSON path is provided, pre-evaluate the data using it. + if (jsonPath && typeof data === "object") { + try { + response = await jsonata(jsonPath).evaluate(response); + } catch (err: any) { + throw new Error(`Error evaluating JSON query: ${err.message}`); + } + } + + // Perform the comparison logic using the chosen operator + // Perform the comparison logic using the chosen operator let jsonQueryExpression; switch (jsonPathOperator) { case ">": case ">=": case "<": case "<=": - jsonQueryExpression = `$.value ${jsonPathOperator} $.control`; + case "!=": + jsonQueryExpression = `$.value ${jsonPathOperator} $.expected`; break; case "==": - jsonQueryExpression = "$string($.value) = $string($.control)"; + jsonQueryExpression = "$string($.value) = $string($.expected)"; break; case "contains": - jsonQueryExpression = "$contains($string($.value), $string($.control))"; - break; - case "custom": - jsonQueryExpression = jsonPath; + jsonQueryExpression = "$contains($string($.value), $string($.expected))"; break; default: throw new Error(`Invalid condition ${jsonPathOperator}`); @@ -688,27 +695,17 @@ export async function evaluateJsonQuery(data: any, jsonPath: string, jsonPathOpe // Evaluate the JSON Query Expression const expression = jsonata(jsonQueryExpression); + const status = await expression.evaluate({ + value: response.toString(), + expected: expectedValue.toString() + }); - let evaluation; - if (jsonPathOperator === "custom") { - evaluation = await expression.evaluate(response); - } else { - evaluation = await expression.evaluate({ - value: response, - control: expectedValue - }); - } - - if (evaluation === undefined) { + if (response === undefined || status === undefined) { throw new Error("Query evaluation returned undefined. Check your query syntax and the structure of the response data."); } - const status = (jsonPathOperator === "custom") - ? evaluation.toString() === expected.toString() - : evaluation; - return { - status, - evaluation + status, // The evaluation of the json query + response // The response from the server or result from initial json-query evaluation }; }