Feat: Add http/http keyword timeout option (#2142)

* feat: Add timeoutMs field

* chore: Update Languages (incl. ko-KR)

* Revert "chore: Update Languages (incl. ko-KR)"

This reverts commit 349331a00b.

* chore: Update ko-KR selectively

* chore: Update en selectively

* Merge manually

* Reorder and show only if http related monitors

* fix: Update Korean translation

* fix: Rename timeoutMs to timeout, rename label, make DOUBLE

* fix: Change minimum step to 0.1, matching DOUBLE type

* Put the sql patch at the end

* Update EditMonitor.vue

* Colocate timeout with retry, fix clampTimeout logic, show default on 0

* Update src/pages/EditMonitor.vue to remove a comment

Co-authored-by: Frank Elsinga <frank@elsinga.de>

* Fix merge issue

* Update the timeout value while finished editing the interval value

---------

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
This commit is contained in:
Jinhyeok Lee 2023-08-07 01:14:56 +09:00 committed by GitHub
parent c6e68fac97
commit 439b6517d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 56 additions and 4 deletions

View file

@ -0,0 +1,6 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
ALTER TABLE monitor
ADD timeout DOUBLE default 0 not null;
COMMIT;

View file

@ -79,6 +79,7 @@ class Database {
"patch-added-kafka-producer.sql": true, "patch-added-kafka-producer.sql": true,
"patch-add-certificate-expiry-status-page.sql": true, "patch-add-certificate-expiry-status-page.sql": true,
"patch-monitor-oauth-cc.sql": true, "patch-monitor-oauth-cc.sql": true,
"patch-add-timeout-monitor.sql": true,
}; };
/** /**

View file

@ -102,6 +102,7 @@ class Monitor extends BeanModel {
active: await this.isActive(), active: await this.isActive(),
forceInactive: !await Monitor.isParentActive(this.id), forceInactive: !await Monitor.isParentActive(this.id),
type: this.type, type: this.type,
timeout: this.timeout,
interval: this.interval, interval: this.interval,
retryInterval: this.retryInterval, retryInterval: this.retryInterval,
resendInterval: this.resendInterval, resendInterval: this.resendInterval,
@ -428,7 +429,7 @@ class Monitor extends BeanModel {
const options = { const options = {
url: this.url, url: this.url,
method: (this.method || "get").toLowerCase(), method: (this.method || "get").toLowerCase(),
timeout: this.interval * 1000 * 0.8, timeout: this.timeout,
headers: { headers: {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"User-Agent": "Uptime-Kuma/" + version, "User-Agent": "Uptime-Kuma/" + version,
@ -648,7 +649,7 @@ class Monitor extends BeanModel {
} }
let res = await axios.get(steamApiUrl, { let res = await axios.get(steamApiUrl, {
timeout: this.interval * 1000 * 0.8, timeout: this.timeout,
headers: { headers: {
"Accept": "*/*", "Accept": "*/*",
"User-Agent": "Uptime-Kuma/" + version, "User-Agent": "Uptime-Kuma/" + version,

View file

@ -716,6 +716,7 @@ let needSetup = false;
bean.headers = monitor.headers; bean.headers = monitor.headers;
bean.basic_auth_user = monitor.basic_auth_user; bean.basic_auth_user = monitor.basic_auth_user;
bean.basic_auth_pass = monitor.basic_auth_pass; bean.basic_auth_pass = monitor.basic_auth_pass;
bean.timeout = monitor.timeout;
bean.oauth_client_id = monitor.oauth_client_id, bean.oauth_client_id = monitor.oauth_client_id,
bean.oauth_client_secret = monitor.oauth_client_secret, bean.oauth_client_secret = monitor.oauth_client_secret,
bean.oauth_auth_method = this.oauth_auth_method, bean.oauth_auth_method = this.oauth_auth_method,
@ -1372,6 +1373,7 @@ let needSetup = false;
// Define default values // Define default values
let retryInterval = 0; let retryInterval = 0;
let timeout = monitorListData[i].timeout || (monitorListData[i].interval * 0.8); // fallback to old value
/* /*
Only replace the default value with the backup file data for the specific version, where it appears the first time Only replace the default value with the backup file data for the specific version, where it appears the first time
@ -1397,6 +1399,7 @@ let needSetup = false;
basic_auth_pass: monitorListData[i].basic_auth_pass, basic_auth_pass: monitorListData[i].basic_auth_pass,
authWorkstation: monitorListData[i].authWorkstation, authWorkstation: monitorListData[i].authWorkstation,
authDomain: monitorListData[i].authDomain, authDomain: monitorListData[i].authDomain,
timeout,
interval: monitorListData[i].interval, interval: monitorListData[i].interval,
retryInterval: retryInterval, retryInterval: retryInterval,
resendInterval: monitorListData[i].resendInterval || 0, resendInterval: monitorListData[i].resendInterval || 0,

View file

@ -59,6 +59,8 @@
"Hostname": "Hostname", "Hostname": "Hostname",
"Port": "Port", "Port": "Port",
"Heartbeat Interval": "Heartbeat Interval", "Heartbeat Interval": "Heartbeat Interval",
"Request Timeout": "Request Timeout",
"timeoutAfter": "Timeout after {0} seconds",
"Retries": "Retries", "Retries": "Retries",
"Heartbeat Retry Interval": "Heartbeat Retry Interval", "Heartbeat Retry Interval": "Heartbeat Retry Interval",
"Resend Notification if Down X times consecutively": "Resend Notification if Down X times consecutively", "Resend Notification if Down X times consecutively": "Resend Notification if Down X times consecutively",

View file

@ -589,6 +589,8 @@
"RadiusCalledStationIdDescription": "접속 스테이션의 식별자", "RadiusCalledStationIdDescription": "접속 스테이션의 식별자",
"RadiusCallingStationId": "접속 요청 스테이션의 Id", "RadiusCallingStationId": "접속 요청 스테이션의 Id",
"RadiusCallingStationIdDescription": "접속 요청 스테이션의 식별자", "RadiusCallingStationIdDescription": "접속 요청 스테이션의 식별자",
"timeoutAfter": "{0}초 후 타임아웃",
"Request Timeout": "요청 타임아웃",
"Query": "쿼리", "Query": "쿼리",
"settingsCertificateExpiry": "TLS 인증서 만료", "settingsCertificateExpiry": "TLS 인증서 만료",
"certificationExpiryDescription": "HTTPS 모니터링 TLS 인증서가 만료되면 알림을 활성화해요:", "certificationExpiryDescription": "HTTPS 모니터링 TLS 인증서가 만료되면 알림을 활성화해요:",

View file

@ -393,7 +393,7 @@
<!-- Interval --> <!-- Interval -->
<div class="my-3"> <div class="my-3">
<label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label> <label for="interval" class="form-label">{{ $t("Heartbeat Interval") }} ({{ $t("checkEverySecond", [ monitor.interval ]) }})</label>
<input id="interval" v-model="monitor.interval" type="number" class="form-control" required :min="minInterval" step="1" :max="maxInterval"> <input id="interval" v-model="monitor.interval" type="number" class="form-control" required :min="minInterval" step="1" :max="maxInterval" @blur="finishUpdateInterval">
</div> </div>
<div class="my-3"> <div class="my-3">
@ -412,6 +412,12 @@
<input id="retry-interval" v-model="monitor.retryInterval" type="number" class="form-control" required :min="minInterval" step="1"> <input id="retry-interval" v-model="monitor.retryInterval" type="number" class="form-control" required :min="minInterval" step="1">
</div> </div>
<!-- Timeout: HTTP / Keyword only -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword'" class="my-3">
<label for="timeout" class="form-label">{{ $t("Request Timeout") }} ({{ $t("timeoutAfter", [ monitor.timeout || clampTimeout(monitor.interval) ]) }})</label>
<input id="timeout" v-model="monitor.timeout" type="number" class="form-control" required min="0" step="0.1">
</div>
<div class="my-3"> <div class="my-3">
<label for="resend-interval" class="form-label"> <label for="resend-interval" class="form-label">
{{ $t("Resend Notification if Down X times consecutively") }} {{ $t("Resend Notification if Down X times consecutively") }}
@ -840,6 +846,7 @@ const monitorDefaults = {
retryInterval: 60, retryInterval: 60,
resendInterval: 0, resendInterval: 0,
maxretries: 1, maxretries: 1,
timeout: 48,
notificationIDList: {}, notificationIDList: {},
ignoreTls: false, ignoreTls: false,
upsideDown: false, upsideDown: false,
@ -1113,6 +1120,13 @@ message HealthCheckResponse {
} }
}, },
"monitor.timeout"(value, oldValue) {
// keep timeout within 80% range
if (value && value !== oldValue) {
this.monitor.timeout = this.clampTimeout(value);
}
},
"monitor.type"() { "monitor.type"() {
if (this.monitor.type === "push") { if (this.monitor.type === "push") {
if (! this.monitor.pushToken) { if (! this.monitor.pushToken) {
@ -1274,6 +1288,10 @@ message HealthCheckResponse {
if (this.monitor.retryInterval === 0) { if (this.monitor.retryInterval === 0) {
this.monitor.retryInterval = this.monitor.interval; this.monitor.retryInterval = this.monitor.interval;
} }
// Handling for monitors that are missing/zeroed timeout
if (!this.monitor.timeout) {
this.monitor.timeout = ~~(this.monitor.interval * 8) / 10;
}
} else { } else {
toast.error(res.msg); toast.error(res.msg);
} }
@ -1445,8 +1463,27 @@ message HealthCheckResponse {
addedDraftGroup(draftGroupName) { addedDraftGroup(draftGroupName) {
this.draftGroupName = draftGroupName; this.draftGroupName = draftGroupName;
this.monitor.parent = -1; this.monitor.parent = -1;
},
// Clamp timeout
clampTimeout(timeout) {
// limit to 80% of interval, narrowly avoiding epsilon bug
const maxTimeout = ~~(this.monitor.interval * 8 ) / 10;
const clamped = Math.max(0, Math.min(timeout, maxTimeout));
// 0 will be treated as 80% of interval
return Number.isFinite(clamped) ? clamped : maxTimeout;
},
finishUpdateInterval() {
// Update timeout if it is greater than the clamp timeout
let clampedValue = this.clampTimeout(this.monitor.interval);
if (this.monitor.timeout > clampedValue) {
this.monitor.timeout = clampedValue;
} }
}, },
},
}; };
</script> </script>