diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index 617f8bee1..a6844df60 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -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 diff --git a/db/knex_migrations/2026-01-15-0000-add-json-query-retry-only-status-code.js b/db/knex_migrations/2026-01-15-0000-add-json-query-retry-only-status-code.js new file mode 100644 index 000000000..dd5f0955a --- /dev/null +++ b/db/knex_migrations/2026-01-15-0000-add-json-query-retry-only-status-code.js @@ -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"); + }); +}; diff --git a/server/model/monitor.js b/server/model/monitor.js index b797b519b..3b8505ae9 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -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++; + } } } diff --git a/server/server.js b/server/server.js index 5efd726ff..885e88340 100644 --- a/server/server.js +++ b/server/server.js @@ -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; diff --git a/server/uptime-calculator.js b/server/uptime-calculator.js index 94d7e7733..d0ad17425 100644 --- a/server/uptime-calculator.js +++ b/server/uptime-calculator.js @@ -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")), diff --git a/src/lang/en.json b/src/lang/en.json index 2ec25d1f7..e68cc6be9 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -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", diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 399ca9dd2..c87978a67 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -1167,6 +1167,24 @@ + +