mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-27 16:54:04 +00:00
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)
This commit is contained in:
parent
eaa935cba0
commit
fdc145bffd
6 changed files with 61 additions and 78 deletions
|
@ -600,23 +600,12 @@ class Monitor extends BeanModel {
|
||||||
} else if (this.type === "json-query") {
|
} else if (this.type === "json-query") {
|
||||||
let data = res.data;
|
let data = res.data;
|
||||||
|
|
||||||
// convert data to object
|
const { status, response } = await evaluateJsonQuery(data, this.jsonPath, this.jsonPathOperator, this.expectedValue);
|
||||||
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, 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") {
|
} else if (this.type === "port") {
|
||||||
|
|
|
@ -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.
|
// 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 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.status = status ? UP : DOWN;
|
||||||
heartbeat.msg = `SNMP value ${status ? "passes" : "does not pass"} `;
|
heartbeat.msg = `SNMP value ${status ? "passes" : "does not pass"} `;
|
||||||
heartbeat.msg += (monitor.jsonPathOperator === "custom")
|
heartbeat.msg += `comparison: ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue}.`;
|
||||||
? `custom query. Query result: ${evaluation}. Expected Value: ${monitor.expectedValue}.`
|
|
||||||
: `comparison: ${value.toString()} ${monitor.jsonPathOperator} ${monitor.expectedValue}.`;
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
heartbeat.status = DOWN;
|
heartbeat.status = DOWN;
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
"Keyword": "Keyword",
|
"Keyword": "Keyword",
|
||||||
"Invert Keyword": "Invert Keyword",
|
"Invert Keyword": "Invert Keyword",
|
||||||
"Expected Value": "Expected Value",
|
"Expected Value": "Expected Value",
|
||||||
"Custom Json Query Expression": "Custom Json Query Expression",
|
"Json Query Expression": "Json Query Expression",
|
||||||
"Friendly Name": "Friendly Name",
|
"Friendly Name": "Friendly Name",
|
||||||
"URL": "URL",
|
"URL": "URL",
|
||||||
"Hostname or IP Address": "Hostname or IP Address",
|
"Hostname or IP Address": "Hostname or IP Address",
|
||||||
|
@ -577,7 +577,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": "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.",
|
"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.",
|
||||||
|
|
|
@ -277,13 +277,13 @@
|
||||||
<!-- Json Query -->
|
<!-- Json Query -->
|
||||||
<!-- For Json Query / SNMP -->
|
<!-- For Json Query / SNMP -->
|
||||||
<div v-if="monitor.type === 'json-query' || monitor.type === 'snmp'" class="my-3">
|
<div v-if="monitor.type === 'json-query' || monitor.type === 'snmp'" class="my-3">
|
||||||
<div v-if="monitor.jsonPathOperator == 'custom'" class="my-2">
|
<div class="my-2">
|
||||||
<label for="jsonPath" class="form-label mb-0">{{ $t("Json Query Expression") }}</label>
|
<label for="jsonPath" class="form-label mb-0">{{ $t("Json Query Expression") }}</label>
|
||||||
<i18n-t tag="div" class="form-text mb-2" keypath="jsonQueryDescription">
|
<i18n-t tag="div" class="form-text mb-2" keypath="jsonQueryDescription">
|
||||||
<a href="https://jsonata.org/">jsonata.org</a>
|
<a href="https://jsonata.org/">jsonata.org</a>
|
||||||
<a href="https://try.jsonata.org/">{{ $t('playground') }}</a>
|
<a href="https://try.jsonata.org/">{{ $t('playground') }}</a>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
<input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control" placeholder="$.value" required>
|
<input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control" placeholder="$" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-flex align-items-start">
|
<div class="d-flex align-items-start">
|
||||||
|
@ -294,14 +294,14 @@
|
||||||
<option value=">=">>=</option>
|
<option value=">=">>=</option>
|
||||||
<option value="<"><</option>
|
<option value="<"><</option>
|
||||||
<option value="<="><=</option>
|
<option value="<="><=</option>
|
||||||
|
<option value="!=">!=</option>
|
||||||
<option value="==">==</option>
|
<option value="==">==</option>
|
||||||
<option value="contains">contains</option>
|
<option value="contains">contains</option>
|
||||||
<option value="custom">custom</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
<label for="expectedValue" class="form-label">{{ $t("Expected Value (Control)") }}</label>
|
<label for="expectedValue" class="form-label">{{ $t("Expected Value") }}</label>
|
||||||
<input v-if="monitor.jsonPathOperator !== 'contains' && monitor.jsonPathOperator !== '==' && monitor.jsonPathOperator !== 'custom'" id="expectedValue" v-model="monitor.expectedValue" type="number" class="form-control" required step=".01">
|
<input v-if="monitor.jsonPathOperator !== 'contains' && monitor.jsonPathOperator !== '=='" id="expectedValue" v-model="monitor.expectedValue" type="number" class="form-control" required step=".01">
|
||||||
<input v-else id="expectedValue" v-model="monitor.expectedValue" type="text" class="form-control" required>
|
<input v-else id="expectedValue" v-model="monitor.expectedValue" type="text" class="form-control" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1330,10 +1330,13 @@ message HealthCheckResponse {
|
||||||
this.monitor.snmpVersion = "1";
|
this.monitor.snmpVersion = "1";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set default jsonPath
|
||||||
|
if (!this.monitor.jsonPath) {
|
||||||
|
this.monitor.jsonPath = "$";
|
||||||
|
}
|
||||||
|
|
||||||
// Set default condition for for jsonPathOperator
|
// Set default condition for for jsonPathOperator
|
||||||
if (this.monitor.type === "json-query") {
|
if (!this.monitor.jsonPathOperator) {
|
||||||
this.monitor.jsonPathOperator = "custom";
|
|
||||||
} else {
|
|
||||||
this.monitor.jsonPathOperator = "==";
|
this.monitor.jsonPathOperator = "==";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
40
src/util.js
40
src/util.js
|
@ -398,7 +398,6 @@ function intHash(str, length = 10) {
|
||||||
}
|
}
|
||||||
exports.intHash = intHash;
|
exports.intHash = intHash;
|
||||||
async function evaluateJsonQuery(data, jsonPath, jsonPathOperator, expectedValue) {
|
async function evaluateJsonQuery(data, jsonPath, jsonPathOperator, expectedValue) {
|
||||||
const expected = isNaN(expectedValue) ? expectedValue.toString() : parseFloat(expectedValue);
|
|
||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = JSON.parse(data);
|
response = JSON.parse(data);
|
||||||
|
@ -406,46 +405,43 @@ async function evaluateJsonQuery(data, jsonPath, jsonPathOperator, expectedValue
|
||||||
catch (_a) {
|
catch (_a) {
|
||||||
response = typeof data === "number" || typeof data === "object" ? data : data.toString();
|
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;
|
let jsonQueryExpression;
|
||||||
switch (jsonPathOperator) {
|
switch (jsonPathOperator) {
|
||||||
case ">":
|
case ">":
|
||||||
case ">=":
|
case ">=":
|
||||||
case "<":
|
case "<":
|
||||||
case "<=":
|
case "<=":
|
||||||
jsonQueryExpression = `$.value ${jsonPathOperator} $.control`;
|
case "!=":
|
||||||
|
jsonQueryExpression = `$.value ${jsonPathOperator} $.expected`;
|
||||||
break;
|
break;
|
||||||
case "==":
|
case "==":
|
||||||
jsonQueryExpression = "$string($.value) = $string($.control)";
|
jsonQueryExpression = "$string($.value) = $string($.expected)";
|
||||||
break;
|
break;
|
||||||
case "contains":
|
case "contains":
|
||||||
jsonQueryExpression = "$contains($string($.value), $string($.control))";
|
jsonQueryExpression = "$contains($string($.value), $string($.expected))";
|
||||||
break;
|
|
||||||
case "custom":
|
|
||||||
jsonQueryExpression = jsonPath;
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Invalid condition ${jsonPathOperator}`);
|
throw new Error(`Invalid condition ${jsonPathOperator}`);
|
||||||
}
|
}
|
||||||
const expression = jsonata(jsonQueryExpression);
|
const expression = jsonata(jsonQueryExpression);
|
||||||
let evaluation;
|
const status = await expression.evaluate({
|
||||||
if (jsonPathOperator === "custom") {
|
value: response.toString(),
|
||||||
evaluation = await expression.evaluate(response);
|
expected: expectedValue.toString()
|
||||||
}
|
});
|
||||||
else {
|
if (response === undefined || status === undefined) {
|
||||||
evaluation = await expression.evaluate({
|
|
||||||
value: response,
|
|
||||||
control: expectedValue
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (evaluation === undefined) {
|
|
||||||
throw new Error("Query evaluation returned undefined. Check your query syntax and the structure of the response data.");
|
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 {
|
return {
|
||||||
status,
|
status,
|
||||||
evaluation
|
response
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
exports.evaluateJsonQuery = evaluateJsonQuery;
|
exports.evaluateJsonQuery = evaluateJsonQuery;
|
||||||
|
|
51
src/util.ts
51
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.
|
* @returns An object containing the status and the evaluation result.
|
||||||
* @throws Error if the evaluation returns undefined.
|
* @throws Error if the evaluation returns undefined.
|
||||||
*/
|
*/
|
||||||
export async function evaluateJsonQuery(data: any, jsonPath: string, jsonPathOperator: string, expectedValue: any): Promise<{ status: boolean; evaluation: any }> {
|
export async function evaluateJsonQuery(data: any, jsonPath: string, jsonPathOperator: string, expectedValue: any): Promise<{ status: boolean; response: any }> {
|
||||||
// Check if inputs are numeric. If not, re-parse as strings. This ensures comparisons are handled correctly.
|
// Attempt to parse data as JSON; if unsuccessful, handle based on data type.
|
||||||
const expected = isNaN(expectedValue) ? expectedValue.toString() : parseFloat(expectedValue);
|
|
||||||
|
|
||||||
let response: any;
|
let response: any;
|
||||||
try {
|
try {
|
||||||
response = JSON.parse(data);
|
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();
|
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;
|
let jsonQueryExpression;
|
||||||
switch (jsonPathOperator) {
|
switch (jsonPathOperator) {
|
||||||
case ">":
|
case ">":
|
||||||
case ">=":
|
case ">=":
|
||||||
case "<":
|
case "<":
|
||||||
case "<=":
|
case "<=":
|
||||||
jsonQueryExpression = `$.value ${jsonPathOperator} $.control`;
|
case "!=":
|
||||||
|
jsonQueryExpression = `$.value ${jsonPathOperator} $.expected`;
|
||||||
break;
|
break;
|
||||||
case "==":
|
case "==":
|
||||||
jsonQueryExpression = "$string($.value) = $string($.control)";
|
jsonQueryExpression = "$string($.value) = $string($.expected)";
|
||||||
break;
|
break;
|
||||||
case "contains":
|
case "contains":
|
||||||
jsonQueryExpression = "$contains($string($.value), $string($.control))";
|
jsonQueryExpression = "$contains($string($.value), $string($.expected))";
|
||||||
break;
|
|
||||||
case "custom":
|
|
||||||
jsonQueryExpression = jsonPath;
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Invalid condition ${jsonPathOperator}`);
|
throw new Error(`Invalid condition ${jsonPathOperator}`);
|
||||||
|
@ -688,27 +695,17 @@ export async function evaluateJsonQuery(data: any, jsonPath: string, jsonPathOpe
|
||||||
|
|
||||||
// Evaluate the JSON Query Expression
|
// Evaluate the JSON Query Expression
|
||||||
const expression = jsonata(jsonQueryExpression);
|
const expression = jsonata(jsonQueryExpression);
|
||||||
|
const status = await expression.evaluate({
|
||||||
|
value: response.toString(),
|
||||||
|
expected: expectedValue.toString()
|
||||||
|
});
|
||||||
|
|
||||||
let evaluation;
|
if (response === undefined || status === undefined) {
|
||||||
if (jsonPathOperator === "custom") {
|
|
||||||
evaluation = await expression.evaluate(response);
|
|
||||||
} else {
|
|
||||||
evaluation = await expression.evaluate({
|
|
||||||
value: response,
|
|
||||||
control: expectedValue
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (evaluation === undefined) {
|
|
||||||
throw new Error("Query evaluation returned undefined. Check your query syntax and the structure of the response data.");
|
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 {
|
return {
|
||||||
status,
|
status, // The evaluation of the json query
|
||||||
evaluation
|
response // The response from the server or result from initial json-query evaluation
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue