Compare commits

..
4 Commits
Author SHA1 Message Date
gru 90d209554e Merge pull request 'Release/0.35.1' (#3) from release/0.35.1 into master
Reviewed-on: #3
2026-11-05 21:54:24 +01:00
Mateusz Gruszczyński 851f172581 v0.35.1 2026-11-05 21:26:06 +01:00
Mateusz Gruszczyński e80e2f4862 v0.35.1 2026-11-05 21:25:54 +01:00
Claude d40c30d0e8 v0.35.1: performance, AGPL licensing and documentation overhaul
- Web Server reads static files inline; proxy middleware chain flattened
- About window with license summary and project links; AGPL-3.0-or-later
- Rewritten documentation: feature list, screenshots, performance guide
- Fix false spikes in per-Vhost request-rate charts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMsgSXoK4Bph2vU6fSBkAH
2026-11-05 18:29:56 +01:00
37 changed files with 81 additions and 669 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
# RustProxy source and documentation maintenance
Current source version: **0.35.3**. The Cargo package and binary are named `rust-proxy`.
Current source version: **0.35.1**. The Cargo package and binary are named `rust-proxy`.
Before editing, read [README.md](README.md), the relevant [documentation guide](docs/README.md) and the actual code and tests. Historical release experiments are not current specifications. Use the bundled `src/admin/openapi.json` for API routes and payloads; the source implementation is authoritative for runtime behavior.
+1 -18
View File
@@ -2,27 +2,10 @@
Release history focused on **features, settings, default behavior, compatibility and fixes**. Changes to internal code structure and preliminary performance experiments are intentionally omitted. Historical descriptions reflect their respective releases; consult the current documentation for the settings available in the latest release.
## 0.35.3 — Overview layout fixes
- **Standard Overview layout restored:** the quick-health tiles added in 0.35.2 (healthy backends, error share, requests rejected by limits, nearest certificate expiry) are now hidden by default and placed at the end of the Overview, so they no longer shift the standard block layout. Enable them in the layout editor when needed.
- **Fixed:** the "Saved default" status next to the history sample count was always visible and looked like a fresh save notification. It now appears only for an unsaved temporary choice or briefly after **Remember choice**.
- **Upgrade notes:** no configuration changes are required. If 0.35.2 already changed your Overview, use **Unlock layout → Reset layout** to restore the default arrangement, or hide the tiles in the layout editor.
## 0.35.2 — memory fix, health check options and Overview tiles
- **Memory use restored:** with a large HOT cache, 0.35.1 used about 25% more memory than 0.35.0 (for example 1.1–1.2 GiB instead of about 800 MiB with a 600 MiB cache). The default build is back to the 0.35.0 memory profile. The faster alternative allocator is available as an optional build feature for servers with spare RAM; the Performance guide explains the trade-off with measurements.
- **Health check port:** active TCP and HTTP health checks can probe a different port than the one receiving traffic, for example a status port 8079 for a backend serving on port 80. It can be set for a whole Vhost or per backend; host, TLS and address restrictions stay the same.
- **Expected response text:** HTTP health checks can require a text fragment in the response body, so a status page that answers 200 while reporting a degraded state marks the backend as down.
- **New Overview tiles:** healthy backends (with the names of failing ones), error share, requests rejected by limits and the nearest certificate expiry.
- **Certificate expiry metrics:** Prometheus now exports the expiry date and remaining days of every installed certificate, including manually uploaded ones, for alerting.
- **Safari:** the Overview no longer jumps when you scroll near the Backends table while live values refresh.
- **Fixed:** network interface traffic below 1 KiB/s is shown as a whole number of bytes instead of a long decimal.
- **Upgrade notes:** no configuration changes are required. Existing Overview layouts receive the new tiles at the end; hide or move them in the layout editor.
## 0.35.1 — performance, licensing and documentation
- **Faster Web Server:** static files are read directly by the request instead of being handed to a background thread pool. Small and large files are served up to 1.8× faster with lower latency and CPU use.
- **Faster reverse proxy and cache:** lighter request processing, header handling and traffic statistics collection. Proxied requests are about 3–7% faster and cache hits about 11% faster on the same hardware.
- **Faster reverse proxy and cache:** lighter request processing, header handling and traffic statistics collection. Proxied requests are about 12–19% faster and cache hits about 20% faster on the same hardware.
- **Vhost charts:** the per-Vhost request-rate chart no longer shows a false spike right after the page opens or when two refreshes arrive at the same moment.
- **Performance guide:** new documentation page with the benchmark method, a comparison with a similarly configured Nginx and production tuning advice.
- **About window:** redesigned with the project description, copyright, a GNU AGPL v3.0-or-later license summary and links to the project website, source code, built-in documentation and the full license text.
Generated
+1 -1
View File
@@ -1943,7 +1943,7 @@ dependencies = [
[[package]]
name = "rust-proxy"
version = "0.35.3"
version = "0.35.1"
dependencies = [
"anyhow",
"arc-swap",
+2 -6
View File
@@ -1,6 +1,6 @@
[package]
name = "rust-proxy"
version = "0.35.3"
version = "0.35.1"
edition = "2021"
rust-version = "1.99.1"
description = "RustProxy - independent reverse proxy with local administration"
@@ -17,7 +17,7 @@ ipnet = "2.11"
rpassword = "7.4"
zeroize = "1.8"
anyhow = "1.0"
mimalloc = { version = "0.1", default-features = false, optional = true }
mimalloc = { version = "0.1", default-features = false }
libc = "0.2"
arc-swap = "1.7"
axum = { version = "0.8.9", default-features = false, features = ["http1", "http2", "json", "query", "tokio", "ws"] }
@@ -72,10 +72,6 @@ systemstat = "=0.2.7"
# Swagger assets are prepared by build.rs and embedded behind the admin guard.
# No adapter build script, implicit version fallback or Cargo-config dependency.
[features]
default = []
mimalloc = ["dep:mimalloc"]
[build-dependencies]
serde_json = "1.0"
+1 -3
View File
@@ -54,7 +54,7 @@ Default managed installation: HTTP on `:80`, HTTPS on `:443` with a self-signed
## Performance
RustProxy streams requests and responses asynchronously on Tokio and Hyper. On one CPU core it serves roughly 15,000 proxied and 26,000 static requests per second with full statistics and logging enabled, and it is faster than a comparably configured Nginx for large proxied responses. Benchmark method, the full comparison and tuning advice are in [Performance](docs/PERFORMANCE.md).
RustProxy streams requests and responses asynchronously on Tokio and Hyper. On one CPU core it serves roughly 14,000 proxied and 24,000 static requests per second with full statistics and logging enabled, and it is faster than a comparably configured Nginx for large proxied responses. Benchmark method, the full comparison and tuning advice are in [Performance](docs/PERFORMANCE.md).
## Building
@@ -66,8 +66,6 @@ RustProxy streams requests and responses asynchronously on Tokio and Hyper. On o
Rust stable (see `rust-version` in `Cargo.toml`), a C compiler and Python 3.10+ are required. The build embeds the panel, the documentation and Swagger UI, so the binary has no runtime web dependencies.
Optional memory allocator: `cargo build --release --features mimalloc` handles heavy HOT cache turnover faster but uses about 25% more memory; see [Performance](docs/PERFORMANCE.md#memory-allocator).
## License
Copyright © 2026 Mateusz Gruszczyński.
+36 -36
View File
@@ -1,13 +1,13 @@
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
ba8bc979322a494cfce59398085259d059142d6a789bbe73ed7992d143123f06 .gitignore
5142c2151f434a42b925a6def77b726abf0c10e8404f8db08c999ca1cad43386 AGENTS.md
c2c23ce76282075cc49bf1609fe693591c900a5a120ff3e209e00f86a97c0141 CHANGELOG.md
77a69eb3906a27fa42d4bcb60e9595e86fc23ad74d72b96f2710527da49f5225 Cargo.lock
5d49aef294682f80d98cc2cc50d3908a8e5684151d38ee5dfbd04cd6cd95f9e0 Cargo.toml
2ffb0549e6c8f12093f2d5c5b222695a391d555208ca69415049dedd6e1d59fe AGENTS.md
195f115f819549f66f2063b52f0c97ec3b7e4f7b7c003636eda6ae18b90f0cdf CHANGELOG.md
8e434690bf511ffcda205e58719abdc7c47ed0d2a14660a8d62f6984231bc3a0 Cargo.lock
59b870fa0f6ff5a26e6068dfa437c4b707d8af7840c5a3d6880423d564f227e4 Cargo.toml
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0 LICENSE
82d1dee2fc68158f0ea73259151461e64c8b18d807cd1fecfb1ed99dc30cf5f4 README.md
cabbed1bd8ae8433b2d179c72339b1464d307b5dbb59aadac9bbb326298a92a9 README.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
c0df6b46bf795d49d00fcd9e2a9247aed0d3a99b6cae1dc5ab6614e8b3d30009 build.rs
95ececaba550a4bcfd6754073877ba0633373b1e1451521b9ac905a51e8ca6eb deploy/README.md
@@ -34,14 +34,14 @@ a1b6b8675d3b8321160406bc716f1355356e1e3dd056d4785be1d06a99313f31 docs/API.md
32599d67a11fd0ebbc612e82b9f602789f4ed4103cda482c6870d8741100c8b4 docs/CERTIFICATES.md
02bb383b5c3bce22616c3cc3db30a9469806dc16decb509973e84c60aa30fc5b docs/CONFIGURATION.md
fec29ce846d6705eb45819d85d382f28e28a146b265feab2e4c7cce2a35e5883 docs/CUSTOM_LOCATIONS.md
6e05af7bf62a41a3a5a71e8f0c036dc611408d04f7130c07b585317aaf59ef0b docs/DASHBOARD.md
47f600d9f70b0ce3b16c34a4aead62ce21b95c29f76a7c229631f782e791021a docs/DASHBOARD.md
11a08bf2110bbeabc924eb049a30177a216af5607ccdaf8be9178e751f78a2c4 docs/DOCKER_DISCOVERY.md
cebf36532f7366acdcf8808d958b86b5f67246043677ee3dba9474a9b81970e0 docs/FEATURES.md
d3592e93f9b24712c2dd27e35e28f657fbfb1c2f627e83eb804094760871f602 docs/FEATURES.md
3939c14e83ef6856a421f358a3f7fce3d50ea375bc1b9f0419f27a3eed658f6f docs/INSTALL.md
694cb7b15c5bbdc94cecd15dc516c05dcb3467849322cdd125b872d36dee4699 docs/LOGS.md
0e9ae9509a78d00951d2c7c3f13eaf518d1f3448a29b048f5933e1f5f8440275 docs/MIGRATION.md
0972248b7062b37e61edcc34dab5fe73ee9bb50195a0aa475c72efa0dd311aa9 docs/MONITORING.md
2a363d1e5dca0168ddbe58498641381e8281089f2e7b15ea5159e5b9eb6dae01 docs/PERFORMANCE.md
1222db8b21bd4c14522953d5bf117fb38d6b84c455ac06b50a65990d131c283a docs/MONITORING.md
cd1ba85b82eb02bb09e22eed5605cbf9831dca1dabe65377dced25d50a5e35d5 docs/PERFORMANCE.md
d5894c07c68ec4230987767198434abee13b0e525842fea21bcbe752fa82742b docs/PHP_FPM.md
9095d0df62a4548459ca739c97fcca37d8902cee1ae04b7f9da57aaea7749a9a docs/README.md
490a6dbe870f2b1fd4ec7816768dde6c86fa7957fc9de2d2f075e123c9222222 docs/RESPONSE_DELIVERY.md
@@ -50,7 +50,7 @@ d5894c07c68ec4230987767198434abee13b0e525842fea21bcbe752fa82742b docs/PHP_FPM.m
bbef60ff1771da9f48af812987766e1e479408873ae2f079b13f1d5c62ecc6ff docs/STREAMS.md
8cfcda59060106151f13e93ca25c1280b4390433b3fbe2921a6eb3c13bf9766d docs/TRANSPORT.md
be6e8af62a7bf958b94ec15e9e3263263e511d1d33abae4e45d114833fa52b07 docs/TROUBLESHOOTING.md
b4abb86942f665230904fad570c1487bc173aa84633801e2999f2bfa197de4a7 docs/VHOSTS.md
6de1f152989250d7e53fff652cbdaa98aeb42310f6c4daa9650fce647ee17266 docs/VHOSTS.md
e67928a9d044e71b681c3e52f9dd8b151d6b72f965d16d5da8b0eadde98b70e9 docs/WEB_SERVER.md
3769f97c74b1a4a2f14d82a6f9c30fa3ee48892f423bf82dbd2ec1bd9280e45a docs/images/about.png
a85a7b9d8cac74fb1b231471d313e6587ccfc2e8abd2885a79067704176c724b docs/images/api.png
@@ -86,12 +86,12 @@ de6550ab8f48d556d752c252212c38ba65491fd224f972c10fbe552f91ed7520 integrations/o
45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 integrations/routeros-oci-build/.env.routeros.example
11aa102de1089ecc375f20c4bd5420fcfd3b2ef23fe0c8bdf04953fcfaf40ef0 integrations/routeros-oci-build/Dockerfile.routeros
7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e integrations/routeros-oci-build/Dockerfile.routeros.dockerignore
4c7416b85ddbf66f8434735dfa6452c8b368f496a120861a2024c5775858ad0e integrations/routeros-oci-build/README.md
6dbaf119accd963ce58656a9a1d38cd66a997a34c8eea9b0c7d4e4f4a6cc700f integrations/routeros-oci-build/README.md
3f092a9ab80221eddd67be4d1a776acbc6e04c1eb6011b588280c28e83b2582f integrations/routeros-oci-build/build-routeros-oci.sh
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md
582dcbe08575602b83610e316ee3d856d391f87d23f6d91dca7361ed58c470f4 langs/en_US.json
2bdd09e7734a65c828c268e89a85a1e6071be54cdf1b378a67406a1fa6dbf0b7 langs/pl_pl.json
c6334d6f120922208d3b04aa7c1d838d6c6d7a48dddc89748c7f438fc76cf937 langs/en_US.json
0bc6dfc7c19a186c3cae9d01b7c9cab879e3f8d621f61d0b0cf71860088ef05a langs/pl_pl.json
9664543e4d03422788a65e42f968c1f026c2235cb5be6fc3e0ad318997077538 make_zip.py
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
@@ -162,7 +162,7 @@ d58a2ed83319176de4e66790ed292447c9497ae031464ec55cfe27174acb639a src/admin/file
77b8c05645969834cc8a58e249a7b60a57d6c47ad1c5cd92ffa5e4a9b5a8e73c src/admin/file_logs_query.rs
444d7715406ffd2305a72decf7fdf633aa4c2752cd881c8fa3b95ed971d79820 src/admin/logging_api.rs
243f4a9c3fa99781e19c3952354d2632c0ce65e1dafac729567cbe3c177f94cb src/admin/mod.rs
61fd5eb6bb4f3cc19151ae96dc5c028e4845479ce1e2bb2691a41094dcd212c9 src/admin/openapi.json
f2525e5e7aca40b04f565a6865170a56546caade45918e96968443a0d5fcdad2 src/admin/openapi.json
1f4ec45d146e00ef664fae37517f8fcb5455d47daec496c02b2fc15ddec49131 src/admin/operations.rs
257d93a29d0bda96462bcf695aba8d0ec0f9412c379e066ec46ae8ee034e3a53 src/admin/panel_assets.rs
f5127855c2994d052678865df36d703b74f4a0afc633daf7718b10da18d26e37 src/admin/panel_content.rs
@@ -186,18 +186,18 @@ c18c369124368f58524652565102817ed69fd25b222b0c5e737ef7632b45dcfb src/admin/web/
aed3aea4eef0011c6b51d39736782e7d1ec52662e422f40e0c7b9ddfc19d0378 src/admin/web/app/certificate-inspection.js
1a4d40a86067493e75b47f2d735d50fa4d161344ce145d4e502b0225144ed45a src/admin/web/app/chart-scales.js
e5b39b5592ba81292571033f26934ac0cd42bcdca7395e91094973cecb3eeaf8 src/admin/web/app/config-history.js
0248fee14bbab0b3d1f308f650061d7a348a8ffe00a46a56e297131f815aae10 src/admin/web/app/core.js
f0bc2a8b7c8faf60938c71f49764ddd4376685e19454c176a61307f885c5369d src/admin/web/app/core.js
622d34e1616fee9a480d208192fc5a84d62d84ef4233d6a7d95e2bfb8cef415b src/admin/web/app/dashboard-inline-layout.js
e4238322a985d3e8b7f2de94b7af31e1c718775590b2c7b3428b878cd43337b7 src/admin/web/app/dashboard-layout-editor.js
a184c50e1ca6bb5a14954e31b15f09ff9978fb939064ca724851bbb4e661dc56 src/admin/web/app/dashboard-presets.js
47eaf9486099468b3d686bdca5c6b8d59aa5f5661baa03b3698320cf618db4e2 src/admin/web/app/overview-extra-metrics.js
25d89adb3b6e995d7c51d9b315e167949d5d8d4403162bf1fbe8e98dfb392dee src/admin/web/app/overview-extra-metrics.js
5e62cccf6a9507fe208e9f41bbd08453c3d8c6a6b83c56d93a2bb22550ea6e9a src/admin/web/app/overview-masonry.js
561f750cacd4d4fcf7d1d1d9167ba296c0b43d7e81f2bff0ab1a070e1b8682d6 src/admin/web/app/overview-samples.js
b52456b41451f156edf5f58492119c22726f2cf40cee938527748997e8ee7412 src/admin/web/app/overview-samples.js
db9012546cae8c58fc2bcbee71c9a95956a13a6db9e6d126c6a3770d45a4e4d2 src/admin/web/app/overview-wide-metrics.js
bfd9fafb3112197e8f875dbf87a9a66c81aef0fe83687601a84a66f5e13f69bc src/admin/web/app/path-permissions.js
46c7cd36397375eb7e8c230c8aa97b1dbea0a63800beb977efe1ec67a14b237e src/admin/web/app/preferences.js
9e2b12d34ab1fb29716645720577ad49b16c03b46c482a03695e8f645d170e79 src/admin/web/app/rate-sampling.js
ac24300022bb98de2ad5c03a7859a2d80b14420e7a72db49ad649f52d92566a9 src/admin/web/app/refresh.js
43f4dddd1c3ea7fa42eeb9d7b655a8db48b5ddcc6c68213e08dfc2fcfbe8b17d src/admin/web/app/refresh.js
7888eaea26c5cecc097ad276b73ae6c63def1db896053def6852ddf9d6739906 src/admin/web/app/runtime.js
31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js
c7a18238ac426de4d1cf84a395ee4dc66410d60a57a3822695dc10adc668d956 src/admin/web/app/settings.js
@@ -208,15 +208,15 @@ e9788a9d1e0bce8775f612a5c842afea8a0dcf01301acdef4b69ff6a52d2ef45 src/admin/web/
a09f060b47771c56e9b9e57f1b896a32a2f87f1fb33401ac76932a91d247c67e src/admin/web/css/00-tokens.css
77b8b748f31ef383c64ba5ca1740e9b6073f0de1e77c2c8c62d0f38bb52dd296 src/admin/web/css/10-base.css
d3f19407ae97b3f6cca015877d369f4e3dc86d8ea7de08e430c85468fa033f5e src/admin/web/css/20-shell.css
98133ee0b23288dc8d0ed2fa0a1375f9145acce108d215a63c76ea96bbdf8e11 src/admin/web/css/30-overview.css
7282b376c4df9e423eb84130b28d74ebb49c3341eed8aa284c29d8da413fd2d0 src/admin/web/css/30-overview.css
dd82b5e3ead91c20bb08cf8d71bbba31ea8e3d108c417913c63409f3e34da7e1 src/admin/web/css/40-editors.css
6af9fcb439823ec63abe2c1dd86c9ecba17c233262fd8ab80d321d64ae2758ab src/admin/web/css/50-settings.css
f8b515c63080c71ee647cfd8c36628271e8357b31cd84f00cf591e9b752b44fe src/admin/web/css/60-monitoring.css
c7c387e21e7bb148689e16f5fda6cd47c870043260dec059bbe66e09ac86fdd9 src/admin/web/css/70-responsive.css
8009afdb04d5170687816730cc930a22fead1fe0248835ca7e8de81d6907395d src/admin/web/css/README.md
8241bd9489dfed082863948829a44a49bf0a8e9cd63c55809655109e393e0918 src/admin/web/css/manifest.json
ee51ab0912e0ade6a3e1c2ea6f533bb2f65a6d3e6ec9087e25b9f0df009c19b2 src/admin/web/dashboard-defaults.json
20d65b22900d0de2f71dbafee2fe9a237e0a76515b386022b94c75d50bdfd926 src/admin/web/dashboard-widgets.json
560a8e047182fbcee08aa4f8d999400a8a5ddca0b6a4640efd46f6b5c34fde91 src/admin/web/dashboard-defaults.json
14213011cfb6ac334c8711b76bbdde97588a7b7ba4eb2592a1e387f877d6cbe0 src/admin/web/dashboard-widgets.json
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
448a4e00e78ca368f9307cc84f87e893e5225dc87fdeadfbb227fd4b1faf1702 src/admin/web/docker.js
fec062a5959450b1711948adf3189ceb37edca6fbead2e5bfc03b7ce397c70b1 src/admin/web/docs.css
@@ -246,7 +246,7 @@ cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/
c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/login.js
734aeac4855f0913a84a82d7b5c17b37de4504b50a8701edeb775d6926929bce src/admin/web/management.js
4481a592fcce9a2aa1db5e4a25619e19fd459c39039b86c0549dfc427eec1b45 src/admin/web/partials/overview-charts.html
69296af507dbc8c211800f4841c3d7ad6a2490aad2e93ecb297246b7470e3be3 src/admin/web/partials/overview-metrics.html
a8afc6e70950bc70b98725eb049d73cff35dc3a3ee659229a15a8e08d3eb4f12 src/admin/web/partials/overview-metrics.html
3928afce2ed6d5f2d3e29475dd29e4c3bd3c6df180ad5119beaff0c7a58e3b3b src/admin/web/prometheus-token.js
90d9a0836662135b79fb6f183c4f3eff60f8a64392d6c5795ccf1d8ef6862263 src/admin/web/redirects.js
2e891f34cad2a2950a47e529e15ed9dd41beedec71280b8035b19cfb91a5d317 src/admin/web/settings.js
@@ -254,9 +254,9 @@ c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/
1ea78a6e5b5d813febf2359ecec7fde42dc1278531053e91428f4d72b8a1d62b src/admin/web/streams.js
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
c7e0ca45c19498d40ef6f4122de23f0e73094e905d39f798942f70d15b9cf86e src/admin/web/transform-panel.js
87917b0388090f2b27ec6460be56fea96dc09b42735319e0a91330748848e288 src/admin/web/vhosts/core.js
9bb38cdb8c0f49144d6058e007e375fb3b2e47359988029fe28addc6a900d0ca src/admin/web/vhosts/editor.js
3776f72d7051af86ac5fbdd4581040dd88a424c0c6cde789301b032b159094d8 src/admin/web/vhosts/health.js
1cb5c0197b64791aedf992677609049f510ffd1944642528b0294d3dc113a2c5 src/admin/web/vhosts/core.js
aaefda612ab04860f7c5a16f42a400fdd229277606237fb38eace1aa2fcd436a src/admin/web/vhosts/editor.js
8338ce13b5e3e3f421437631124cb691aeb5b9e17409d256b4b9bcac457b3c59 src/admin/web/vhosts/health.js
b3239e67901f1796d7a974ade0796f2e3590ff8064628a5629b78c7f6ce2f32e src/admin/web/vhosts/protocol.js
8c3da49973c62333559b0e3588c02beb0b425c025eac5cfa82a1bb968f70d9dd src/admin/web/vhosts/rates.js
903df941cfd04bd2e34178b5b72911f5c0a6be9aa593ce544c685aacbd757da2 src/admin/web/vhosts/render.js
@@ -282,13 +282,13 @@ d4a388f0897e50ad26e1d8ea527eccdf656557abb4b88fe714cc2b1969d8715e src/authentik/
859cec0bcd604f205d7d8e14d1c810e33d97fd7cf00ddcc30877bcea7c5fc2b5 src/backup/tests.rs
5293e1b06f82654929805b53b9092a82b7503a54b34da3e882a999c5f14ee579 src/backup/validation.rs
eec564a7ef886d18ab75289025a47d62bb702f07d63f92d2f83b7afeecebbb15 src/balance.rs
9158a2bbb303c53bd3d2c779d9eb6e4470d028c4f28f8e329788e9c8c830f843 src/balance/health.rs
458733ad4ec56d58112de6ffa3539ca7e9fa8311048a4e120da96b5f899d5dae src/balance/lease_runtime.rs
bd693ea1d15d8da2abc08c6b5b50d42b693d9d4e9344205e9ad7ae779c19387e src/balance/model.rs
ced69832cb32e7dbd0203d74f5a9c60c2cdc682729f4c5e2c01e7c082e7adc24 src/balance/pool_core.rs
5006ece2fbb377c3944f67d9a7df9bed18861f010a7025d59259db5ae27a00b7 src/balance/health.rs
344631248c71164187497a10d177d8eaa3e2f4a08111520d2536ec89ae55e1e0 src/balance/lease_runtime.rs
ebe2b95f8fc5904850a88207f0690449ced49b28dc14ce5608b706659e338fca src/balance/model.rs
a283df0f06bde3eafde24aa0d456b084f81864b4761b7162671f3b4e73620ccf src/balance/pool_core.rs
477132dd381e23f1be197c32ff58a0925f41d3e6aa63fe77c10973cefb2b76f6 src/balance/pool_selection.rs
d789bf40cf9433e2fd16409d112210fe93154588b1ac9bddc9c7f78e1db14426 src/balance/pool_view.rs
a361c341932ffd186c0b21356c91ab1fe8043f5020d40798266d15ae896a114e src/balance/tests.rs
3e75a3cba5f1cd1af996ab71b6ad50f6aa05ac9cd97ee3cf88fce7c6b7271fbe src/balance/tests.rs
ae06d238b1451328c751a511476ef39c012f3a397393f939ca02f96ecd0119a6 src/basic_auth/mod.rs
9a8858b7ff461a4d961ba701ca3ed6eaec15c3f7f05eef8e0e30edc63617605a src/basic_auth/tests.rs
e35648537d5ac0bfb604997200d1be25652ee4e8c49bf8b720b68df087bd0249 src/cache/cache_regression_tests.rs
@@ -334,12 +334,12 @@ a1132b980833b1b4ce2bba9aa681ff255a0e2d61faaa65a316860da503ea1e27 src/cache/v022
1a2eb6684d9ea4a7e90970d936c75f7c8bbec46a4181fdf273be9bdac8eec847 src/compression/mod.rs
8de82d9188c9c337690c6ec4afdad9fff20651c358e34602a93d8bb8c3d2489a src/compression/tests.rs
e695079333ebdc2a6e7b3337cfabef171b3a0a01da0597b77feff70d417f833e src/config.rs
d727e5ebd6c6184943d5f773c6b5244954832139c744851c1357614880f09c6d src/config/backend.rs
9f6272bd6d1f403cf6324b12472fd5c7d911a387139b57105ba1819e48ea0a54 src/config/backend.rs
367b46f2b57143db17044378b1f5a55bab1eae3ad6ba40f388896995d1b2a13d src/config/compile.rs
fbea01fea1213d98aea812c1b78d5db289e09200b98ae514d742235cd3fa70e1 src/config/internal_routing.rs
cc75698be2828000e88f2c482bd26f2fa4050e3432bec1b5a581dca17fef4a37 src/config/model.rs
0ebb02ae4f7403e347fed433d93ce9a4a46b1532cb9c4b2b4dc17885f871ed6f src/config/model.rs
75839458ddbc7480ec4492a95eeb67b617ba7d62f7b67e7a767a3453b74dbcbc src/config/route_policy_tests.rs
736b46108c72220b8142f4f2ab128876b6bd82e0d09891578abdce6aa03c82f3 src/config/routing.rs
365483b6aca6ce5880bec5235db869b1bbc2e2551945c99303cd005903fe6207 src/config/routing.rs
dd9a6ad8f58e0cdd1ae74f3f972fee0ac14383cd8b8f518403b9a36c169f35bd src/config/snapshot.rs
477cddaa1e1c0b9b605a2f550971a04cf30287519cba70cfbb2e1860e3a93ff2 src/config/tests.rs
f35de0ee1516ece6bbfd2b2a7db7d1e0f5048b0ec47cf2a3765dffb799abfd93 src/config/transport_tests.rs
@@ -383,7 +383,7 @@ e9795eaa3e743c09f5ab0e18e6d359bc97a3aee55510c74923532d64df90c873 src/logging/fo
66c4596e644d9dd539d468575e2de6459bac2624fb1e1064cdae61a21c2e0240 src/logging/model.rs
bf898b318feb9bd3dd2c335b6b1bad172da63fd5cefbff3e267421ce37c076fb src/logging/runtime.rs
3d6ea5508bc9609c6ce99a3a1d3fec2fafb95a2a8de579a80262b73c6eed3a63 src/logging/tests.rs
cdcb3806669207c17279da959bbfcfacf18cc192f24e862c0afa0c389a41631a src/main.rs
e1c61fdfc3f429ad3fa2116921381cbc5d2f850fa119a16c8f691d6e3e184724 src/main.rs
87e69b46c548b861e9d90d9d2e42dfc2630891f4583800b1942f9065d25e29cd src/main/admin_runtime.rs
224da3127b5f846a8932fb7f4b6e23d0dd590dc0170506267381d66042c64666 src/main/bootstrap.rs
5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs
@@ -403,11 +403,11 @@ c4dbe0f8ded237d45b6a3977bdc7544296e4408996815cae9e28af028419d0b7 src/oidc/tests
eb95c68c8639ce3ae658eea3334795cdb5e129fb2ba10955f7dffe06d8909b57 src/pages/mod.rs
86008e469c84a7242b7d4947e2116694ababa2cb26120f246d00923a7c019720 src/pages/tests.rs
5650a3f70162abd97021e0a51a92cac2c4dd39004426ef99dff36cbb0741aeac src/prometheus.rs
6c97eb9c593a7802422100e8130921e63b6b9b1b0387b6860af9f49581cd50b3 src/prometheus/core.rs
192a780d5874afc158d62a7dbe1687d734635afe9342fff526b8a2f6db4e5633 src/prometheus/core.rs
2321ee667569803dfe6c92c0e98adba0ac8fcdc6a3eacd5857233f1acae93daf src/prometheus/docker_misc.rs
cc909edf740a091f9057912f555dd888b644fa1cb49647fe0e2e1f5dba8ff5af src/prometheus/json.rs
f9aab3297fcf647295849f77cbf99197f4dd27efd6817e54c03451e04972430c src/prometheus/model.rs
f8a9541b568ade946dfc0b3353548a773a0126f705b537815ce0523439b07ef4 src/prometheus/tests.rs
37badc9ae3c63a918a4b8c956beeefdacd78caafa94dd214903d37b62989adf4 src/prometheus/tests.rs
028e1dd9f2ec2afaf699a6b21c2d4b0d96e76f11f5e4ed009c233dadf0f3be51 src/prometheus/traffic.rs
cdbfa0e7f49200dc70ea6df8f0594399ee3e0c5fc3d279cc336e2d83d0f8a535 src/prometheus/vhosts_backends.rs
f1b0254f53b1a4336bd8170ad441ba072b49a114e497b985cb20699b4e375044 src/prometheus/web_php.rs
-13
View File
@@ -14,19 +14,6 @@ page has its own stylesheet.
The Overview contains live counters, charts and optional backend, cache, PHP, Docker and Stream cards. Under **Settings → Appearance → Dashboard layout** or the Overview editor, configure visibility, order, card width, density and the number of history samples. A hidden card stops consuming its optional browser-side visualization work but does not turn off its underlying proxy feature.
Optional quick-health tiles (since 0.35.2, hidden by default; enable them in the layout editor or **Settings → Appearance → Dashboard layout**):
| Tile | Shows |
|---|---|
| **Healthy backends** | Monitored backend members passing health checks as `up / total`, with the names of failing members. |
| **Error share** | Percentage of HTTP requests that ended with an error during the last sampling interval. |
| **Rejected by limits / s** | Requests and connections refused by `--max-connections` / `--max-inflight`; sustained values mean the limits are too low for the load. |
| **Nearest certificate expiry** | Days until the earliest installed certificate expires; the bar is full at 90 days or more. |
Enabled tiles are added at the end of the Overview, so the standard block layout does not change.
The **Remember choice** control next to the history sample count shows a status only when the current choice is temporary (not saved) or briefly after saving.
The unlocked Overview layout exposes a single **Save and lock** action (plus Cancel), removing the redundant lock-and-save button.
**Dashboard Presets** supports up to **16 named layouts per administrator**. Save, load, rename, overwrite and delete presets; a factory layout can be restored. Presets preserve the dashboard arrangement, not the live traffic values. Interface theme, scale and other display settings are also account-scoped and do not alter proxy routing or rewrite shared configuration.
+2 -3
View File
@@ -11,7 +11,7 @@ RustProxy is a single service that combines a reverse proxy, a static/PHP web se
| Vhosts | Exact and wildcard hostnames, aliases, several listeners per Vhost, per-Vhost HTTP/2 and HTTP/3 switches |
| Backends | HTTP and HTTPS origins, HTTP/1.1, automatic ALPN (H2/H1), strict HTTP/2 and H2C, optional upstream certificate verification with custom CA and server name |
| Load balancing | Least connections (default), round robin, least traffic, adaptive and IP hash; member weights, backup and draining members, sticky cookie or IP affinity |
| Health checks | Active TCP or HTTP probes with thresholds, optional separate probe port, expected response text and live health status per member |
| Health checks | Active TCP or HTTP probes with thresholds and live health status per member |
| Connection reuse | Bounded keep-alive pools with idle and age limits, pool recycling and protocol telemetry |
| Custom locations | Prefix, exact and regex paths, path rewriting, per-location backends, Web Services, local responses, redirects and internal error fallbacks |
| WebSockets | HTTP/1.1 Upgrade, RFC 8441 (HTTP/2) and RFC 9220 (HTTP/3) Extended CONNECT |
@@ -45,7 +45,6 @@ RustProxy is a single service that combines a reverse proxy, a static/PHP web se
- Managed ACME with HTTP-01 and DNS-01 (Cloudflare, OVH, RFC 2136), automatic renewal and job history.
- Certificate inventory with expiry, coverage check and bulk assignment to Vhosts.
- Restricted certificate API keys for deploy/reload from external systems such as OPNsense.
- Prometheus metrics for the expiry date and remaining days of every installed certificate, ready for alerting.
![Certificates](images/certificates.png)
@@ -75,7 +74,7 @@ RustProxy is a single service that combines a reverse proxy, a static/PHP web se
## Monitoring and logs
- Overview dashboard with requests per second, bandwidth, status codes, error share, cache hit ratio, protocol mix, system load, backend health, requests rejected by limits and nearest certificate expiry.
- Overview dashboard with requests per second, bandwidth, status codes, cache hit ratio, protocol mix, system load and backend health.
- Customizable dashboard layouts and saved presets.
- Per-Vhost, per-backend, per-Web-Service and per-Stream statistics.
- Live request log in the browser and in the terminal (`--livelogs`).
-8
View File
@@ -44,14 +44,6 @@ scrape_configs:
- targets: ['127.0.0.1:9191']
```
Certificate expiry is exported for every installed certificate, including manually uploaded ones: `rust_proxy_certificate_not_after_timestamp_seconds{certificate="…"}` and `rust_proxy_certificate_days_remaining{certificate="…"}` (negative after expiry). Values are read from the certificate files and refreshed at most every five minutes. Example alert:
```yaml
- alert: RustProxyCertificateExpiresSoon
expr: rust_proxy_certificate_days_remaining < 14
for: 1h
```
When using a bearer token, configure Prometheus `authorization` or `bearer_token_file` securely rather than embedding a clear token in an exported dashboard. Review `grafana/README.md` and `grafana/rustproxy-overview.json` for chart examples; `nagios/README.md` contains external check scripts.
## JSON metrics API for external dashboards
+8 -36
View File
@@ -20,7 +20,7 @@ Results are only comparable when both servers do the same work. A stock Nginx in
- Machine: 2 vCPU virtual machine, Linux 6.18, loopback networking.
- Each server is pinned to one CPU core (`worker_processes 1` for Nginx, `taskset` for RustProxy). The load generator and the origin share the second core.
- Load: `wrk -t1 -c50 -d8s`, HTTP/1.1 keep-alive, two or three runs per scenario, average reported.
- Load: `wrk -t1 -c50 -d8s`, HTTP/1.1 keep-alive, two runs per scenario, average reported.
- Origin: Nginx serving static files with `Cache-Control: public, max-age=600`.
- Nginx 1.24 and RustProxy release builds (`lto = "thin"`, `codegen-units = 1`).
@@ -28,50 +28,23 @@ Results are only comparable when both servers do the same work. A stock Nginx in
Requests per second on one CPU core (higher is better):
| Scenario | Nginx | RustProxy 0.35.0 | RustProxy 0.35.2 | 0.35.2 vs 0.35.0 |
| Scenario | Nginx | RustProxy 0.35.0 | RustProxy 0.35.1 | 0.35.1 vs 0.35.0 |
|---|---:|---:|---:|---:|
| Reverse proxy, 1 KiB response | 61,400 | 14,700 | 15,100 | +3% |
| Reverse proxy, 100 KiB response | 10,600 | 14,500 | 15,500 | +7% |
| Cache hit, 1 KiB response | 53,300 | 22,500 | 25,000 | +11% |
| Web Server, 1 KiB file | 85,000 | 15,200 | 26,000 | +71% |
| Web Server, 100 KiB file | 25,900 | 9,200 | 16,900 | +84% |
| Reverse proxy, 1 KiB response | 54,700 | 12,700 | 14,200 | +12% |
| Reverse proxy, 100 KiB response | 9,600 | 11,400 | 13,600 | +19% |
| Cache hit, 1 KiB response | 51,000 | 19,500 | 23,600 | +21% |
| Web Server, 1 KiB file | 78,500 | 13,400 | 23,900 | +78% |
| Web Server, 100 KiB file | 22,500 | 8,000 | 14,900 | +86% |
Reading the results:
- **Large proxied responses:** RustProxy streams the body directly between connections and is faster than Nginx in its default buffering mode, which spools larger upstream responses to temporary files.
- **Small requests:** Nginx remains ahead, because its fixed per-request cost is lower. In RustProxy this cost is dominated by per-request accounting (live statistics, traffic analytics, request IDs and logs) and by the asynchronous runtime. For most deployments it is far below the capacity of the backends behind the proxy.
- **Static files:** since 0.35.1 RustProxy reads files directly on the request task instead of a background thread pool, which nearly doubles static throughput.
- **Why small requests and cache hits trail Nginx:** each proxied request crosses the loopback network twice (client and backend), and on a VM about half of the CPU time goes to the kernel for those writes and the wake-ups of the other side. Most of the remaining time is RustProxy's per-request work that Nginx does not do in the same way: request IDs, live per-Vhost statistics, traffic analytics and structured access-log entries, plus the asynchronous runtime. A cache hit saves the backend hop but still pays for this accounting, so its gap is similar. No single function dominates the profile; closing the gap further would require architectural changes rather than tuning.
- **Static files:** 0.35.1 reads files directly on the request task instead of a background thread pool, which nearly doubles static throughput.
- **CPU per request:** at the 1 KiB proxy scenario RustProxy uses about 70 µs of CPU time per request on the test machine, Nginx about 17 µs. A single core therefore sustains well over 10,000 proxied requests per second.
Throughput scales with additional CPU cores; RustProxy uses all available cores automatically.
## Memory allocator
Release builds use the operating system's allocator (glibc on most Linux distributions). An alternative allocator, [mimalloc](https://github.com/microsoft/mimalloc), can be compiled in as an optional feature:
```bash
cargo build --release --features mimalloc
```
It is off by default because of how it behaves with a large HOT cache. Measured on the test machine with a 300 MiB HOT cache under constant turnover (random objects of 1–260 KiB, continuous evictions):
| Build | Process memory (RSS) | Throughput under cache turnover |
|---|---:|---:|
| System allocator (default) | 614 MiB | 5,700 req/s |
| mimalloc | 760 MiB (+24%) | 20,300 req/s (3.5×) |
| mimalloc with `MIMALLOC_PURGE_DELAY=0` | 720 MiB (+17%) | 18,600 req/s |
In the standard scenarios from the results table (proxy, cache hits, static files) the two allocators performed within measurement noise. On a production server with a 600 MiB HOT cache the same difference showed as about 800 MiB versus 1.1–1.2 GiB of process memory.
Choose mimalloc only when all of these apply:
- the HOT cache holds many objects that are replaced constantly (high eviction rate on the **Cache** page);
- throughput under that turnover matters more than memory;
- the server has at least 30% RAM headroom above the configured HOT cache size.
Otherwise keep the default. The system allocator returns memory from evicted cache entries to the operating system right away, so process memory stays close to the configured cache size.
## Production tuning
| Setting | Recommendation |
@@ -83,7 +56,6 @@ Otherwise keep the default. The system allocator returns memory from evicted cac
| Logging | Keep stored log files on fast local storage and use retention limits on busy servers. |
| Limits | `--max-connections` (default 512) and `--max-inflight` (default 256) cap concurrency. Raise them on busy servers with enough memory. |
| `sendfile` | Optional for large cleartext static files on Linux. On the test machine it gave no measurable gain, so keep `off` unless your own measurements show otherwise. |
| Memory allocator | Keep the default system allocator. See [Memory allocator](#memory-allocator) for when the optional `mimalloc` build is worth its extra memory. |
| TLS | TLS 1.3 with session resumption keeps handshake cost low; HTTP/2 or HTTP/3 reduce connection setup for browsers. |
## Reproducing the test
-2
View File
@@ -65,8 +65,6 @@ Connection reuse is bounded by configured idle-pool/recycle limits. The default
Backend pools support scheduling/weighting, standby members, draining and optional session affinity. Check the **Backends** status view for member health, recently observed HTTP response code, upstream protocol and resource usage. A reported status from normal traffic is not necessarily the result of a health probe.
- Health monitoring may use TCP or HTTP checks with a path, Host, interval and thresholds. Avoid write endpoints; use a lightweight `/health` or equivalent.
- **Health check port** (`health_check.port`) probes a different port than the one receiving traffic, like `check port` in HAProxy. Example: a backend serving traffic on `http://10.0.0.5:80` with a status endpoint on port `8079` uses `"health_check": {"mode": "http", "path": "/status", "port": 8079}`. The probe keeps the backend host, scheme, TLS settings and address restrictions; only the port changes. Leave it empty to probe the traffic port. The port can be set for the whole pool or per backend.
- **Expected response text** (`health_check.expect_body`) makes an HTTP probe fail unless the first 64 KiB of the response body contain the given text, like `http-check expect string` in HAProxy. Use it for status pages that answer `200` even when the application is degraded, for example `"expect_body": "\"status\":\"ok\""`. Failed matches appear as `http_body` in backend health details.
- Each member can override the pool health check or disable active/passive monitoring with `health_check_enabled=false`. Disabling checks treats the member as available, not proven healthy.
- New monitored pools can temporarily have no eligible healthy member until rise checks succeed. A failed/incorrect check can produce `503` even if the process is reachable.
- `backup` members act as fallbacks; `drain` prevents new normal selection while letting existing work complete within normal lifetimes. Explicitly pinned backends need careful policy review.
+2 -2
View File
@@ -77,10 +77,10 @@ ENV_FILE=integrations/routeros-oci-build/.env \
bash integrations/routeros-oci-build/build-routeros-oci.sh push
```
For example, with RustProxy version `0.35.3` and the defaults this publishes:
For example, with RustProxy version `0.35.1` and the defaults this publishes:
```text
zot.example.net/network/rustproxy:0.35.3
zot.example.net/network/rustproxy:0.35.1
zot.example.net/network/rustproxy:latest
zot.example.net/network/rustproxy:routeros
```
+1 -20
View File
@@ -457,9 +457,6 @@
"vhHttp": "HTTP status + response time",
"vhHealthPath": "HTTP check path",
"vhHealthHost": "HTTP Host override (optional)",
"vhHealthPort": "Health check port (optional)",
"vhHealthPortPlaceholder": "Backend port",
"vhHealthExpectBody": "Expected text in HTTP response (optional)",
"vhInterval": "Interval (ms)",
"vhTimeout": "Timeout (ms)",
"vhSlow": "Slow threshold (ms; 0 = off)",
@@ -467,7 +464,7 @@
"vhRise": "Successes before recovery",
"vhStatusMin": "Minimum HTTP status",
"vhStatusMax": "Maximum HTTP status",
"vhHealthNote": "HTTP probes use HTTP/1.1 and measure time to headers, not body duration. When Host override is blank, RustProxy automatically uses the first exact DNS hostname of the vhost; if none exists it falls back to the backend host. Use TCP for h2c-only services. New backends wait for successful probes. Active health is independent from passive health. Health check port probes a different port than traffic (for example a status port 8079 for a backend on port 80); host, TLS and address restrictions stay the same. Expected text marks the backend as down unless the first 64 KiB of the HTTP probe response contain it.",
"vhHealthNote": "HTTP probes use HTTP/1.1 and measure time to headers, not body duration. When Host override is blank, RustProxy automatically uses the first exact DNS hostname of the vhost; if none exists it falls back to the backend host. Use TCP for h2c-only services. New backends wait for successful probes. Active health is independent from passive health.",
"vhPassiveHealth": "Passive Backend Health",
"vhPassiveEnabled": "Allow normal request failures to mark a backend unhealthy",
"vhPassiveHelp": "Off by default. Errors are still logged and counted, but normal user traffic cannot change backend health until this is explicitly enabled.",
@@ -2772,22 +2769,6 @@
"overviewExtraCacheMissRateDetail": "Rate of cache misses.",
"overviewExtraBackendActive": "In-flight backend requests",
"overviewExtraBackendActiveDetail": "Current HTTP upstream requests across backends.",
"overviewExtraBackendsHealthy": "Healthy backends",
"overviewExtraBackendsHealthyDescription": "Monitored backend members that currently pass health checks, with the names of failing members.",
"overviewExtraBackendsNone": "No monitored backends",
"overviewExtraBackendsDown": "Down",
"overviewExtraBackendsAllUp": "All monitored backends are up",
"overviewExtraErrorShare": "Error share",
"overviewExtraErrorShareDescription": "Share of HTTP requests that ended with an error in the last sampling interval.",
"overviewExtraErrorShareDetail": "Errors / requests",
"overviewExtraRejectedRate": "Rejected by limits / s",
"overviewExtraRejectedRateDescription": "Requests and connections rejected by concurrency limits. Sustained values suggest raising max-connections or max-inflight.",
"overviewExtraRejectedDetail": "Total requests / connections",
"overviewExtraCertExpiry": "Nearest certificate expiry",
"overviewExtraCertExpiryDescription": "Days until the earliest installed certificate expires; the bar is full at 90 days or more.",
"overviewExtraCertNone": "No certificates installed",
"overviewExtraCertCount": "certificates",
"overviewExtraDaysUnit": "days",
"overviewWideProtocolRates": "HTTP/1.1 · HTTP/2 · HTTP/3 traffic",
"overviewWideProtocolRatesDetail": "Requests per second by frontend protocol.",
"overviewWideCpu": "System and process CPU",
+1 -20
View File
@@ -457,9 +457,6 @@
"vhHttp": "Status HTTP + czas odpowiedzi",
"vhHealthPath": "Ścieżka testu HTTP",
"vhHealthHost": "Własny Host testu HTTP (opcjonalny)",
"vhHealthPort": "Port testu zdrowia (opcjonalny)",
"vhHealthPortPlaceholder": "Port backendu",
"vhHealthExpectBody": "Oczekiwany tekst w odpowiedzi HTTP (opcjonalny)",
"vhInterval": "Interwał (ms)",
"vhTimeout": "Limit czasu (ms)",
"vhSlow": "Próg wolnej odpowiedzi (ms; 0 = wył.)",
@@ -467,7 +464,7 @@
"vhRise": "Sukcesy przed przywróceniem",
"vhStatusMin": "Minimalny status HTTP",
"vhStatusMax": "Maksymalny status HTTP",
"vhHealthNote": "Sondy HTTP używają HTTP/1.1 i mierzą czas do nagłówków, nie czas całego body. Gdy pole Host jest puste, RustProxy automatycznie używa pierwszej dokładnej domeny DNS vhosta; jeśli jej nie ma, używa hosta backendu. Dla usług tylko h2c użyj TCP. Nowe backendy czekają na udaną sondę. Active health jest niezależny od passive health. Port testu zdrowia pozwala sprawdzać inny port niż ruch (np. port statusu 8079 dla backendu na porcie 80); host, TLS i ograniczenia adresów pozostają te same. Oczekiwany tekst oznacza backend jako niedostępny, jeśli pierwsze 64 KiB odpowiedzi testu HTTP go nie zawiera.",
"vhHealthNote": "Sondy HTTP używają HTTP/1.1 i mierzą czas do nagłówków, nie czas całego body. Gdy pole Host jest puste, RustProxy automatycznie używa pierwszej dokładnej domeny DNS vhosta; jeśli jej nie ma, używa hosta backendu. Dla usług tylko h2c użyj TCP. Nowe backendy czekają na udaną sondę. Active health jest niezależny od passive health.",
"vhPassiveHealth": "Pasywne zdrowie backendu",
"vhPassiveEnabled": "Pozwól, aby błędy zwykłych requestów oznaczały backend jako niedostępny",
"vhPassiveHelp": "Domyślnie wyłączone. Błędy nadal są logowane i zliczane, ale zwykły ruch użytkowników nie zmienia stanu backendu bez świadomego włączenia.",
@@ -2772,22 +2769,6 @@
"overviewExtraCacheMissRateDetail": "Tempo nietrafień w cache.",
"overviewExtraBackendActive": "Żądania do backendów",
"overviewExtraBackendActiveDetail": "Suma aktualnych żądań do backendów HTTP.",
"overviewExtraBackendsHealthy": "Sprawne backendy",
"overviewExtraBackendsHealthyDescription": "Monitorowane backendy, które obecnie przechodzą testy zdrowia, wraz z nazwami niedziałających.",
"overviewExtraBackendsNone": "Brak monitorowanych backendów",
"overviewExtraBackendsDown": "Niedostępne",
"overviewExtraBackendsAllUp": "Wszystkie monitorowane backendy działają",
"overviewExtraErrorShare": "Udział błędów",
"overviewExtraErrorShareDescription": "Odsetek żądań HTTP zakończonych błędem w ostatnim interwale próbkowania.",
"overviewExtraErrorShareDetail": "Błędy / żądania",
"overviewExtraRejectedRate": "Odrzucone przez limity / s",
"overviewExtraRejectedRateDescription": "Żądania i połączenia odrzucone przez limity współbieżności. Stałe wartości oznaczają, że warto podnieść max-connections lub max-inflight.",
"overviewExtraRejectedDetail": "Łącznie żądania / połączenia",
"overviewExtraCertExpiry": "Najbliższe wygaśnięcie certyfikatu",
"overviewExtraCertExpiryDescription": "Dni do wygaśnięcia najwcześniej kończącego się certyfikatu; pasek jest pełny przy 90 dniach lub więcej.",
"overviewExtraCertNone": "Brak zainstalowanych certyfikatów",
"overviewExtraCertCount": "certyfikatów",
"overviewExtraDaysUnit": "dni",
"overviewWideProtocolRates": "Ruch HTTP/1.1 · HTTP/2 · HTTP/3",
"overviewWideProtocolRatesDetail": "Tempo żądań według protokołu frontendu.",
"overviewWideCpu": "CPU systemu i procesu",
+1 -30
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "RustProxy Administration API",
"version": "0.35.3",
"version": "0.35.1",
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust. The API also permits explicitly scoped GeneralApiBearer tokens on a narrow route allowlist, with RO/RW and source CIDR checks; API-token management and Prometheus-token management are browser-session-only."
},
"servers": [
@@ -3434,19 +3434,6 @@
"nullable": true,
"description": "Optional HTTP Host override. When null for an HTTP vhost probe, RustProxy uses the first exact vhost DNS hostname; if none exists it falls back to the backend host."
},
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"nullable": true,
"description": "Optional port for active probes. When null or equal to the backend port, probes use the backend address; otherwise TCP and HTTP probes connect to the same host on this port with the same TLS settings and address restrictions."
},
"expect_body": {
"type": "string",
"nullable": true,
"maxLength": 256,
"description": "Optional text that must appear in the first 64 KiB of an HTTP probe response body. A response with an accepted status but without this text counts as a failed probe (result http_body)."
},
"interval_ms": {
"type": "integer",
"minimum": 250,
@@ -3779,10 +3766,6 @@
"extra_h3_share",
"extra_cache_miss_rate",
"extra_backend_active",
"extra_backends_healthy",
"extra_error_share",
"extra_rejected_rate",
"extra_cert_expiry",
"wide_protocol_rates",
"wide_cpu",
"wide_memory",
@@ -3864,10 +3847,6 @@
"extra_h3_share",
"extra_cache_miss_rate",
"extra_backend_active",
"extra_backends_healthy",
"extra_error_share",
"extra_rejected_rate",
"extra_cert_expiry",
"wide_protocol_rates",
"wide_cpu",
"wide_memory",
@@ -3956,10 +3935,6 @@
"extra_h3_share",
"extra_cache_miss_rate",
"extra_backend_active",
"extra_backends_healthy",
"extra_error_share",
"extra_rejected_rate",
"extra_cert_expiry",
"wide_protocol_rates",
"wide_cpu",
"wide_memory",
@@ -4029,10 +4004,6 @@
"extra_h3_share",
"extra_cache_miss_rate",
"extra_backend_active",
"extra_backends_healthy",
"extra_error_share",
"extra_rejected_rate",
"extra_cert_expiry",
"wide_protocol_rates",
"wide_cpu",
"wide_memory",
+2 -18
View File
@@ -165,7 +165,7 @@ function setPanel(name, navigate = true) {
if (name === "filelogs") void window.rustProxyFileLogs?.refresh();
if (name === "tls") void refreshCertificateInventory().then(() => { if (authenticated && currentPanel === "tls") renderCertificates(); });
}
function bytes(value) { if (value < 1024) return `${Math.round(value)} B`; for (const unit of ["KiB", "MiB", "GiB", "TiB"]) { value /= 1024; if (value < 1024 || unit === "TiB") return `${value.toFixed(value < 10 ? 1 : 0)} ${unit}`; } }
function bytes(value) { if (value < 1024) return `${value} B`; for (const unit of ["KiB", "MiB", "GiB", "TiB"]) { value /= 1024; if (value < 1024 || unit === "TiB") return `${value.toFixed(value < 10 ? 1 : 0)} ${unit}`; } }
function uptime(value) {
let seconds = Math.max(0, Math.floor(Number(value) || 0));
if (seconds < 60) return `${seconds}s`;
@@ -232,23 +232,7 @@ window.rustProxyOverviewSamples.onChange(() => {
}
redrawOverviewHistoryCharts();
});
// Safari has no CSS scroll anchoring: keep the element under the viewport centre in place
// when a live refresh changes the height of content above it.
const nativeScrollAnchoring = typeof CSS !== "undefined" && CSS.supports?.("overflow-anchor", "auto");
function preserveScrollAnchor(render) {
if (nativeScrollAnchoring || window.scrollY <= 0) return render();
const probe = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2);
const anchor = probe?.closest?.("article, tr, .mini-live-chart, .network-interface-row") || probe;
const before = anchor?.isConnected ? anchor.getBoundingClientRect().top : null;
const result = render();
if (before !== null && anchor.isConnected) {
const shift = anchor.getBoundingClientRect().top - before;
if (Math.abs(shift) >= 1) window.scrollBy(0, shift);
}
return result;
}
function renderStatus(s, systemSample = false) { return preserveScrollAnchor(() => renderStatusNow(s, systemSample)); }
function renderStatusNow(s, systemSample = false) {
function renderStatus(s, systemSample = false) {
const firstStatus = latestStatus === null;
latestStatus = {
...(latestStatus || {}), ...s,
+2 -39
View File
@@ -8,12 +8,10 @@ window.rustProxyExtraOverview = (() => {
"extra_response_latency", "extra_backend_unhealthy",
"extra_total_rps", "extra_active_exchanges", "extra_h1_share", "extra_h3_share",
"extra_cache_miss_rate", "extra_backend_active",
"extra_backends_healthy", "extra_error_share", "extra_rejected_rate", "extra_cert_expiry",
];
const bounded = new Set([
"extra_process_fds", "extra_cache_memory", "extra_cold_queue_memory", "extra_cold_storage",
"extra_h1_share", "extra_h2_share", "extra_h3_share",
"extra_backends_healthy", "extra_cert_expiry",
]);
const histories = new Map(), lastCounter = new Map(), lastValue = new Map();
const lastUnits = new Map(), lastSegments = new Map();
@@ -100,36 +98,11 @@ window.rustProxyExtraOverview = (() => {
unit: "count", detail: ""
};
}
if (id === "extra_backends_healthy") {
const members = Array.isArray(status.backend_pools)
? status.backend_pools.flatMap(pool => (pool.members || []).map(member => ({ pool: pool.id, ...member }))) : null;
if (!members) return { value: null, unit: "count", detail: "" };
const monitored = members.filter(member => member.health_check_enabled !== false && member.health_mode !== "off");
const down = monitored.filter(member => member.status === "down" || member.healthy === false);
const up = monitored.length - down.length;
return {
value: monitored.length ? `${fmt(up)} / ${fmt(monitored.length)}` : null, unit: "text",
meterPercent: monitored.length ? up / monitored.length * 100 : null,
detail: !monitored.length ? t("overviewExtraBackendsNone")
: down.length ? `${t("overviewExtraBackendsDown")}: ${down.slice(0, 3).map(member => `${member.pool}/${member.id}`).join(", ")}${down.length > 3 ? " …" : ""}`
: t("overviewExtraBackendsAllUp"),
};
}
if (id === "extra_cert_expiry") {
const inventory = typeof certificateInventory !== "undefined" && certificateInventory instanceof Map ? [...certificateInventory.values()] : [];
const dated = inventory.filter(cert => Number.isFinite(Number(cert?.days_remaining)))
.sort((a, b) => Number(a.days_remaining) - Number(b.days_remaining));
if (!dated.length) return { value: null, unit: "days", detail: t("overviewExtraCertNone") };
const nearest = dated[0], days = Number(nearest.days_remaining);
const name = (Array.isArray(nearest.covered_hosts) && nearest.covered_hosts[0]) || (Array.isArray(nearest.hosts) && nearest.hosts[0]) || nearest.id;
return { value: days, unit: "days", meterPercent: Math.max(0, Math.min(100, days / 90 * 100)), detail: `${name} · ${fmt(dated.length)} ${t("overviewExtraCertCount")}` };
}
if (id === "extra_cache_memory") return ramBudget(c.resident_bytes, c.hot_limit_bytes);
if (id === "extra_cold_queue_memory") return ramBudget(c.cold_queue_bytes, c.cold_queue_limit_bytes);
if (id === "extra_cold_storage") return ramBudget(c.cold_bytes, c.cold_limit_bytes);
if (!["extra_tls_error_rate", "extra_request_error_rate", "extra_cache_hit_rate", "extra_h2_share",
"extra_total_rps", "extra_h1_share", "extra_h3_share", "extra_cache_miss_rate",
"extra_error_share", "extra_rejected_rate"].includes(id))
"extra_total_rps", "extra_h1_share", "extra_h3_share", "extra_cache_miss_rate"].includes(id))
return { value: null, unit: "count", detail: "" };
const fields = ({
extra_tls_error_rate: [m.tls_handshake_errors],
@@ -140,10 +113,8 @@ window.rustProxyExtraOverview = (() => {
extra_h3_share: [m.http3_requests, m.requests],
extra_total_rps: [m.requests],
extra_cache_miss_rate: [c.misses],
extra_error_share: [m.errors, m.requests],
extra_rejected_rate: [m.rejected_requests, m.rejected_connections],
})[id].map(positive);
const unit = ["extra_cache_hit_rate", "extra_h2_share", "extra_h1_share", "extra_h3_share", "extra_error_share"].includes(id)
const unit = ["extra_cache_hit_rate", "extra_h2_share", "extra_h1_share", "extra_h3_share"].includes(id)
? "percent" : "rate";
if (fields.some(v => v === null)) {
lastCounter.delete(id);
@@ -153,12 +124,6 @@ window.rustProxyExtraOverview = (() => {
const result = elapsedDelta(id, fields, now);
if (!result) return { value: null, unit, detail: "", slices: null };
const { deltas, seconds } = result;
if (id === "extra_rejected_rate") return {
value: (deltas[0] + deltas[1]) / seconds, unit, detail: `${t("overviewExtraRejectedDetail")}: ${fmt(fields[0])} / ${fmt(fields[1])}`
};
if (id === "extra_error_share") return {
value: deltas[1] > 0 ? Math.min(100, deltas[0] / deltas[1] * 100) : 0, unit, detail: `${t("overviewExtraErrorShareDetail")}: ${fmt(deltas[0])} / ${fmt(deltas[1])}`
};
if (unit === "rate") return { value: deltas[0] / seconds, unit, detail: "" };
const sum = id === "extra_cache_hit_rate" ? deltas[0] + deltas[1] : deltas[1];
return {
@@ -172,8 +137,6 @@ window.rustProxyExtraOverview = (() => {
if (unit === "bytes") return fmtBytes(value);
if (unit === "ms") return `${fmt(value, 1)} ms`;
if (unit === "rate") return `${fmt(value, 1)}/s`;
if (unit === "days") return `${fmt(value)} ${t("overviewExtraDaysUnit")}`;
if (unit === "text") return String(value);
return fmt(value);
}
function points(id, history, unit) {
+4 -13
View File
@@ -9,27 +9,18 @@ window.rustProxyOverviewSamples = (() => {
} catch { /* Private browsing or storage policy: temporary selection still works. */ }
let limit = saved;
const callbacks = new Set();
let confirmationTimer = null;
const getLimit = () => limit;
const trim = array => {
if (Array.isArray(array) && array.length > limit) array.splice(0, array.length - limit);
return array;
};
// The status is shown only for a temporary choice, a storage problem or briefly after saving.
function sync(messageKey = limit === saved ? null : "overviewSamplesTemporary", confirmation = false) {
function sync(messageKey = limit === saved ? "overviewSamplesSaved" : "overviewSamplesTemporary") {
const select = document.getElementById("overview-live-samples");
if (select) select.value = String(limit);
const state = document.getElementById("overview-samples-state");
if (state) {
clearTimeout(confirmationTimer);
if (messageKey) {
state.dataset.i18n = messageKey;
state.textContent = t(messageKey);
state.style.visibility = "visible";
} else {
state.style.visibility = "hidden";
}
if (confirmation) confirmationTimer = setTimeout(() => { state.style.visibility = "hidden"; }, 2500);
state.dataset.i18n = messageKey;
state.textContent = t(messageKey);
}
const save = document.getElementById("overview-samples-save");
if (save) save.disabled = limit === saved;
@@ -52,7 +43,7 @@ window.rustProxyOverviewSamples = (() => {
const changed = count !== limit;
limit = count;
if (changed) for (const callback of callbacks) callback(limit);
if (persist) sync("overviewSamplesSaved", true); else sync();
sync();
return true;
}
function init() {
+1 -1
View File
@@ -182,7 +182,7 @@ const rustProxyRefresh = (() => {
const value = await api("/api/system-info", { signal });
if (!isCurrent()) return;
latestStatus = { ...(latestStatus || {}), ...value };
if (latestStatus.metrics) preserveScrollAnchor(() => renderDiagnostics(latestStatus, false, true));
if (latestStatus.metrics) renderDiagnostics(latestStatus, false, true);
window.rustProxySettings?.render();
}
});
-14
View File
@@ -2119,17 +2119,3 @@ html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .over
color: var(--muted);
font: 400 10px var(--mono);
}
/* Live values stay on one line so refreshes do not change card heights. */
.mini-live-chart-head> :last-child,
.network-interface-row-head .network-interface-head-actions {
flex: 0 0 auto;
white-space: nowrap;
}
.mini-live-chart-head>span:first-child {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
+2 -10
View File
@@ -78,11 +78,7 @@
"www_failures",
"www_traffic",
"www_top_services",
"www_status_trend",
"extra_backends_healthy",
"extra_error_share",
"extra_rejected_rate",
"extra_cert_expiry"
"www_status_trend"
],
"dashboard_hidden_widgets": [
"cache_outcomes",
@@ -136,11 +132,7 @@
"www_status_trend",
"extra_cold_queue_memory",
"wide_cache_activity",
"backend_rotations",
"extra_backends_healthy",
"extra_error_share",
"extra_rejected_rate",
"extra_cert_expiry"
"backend_rotations"
],
"dashboard_density": "compact",
"overview_samples": 600
-44
View File
@@ -867,49 +867,5 @@
"category": "web_server",
"default_hidden": true,
"display_type": "chart"
},
{
"id": "extra_backends_healthy",
"label_key": "overviewExtraBackendsHealthy",
"description_key": "overviewExtraBackendsHealthyDescription",
"element_selector": "[data-dashboard-widget=\"extra_backends_healthy\"]",
"data_source": "backend_status",
"no_data": "neutral_dash",
"category": "backends",
"default_hidden": true,
"display_type": "value_chart"
},
{
"id": "extra_error_share",
"label_key": "overviewExtraErrorShare",
"description_key": "overviewExtraErrorShareDescription",
"element_selector": "[data-dashboard-widget=\"extra_error_share\"]",
"data_source": "status_metrics",
"no_data": "neutral_dash",
"category": "performance",
"default_hidden": true,
"display_type": "value_chart"
},
{
"id": "extra_rejected_rate",
"label_key": "overviewExtraRejectedRate",
"description_key": "overviewExtraRejectedRateDescription",
"element_selector": "[data-dashboard-widget=\"extra_rejected_rate\"]",
"data_source": "status_metrics",
"no_data": "neutral_dash",
"category": "performance",
"default_hidden": true,
"display_type": "value_chart"
},
{
"id": "extra_cert_expiry",
"label_key": "overviewExtraCertExpiry",
"description_key": "overviewExtraCertExpiryDescription",
"element_selector": "[data-dashboard-widget=\"extra_cert_expiry\"]",
"data_source": "system_status",
"no_data": "neutral_dash",
"category": "overview",
"default_hidden": true,
"display_type": "value_chart"
}
]
@@ -365,36 +365,6 @@
</polyline>
</svg><small id="overview-extra-extra_backend_unhealthy-detail">-</small>
</article>
<article class="stat stat-meter" data-dashboard-widget="extra_backends_healthy" hidden>
<span data-i18n="overviewExtraBackendsHealthy">overviewExtraBackendsHealthy</span><strong id="overview-extra-extra_backends_healthy">-</strong>
<div class="overview-meter" id="overview-extra-extra_backends_healthy-meter" role="progressbar"
data-i18n-aria="overviewExtraBackendsHealthy" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
<span class="overview-meter-fill" id="overview-extra-extra_backends_healthy-meter-fill"></span>
</div><small id="overview-extra-extra_backends_healthy-detail">-</small>
</article>
<article class="stat stat-chart" data-dashboard-widget="extra_error_share" hidden>
<span data-i18n="overviewExtraErrorShare">overviewExtraErrorShare</span><strong id="overview-extra-extra_error_share">-</strong>
<svg preserveAspectRatio="none" data-i18n-aria="overviewExtraErrorShare" role="img" viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_error_share-line" points="0,55 300,55">
</polyline>
</svg><small id="overview-extra-extra_error_share-detail">-</small>
</article>
<article class="stat stat-chart" data-dashboard-widget="extra_rejected_rate" hidden>
<span data-i18n="overviewExtraRejectedRate">overviewExtraRejectedRate</span><strong id="overview-extra-extra_rejected_rate">-</strong>
<svg preserveAspectRatio="none" data-i18n-aria="overviewExtraRejectedRate" role="img" viewBox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="overview-extra-extra_rejected_rate-line" points="0,55 300,55">
</polyline>
</svg><small id="overview-extra-extra_rejected_rate-detail">-</small>
</article>
<article class="stat stat-meter" data-dashboard-widget="extra_cert_expiry" hidden>
<span data-i18n="overviewExtraCertExpiry">overviewExtraCertExpiry</span><strong id="overview-extra-extra_cert_expiry">-</strong>
<div class="overview-meter" id="overview-extra-extra_cert_expiry-meter" role="progressbar"
data-i18n-aria="overviewExtraCertExpiry" aria-valuemin="0" aria-valuemax="100" aria-valuetext="-">
<span class="overview-meter-fill" id="overview-extra-extra_cert_expiry-meter-fill"></span>
</div><small id="overview-extra-extra_cert_expiry-detail">-</small>
</article>
<article class="stat stat-chart" data-dashboard-widget="extra_total_rps" hidden>
<span data-i18n="overviewExtraTotalRps">HTTP requests / s</span><strong
id="overview-extra-extra_total_rps">-</strong>
-3
View File
@@ -38,7 +38,6 @@ const num = (name, scope) => { const value = Number(str(name, scope)); if (!Numb
const yes = (name, scope) => read(name, scope).checked;
const lines = value => value.split(/[\n,]+/).map(s => s.trim()).filter(Boolean);
const nullable = (name, scope) => str(name, scope) || null;
const optionalPort = (name, scope) => { const value = str(name, scope); return value ? Number(value) : null; };
const defaultProxyTimeouts = () => ({ connect_timeout_seconds: 10, response_header_timeout_seconds: 120, read_timeout_seconds: 60, send_timeout_seconds: 60 });
const changed = () => { dirty = true; };
const fillWebServices = (select, chosen = "") => {
@@ -237,8 +236,6 @@ function memberRow(parent, value = { id: `origin-${parent.children.length + 1}`,
field(healthFields, "member-health-mode", "vhHealthMode", health.mode || "tcp", "select", { values: [["off", "vhOff"], ["tcp", "vhTcp"], ["http", "vhHttp"]] });
field(healthFields, "member-health-path", "vhHealthPath", health.path ?? "/", "text", { maxLength: 1024 });
field(healthFields, "member-health-host", "vhHealthHost", health.host ?? "", "text", { maxLength: 253 });
field(healthFields, "member-health-port", "vhHealthPort", health.port ?? "", "number", { min: 1, max: 65535, step: 1 }).placeholder = t("vhHealthPortPlaceholder");
field(healthFields, "member-health-expect-body", "vhHealthExpectBody", health.expect_body ?? "", "text", { maxLength: 256 });
field(healthFields, "member-health-interval", "vhInterval", health.interval_ms ?? 5000, "number", { min: 250, max: 300000, required: true });
field(healthFields, "member-health-timeout", "vhTimeout", health.timeout_ms ?? 2000, "number", { min: 50, max: 60000, required: true });
field(healthFields, "member-health-slow", "vhSlow", health.slow_response_ms ?? 1500, "number", { min: 0, max: 60000, required: true });
+2 -4
View File
@@ -238,8 +238,6 @@ function load(value = defaults(), migrationIds = [], show = true) {
const healthOffWarning = note(g, "vhHealthOffWarning"); const syncHealthWarning = () => { healthOffWarning.hidden = healthMode.value !== "off"; }; healthMode.addEventListener("change", syncHealthWarning); syncHealthWarning();
field(g, "health-path", "vhHealthPath", health.path || "/", "text", { required: true, maxLength: 1024 });
field(g, "health-host", "vhHealthHost", health.host || "", "text", { maxLength: 260 });
field(g, "health-port", "vhHealthPort", health.port ?? "", "number", { min: 1, max: 65535, step: 1 }).placeholder = t("vhHealthPortPlaceholder");
field(g, "health-expect-body", "vhHealthExpectBody", health.expect_body || "", "text", { maxLength: 256 });
for (const [name, key, val, min, max] of [["interval", "vhInterval", health.interval_ms ?? 5000, 250, 300000], ["timeout", "vhTimeout", health.timeout_ms ?? 2000, 50, 60000], ["slow", "vhSlow", health.slow_response_ms ?? 1500, 0, 60000], ["fall", "vhFall", health.fall ?? 3, 1, 20], ["rise", "vhRise", health.rise ?? 2, 1, 20], ["status-min", "vhStatusMin", health.expected_status_min ?? 200, 100, 599], ["status-max", "vhStatusMax", health.expected_status_max ?? 399, 100, 599]]) field(g, name, key, val, "number", { required: true, min, max }); note(g, "vhHealthNote");
backendHealthEditorControl = backendHealthEditor(g, members);
note(g, "backendHealthOverridesHelp");
@@ -562,7 +560,7 @@ function collect() {
pool_eligible: yes("member-pool", row), backup: yes("member-backup", row), drain: yes("member-drain", row),
health_check_enabled: yes("member-health-enabled", row),
health_check: yes("member-health-custom", row.memberHealthPanel) ? {
mode: str("member-health-mode", row.memberHealthPanel), path: str("member-health-path", row.memberHealthPanel), host: nullable("member-health-host", row.memberHealthPanel), port: optionalPort("member-health-port", row.memberHealthPanel), expect_body: nullable("member-health-expect-body", row.memberHealthPanel),
mode: str("member-health-mode", row.memberHealthPanel), path: str("member-health-path", row.memberHealthPanel), host: nullable("member-health-host", row.memberHealthPanel),
interval_ms: num("member-health-interval", row.memberHealthPanel), timeout_ms: num("member-health-timeout", row.memberHealthPanel), slow_response_ms: num("member-health-slow", row.memberHealthPanel),
fall: num("member-health-fall", row.memberHealthPanel), rise: num("member-health-rise", row.memberHealthPanel),
expected_status_min: num("member-health-min", row.memberHealthPanel), expected_status_max: num("member-health-max", row.memberHealthPanel)
@@ -632,7 +630,7 @@ function collect() {
load_balancing: {
algorithm: str("algorithm"), slow_start_seconds: num("slow-start"), keepalive: { enabled: yes("keepalive"), idle_timeout_seconds: num("idle-timeout"), max_idle_per_host: num("max-idle"), tcp_keepalive_seconds: num("tcp-keepalive"), pool_recycle_requests: num("pool-recycle-requests"), pool_recycle_seconds: num("pool-recycle-seconds"), http1_max_buffer_bytes: num("http1-max-buffer") },
retry: { max_retries: num("retry-max"), retry_connect_errors: yes("retry-connect"), retry_timeouts: yes("retry-timeouts"), retry_statuses: str("retry-statuses").split(/[\s,]+/).filter(Boolean).map(value => Number(value)) },
health_check: { mode: str("health-mode"), path: str("health-path"), host: nullable("health-host"), port: optionalPort("health-port"), expect_body: nullable("health-expect-body"), interval_ms: num("interval"), timeout_ms: num("timeout"), slow_response_ms: num("slow"), fall: num("fall"), rise: num("rise"), expected_status_min: num("status-min"), expected_status_max: num("status-max") },
health_check: { mode: str("health-mode"), path: str("health-path"), host: nullable("health-host"), interval_ms: num("interval"), timeout_ms: num("timeout"), slow_response_ms: num("slow"), fall: num("fall"), rise: num("rise"), expected_status_min: num("status-min"), expected_status_max: num("status-max") },
passive_health: {
enabled: yes("passive-enabled"),
count_connect_errors: yes("passive-connect"),
+1 -1
View File
@@ -2,7 +2,7 @@
function inheritBackendHealthInputs(panel) {
for (const [local, shared] of [
["member-health-mode", "health-mode"], ["member-health-path", "health-path"],
["member-health-host", "health-host"], ["member-health-port", "health-port"], ["member-health-expect-body", "health-expect-body"], ["member-health-interval", "interval"],
["member-health-host", "health-host"], ["member-health-interval", "interval"],
["member-health-timeout", "timeout"], ["member-health-slow", "slow"],
["member-health-fall", "fall"], ["member-health-rise", "rise"],
["member-health-min", "status-min"], ["member-health-max", "status-max"]
-1
View File
@@ -450,7 +450,6 @@ pub struct Member {
pub backend: Arc<CompiledBackend>,
pub definition: PoolMember,
pub state: Arc<BackendState>,
health_probe: Option<Arc<CompiledBackend>>,
health_settings: Arc<HealthCheck>,
passive_settings: Arc<PassiveHealth>,
token: String,
+7 -37
View File
@@ -291,36 +291,12 @@ impl Pool {
text
}
}
const PROBE_BODY_LIMIT: usize = 65_536;
async fn probe_body_contains(mut body: hyper::body::Incoming, expected: &[u8]) -> bool {
use http_body_util::BodyExt;
let mut seen = Vec::new();
while seen.len() < PROBE_BODY_LIMIT {
match body.frame().await {
Some(Ok(frame)) => {
if let Ok(data) = frame.into_data() {
let room = PROBE_BODY_LIMIT - seen.len();
seen.extend_from_slice(&data[..data.len().min(room)]);
if seen.windows(expected.len()).any(|window| window == expected) {
return true;
}
}
}
Some(Err(_)) => return false,
None => break,
}
}
seen.windows(expected.len()).any(|window| window == expected)
}
async fn probe(member: Arc<Member>, settings: Arc<HealthCheck>) {
let start = Instant::now();
let target = member.health_probe.as_deref().unwrap_or(&member.backend);
let result = tokio::time::timeout(Duration::from_millis(settings.timeout_ms), async {
match settings.mode {
HealthMode::Tcp => {
crate::upstream::tcp_probe(&target.origin, &target.address_policy)
crate::upstream::tcp_probe(&member.backend.origin, &member.backend.address_policy)
.await
.map(|_| "ok")
.unwrap_or("connect")
@@ -329,14 +305,14 @@ async fn probe(member: Arc<Member>, settings: Arc<HealthCheck>) {
let request = Request::builder()
.uri(format!(
"{}{}",
target.request_origin, settings.path
member.backend.request_origin, settings.path
))
.header(
"host",
settings
.host
.as_deref()
.unwrap_or_else(|| target.host_header.to_str().unwrap_or("")),
.unwrap_or_else(|| member.backend.host_header.to_str().unwrap_or("")),
)
.header("connection", "close")
.header(
@@ -345,20 +321,14 @@ async fn probe(member: Arc<Member>, settings: Arc<HealthCheck>) {
)
.body(Body::empty());
match request {
Ok(request) => match target.websocket_client.request(request).await {
Ok(request) => match member.backend.websocket_client.request(request).await {
Ok(response) => {
if !(settings.expected_status_min..=settings.expected_status_max)
if (settings.expected_status_min..=settings.expected_status_max)
.contains(&response.status().as_u16())
{
"http_status"
} else if let Some(expected) = settings.expect_body.as_deref() {
if probe_body_contains(response.into_body(), expected.as_bytes()).await {
"ok"
} else {
"http_body"
}
} else {
"ok"
} else {
"http_status"
}
}
Err(_) => "connect",
-11
View File
@@ -99,8 +99,6 @@ pub struct HealthCheck {
pub mode: HealthMode,
pub path: String,
pub host: Option<String>,
pub port: Option<u16>,
pub expect_body: Option<String>,
pub interval_ms: u64,
pub timeout_ms: u64,
pub slow_response_ms: u64,
@@ -115,8 +113,6 @@ impl Default for HealthCheck {
mode: HealthMode::Tcp,
path: "/".into(),
host: None,
port: None,
expect_body: None,
interval_ms: 5000,
timeout_ms: 2000,
slow_response_ms: 1500,
@@ -265,13 +261,6 @@ impl Settings {
&& !h.path.bytes().any(|b| b <= 32 || b == 127),
"invalid health HTTP path"
);
ensure!(h.port != Some(0), "health check port must be 1..65535");
if let Some(expected) = &h.expect_body {
ensure!(
!expected.is_empty() && expected.len() <= 256 && !expected.chars().any(char::is_control),
"health check expected body text must be 1..256 printable characters"
);
}
if let Some(host) = &h.host {
crate::config::request_host(host)?;
let _: HeaderValue = host.parse()?;
-1
View File
@@ -24,7 +24,6 @@ impl Pool {
let passive_settings = Arc::new(settings.passive_for(&definition));
Arc::new(Member {
state: Arc::new(BackendState::new(health_settings.mode)),
health_probe: backend.health_probe.clone(),
definition,
backend,
health_settings,
-129
View File
@@ -832,133 +832,4 @@ mod tests {
assert_eq!(state.last_status.load(Ordering::Relaxed), 503);
assert_eq!(lock(&state.last_failure).detail, "status=503");
}
fn health_port_config(backend_port: u16, health: serde_json::Value) -> crate::config::Config {
serde_json::from_value(serde_json::json!({
"listeners":{"endpoints":[{"id":"http","binding":{"address":"127.0.0.1:8080"}}]},
"vhosts":[{
"id":"app","frontend":{"hosts":["app.localhost"],"listener_ids":["http"]},
"backends":[{"id":"a","url":format!("http://127.0.0.1:{backend_port}"),"allow_private":true}],
"load_balancing":{"health_check":health}
}]})).unwrap()
}
fn health_port_policy() -> crate::config::ValidationPolicy {
crate::config::ValidationPolicy {
proxy_addr: "127.0.0.1:8080".parse().unwrap(),
admin_addr: "127.0.0.1:9090".parse().unwrap(),
tls_addr: None,
cert_dir: None,
data_dir: None,
}
}
#[test]
fn health_check_port_compiles_separate_probe_target() {
let policy = health_port_policy();
let snapshot = crate::config::compile(
health_port_config(9000, serde_json::json!({"mode":"tcp","port":8079})), 0, &policy).unwrap();
let backend = &snapshot.pools[0].members[0].backend;
assert_eq!(backend.origin, "http://127.0.0.1:9000");
assert_eq!(backend.health_probe.as_ref().unwrap().origin, "http://127.0.0.1:8079");
for health in [
serde_json::json!({"mode":"tcp","port":9000}),
serde_json::json!({"mode":"tcp"}),
serde_json::json!({"mode":"off","port":8079}),
] {
let snapshot = crate::config::compile(health_port_config(9000, health), 0, &policy).unwrap();
assert!(snapshot.pools[0].members[0].backend.health_probe.is_none());
}
assert!(crate::config::compile(
health_port_config(9000, serde_json::json!({"mode":"tcp","port":0})), 0, &policy).is_err());
assert!(crate::config::compile(
health_port_config(9000, serde_json::json!({"mode":"http","port":9090})), 0, &policy).is_err());
}
#[tokio::test]
async fn health_check_probes_configured_port_instead_of_traffic_port() {
let traffic = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let traffic_port = traffic.local_addr().unwrap().port();
let closed_port = {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
listener.local_addr().unwrap().port()
};
let policy = health_port_policy();
let config = health_port_config(traffic_port, serde_json::json!({
"mode":"tcp","port":closed_port,"fall":1,"rise":1,"interval_ms":250,"timeout_ms":200,"slow_response_ms":0}));
let snapshot = crate::config::compile(config, 0, &policy).unwrap();
let member = snapshot.pools[0].members[0].clone();
let settings = member.health_settings.clone();
probe(member.clone(), settings.clone()).await;
assert!(!member.state.healthy(), "probe must use the closed health port");
let health = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let health_port = health.local_addr().unwrap().port();
drop(traffic);
let config = health_port_config(traffic_port, serde_json::json!({
"mode":"tcp","port":health_port,"fall":1,"rise":1,"interval_ms":250,"timeout_ms":200,"slow_response_ms":0}));
let snapshot = crate::config::compile(config, 0, &policy).unwrap();
let member = snapshot.pools[0].members[0].clone();
let settings = member.health_settings.clone();
probe(member.clone(), settings).await;
assert!(member.state.healthy(), "probe must succeed on the open health port while traffic port is closed");
tokio::time::timeout(Duration::from_secs(1), health.accept())
.await
.expect("health port must receive the probe connection")
.unwrap();
}
async fn one_shot_http(body: &'static str) -> (u16, tokio::task::JoinHandle<()>) {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = listener.local_addr().unwrap().port();
let task = tokio::spawn(async move {
while let Ok((mut socket, _)) = listener.accept().await {
let mut buffer = [0u8; 2048];
let _ = socket.read(&mut buffer).await;
let response = format!(
"HTTP/1.1 200 OK\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
body.len(), body
);
let _ = socket.write_all(response.as_bytes()).await;
}
});
(port, task)
}
#[tokio::test]
async fn http_health_check_requires_expected_body_text() {
let policy = health_port_policy();
for (body, healthy) in [("{\"status\":\"ok\"}", true), ("{\"status\":\"degraded\"}", false)] {
let (port, server) = one_shot_http(body).await;
let config = health_port_config(port, serde_json::json!({
"mode":"http","path":"/health","expect_body":"\"status\":\"ok\"",
"fall":1,"rise":1,"interval_ms":1000,"timeout_ms":800,"slow_response_ms":0}));
let snapshot = crate::config::compile(config, 0, &policy).unwrap();
let member = snapshot.pools[0].members[0].clone();
probe(member.clone(), member.health_settings.clone()).await;
assert_eq!(member.state.healthy(), healthy, "body {body}");
if !healthy {
assert_eq!(lock(&member.state.health).result, "http_body");
}
server.abort();
}
for invalid in ["", "line\nbreak"] {
assert!(crate::config::compile(
health_port_config(9000, serde_json::json!({"mode":"http","expect_body":invalid})), 0, &policy).is_err());
}
}
#[tokio::test]
async fn health_check_port_survives_live_reload_with_reused_clients() {
let policy = health_port_policy();
let defaults = (policy.proxy_addr, policy.tls_addr);
let first = crate::config::compile_reusing(
health_port_config(9000, serde_json::json!({"mode":"tcp"})), 0, &policy, defaults, None).unwrap();
assert!(first.pools[0].members[0].health_probe.is_none());
let second = crate::config::compile_reusing(
health_port_config(9000, serde_json::json!({"mode":"tcp","port":8079})), 1, &policy, defaults, Some(&first)).unwrap();
let member = &second.pools[0].members[0];
assert!(Arc::ptr_eq(&member.backend, &first.pools[0].members[0].backend), "traffic clients are reused");
assert_eq!(member.health_probe.as_ref().unwrap().origin, "http://127.0.0.1:8079");
let third = crate::config::compile_reusing(
health_port_config(9000, serde_json::json!({"mode":"tcp"})), 2, &policy, defaults, Some(&second)).unwrap();
assert!(third.pools[0].members[0].health_probe.is_none());
}
}
-15
View File
@@ -104,23 +104,8 @@ pub(crate) fn compile_backend_with_timeout(
targets_admin_listener,
client,
websocket_client,
health_probe: None,
})
}
/// Backend copy used only by active health checks that probe a different port.
pub(crate) fn health_probe_backend(backend: &Backend, port: Option<u16>) -> Result<Option<Backend>> {
let Some(port) = port else { return Ok(None) };
let mut url = Url::parse(&backend.url).context("invalid backend URL")?;
if url.port_or_known_default() == Some(port) {
return Ok(None);
}
url.set_port(Some(port))
.map_err(|_| anyhow::anyhow!("backend URL does not accept a health check port"))?;
let mut probe = backend.clone();
probe.url = url.as_str().trim_end_matches('/').to_owned() + "/";
Ok(Some(probe))
}
pub fn compile(config: Config, revision: u64, policy: &ValidationPolicy) -> Result<Snapshot> {
compile_with_defaults(
config,
-1
View File
@@ -224,7 +224,6 @@ pub struct CompiledBackend {
pub tls_verify: bool,
pub accept_local_listener_risk: bool,
pub targets_admin_listener: bool,
pub health_probe: Option<Arc<CompiledBackend>>,
}
#[derive(Debug, Clone)]
pub enum CompiledSource {
+3 -24
View File
@@ -99,38 +99,17 @@ fn compile_routing(
});
let mut compiled_members = Vec::new();
for definition in host.backends.iter().filter(|_| needs_proxy_pool) {
let connect_timeout = host
.proxy_timeouts
.connect_timeout()
.unwrap_or(std::time::Duration::from_secs(10));
let mut backend = compile_backend_with_timeout(
&definition.backend(),
desired_policy,
guarded_bindings,
&host.load_balancing.keepalive,
connect_timeout,
host.proxy_timeouts
.connect_timeout()
.unwrap_or(std::time::Duration::from_secs(10)),
)
.with_context(|| format!("vhost {} backend {}", host.id, definition.id))?;
backend.id = format!("{}/{}", host.id, definition.id);
let health = host.load_balancing.health_for(definition);
if health.mode != crate::balance::HealthMode::Off {
if let Some(probe) =
health_probe_backend(&definition.backend(), health.port)?
{
let mut probe = compile_backend_with_timeout(
&probe,
desired_policy,
guarded_bindings,
&host.load_balancing.keepalive,
connect_timeout,
)
.with_context(|| {
format!("vhost {} backend {} health check port", host.id, definition.id)
})?;
probe.id = format!("{}/{}#health", host.id, definition.id);
backend.health_probe = Some(Arc::new(probe));
}
}
compiled_members.push(Arc::new(backend));
}
let pool = if compiled_members.is_empty() {
-1
View File
@@ -1,7 +1,6 @@
#![allow(clippy::too_many_arguments)]
#![cfg_attr(test, allow(clippy::field_reassign_with_default))]
#[cfg(feature = "mimalloc")]
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
-59
View File
@@ -1,41 +1,3 @@
/// Certificate expiry read from the certificate files, refreshed at most every five minutes.
fn certificate_expiry(
app: &App,
certificates: &[crate::tls::Certificate],
) -> std::collections::HashMap<String, i64> {
type Cached = (std::time::Instant, Vec<(String, String)>, std::collections::HashMap<String, i64>);
static CACHE: std::sync::Mutex<Option<Cached>> = std::sync::Mutex::new(None);
let wanted: Vec<(String, String)> = certificates
.iter()
.map(|cert| (cert.id.clone(), cert.cert_file.clone()))
.collect();
let mut cache = CACHE.lock().unwrap_or_else(|e| e.into_inner());
if let Some((at, files, values)) = cache.as_ref() {
if at.elapsed() < std::time::Duration::from_secs(300) && *files == wanted {
return values.clone();
}
}
let values = read_certificate_expiry(app.policy.cert_dir.as_deref(), &wanted);
*cache = Some((std::time::Instant::now(), wanted, values.clone()));
values
}
fn read_certificate_expiry(
directory: Option<&std::path::Path>,
certificates: &[(String, String)],
) -> std::collections::HashMap<String, i64> {
let Some(directory) = directory else { return Default::default() };
certificates
.iter()
.filter_map(|(id, file)| {
crate::tls::certificate_metadata(directory, file)
.ok()
.and_then(|metadata| metadata.not_after_unix)
.map(|not_after| (id.clone(), not_after))
})
.collect()
}
fn render_core(app: &App, snapshot: &crate::config::Snapshot, out: &mut String, f: &mut Families) {
let config = &snapshot.envelope.config;
let settings = &config.prometheus;
@@ -139,29 +101,8 @@ fn render_core(app: &App, snapshot: &crate::config::Snapshot, out: &mut String,
config.tls.certificates.len(),
);
let renewals = app.acme.renewal_view();
let expiry = certificate_expiry(app, &config.tls.certificates);
for certificate in &config.tls.certificates {
let l = labels(&[("certificate", &certificate.id)]);
if let Some(not_after) = expiry.get(&certificate.id).copied() {
f.add(
"rust_proxy_certificate_not_after_timestamp_seconds",
"gauge",
"Expiry time of the installed certificate (Unix seconds).",
&l,
not_after,
);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|elapsed| elapsed.as_secs() as i64)
.unwrap_or_default();
f.add(
"rust_proxy_certificate_days_remaining",
"gauge",
"Whole days until the installed certificate expires; negative when expired.",
&l,
(not_after - now).div_euclid(86_400),
);
}
let issuer = certificate.issuer.as_ref();
f.add(
"rust_proxy_certificate_auto_renew_enabled",
-14
View File
@@ -218,18 +218,4 @@ mod tests {
assert_eq!(response.status(), StatusCode::OK);
store.close().await;
}
#[test]
fn certificate_expiry_is_read_from_installed_certificate_files() {
let dir = tempfile::tempdir().unwrap();
let generated = rcgen::generate_simple_self_signed(vec!["expiry.localhost".into()]).unwrap();
std::fs::write(dir.path().join("site.pem"), generated.cert.pem()).unwrap();
let values = read_certificate_expiry(
Some(dir.path()),
&[("site".into(), "site.pem".into()), ("missing".into(), "missing.pem".into())],
);
assert!(values.get("site").is_some_and(|not_after| *not_after > 0));
assert!(!values.contains_key("missing"));
assert!(read_certificate_expiry(None, &[("site".into(), "site.pem".into())]).is_empty());
}
}