Merge branch 'master' into feature/response-body
This commit is contained in:
commit
d77e1c6d61
4
.github/workflows/autofix.yml
vendored
4
.github/workflows/autofix.yml
vendored
@ -42,4 +42,8 @@ jobs:
|
||||
run: npm run fmt
|
||||
continue-on-error: true
|
||||
|
||||
- name: Compile TypeScript
|
||||
run: npm run tsc
|
||||
continue-on-error: true
|
||||
|
||||
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
exports.up = function (knex) {
|
||||
// Add new column to table monitor for json-query retry behavior
|
||||
return knex.schema.alterTable("monitor", function (table) {
|
||||
table.boolean("retry_only_on_status_code_failure").defaultTo(false).notNullable();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.alterTable("monitor", function (table) {
|
||||
table.dropColumn("retry_only_on_status_code_failure");
|
||||
});
|
||||
};
|
||||
@ -147,6 +147,7 @@ class Monitor extends BeanModel {
|
||||
timeout: this.timeout,
|
||||
interval: this.interval,
|
||||
retryInterval: this.retryInterval,
|
||||
retryOnlyOnStatusCodeFailure: Boolean(this.retry_only_on_status_code_failure),
|
||||
resendInterval: this.resendInterval,
|
||||
keyword: this.keyword,
|
||||
invertKeyword: this.isInvertKeyword(),
|
||||
@ -966,12 +967,32 @@ class Monitor extends BeanModel {
|
||||
// Just reset the retries
|
||||
if (this.isUpsideDown() && bean.status === UP) {
|
||||
retries = 0;
|
||||
} else if (this.maxretries > 0 && retries < this.maxretries) {
|
||||
retries++;
|
||||
bean.status = PENDING;
|
||||
} else if (this.type === "json-query" && this.retry_only_on_status_code_failure) {
|
||||
// For json-query monitors with retry_only_on_status_code_failure enabled,
|
||||
// only retry if the error is NOT from JSON query evaluation
|
||||
// JSON query errors have the message "JSON query does not pass..."
|
||||
const isJsonQueryError =
|
||||
typeof error.message === "string" && error.message.includes("JSON query does not pass");
|
||||
|
||||
if (isJsonQueryError) {
|
||||
// Don't retry on JSON query failures, mark as DOWN immediately
|
||||
retries = 0;
|
||||
} else if (this.maxretries > 0 && retries < this.maxretries) {
|
||||
retries++;
|
||||
bean.status = PENDING;
|
||||
} else {
|
||||
// Continue counting retries during DOWN
|
||||
retries++;
|
||||
}
|
||||
} else {
|
||||
// Continue counting retries during DOWN
|
||||
retries++;
|
||||
// General retry logic for all other monitor types
|
||||
if (this.maxretries > 0 && retries < this.maxretries) {
|
||||
retries++;
|
||||
bean.status = PENDING;
|
||||
} else {
|
||||
// Continue counting retries during DOWN
|
||||
retries++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -753,6 +753,10 @@ let needSetup = false;
|
||||
}
|
||||
|
||||
bean.import(monitor);
|
||||
// Map camelCase frontend property to snake_case database column
|
||||
if (monitor.retryOnlyOnStatusCodeFailure !== undefined) {
|
||||
bean.retry_only_on_status_code_failure = monitor.retryOnlyOnStatusCodeFailure;
|
||||
}
|
||||
bean.user_id = socket.userID;
|
||||
|
||||
bean.validate();
|
||||
@ -908,6 +912,7 @@ let needSetup = false;
|
||||
bean.snmpVersion = monitor.snmpVersion;
|
||||
bean.snmpOid = monitor.snmpOid;
|
||||
bean.jsonPathOperator = monitor.jsonPathOperator;
|
||||
bean.retry_only_on_status_code_failure = Boolean(monitor.retryOnlyOnStatusCodeFailure);
|
||||
bean.timeout = monitor.timeout;
|
||||
bean.rabbitmqNodes = JSON.stringify(monitor.rabbitmqNodes);
|
||||
bean.rabbitmqUsername = monitor.rabbitmqUsername;
|
||||
|
||||
@ -217,7 +217,7 @@ class UptimeCalculator {
|
||||
let flatStatus = this.flatStatus(status);
|
||||
|
||||
if (flatStatus === DOWN && ping > 0) {
|
||||
log.debug("uptime-calc", "The ping is not effective when the status is DOWN");
|
||||
log.debug("uptime_calc", "The ping is not effective when the status is DOWN");
|
||||
}
|
||||
|
||||
let divisionKey = this.getMinutelyKey(date);
|
||||
@ -295,7 +295,7 @@ class UptimeCalculator {
|
||||
|
||||
// Don't store data in test mode
|
||||
if (process.env.TEST_BACKEND) {
|
||||
log.debug("uptime-calc", "Skip storing data in test mode");
|
||||
log.debug("uptime_calc", "Skip storing data in test mode");
|
||||
return date;
|
||||
}
|
||||
|
||||
@ -358,7 +358,7 @@ class UptimeCalculator {
|
||||
if (!this.migrationMode) {
|
||||
// Remove the old data
|
||||
// TODO: Improvement: Convert it to a job?
|
||||
log.debug("uptime-calc", "Remove old data");
|
||||
log.debug("uptime_calc", "Remove old data");
|
||||
await R.exec("DELETE FROM stat_minutely WHERE monitor_id = ? AND timestamp < ?", [
|
||||
this.monitorID,
|
||||
this.getMinutelyKey(currentDate.subtract(this.statMinutelyKeepHour, "hour")),
|
||||
|
||||
@ -86,6 +86,8 @@
|
||||
"resendEveryXTimes": "Resend every {0} times",
|
||||
"resendDisabled": "Resend disabled",
|
||||
"retriesDescription": "Maximum retries before the service is marked as down and a notification is sent",
|
||||
"Only retry if status code check fails": "Only retry if status code check fails",
|
||||
"retryOnlyOnStatusCodeFailureDescription": "If enabled, retries will only occur when the HTTP status code check fails (e.g., server is down). If the status code check passes but the JSON query fails, the monitor will be marked as down immediately without retries.",
|
||||
"ignoredTLSError": "TLS/SSL errors have been ignored",
|
||||
"ignoreTLSError": "Ignore TLS/SSL errors for HTTPS websites",
|
||||
"ignoreTLSErrorGeneral": "Ignore TLS/SSL error for connection",
|
||||
|
||||
@ -1167,6 +1167,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Retry only on status code failure: JSON Query only -->
|
||||
<div v-if="monitor.type === 'json-query' && monitor.maxretries > 0" class="my-3">
|
||||
<div class="form-check">
|
||||
<input
|
||||
id="retry-only-on-status-code-failure"
|
||||
v-model="monitor.retryOnlyOnStatusCodeFailure"
|
||||
type="checkbox"
|
||||
class="form-check-input"
|
||||
/>
|
||||
<label for="retry-only-on-status-code-failure" class="form-check-label">
|
||||
{{ $t("Only retry if status code check fails") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
{{ $t("retryOnlyOnStatusCodeFailureDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timeout: HTTP / JSON query / Keyword / Ping / RabbitMQ / SNMP / Websocket Upgrade only -->
|
||||
<div
|
||||
v-if="
|
||||
@ -2240,6 +2258,7 @@ const monitorDefaults = {
|
||||
retryInterval: 60,
|
||||
resendInterval: 0,
|
||||
maxretries: 0,
|
||||
retryOnlyOnStatusCodeFailure: false,
|
||||
notificationIDList: {},
|
||||
ignoreTls: false,
|
||||
upsideDown: false,
|
||||
|
||||
154
src/util.js
154
src/util.js
@ -10,97 +10,12 @@
|
||||
*/
|
||||
var _a;
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.CONSOLE_STYLE_FgPink =
|
||||
exports.CONSOLE_STYLE_FgBrown =
|
||||
exports.CONSOLE_STYLE_FgViolet =
|
||||
exports.CONSOLE_STYLE_FgLightBlue =
|
||||
exports.CONSOLE_STYLE_FgLightGreen =
|
||||
exports.CONSOLE_STYLE_FgOrange =
|
||||
exports.CONSOLE_STYLE_FgGray =
|
||||
exports.CONSOLE_STYLE_FgWhite =
|
||||
exports.CONSOLE_STYLE_FgCyan =
|
||||
exports.CONSOLE_STYLE_FgMagenta =
|
||||
exports.CONSOLE_STYLE_FgBlue =
|
||||
exports.CONSOLE_STYLE_FgYellow =
|
||||
exports.CONSOLE_STYLE_FgGreen =
|
||||
exports.CONSOLE_STYLE_FgRed =
|
||||
exports.CONSOLE_STYLE_FgBlack =
|
||||
exports.CONSOLE_STYLE_Hidden =
|
||||
exports.CONSOLE_STYLE_Reverse =
|
||||
exports.CONSOLE_STYLE_Blink =
|
||||
exports.CONSOLE_STYLE_Underscore =
|
||||
exports.CONSOLE_STYLE_Dim =
|
||||
exports.CONSOLE_STYLE_Bright =
|
||||
exports.CONSOLE_STYLE_Reset =
|
||||
exports.PING_PER_REQUEST_TIMEOUT_DEFAULT =
|
||||
exports.PING_PER_REQUEST_TIMEOUT_MAX =
|
||||
exports.PING_PER_REQUEST_TIMEOUT_MIN =
|
||||
exports.PING_COUNT_DEFAULT =
|
||||
exports.PING_COUNT_MAX =
|
||||
exports.PING_COUNT_MIN =
|
||||
exports.PING_GLOBAL_TIMEOUT_DEFAULT =
|
||||
exports.PING_GLOBAL_TIMEOUT_MAX =
|
||||
exports.PING_GLOBAL_TIMEOUT_MIN =
|
||||
exports.PING_PACKET_SIZE_DEFAULT =
|
||||
exports.PING_PACKET_SIZE_MAX =
|
||||
exports.PING_PACKET_SIZE_MIN =
|
||||
exports.MIN_INTERVAL_SECOND =
|
||||
exports.MAX_INTERVAL_SECOND =
|
||||
exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND =
|
||||
exports.SQL_DATETIME_FORMAT =
|
||||
exports.SQL_DATE_FORMAT =
|
||||
exports.STATUS_PAGE_MAINTENANCE =
|
||||
exports.STATUS_PAGE_PARTIAL_DOWN =
|
||||
exports.STATUS_PAGE_ALL_UP =
|
||||
exports.STATUS_PAGE_ALL_DOWN =
|
||||
exports.MAINTENANCE =
|
||||
exports.PENDING =
|
||||
exports.UP =
|
||||
exports.DOWN =
|
||||
exports.appName =
|
||||
exports.isNode =
|
||||
exports.isDev =
|
||||
void 0;
|
||||
exports.TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD =
|
||||
exports.evaluateJsonQuery =
|
||||
exports.intHash =
|
||||
exports.localToUTC =
|
||||
exports.utcToLocal =
|
||||
exports.utcToISODateTime =
|
||||
exports.isoToUTCDateTime =
|
||||
exports.parseTimeFromTimeObject =
|
||||
exports.parseTimeObject =
|
||||
exports.getMonitorRelativeURL =
|
||||
exports.genSecret =
|
||||
exports.getCryptoRandomInt =
|
||||
exports.getRandomInt =
|
||||
exports.getRandomArbitrary =
|
||||
exports.TimeLogger =
|
||||
exports.polyfill =
|
||||
exports.log =
|
||||
exports.debug =
|
||||
exports.ucfirst =
|
||||
exports.sleep =
|
||||
exports.flipStatus =
|
||||
exports.badgeConstants =
|
||||
exports.CONSOLE_STYLE_BgGray =
|
||||
exports.CONSOLE_STYLE_BgWhite =
|
||||
exports.CONSOLE_STYLE_BgCyan =
|
||||
exports.CONSOLE_STYLE_BgMagenta =
|
||||
exports.CONSOLE_STYLE_BgBlue =
|
||||
exports.CONSOLE_STYLE_BgYellow =
|
||||
exports.CONSOLE_STYLE_BgGreen =
|
||||
exports.CONSOLE_STYLE_BgRed =
|
||||
exports.CONSOLE_STYLE_BgBlack =
|
||||
void 0;
|
||||
exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = exports.CONSOLE_STYLE_FgViolet = exports.CONSOLE_STYLE_FgLightBlue = exports.CONSOLE_STYLE_FgLightGreen = exports.CONSOLE_STYLE_FgOrange = exports.CONSOLE_STYLE_FgGray = exports.CONSOLE_STYLE_FgWhite = exports.CONSOLE_STYLE_FgCyan = exports.CONSOLE_STYLE_FgMagenta = exports.CONSOLE_STYLE_FgBlue = exports.CONSOLE_STYLE_FgYellow = exports.CONSOLE_STYLE_FgGreen = exports.CONSOLE_STYLE_FgRed = exports.CONSOLE_STYLE_FgBlack = exports.CONSOLE_STYLE_Hidden = exports.CONSOLE_STYLE_Reverse = exports.CONSOLE_STYLE_Blink = exports.CONSOLE_STYLE_Underscore = exports.CONSOLE_STYLE_Dim = exports.CONSOLE_STYLE_Bright = exports.CONSOLE_STYLE_Reset = exports.PING_PER_REQUEST_TIMEOUT_DEFAULT = exports.PING_PER_REQUEST_TIMEOUT_MAX = exports.PING_PER_REQUEST_TIMEOUT_MIN = exports.PING_COUNT_DEFAULT = exports.PING_COUNT_MAX = exports.PING_COUNT_MIN = exports.PING_GLOBAL_TIMEOUT_DEFAULT = exports.PING_GLOBAL_TIMEOUT_MAX = exports.PING_GLOBAL_TIMEOUT_MIN = exports.PING_PACKET_SIZE_DEFAULT = exports.PING_PACKET_SIZE_MAX = exports.PING_PACKET_SIZE_MIN = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isNode = exports.isDev = void 0;
|
||||
exports.TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD = exports.evaluateJsonQuery = exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.badgeConstants = exports.CONSOLE_STYLE_BgGray = exports.CONSOLE_STYLE_BgWhite = exports.CONSOLE_STYLE_BgCyan = exports.CONSOLE_STYLE_BgMagenta = exports.CONSOLE_STYLE_BgBlue = exports.CONSOLE_STYLE_BgYellow = exports.CONSOLE_STYLE_BgGreen = exports.CONSOLE_STYLE_BgRed = exports.CONSOLE_STYLE_BgBlack = void 0;
|
||||
const dayjs_1 = require("dayjs");
|
||||
const jsonata = require("jsonata");
|
||||
exports.isDev = process.env.NODE_ENV === "development";
|
||||
exports.isNode =
|
||||
typeof process !== "undefined" &&
|
||||
((_a = process === null || process === void 0 ? void 0 : process.versions) === null || _a === void 0
|
||||
? void 0
|
||||
: _a.node);
|
||||
exports.isNode = typeof process !== "undefined" && ((_a = process === null || process === void 0 ? void 0 : process.versions) === null || _a === void 0 ? void 0 : _a.node);
|
||||
const dayjs = exports.isNode ? require("dayjs") : dayjs_1.default;
|
||||
exports.appName = "Uptime Kuma";
|
||||
exports.DOWN = 0;
|
||||
@ -253,7 +168,8 @@ class Logger {
|
||||
let now;
|
||||
if (dayjs.tz) {
|
||||
now = dayjs.tz(new Date()).format();
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
now = dayjs().format();
|
||||
}
|
||||
const levelColor = consoleLevelColors[level];
|
||||
@ -272,7 +188,8 @@ class Logger {
|
||||
}
|
||||
modulePart = "[" + moduleColor + module + exports.CONSOLE_STYLE_Reset + "]";
|
||||
levelPart = levelColor + `${level}:` + exports.CONSOLE_STYLE_Reset;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
timePart = now;
|
||||
modulePart = `[${module}]`;
|
||||
levelPart = `${level}:`;
|
||||
@ -346,21 +263,21 @@ function getRandomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
exports.getRandomInt = getRandomInt;
|
||||
const getRandomBytes = (
|
||||
typeof window !== "undefined" && window.crypto
|
||||
? function () {
|
||||
return (numBytes) => {
|
||||
const randomBytes = new Uint8Array(numBytes);
|
||||
for (let i = 0; i < numBytes; i += 65536) {
|
||||
window.crypto.getRandomValues(randomBytes.subarray(i, i + Math.min(numBytes - i, 65536)));
|
||||
}
|
||||
return randomBytes;
|
||||
};
|
||||
}
|
||||
: function () {
|
||||
return require("crypto").randomBytes;
|
||||
}
|
||||
)();
|
||||
const getRandomBytes = (typeof window !== "undefined" && window.crypto
|
||||
?
|
||||
function () {
|
||||
return (numBytes) => {
|
||||
const randomBytes = new Uint8Array(numBytes);
|
||||
for (let i = 0; i < numBytes; i += 65536) {
|
||||
window.crypto.getRandomValues(randomBytes.subarray(i, i + Math.min(numBytes - i, 65536)));
|
||||
}
|
||||
return randomBytes;
|
||||
};
|
||||
}
|
||||
:
|
||||
function () {
|
||||
return require("crypto").randomBytes;
|
||||
})();
|
||||
function getCryptoRandomInt(min, max) {
|
||||
const range = max - min;
|
||||
if (range >= Math.pow(2, 32)) {
|
||||
@ -386,7 +303,8 @@ function getCryptoRandomInt(min, max) {
|
||||
randomValue = randomValue & mask;
|
||||
if (randomValue <= range) {
|
||||
return min + randomValue;
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
return getCryptoRandomInt(min, max);
|
||||
}
|
||||
}
|
||||
@ -467,7 +385,8 @@ async function evaluateJsonQuery(data, jsonPath, jsonPathOperator, expectedValue
|
||||
let response;
|
||||
try {
|
||||
response = JSON.parse(data);
|
||||
} catch (_a) {
|
||||
}
|
||||
catch (_a) {
|
||||
response =
|
||||
(typeof data === "object" || typeof data === "number") && !Buffer.isBuffer(data) ? data : data.toString();
|
||||
}
|
||||
@ -479,17 +398,13 @@ async function evaluateJsonQuery(data, jsonPath, jsonPathOperator, expectedValue
|
||||
if (Array.isArray(response)) {
|
||||
const responseStr = JSON.stringify(response);
|
||||
const truncatedResponse = responseStr.length > 25 ? responseStr.substring(0, 25) + "...]" : responseStr;
|
||||
throw new Error(
|
||||
"JSON query returned the array " +
|
||||
truncatedResponse +
|
||||
", but a primitive value is required. " +
|
||||
"Modify your query to return a single value via [0] to get the first element or use an aggregation like $count(), $sum() or $boolean()."
|
||||
);
|
||||
throw new Error("JSON query returned the array " +
|
||||
truncatedResponse +
|
||||
", but a primitive value is required. " +
|
||||
"Modify your query to return a single value via [0] to get the first element or use an aggregation like $count(), $sum() or $boolean().");
|
||||
}
|
||||
if (typeof response === "object" || response instanceof Date || typeof response === "function") {
|
||||
throw new Error(
|
||||
`The post-JSON query evaluated response from the server is of type ${typeof response}, which cannot be directly compared to the expected value`
|
||||
);
|
||||
throw new Error(`The post-JSON query evaluated response from the server is of type ${typeof response}, which cannot be directly compared to the expected value`);
|
||||
}
|
||||
let jsonQueryExpression;
|
||||
switch (jsonPathOperator) {
|
||||
@ -517,15 +432,14 @@ async function evaluateJsonQuery(data, jsonPath, jsonPathOperator, expectedValue
|
||||
expected: expectedValue.toString(),
|
||||
});
|
||||
if (status === undefined) {
|
||||
throw new Error(
|
||||
"Query evaluation returned undefined. Check query syntax and the structure of the response data"
|
||||
);
|
||||
throw new Error("Query evaluation returned undefined. Check query syntax and the structure of the response data");
|
||||
}
|
||||
return {
|
||||
status,
|
||||
response,
|
||||
};
|
||||
} catch (err) {
|
||||
}
|
||||
catch (err) {
|
||||
response = JSON.stringify(response);
|
||||
response = response && response.length > 50 ? `${response.substring(0, 100)}… (truncated)` : response;
|
||||
throw new Error(`Error evaluating JSON query: ${err.message}. Response from server was: ${response}`);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user