From 643999f17e3ae2ed6d724d4f82cb98f7dbfc8e45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Tue, 10 Nov 2026 11:36:23 +0100 Subject: [PATCH] v0.36.10 --- AGENTS.md | 2 +- CHANGELOG.md | 7 ++ Cargo.lock | 2 +- Cargo.toml | 2 +- SHA256SUMS | 129 ++++++++++---------- integrations/routeros-oci-build/README.md | 4 +- langs/en_US.json | 9 ++ langs/pl_pl.json | 9 ++ src/admin/openapi.json | 20 +++- src/admin/preferences.rs | 1 + src/admin/preferences_columns.rs | 14 +++ src/admin/status.rs | 1 + src/admin/status_live.rs | 1 + src/admin/tests.rs | 16 ++- src/admin/web/css/30-overview.css | 32 +++++ src/admin/web/index.html | 21 ++++ src/admin/web/vhosts/core.js | 1 + src/admin/web/vhosts/render.js | 138 ++++++++++++++++++++-- src/cache/dashboard.rs | 3 + src/cache/dashboard_tests.rs | 17 +++ src/cache/model/entry.rs | 7 ++ src/cache/stats.rs | 21 ++++ src/metrics/vhost/mod.rs | 27 +++++ src/metrics/vhost/tests.rs | 21 ++++ tests/panel_refresh_contract.js | 2 +- 25 files changed, 421 insertions(+), 86 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 02e9f48..5cf0b3c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # RustProxy source and documentation maintenance -Current source version: **0.36.10**. The Cargo package and binary are named `rust-proxy`. +Current source version: **0.36.11**. 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index f62b924..f18b1bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ 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.36.11 — more Backends metrics + +- **Backends overview — new columns:** seven more metrics, each sortable and switchable under **Columns**, fill the empty space of wide screens. **HTTP codes** (on by default) shows the 2xx share, any 4xx/5xx shares and a stacked 2xx/3xx/4xx/5xx bar. **Cache** (on by default) shows a HIT-ratio ring and how many bytes were served from cache versus fetched from the backend (ORIGIN); the tooltip lists HIT/MISS/BYPASS counts. **Req/s** shows the current request rate of the backend. **p95 / p99** shows latency percentiles, read from the latency histogram (shown as the bucket's upper bound; above 5 s is shown as ">5 s"). **Probe** shows the last active health-check latency and how long ago it ran. **Error causes** shows up to three most frequent failure counters (5xx, timeout, connect, TLS, DNS, I/O, queue, limit, retry). **Protocol** shows the last observed upstream protocol (H1/H2) and the HTTP/2 share. HTTP codes, Cache and p95 / p99 are counted per vhost, so all backends of one vhost show the same value. The vhosts panel backend table offers the same columns as text. +- **Status API:** each `vhost_metrics` entry in `GET /api/status` (full and `view=live`) adds `status_classes` (1xx–5xx counters), `p95_latency_ms` and `p99_latency_ms` (`null` without traffic). A new top-level `vhost_cache_totals` object (vhost ID → `hits`, `misses`, `bypasses`, `saved_body_bytes`) sums the cache domain statistics per vhost. +- **Preferences API:** `backend_table_columns` and `backend_table_sort` accept `rate`, `statuses`, `p95`, `cache`, `probe`, `causes` and `protocol`. The default column set adds `statuses` and `cache`; the other new columns are opt-in. +- **Upgrade notes:** no configuration or database migration is required. An account that has already saved its own column choice keeps it, so enable the new columns under **Columns** in the Backends card. + ## 0.36.10 — Fit to screen, collapsible menu and smoother startup - **Fit to screen (auto-arrange):** a **Fit to screen** switch in the unlocked layout toolbar (account preference `dashboard_auto_fit`, **on by default**) re-arranges a custom Overview for the current screen width, so the layout designed on a wide screen stays tidy on laptops and tablets. Each card keeps its saved width (but at least a readable minimum for its type), cards are placed in saved order into the first free spot from the top, stat tiles that start on one row get one height, a card whose side neighbour ends slightly lower is extended to that edge (only while the dashboard grows by at most two stat tiles), remaining holes are filled by widening a neighbouring card sideways or growing it into free space above or below. When a card is placed, a free strip beside it that is narrower than every card still to be placed is taken by that card at once, and at the end of the grid a chart or panel reaches the last row instead of leaving an empty area. The saved widths, heights and order are never changed; switch it off to see the layout exactly as saved. While a block is dragged the normal flow is shown so the drop placeholder stays accurate. diff --git a/Cargo.lock b/Cargo.lock index f5fc17b..0ba0881 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1943,7 +1943,7 @@ dependencies = [ [[package]] name = "rust-proxy" -version = "0.36.10" +version = "0.36.11" dependencies = [ "anyhow", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index f8bd167..74fa1df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-proxy" -version = "0.36.10" +version = "0.36.11" edition = "2021" rust-version = "1.99.1" description = "RustProxy - independent reverse proxy with local administration" diff --git a/SHA256SUMS b/SHA256SUMS index b9e0b9f..fd1c87b 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,10 +1,10 @@ 61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example e76e688874828e3743d03693b1d2872b9633b4db09909673786caf30f7cfe489 .gitignore -9648830349e8ff28509f8a62a4f62c3cca1721498118cf9fd71e2cb7d95fb4ac AGENTS.md -11b4e849d5933ca383fc4e3b61a3f28e19deef7ee6d768e5108008e58b8dbc3e CHANGELOG.md -b7b5cd49c635e709301850d07d24014876e3d4e0942dff320bc1cd621a2aeb55 Cargo.lock -7a9f5c1cb40414c8cb1e5a3af9c1f1322c69769b76c7c8701d4ee81338168a7c Cargo.toml +c68dfaf860c4ceaa9de2510330eb05921126ff23191087ca22ca65e5b78ed59a AGENTS.md +5659076e0d6bf903a9936bf43a716916808b3b0312c4076b07714f84370bdc0b CHANGELOG.md +9052c7f968e072cfce8dad7e6e8c72ee526dd1b97a3bdc0aee734ef32e67bfdd Cargo.lock +3eaef17f8ed43af9ab112eae4c3da46b8a1cd27c3b526c6f684e99923e828938 Cargo.toml 25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile 0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0 LICENSE 82d1dee2fc68158f0ea73259151461e64c8b18d807cd1fecfb1ed99dc30cf5f4 README.md @@ -88,12 +88,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 -6f7a659cd2f13dc0a091049c15800967481fcd6c44984332f6f37bcc758cd5a1 integrations/routeros-oci-build/README.md +6da1af96e4faf09e7d9983c656eeb2bd01b205f0a72cde3ae3494af2fc84a9c3 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 -ab85a75c125279859f70aa00f5eec44914c8e7a571593c09dd15a400eaddad80 langs/en_US.json -e63c7fa00f90410e3fa0d087dcfb09d639310680011fe3221ac682db8eb84c56 langs/pl_pl.json +773661d94fbc6fc04a528ebb1bf8e67c77155b21b1d20c7360021d316ab599ee langs/en_US.json +b8c983393812f1a8086ea7b89d47d8da699cb721fd640cb3e03b684145da8963 langs/pl_pl.json 8ed80f14633970899d56f4ea72684a8d4b1f2843c1c78bfacc4cbb5f7ce9ef4b make_zip.py e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql 20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql @@ -165,68 +165,69 @@ d58a2ed83319176de4e66790ed292447c9497ae031464ec55cfe27174acb639a src/admin/file 77b8c05645969834cc8a58e249a7b60a57d6c47ad1c5cd92ffa5e4a9b5a8e73c src/admin/file_logs_query.rs 743bb7436f77e04015328a0e7e7d425e43ef8aa8d61467bde157e462c1dcdc2e src/admin/logging_api.rs 243f4a9c3fa99781e19c3952354d2632c0ce65e1dafac729567cbe3c177f94cb src/admin/mod.rs -5d7d041c7eaaf4badb9e9d8d48d0876742142a670137e66ae1b9b9b252481fc2 src/admin/openapi.json +02088b91cb61389e610b47abfe15c1c124749611967466e9845594fecdb30b87 src/admin/openapi.json f7a356109786635dd58cba590c71c796dbf36b47f25845190c75ee5c6a9fcb86 src/admin/operations.rs 257d93a29d0bda96462bcf695aba8d0ec0f9412c379e066ec46ae8ee034e3a53 src/admin/panel_assets.rs f5127855c2994d052678865df36d703b74f4a0afc633daf7718b10da18d26e37 src/admin/panel_content.rs -80d7b07d2f0e0494e2a8c53f530b9d1ebc9ba68a98e95059f4553249ccaed65e src/admin/preferences.rs +ed2b01ec23a24af877c8a0abd67d63639e8bb49f5b33c0ee844928d972561750 src/admin/preferences.rs cc3b08a80f44bc141107eef2ec41802b7f1f7b24ddc220f6375f495736bc0cfa src/admin/preferences_columns.rs -125fa7ea16d126a60f213abf4e5a7adf62c1a830ef96882fa4e0dfc2059f82a0 src/admin/preferences_tests.rs +ccf3d947e2c2e0e3e330ba7ece6f1d9e1c97fd59fda15d9bc40b4f5c18af3124 src/admin/preferences_tests.rs 62ecbae5dee00aeeca3eb806e2605372471dfab971cde8f66f11e711df8fcea1 src/admin/public/mod.rs 7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs 6930b4f4b45257772b321b20a9becdf9bc4610274852948ba38c2d0cdf02dfaf src/admin/refresh_preferences.rs -d151a5a8008349fda5af06c46692ea0c8d1e64059cfe81df412ba8a781d81b94 src/admin/router.rs +66eea4e8fd1c60a64090bd9e97b36903b10b1e2f7089c1a333e057d1d421bd6f src/admin/router.rs e135431cb078ddb483165581123c64af8f00ea67acb3a395ec3e53c85d0d0d45 src/admin/session.rs c4a3f1ab3d74f153f8bb94a21105feba6e04663e5be6354537f5897fc60cb92c src/admin/status.rs 6a405b189f40db88ca635ad054fd43801aad8bbd3e7f00f1b0e99bb57d4cb941 src/admin/status_live.rs 8315c856cd50f1072593ecb30c274d42d82f078b5fb93e7c68a8857cd81b0841 src/admin/status_system.rs cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs -95ce9f0025a8c6ca578d12231259d5baea38b493969c5160a2742ba31161a309 src/admin/tests.rs +1d83309ba017e086459f761302d2c95220fb5a2b82f2663c531ce66611b0e373 src/admin/tests.rs c18c369124368f58524652565102817ed69fd25b222b0c5e737ef7632b45dcfb src/admin/web/acl-picker.js 48fcd818c522fe80f2c7b2e218f87de6a7772ffd0a24e93502780b03ec78e335 src/admin/web/acme.js 86abf50f39862d821cd2e43ca425671aa85289272915132c5a9a5dd1fc4182e2 src/admin/web/api-tokens.js faa9735c2a02746b2954bc4955eb609f94afebb685cd5521d2629640cea2777a src/admin/web/app/backend-dashboard.js aed3aea4eef0011c6b51d39736782e7d1ec52662e422f40e0c7b9ddfc19d0378 src/admin/web/app/certificate-inspection.js f6b0533ffd6d0e6471519120c440619a7fb4b255854ba3dd23d81cb35cd21afb src/admin/web/app/chart-scales.js -e5b39b5592ba81292571033f26934ac0cd42bcdca7395e91094973cecb3eeaf8 src/admin/web/app/config-history.js -2bc0b8f29b76abe2ea3d8666b8dd4d7e58f3873ab2503ec056b2c307c7508a77 src/admin/web/app/core.js -1db5fd8f7e1695dd97bbc81a6da276c5a1f171454edf11c5b91c1ea5bf837d42 src/admin/web/app/dashboard-inline-layout.js -e87de2bc141cdb7d3ec62cbbe4debebb047e8d5adf6015743a97dfb0cfd268ba src/admin/web/app/dashboard-layout-editor.js +dedda129e173a347ebca9445c8a313849d1d678c256deabeb18f9977559e021a src/admin/web/app/config-history.js +d20a1cfc53c160e809c34dc83f19be9cb19f075011c4c3f9a9d755230ac2ba18 src/admin/web/app/core.js +073c52c7c7d246703087aa9a8df3c3d8742e651da55c0edd6f2baef26bd241b0 src/admin/web/app/dashboard-inline-layout.js +b269cd90710d2e86b4c9b685d4d141271ef21fe6256d3b03b8f4018d00c48c7e src/admin/web/app/dashboard-layout-editor.js 716d33249a52548f10153d8e2f5bea962a5f0427525272ddd3a1db636647672c src/admin/web/app/dashboard-presets.js -0be1024d9ff657044e04ed71f275cff3b432406d5fdcdeac726daaee54444abd src/admin/web/app/icons.js -e5a92fa1cb7e049082f0764b245560c41a9a34bcd8a9536fd872d9169fa5465d src/admin/web/app/logo-animation.js -4ea37d56919772d239817b7afa36d6d6386452f9d7db6a889940fe44c29d7508 src/admin/web/app/memory-breakdown.js +99c3e775b3a57d9a783ca9350f48132eecbb0471f10daa6961cb2bcc9de630db src/admin/web/app/icons.js +9fc3165febd83a6661b2827aacd612fbc0f834e99ef1585ec54e03e97fe54a72 src/admin/web/app/logo-animation.js +92ed982c3ac4bc4218a24a200d2a33f30fc6705b77b260db2be31b0ce91a7df9 src/admin/web/app/memory-breakdown.js 32aa11f041eafdf16e66adef6f5847a67413785224da2c19ecbf825996622aff src/admin/web/app/overview-extra-metrics.js -5e62cccf6a9507fe208e9f41bbd08453c3d8c6a6b83c56d93a2bb22550ea6e9a src/admin/web/app/overview-masonry.js -959a8f49a251629a501163b4b02841c7ec821dc5a0cca57c8f1ef45464266f17 src/admin/web/app/overview-rows.js -4d1d4760cdff44771c9d880a7c0da81d7233ef6e3a4868111a522703831a7a54 src/admin/web/app/overview-samples.js +3049a308881f98ab19ffd95b4bb88976702c61f2fc82792b4402f0212fcd7403 src/admin/web/app/overview-masonry.js +bbbbe153ff37aa4cac0a83319e5b56a6442cabd1ef819380ae76e95bf79c7628 src/admin/web/app/overview-rows.js +6f10f3239aea505aa01796620981eed873930c129ed5a2fd7eb439e0e0d24558 src/admin/web/app/overview-samples.js db9012546cae8c58fc2bcbee71c9a95956a13a6db9e6d126c6a3770d45a4e4d2 src/admin/web/app/overview-wide-metrics.js -bfd9fafb3112197e8f875dbf87a9a66c81aef0fe83687601a84a66f5e13f69bc src/admin/web/app/path-permissions.js -ea6c65fd2584665a7b6fa48c2c024f7d214d7b5f354193f29a9da8bc0dc4b2e0 src/admin/web/app/preferences.js -9e2b12d34ab1fb29716645720577ad49b16c03b46c482a03695e8f645d170e79 src/admin/web/app/rate-sampling.js +e799e0406c7ff54cbf71faf704a20265bdf5180a8ed2f26388d0775231365410 src/admin/web/app/path-permissions.js +66601eb2810b2eb224ddb4fed1ecd9e5f5cb9c51878432e9ea36908ed4a865eb src/admin/web/app/preferences.js +30ec0a7027215eae63946689af2c042de0cf468a6b1054535694da4dd4214cdb src/admin/web/app/rate-sampling.js 3b8e566d81a5b94ef21c73e7805ae128454f085554de9358040de266bcbdb0e0 src/admin/web/app/refresh.js -bebfe4f835d69df1937dd34ebb2bc6da3b701a5e1825a56b89e7199fcc524534 src/admin/web/app/runtime.js +399a52a076872fa031113d475794e029cb7eb3813a3471eb54cbbac5e765ce03 src/admin/web/app/runtime.js 31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js c7a18238ac426de4d1cf84a395ee4dc66410d60a57a3822695dc10adc668d956 src/admin/web/app/settings.js +2281f06990f86acd3ff12fa368e1b73e4c83601e20245f24abbce389dfccb77f src/admin/web/app/sidebar.js b7ead49224a4ec67181d3f24f9d9634bd6d76756f826a4d5d993300637d1d8f3 src/admin/web/app/stream-dashboard.js 7de070cc8cdde45d7e0932259299e341e25b32c9ffbd87e4fcce934d0f090de4 src/admin/web/app/web-overview.js -f87af546830f01411dcf93749b4f75ca0f0bb3745a3390eff73ee14fc768cef0 src/admin/web/cache-panel.js -9f323507649910561518b63cf6632e81b3ecc4b09501ae53b635705f34ea6587 src/admin/web/certificate-api.js -4810137aa4217a86770617d442cf5dc6bec0d9f19e20bb2155e932a9a111cedf src/admin/web/css/00-tokens.css -84584a08df60190f39c3d3d39c4c30fcd53eba6980ea1f237e28f546585bf588 src/admin/web/css/10-base.css -8a1996d37f138790c12a704deedde9de74f733e7dcbb6be816fd68c281e8e0a9 src/admin/web/css/20-shell.css -f6f58db86036ddbe569f6c20f7867519046771b5991bff5cedcd34a6ce7c9b5e src/admin/web/css/30-overview.css -ca93e41d1c6001d82f4891cacdd712e896a0355d6db9f76cf62dd2f2225a8674 src/admin/web/css/40-editors.css -87df062d7478176641364c07de396ca988a37b1a8bcb892b53cc578fe8471850 src/admin/web/css/50-settings.css -74419f367c4128824ec2f0901a99f54f68439d5d949d41d9cf3d6bd57568343e src/admin/web/css/60-monitoring.css -0ec5a4b64609bb31373d95fda8011bc42c0748c75a29d28de1841813f0e04f78 src/admin/web/css/70-responsive.css +3563508f45f740f0cc0e84c72eb79045c0cb54d6fa06a50326a3a219221e0010 src/admin/web/cache-panel.js +99ed8c37e4fee646b9b26c96edbba4d823175dc3354c17c6574a7fc9a3dcbb0e src/admin/web/certificate-api.js +43d702b676cd4f9dbe42e2ca65d938afe19086e67ab6e24af0b81d6947bee215 src/admin/web/css/00-tokens.css +8612407692e9cf6e9d827f27efcd056b8065422a8bae2570ac5b5b1e2eebf575 src/admin/web/css/10-base.css +e711163fb144e38c3c961c20c38be55ebbb2e75898ab0ac1e58762e9ab8ff005 src/admin/web/css/20-shell.css +f5c40c506bc0ece3396db5d7bebe6fda4af54584d0a772c8495feed9dc9dce6f src/admin/web/css/30-overview.css +e5cc169992ba16857f6dea4aef51c26df8f1317e36769caaa235388ec3abb680 src/admin/web/css/40-editors.css +fb1a22d6fb116ca7ab1bbfeb87c3d3f9dc7cb4386b9c4a570110e0fcbb4b7904 src/admin/web/css/50-settings.css +31c927777bb844c40305b96b6771b4f3956590fc8346a6b9f08724bad676bce2 src/admin/web/css/60-monitoring.css +e4f276e7ab5ada8342b0c34a95a0e55f3cd5e446c4620ad1a8e1f07e287793a2 src/admin/web/css/70-responsive.css 8009afdb04d5170687816730cc930a22fead1fe0248835ca7e8de81d6907395d src/admin/web/css/README.md 8241bd9489dfed082863948829a44a49bf0a8e9cd63c55809655109e393e0918 src/admin/web/css/manifest.json b369bd8927ee84d05f546c267dd48f849a0857ca4355847302d6ac2381100440 src/admin/web/dashboard-defaults.json 6f2631ffbd130db1f69f153749c018ed488116061a3462e0eeb38e5496fb1457 src/admin/web/dashboard-widgets.json 50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html bd170341634d7fe98172e0714b3cb8ee299a31abdbc9ab1309f5e61816dbcf0d src/admin/web/docker.js -fec062a5959450b1711948adf3189ceb37edca6fbead2e5bfc03b7ce397c70b1 src/admin/web/docs.css +9d17b9bb74185144472345bd2b034f1ffdbe0f4ef3055e4fe4ef69e32f780e7a src/admin/web/docs.css aeffd403fa1823df7d9fa4c48280b4033b2d15e4cb9f66608caff89290de0a52 src/admin/web/docs.html fd8506a91f769b57b62d9d8e389fb67764599354bf702958a913be79f779707b src/admin/web/docs.js 8fbc2a837d6f20a7f96c856a91fcba967b40a33494fe27ec876ee1b3b685178f src/admin/web/drafts.js @@ -244,9 +245,9 @@ ff72fc4c6a11907a16234c4b3c7d7f7bcf82eb72868dc3fbf654b04aff806fc8 src/admin/web/ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/error_504.html 003d382cc8dd69ad7237679632c04a9389366bb71dd7dca8ee64ee06614aa9a5 src/admin/web/error_auth_provider.html 0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg -7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js +453b2f871a49c6517cbd5d811ec1a28a7ca8f08796543fda6164f2c7c624824c src/admin/web/file-logs.js 882e99059bb2ff110c452eadc0b5779a16af1e34cb1b2536c80cf2056f8c3e8a src/admin/web/host404.js -ba4c7aa74400f35e1bee32c3ff86fa8793ae69685a20d81776c762ea681d5c5a src/admin/web/index.html +10ebca279f270da039cbaa0bfea1e2ab42785e32cbeef91e0c3074fed16e5e75 src/admin/web/index.html 5746049029caa135646ebcefb3d419b07eda17b560a9c015c59d27f815df64d8 src/admin/web/integrations.js 7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json ac9f667273918e18406df06cb2613c923f50a88897693a03a18f500e22dc6ec2 src/admin/web/login.html @@ -261,12 +262,12 @@ f619250d8da61517eb2151f17b640b06057e091640280091e08afd483d67e5ff src/admin/web/ a5191e1272939014a21b8d16588704952e9a705b02bd87fd8f8baca0a64ca919 src/admin/web/streams.js ae0259cf32f53d69a22a640b1c34562018d4ffb3864494012de3a31c0bb4ac1e src/admin/web/traffic-panel.js 675ea169d4cc55fda5e291af536338b882bede1eaa6c9d5cc7dfde0779c7c4d1 src/admin/web/transform-panel.js -1657db2c683292411c53e4bc0f67784529b7bc367552901d013e2c88793d613f src/admin/web/vhosts/core.js -3f5fc9649801d091708e495634192fecd76482254eb671551427d3ee580eac65 src/admin/web/vhosts/editor.js +8fa4a8d27e87fc6ea0a8f8492e637fe09504cc9a728c27361da2559007e70698 src/admin/web/vhosts/core.js +dc0fc85c64eef983d575f1b220c53c28cf5d4944b77860a57f9f28c6fde2a386 src/admin/web/vhosts/editor.js 3776f72d7051af86ac5fbdd4581040dd88a424c0c6cde789301b032b159094d8 src/admin/web/vhosts/health.js b3239e67901f1796d7a974ade0796f2e3590ff8064628a5629b78c7f6ce2f32e src/admin/web/vhosts/protocol.js 8c3da49973c62333559b0e3588c02beb0b425c025eac5cfa82a1bb968f70d9dd src/admin/web/vhosts/rates.js -eb9d692cb0437860bed0da512e321e458a3843216773fd486dd1c1f57287cacb src/admin/web/vhosts/render.js +475cfb0343b3dc5e12525de177c98c952d33ba128f2255919e271ae097ee80cf src/admin/web/vhosts/render.js 4dfdeb7c73c2775eacaca65fd8ef1dc80b275421b15522d319d338f5b96288cc src/admin/web/web-server-profiles.js 9e8e99e3cdfc32097d0c63dabb19d1e439b3be2264baa67678b46715b3a5bb4e src/admin/web/web-server.js 2d40c2cb32d7365df0c9cd6c8cb6f0483c7a799786c648f4836a0d9740c6c5ef src/auth/files/mod.rs @@ -298,14 +299,14 @@ d789bf40cf9433e2fd16409d112210fe93154588b1ac9bddc9c7f78e1db14426 src/balance/po a361c341932ffd186c0b21356c91ab1fe8043f5020d40798266d15ae896a114e src/balance/tests.rs ae06d238b1451328c751a511476ef39c012f3a397393f939ca02f96ecd0119a6 src/basic_auth/mod.rs 9a8858b7ff461a4d961ba701ca3ed6eaec15c3f7f05eef8e0e30edc63617605a src/basic_auth/tests.rs -90f26bcd36720076f874cd0a21230316b7303f68e23839e987cf51e13c23352b src/cache/cache_regression_tests.rs +9a7c0d0e5bcad1c7efdf2a4621346a340abf1b7b82902d2655267da4b8131be7 src/cache/cache_regression_tests.rs 7def03ace07dc38a4806277d75b6ce673d33ddaaf1c4855fa0a3372be1cf9489 src/cache/cold.rs b75ff512a1c6344483e00d2c67578ceea6b5472c279da0246f3622daa68d7e16 src/cache/cold/cleanup.rs 5fde9e9f77277d557372e4614264512804192bb51f3d2eae785988c17b2e7253 src/cache/cold/cold_tier_tests.rs 3c3897980c31a69d96f9295689d1ae19f2190da758c69ee7a0541f4171288bb8 src/cache/cold/demotion.rs 17b2a157b03f43f49ed05f1f3391aef025a453b748056c67229cb3ceaa53bb7a src/cache/cold/startup.rs 83095bc93132f306c537b31ba929669fedac99af3c4f133d0533b5ed7ac4e6ed src/cache/cold/worker.rs -c6c54984cf3460e6227d40d8d6917d7ca5d809ac364c80effe354c64133c765d src/cache/collapse.rs +7f81a07400ab680a34affa32b63199bb5ca7d4511dd9d58465f68279b32d3232 src/cache/collapse.rs 346fdb40d85627b583a01fa17c65e45c822dc7216198cdeadbb3d07904286fa3 src/cache/dashboard.rs be3d80cd340edb47755190a668e2f2a2776940665f95ae7110893b706eb94faa src/cache/dashboard_tests.rs 847c660e6cb7d752f9dac2807c5045a000172c25b0e437cbade06ad7b101dcba src/cache/fill.rs @@ -315,7 +316,7 @@ bbd9f372648d89b5e14d5b58029582d37f66a89752caf2c14eb1cbf8f24bb7a0 src/cache/inva 6223eaea3f061b111330cc26b28d85b9b239bef30328025f2ef337291e22a445 src/cache/invalidation/invalidate.rs c1ee3d13b373672c53b5c9ebfe5c4b66b89f2f2913c328defd5cf6502f96a654 src/cache/lifecycle.rs bd831807ca4720b3a41a6ea1d9a6c0ed2d935225e2dacbd9313be064f2e4a20e src/cache/lookup.rs -c2e258da25fbf6df7da3de5a80a04d74e85c03ab22c175d7c9a6340b8ea1f1b5 src/cache/lookup/hot.rs +dd2d3132d4a8d2f453021d27bd8c804159edd86d286d4c3ddf6b47aadca80a09 src/cache/lookup/hot.rs 4148106b9baf70e41eeba24233b1edbe8dd6f927c11f2c7647e89fbbfb60555e src/cache/lookup/hot_hit_034_tests.rs e9c6ab0c2be9f96573f826e6a1b9e4c2b8b526f779e72e95a6463ae07e45ab55 src/cache/lookup/insert.rs 7f02dc8eeb6123cb22307b76985b606afda2d87b7f845894685cfe4a9aee8b2a src/cache/lookup/tiered.rs @@ -326,28 +327,28 @@ b96e30ef2b12ad7647915457f087a4d11b6141847bc5e971a75c2739c88c3d8c src/cache/mode f52123b588205ae96c7cded93569e99a1b4db4b361e1f7a91fc703afa6b6f6e5 src/cache/model/entry.rs ff1c3a1a3bc0d26218dcc943ef951c2a212c180bbf6e4bf1c542ffbf12a9f150 src/cache/model/http_invalidation.rs ee9508498d420a726abee1d12ca21fd5117c1908ff53c2ce29f01de4380f26ac src/cache/model/maintenance.rs -723c6f0580db70b1ef40269b28a4aadc89f024b25be00afe73a971950663d252 src/cache/model/settings.rs +38acaba0131b32d2f2174dd708439df654934fe9106504917954a66d1d7e5e6d src/cache/model/settings.rs 14f3f8ae5252a2dc13fcf9556523ffb945b77ee92a5fb4939cd27e113fc61660 src/cache/model/views.rs e26dce02df9c043329ca164fed215d271781bce8fb03240f528331bf3df6b9d7 src/cache/policy.rs 70c255e15f2b8c030988a2263b0e07b5c16e7e26770c72becbcec6da4550fcd9 src/cache/policy/request.rs -6ae7a79886dbee537d15830c4647f59f158a2b31425d1c0f06218590c261c968 src/cache/policy/response.rs +5ce9e2b6cc031b4aea533c4b7375417496e998b17e1ab3cd4f6058e8da503a4b src/cache/policy/response.rs 12a6d5d9d6711e956391b4b952e8149658b8bf64cfcc0a4c59c281f2630a6aac src/cache/policy/scope_tests.rs c3a4300f7cdb6b2b7c16fbc489d49ba2451331bb4a55a2d9f618c684fd61eecb src/cache/range.rs e1d3f0512d6290b0ac4998f47398a0a7c404db249c5bbdbc4ad4767071013a72 src/cache/stats.rs 8a300f5dad44815401eb23f4f5a157893761852ba4f3005041e0eb3f16892ce3 src/cache/tests.rs -98af70ada3b14f3ecc94c9c76b028c1b0de1ffd43d9836a0749de763f64d5402 src/cache/tickets.rs +ce3d62de66b9dc73c81320478b3090408bbad94e7dbe2885576240fc9270dfed src/cache/tickets.rs a1132b980833b1b4ce2bba9aa681ff255a0e2d61faaa65a316860da503ea1e27 src/cache/v022_cache_tests.rs 4ba0a2e1c21a79cf68bf34cca1f2c2a3d7b20aef38f3b9f510743e60bc1d425f src/cache/varnish_compatible_tests.rs 1a2eb6684d9ea4a7e90970d936c75f7c8bbec46a4181fdf273be9bdac8eec847 src/compression/mod.rs 8de82d9188c9c337690c6ec4afdad9fff20651c358e34602a93d8bb8c3d2489a src/compression/tests.rs e695079333ebdc2a6e7b3337cfabef171b3a0a01da0597b77feff70d417f833e src/config.rs -2c4312dd7410dc231ed204d7d201d716eedb41045fd95df7cbb51d5561540d99 src/config/backend.rs +9f138f86a8d73dfc2d28a2c66d131751c8c85af9c9ccdf22938736973c963678 src/config/backend.rs 6ad644fc4ae821d2c30329ace36652a7ac12d674e71e7dbdcdda3554c983f6f9 src/config/compile.rs fbea01fea1213d98aea812c1b78d5db289e09200b98ae514d742235cd3fa70e1 src/config/internal_routing.rs cc75698be2828000e88f2c482bd26f2fa4050e3432bec1b5a581dca17fef4a37 src/config/model.rs 75839458ddbc7480ec4492a95eeb67b617ba7d62f7b67e7a767a3453b74dbcbc src/config/route_policy_tests.rs e442c6561723e8dbf73d25912227cebf7aa84ac1523e8bda8e6b4bdad1b674df src/config/routing.rs -5bc35ebca158aa399a1d2273c21130e1cfaf5c89cb401689ef863d7deca6b801 src/config/snapshot.rs +b33c45184e6451e15ce238cc922d2116269a1bf1d72093c2cdc068c4386a35bd src/config/snapshot.rs 477cddaa1e1c0b9b605a2f550971a04cf30287519cba70cfbb2e1860e3a93ff2 src/config/tests.rs f35de0ee1516ece6bbfd2b2a7db7d1e0f5048b0ec47cf2a3765dffb799abfd93 src/config/transport_tests.rs a09f361d70510d6226f4dd6785e649c3dc01ccafc70a4c8b0dcd098e224129b7 src/db.rs @@ -357,16 +358,16 @@ c290d0cb02dfa5d2c60ecabac1cfb20c8884e3d14cca1944906c6d8e677f1784 src/db/api_tok 2ff744223934685439374572b83fd639bb3eabdbac168bea4a6501a6e9ce0ee0 src/db/history.rs c05a02618d476e4670b4f9930555f199cac776cdf09edb8fcc1a675695b8f99f src/db/tests.rs dada314e1709e960eb2e225b4d10c3cd12606c62c4bc2c0220aec1ba133e4ade src/demo.rs -9c6b538245b2bcdbef851800ef87d11a6c370de5451488a08d9fab6d67fbbac6 src/diagnostics.rs +d2ef07d1565569f520a6a8eb01f4ea9bb673499d038018d52e65fe95e800bb27 src/diagnostics.rs 045593a42a6e4f632e5052c655788f0843faa313fc929265b89509f6dfff91ed src/docker.rs 9a68e0d5bd98541314f1ab5a4c2e344e9837f315bd39ea3988ffab6a0f0425ab src/docker/client.rs 8feba6d9eb7fde798cbe8a9e339d6a5346cfef039980fce46727386717635eec src/docker/model.rs f095e5e3c564df409874019c647f7cec2156bf5a845b5b84864879fd07cf80ce src/docker/reconcile.rs 2e8d4841665cc1d29e6262a1789bb64ced1cecd0036681cc1ef7b5d5bfe22065 src/docker/tests.rs -0b6831d03841a1dd2d6ea73d8268d0b3fda678c6d9332447e269f1e45c60c0d3 src/forward_auth/mod.rs +34eb55a240928e98bd34e790ac63096b9ca8bf7a3008676dbbd11e1a392b8541 src/forward_auth/mod.rs 6c25951cf69e972c47cdf815f22a5062916b2b768e39d55a26a5fc9e625a2344 src/forward_auth/tests.rs 5d32cf9177a33d6fb50452541805b10ae1b0833e709d833f5bd1fc6907193ffb src/forwarding.rs -b266c0a464bb0c6b581e423d08304a0121f126e8e42ef3a8469cadad42810fdf src/forwarding/peer_formatting.rs +68daa00176a0b47d49ccb3af15791690b38d3e9eea2be36ba0ee5ec7f0fcec42 src/forwarding/peer_formatting.rs b821e2efd2939cb13d01e402532f33feba6682acb6a7a3223be086c1cd171820 src/forwarding/tests.rs 0541ad50b7bde1fd958fca59d2a4f9a0fde197bcaef68e6bf892ffdeaac46be2 src/geoip.rs e3646c044604bbc169aeba549d973d37b8ac52f88bbab4a0ef74c6ea69681eaa src/geoip/mmdb.rs @@ -408,9 +409,9 @@ c19998732c2d65b464f43d17a58644db5bcf7b4a6dd4c31a65a091c57692a5bb src/oidc/mod.r c4dbe0f8ded237d45b6a3977bdc7544296e4408996815cae9e28af028419d0b7 src/oidc/tests.rs 992f49379e44361d98088d8f8ca1013d62846a54a5a35c089a5e0e1a9488e08d src/oidc/token.rs eb95c68c8639ce3ae658eea3334795cdb5e129fb2ba10955f7dffe06d8909b57 src/pages/mod.rs -86008e469c84a7242b7d4947e2116694ababa2cb26120f246d00923a7c019720 src/pages/tests.rs +d09dc18863b0508a5e8d54a5305904170f3e8f12857b7a0d5393a11849541e50 src/pages/tests.rs 5650a3f70162abd97021e0a51a92cac2c4dd39004426ef99dff36cbb0741aeac src/prometheus.rs -6c97eb9c593a7802422100e8130921e63b6b9b1b0387b6860af9f49581cd50b3 src/prometheus/core.rs +b9d71633703469d1d14e89c4d8a7698f4d354e5111cfe6ed3b2587566d0e2333 src/prometheus/core.rs 2321ee667569803dfe6c92c0e98adba0ac8fcdc6a3eacd5857233f1acae93daf src/prometheus/docker_misc.rs cc909edf740a091f9057912f555dd888b644fa1cb49647fe0e2e1f5dba8ff5af src/prometheus/json.rs f9aab3297fcf647295849f77cbf99197f4dd27efd6817e54c03451e04972430c src/prometheus/model.rs @@ -435,7 +436,7 @@ d0d64fd4dd89efd4a9fe089ff4551815cbeaf5b883ddcff803f1b13b5e918dc2 src/proxy/forw ccc54869ffbcf1edae9ddb96f6dc2191739f8804ee0318b7a6844def6c4693e5 src/proxy/forward/success_prepare.rs c965dba1d2639b338f3f3cfc17f760786a30abab708dfb7920ea3bc94d4d734b src/proxy/forward/upstream.rs 5d72200f3b5308215b7a3d7b1f910932eaa4712f9e2954afdfbe2f252e2a2d3c src/proxy/forward/web_source.rs -602879a0f8f475fe63b5e27238f4b371277a6780aa970c6b140c8d7fd3074e37 src/proxy/forward_entry.rs +5f243b21fd719a0c0bc6cdb359b0272c6c11cd9e7c7152d94787cae5476b61bd src/proxy/forward_entry.rs 9b25f5e3ea4443a63ee6f45d12e1957dd316fe8e85325fc00ea416faaab1025a src/proxy/h2_graceful.rs 30057732c35c369740a176f7da285e35a4157571a04328f6c6f1494d4ec8550b src/proxy/headers/authority_tests.rs e87437ccf80e50e0159d204dfdd1f8dc0739b6e375be192fce0af749810258cf src/proxy/headers/mod.rs @@ -451,8 +452,8 @@ c3887e344249ce492862c365896731127b7b6fc51bbfda274d7e7aad3a70be0c src/proxy/web_ 5522456cc85948228250feac92308d53b130c4a7af98d24f669aca4dfe6999b6 src/proxy/web_cache_revalidation/tests.rs 3d591201595a3b1e928ded20a8736477a47e5acc3bcbde158f7c17547ce155ba src/proxy/websocket.rs aa58bacc35458ac615680e774a235191f80abf0db978b68095571fce94c2b502 src/proxy/websocket/handshake.rs -c02d42b5c7eeedbc54c58e98691f7d8e39f8e9be63a48f6d6b4ad6b8ea431b7e src/proxy/websocket/http2_tests.rs -faedf6cc2bd4c42f342a0aa188b634d6b8d280ca0e121032977eadaecb88e9d7 src/proxy/websocket/http3_tests.rs +02eae55007334a289c974c0a050ebe9360822574e5982950245a18e1af05a9fc src/proxy/websocket/http2_tests.rs +15ff0584479e176f44fea61ea9c37c0f22606895fe7cbea8ec87bebd5284c7b7 src/proxy/websocket/http3_tests.rs 7e9c4d4c207badabf90630223964bb2ad641f06d4149843cab751f192b8b4adf src/proxy/websocket/tests.rs a44421714aab37f7ceda19e3ec2512c645d2a3ce5dcb3da5bc7a0d87a442d6a8 src/proxy/websocket/tunnel.rs 999cbf85cb6e917db529e30ba4a5eed1cc9e95a054da7b3039c730bb80061449 src/proxy/websocket/validation.rs @@ -498,12 +499,12 @@ e0676801259fb09a73825d959b89630bfa0b6f1fd1efd3006af29f60537a2fdf src/system_sta df98becda799b6bf4ff5e2fec7d0e0a486e6b7eca6f9f5ad49ffdafd49a4f498 src/tls.rs 2af2164ecdf2b7fd9eef7c6e358dec053dfac9aedaa501dbb75df742d18638b2 src/tls/acme_tests.rs 93e0576ac9077e645a72d164bad34cd75fe6032dd0516284d08bddd2fbda4b7c src/tls/certificates.rs -68cc35b3cae43b0321cf667a613310a4b9ec5d8994461d6f3543b1852771bf08 src/tls/external_paths.rs +5703876c4697512f714e9813e56acb6f852c921140b16d135feee3d0dd21e520 src/tls/external_paths.rs 937b95bb8621ab98d7cad3d223e4dee305350fbd0149b7527396d3bdcc28a68b src/tls/external_paths_tests.rs 1ff95e95d38eedeefcf8a931569d628763d5a328aa5573c73c928f06795037ba src/tls/server_config.rs -94b70c52fdc7bd6e32fba9eed9124d030c692246f6990d61e01b891765275add src/tls/tests.rs +688b929615ebc1d873461237c3de6dfa169984230266a59aad78d93f0eaa2cd6 src/tls/tests.rs ce51a3280331d67c8406d2c5bc156ac0b492afa58e1f328bcda76db0720a6a9d src/tls/types.rs -a897c48e3528565fef8d4e32f0c3c078adfb68655c80e1af011f44a8bcfac6f6 src/traffic/mod.rs +58f01b9a6dd1095877cc71445b3ccd651d8b2a1a9c0ad543547840834ec1d91b src/traffic/mod.rs bd62b4ecdcf831d183c7720766a11703c8612d614e3abf38d2bff28df34710fd src/traffic/tests.rs bfbbaa3b949ae946fa929bfbae3fd9a25f2ce792abbd0ed370ccf5683250bc4d src/transform/apply.rs ae30a984fa39d5c45a676effbc880c923078ba8120adff1bff10e108350b31b5 src/transform/compile.rs @@ -552,7 +553,7 @@ a484113f2c1fea4e98a437acd8a28be6490d7e612f160a8728ceeaadd363f8ee src/web_server e9ba6e09ba2c8f8a33410c219d5ea29214cc53e279a43b17c110d24f17a5a71d src/web_server/request.rs d98c80663847d6d83073df074ebff65952755415574708abcc153bd8d3adfaa9 src/web_server/response.rs b38a1c9e648e3606a534ebdf74f6315863a326c6de1ca8edc433abe9c1fed7b4 src/web_server/response/file.rs -4733bb1824dd0a12b8804fb8d00791c3f9fda4c09b41503d5d8079d9607df058 src/web_server/response/file_body/mod.rs +df87b67c75bbe58d7e077ed4c4cfa105ec680e89dd0c25af7dbd793d418a01ad src/web_server/response/file_body/mod.rs e5f5fbe702f66bfcbcbc1d1f3cd3d89bdbb929bb86aed94868a2a06a4b9e59e3 src/web_server/response/file_body/tests.rs 7da903c1a69be0ae6b85772b3208c518679efa8ed9d3e98adb694483518156b6 src/web_server/response/headers.rs 2665d4eac9a6430d68224874784cb65d9b9d424353af77735caf044401d08cb4 src/web_server/routing.rs @@ -561,9 +562,9 @@ b791762b4ab7a43c3be19df84e523faf3dfb4df055f66d02178f93edb60d2204 src/web_server 714418c86ab241020a561fcf662a8fe324a6a70cc2fd3a313c56d7939c14b9e8 src/web_server/tests.rs f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json 102b83171a6429b6375d8001d6c5294702385cd58ee55727cb657b1f3566e877 tests/panel_icons_browser.py -52e61c389a60d147184ee76925cac76e454f8f2e3cb91039bb06e17787ab900f tests/panel_icons_contract.js +c4030b7d224553312012b1f25153dd3545e83de7bccff2894c0e8f7e0e84b721 tests/panel_icons_contract.js 01af3008be27fabb486c141c67099af104f2725cc56f4c656aea9741a3c150cd tests/panel_interface_contract.py -626db3f4a7d83c0925a911db5ee6ce4b77048b7c2a9b91cd2d5d7dfa548cddd5 tests/panel_refresh_contract.js +87417f12a236efb7b20b10ebdcd92f2f862a86c7d829e7f8afb4f8a460c30f33 tests/panel_refresh_contract.js e24a0f84f1bfc46964df865ac387d651f7b384d781cbc8f3a9a5b0b60f3d69ca tests/vhost_auth_selection.js cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 third_party/swagger-ui-5.33.1/LICENSE 0d20d1adef18aee3f40dd258172155521ce702ac445cb5f7b7d60ed32dad2fb2 third_party/swagger-ui-5.33.1/NOTICE diff --git a/integrations/routeros-oci-build/README.md b/integrations/routeros-oci-build/README.md index 586d99b..328c227 100644 --- a/integrations/routeros-oci-build/README.md +++ b/integrations/routeros-oci-build/README.md @@ -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.36.10` and the defaults this publishes: +For example, with RustProxy version `0.36.11` and the defaults this publishes: ```text -zot.example.net/network/rustproxy:0.36.10 +zot.example.net/network/rustproxy:0.36.11 zot.example.net/network/rustproxy:latest zot.example.net/network/rustproxy:routeros ``` diff --git a/langs/en_US.json b/langs/en_US.json index 07a0f68..7150710 100644 --- a/langs/en_US.json +++ b/langs/en_US.json @@ -2306,6 +2306,15 @@ "backendEventFailed": "Failed", "backendEventRecovered": "Recovered", "backendEventNone": "No failures", + "backendColRate": "Req/s", + "backendColStatuses": "HTTP codes", + "backendColP95": "p95 / p99", + "backendColCache": "Cache", + "backendColProbe": "Probe", + "backendColCauses": "Error causes", + "backendColProtocol": "Protocol", + "backendVhostScope": "Whole vhost (all backends)", + "backendBucketBound": "histogram bucket upper bound", "backendAgo": "{value} ago", "backendDetailsToggle": "Show client generation details", "processOverviewTitle": "Process", diff --git a/langs/pl_pl.json b/langs/pl_pl.json index 5d1e6ae..8e38f24 100644 --- a/langs/pl_pl.json +++ b/langs/pl_pl.json @@ -2306,6 +2306,15 @@ "backendEventFailed": "Awaria", "backendEventRecovered": "Przywrócony", "backendEventNone": "Brak awarii", + "backendColRate": "Żąd./s", + "backendColStatuses": "Kody HTTP", + "backendColP95": "p95 / p99", + "backendColCache": "Cache", + "backendColProbe": "Sonda", + "backendColCauses": "Przyczyny błędów", + "backendColProtocol": "Protokół", + "backendVhostScope": "Cały vhost (wszystkie backendy)", + "backendBucketBound": "górna granica przedziału histogramu", "backendAgo": "{value} temu", "backendDetailsToggle": "Pokaż szczegóły generacji klienta", "processOverviewTitle": "Proces", diff --git a/src/admin/openapi.json b/src/admin/openapi.json index 3432e1a..97a204f 100644 --- a/src/admin/openapi.json +++ b/src/admin/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "info": { "title": "RustProxy Administration API", - "version": "0.36.10", + "version": "0.36.11", "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": [ @@ -4268,7 +4268,7 @@ "backend_table_columns": { "type": "array", "minItems": 1, - "maxItems": 14, + "maxItems": 21, "uniqueItems": true, "items": { "type": "string", @@ -4281,6 +4281,13 @@ "errors", "latency", "traffic", + "rate", + "statuses", + "p95", + "cache", + "probe", + "causes", + "protocol", "generation", "generationRequests", "generationAge", @@ -4298,6 +4305,8 @@ "errors", "latency", "traffic", + "statuses", + "cache", "generation", "generationRequests", "generationAge", @@ -4318,6 +4327,13 @@ "errors", "latency", "traffic", + "rate", + "statuses", + "p95", + "cache", + "probe", + "causes", + "protocol", "generation", "generationRequests", "generationAge", diff --git a/src/admin/preferences.rs b/src/admin/preferences.rs index 81b05cd..3c7d06e 100644 --- a/src/admin/preferences.rs +++ b/src/admin/preferences.rs @@ -84,6 +84,7 @@ fn default_file_log_columns() -> Vec { fn default_backend_table_columns() -> Vec { BACKEND_TABLE_COLUMNS .iter() + .filter(|value| !BACKEND_TABLE_OPTIONAL_COLUMNS.contains(value)) .map(|value| (*value).to_owned()) .collect() } diff --git a/src/admin/preferences_columns.rs b/src/admin/preferences_columns.rs index cf6de53..97659b9 100644 --- a/src/admin/preferences_columns.rs +++ b/src/admin/preferences_columns.rs @@ -23,6 +23,13 @@ const BACKEND_TABLE_COLUMNS: &[&str] = &[ "errors", "latency", "traffic", + "rate", + "statuses", + "p95", + "cache", + "probe", + "causes", + "protocol", "generation", "generationRequests", "generationAge", @@ -30,6 +37,13 @@ const BACKEND_TABLE_COLUMNS: &[&str] = &[ "lastFailure", "lastRecovery", ]; +const BACKEND_TABLE_OPTIONAL_COLUMNS: &[&str] = &[ + "rate", + "p95", + "probe", + "causes", + "protocol", +]; const VHOST_LIST_SORT_COLUMNS: &[&str] = &[ "id", "hostname", diff --git a/src/admin/status.rs b/src/admin/status.rs index 223cb12..a3de71e 100644 --- a/src/admin/status.rs +++ b/src/admin/status.rs @@ -174,6 +174,7 @@ fn status_payload(state: &App) -> Value { .iter() .map(|(id, metrics)| (id.clone(), metrics.view())) .collect::>()); + payload["vhost_cache_totals"] = json!(state.cache.scope_totals()); let mut backend_pool_domains = std::collections::BTreeMap::new(); for agent in &snapshot.envelope.config.docker_agents.agents { for assignment in agent diff --git a/src/admin/status_live.rs b/src/admin/status_live.rs index 0bbf60d..a4fb387 100644 --- a/src/admin/status_live.rs +++ b/src/admin/status_live.rs @@ -61,6 +61,7 @@ fn live_status_payload(state: &App) -> Value { "backends": snapshot.envelope.config.vhosts.iter().map(|v| v.backends.len()).sum::(), "backend_pools": snapshot.pools.iter().map(|p| p.view()).collect::>(), "vhost_metrics": snapshot.vhost_metrics.iter().map(|(id, metrics)| (id.clone(), metrics.view())).collect::>(), + "vhost_cache_totals": state.cache.scope_totals(), "streams": {"metrics": stream_metrics}, "logging": {"sinks": state.logs.stats(), "error_file": crate::logging::error_log_stats()} }) diff --git a/src/admin/tests.rs b/src/admin/tests.rs index 9937474..3a0a048 100644 --- a/src/admin/tests.rs +++ b/src/admin/tests.rs @@ -2155,8 +2155,18 @@ mod tests { ); assert_eq!( initial["backend_table_columns"].as_array().unwrap().len(), - BACKEND_TABLE_COLUMNS.len() + BACKEND_TABLE_COLUMNS.len() - BACKEND_TABLE_OPTIONAL_COLUMNS.len() ); + assert!(initial["backend_table_columns"] + .as_array() + .unwrap() + .iter() + .any(|column| column == "cache")); + assert!(!initial["backend_table_columns"] + .as_array() + .unwrap() + .iter() + .any(|column| column == "probe")); assert_eq!(initial["backend_table_sort"], "vhost"); assert_eq!(initial["backend_table_descending"], false); @@ -2193,8 +2203,8 @@ mod tests { json!(default_file_log_columns()) ); assert_eq!( - legacy["backend_table_columns"].as_array().unwrap().len(), - BACKEND_TABLE_COLUMNS.len() + legacy["backend_table_columns"], + json!(default_backend_table_columns()) ); assert_eq!(legacy["backend_table_sort"], "vhost"); assert_eq!(legacy["backend_table_descending"], false); diff --git a/src/admin/web/css/30-overview.css b/src/admin/web/css/30-overview.css index 48f6c09..a192d9c 100644 --- a/src/admin/web/css/30-overview.css +++ b/src/admin/web/css/30-overview.css @@ -2848,3 +2848,35 @@ html[data-table-density=ultra] .mb-bar { height: 16px; } .bo-traffic-text .bo-out b { color: var(--green); font-weight: 500; } .bo-traffic-text b.is-zero { color: var(--text-subtle); } html[data-table-density=ultra] .bo-traffic-text { line-height: 1.25; } +.bo-statuses { min-width: 150px; } +.bo-statuses small { display: inline-flex; gap: 6px; } +.bo-statuses small b { font-weight: 500; } +.bo-statuses small .is-warn { color: var(--accent); } +.bo-statuses small .is-bad { color: var(--red); } +.bo-stack { display: flex; gap: 1px; height: 3px; margin-top: 5px; overflow: hidden; background: var(--line); border-radius: var(--rp-progress-radius, 0); } +.bo-stack>i { display: block; flex: 1 1 0; height: 100%; } +.bo-stack>i.is-ok { background: var(--green); opacity: .8; } +.bo-stack>i.is-info { background: var(--blue); } +.bo-stack>i.is-warn { background: var(--accent); } +.bo-stack>i.is-bad { background: var(--red); } +.bo-pair { font-family: var(--mono); font-variant-numeric: tabular-nums; text-align: right; } +.bo-pair small { display: block; margin-top: 1px; color: var(--text-subtle); font-size: .9em; } +.bo-cache { min-width: 200px; } +.bo-cache .bo-chartcell { justify-content: flex-start; } +.bo-cache-hit { display: inline-flex; align-items: center; gap: 6px; min-width: 70px; } +.bo-ring { width: 20px; height: 20px; flex: 0 0 auto; } +.bo-ring circle { fill: none; stroke-width: 3; } +.bo-ring-track { stroke: var(--line); } +.bo-ring-value { stroke: var(--green); stroke-linecap: butt; } +.bo-traffic-text .bo-hit b { color: var(--green); font-weight: 500; } +.bo-traffic-text .bo-origin b { color: var(--text); font-weight: 500; } +.bo-causes { display: flex; flex-wrap: nowrap; align-items: center; gap: 4px; } +.bo-cause { display: inline-flex; align-items: center; gap: 5px; padding: 1px 6px; border: 1px solid var(--danger-line-strong); border-radius: var(--rp-radius-control, 0); color: var(--red); font: 11px var(--mono); } +.bo-cause b { font-weight: 600; } +.bo-cause.is-soft { border-color: var(--field-line); color: var(--muted); } +.bo-causes small { color: var(--text-subtle); font: 11px var(--mono); } +html[data-table-density=ultra] .bo-pair small { display: inline; margin-left: 6px; } +html[data-table-density=ultra] .bo-ring { width: 14px; height: 14px; } +html[data-table-density=ultra] .bo-stack { margin-top: 2px; } +html[data-table-density=compact] .bo-ring { width: 18px; height: 18px; } +html[data-table-density=compact] .bo-stack { margin-top: 3px; } diff --git a/src/admin/web/index.html b/src/admin/web/index.html index 91aea1c..70ff944 100644 --- a/src/admin/web/index.html +++ b/src/admin/web/index.html @@ -663,6 +663,20 @@ + + + + + + + @@ -700,6 +714,13 @@ Errors Latency Traffic/s + Req/s + HTTP codes + p95 / p99 + Cache + Probe + Error causes + Protocol Client gen. diff --git a/src/admin/web/vhosts/core.js b/src/admin/web/vhosts/core.js index a8eb979..5f5a644 100644 --- a/src/admin/web/vhosts/core.js +++ b/src/admin/web/vhosts/core.js @@ -2,6 +2,7 @@ let basicEditor = null, locationsReader = () => [], aclControl = null, editing = null, revision = null, dirty = false, initialized = false, readBindings = () => [], readHosts = () => [], countryRatesReader = () => [], uaRatesReader = () => [], pagesEditor = null, stagedCertificates = [], uploading = false, draftEpoch = 0, backendHealthEditorControl = null; const chartHistory = new Map(); const backendTrafficHistory = new Map(); +const backendRequestHistory = new Map(); const vhostTrafficHistory = new Map(); const dialog = $("vhost-dialog"); const form = $("vhost-form"), container = $("vhost-fields"); diff --git a/src/admin/web/vhosts/render.js b/src/admin/web/vhosts/render.js index e6e5ba6..4f525a9 100644 --- a/src/admin/web/vhosts/render.js +++ b/src/admin/web/vhosts/render.js @@ -151,9 +151,11 @@ document.addEventListener("scroll", event => { if (activeVhostBackendPopup && !activeVhostBackendPopup.node.contains(event.target)) positionVhostBackendPopup(); }, true); -const poolColumns = ["vhost", "backend", "status", "active", "requests", "errors", "latency", "traffic", "generation", "generationRequests", "generationAge", "rotations", "lastFailure", "lastRecovery"]; +const poolColumns = ["vhost", "backend", "status", "active", "requests", "errors", "latency", "traffic", "rate", "statuses", "p95", "cache", "probe", "causes", "protocol", "generation", "generationRequests", "generationAge", "rotations", "lastFailure", "lastRecovery"]; +const poolOptionalColumns = ["rate", "p95", "probe", "causes", "protocol"]; const vhostSortColumns = ["id", "hostname", "status", "listeners", "certificate", "backends"]; -const defaultPoolTablePrefs = () => ({ sort: "vhost", descending: false, columns: [...poolColumns] }); +const defaultPoolColumns = () => poolColumns.filter(key => !poolOptionalColumns.includes(key)); +const defaultPoolTablePrefs = () => ({ sort: "vhost", descending: false, columns: defaultPoolColumns() }); const defaultVhostListPrefs = () => ({ sort: "id", descending: false }); let poolTablePrefs = defaultPoolTablePrefs(), vhostListPrefs = defaultVhostListPrefs(), poolPreferenceSaveRunning = false, poolPreferenceSaveQueued = false; function normalizePoolTablePrefs(preferences) { @@ -161,7 +163,7 @@ function normalizePoolTablePrefs(preferences) { return { sort: poolColumns.includes(preferences?.backend_table_sort) ? preferences.backend_table_sort : "vhost", descending: preferences?.backend_table_descending === true, - columns: selected.length ? selected : [...poolColumns], + columns: selected.length ? selected : defaultPoolColumns(), }; } function normalizeVhostListPrefs(preferences) { @@ -256,7 +258,7 @@ function initPoolTableTools() { })); syncPoolColumns(); syncPoolSortDirection(); } -const BO_SORT_LABELS = { vhost: "vhVhost", backend: "backend", status: "status", active: "vhActive", requests: "vhRequests", errors: "vhErrors", latency: "vhLatency", traffic: "vhTraffic", generation: "vhClientGeneration", generationRequests: "vhGenerationRequests", generationAge: "vhGenerationAge", rotations: "vhClientRotations", lastFailure: "vhLastFailure", lastRecovery: "vhLastRecovery" }; +const BO_SORT_LABELS = { vhost: "vhVhost", backend: "backend", status: "status", active: "vhActive", requests: "vhRequests", errors: "vhErrors", latency: "vhLatency", traffic: "vhTraffic", rate: "backendColRate", statuses: "backendColStatuses", p95: "backendColP95", cache: "backendColCache", probe: "backendColProbe", causes: "backendColCauses", protocol: "backendColProtocol", generation: "vhClientGeneration", generationRequests: "vhGenerationRequests", generationAge: "vhGenerationAge", rotations: "vhClientRotations", lastFailure: "vhLastFailure", lastRecovery: "vhLastRecovery" }; const BO_DETAIL_COLUMNS = ["generation", "generationRequests", "generationAge", "rotations", "lastFailure", "lastRecovery"]; const BO_SAMPLES = 30; const boOpen = new Set(), boSeries = new Map(); @@ -270,6 +272,54 @@ function buildBackendOverviewTools() { input.type = "checkbox"; input.dataset.poolColumnToggle = key; text.dataset.i18n = BO_SORT_LABELS[key]; label.append(input, text); options.append(label); } } +const BO_CAUSES = [["http_5xx_total", "5xx"], ["timeouts_total", "timeout"], ["connect_errors_total", "connect"], ["tls_errors_total", "TLS"], ["dns_errors_total", "DNS"], ["io_errors_total", "I/O"], ["queue_timeouts_total", "queue"], ["capacity_rejections_total", "limit"], ["retry_attempts_total", "retry"]]; +function boMs(value) { + if (!(value > 0)) return "-"; + if (value >= 10000) return ">5 s"; + return value >= 1000 ? `${Number((value / 1000).toFixed(1))} s` : `${Math.round(value)} ms`; +} +function boPercent(part, total) { + if (!(total > 0) || !(part > 0)) return "0%"; + const pct = part / total * 100; + return pct < 0.1 ? "<0.1%" : `${pct.toFixed(pct < 10 ? 1 : 0)}%`; +} +function backendRequestRate(key, requests, snapshot, now, seen) { + seen.add(key); + const previous = backendRequestHistory.get(key); + if (previous?.snapshot === snapshot) return previous.rate; + const continuous = rustProxyRateSampling.continuous(previous, now, rustProxyRateSampling.cadence("overview_seconds")); + const rate = continuous && requests >= previous.requests && now > previous.at ? (requests - previous.requests) / ((now - previous.at) / 1000) : previous?.rate || 0; + backendRequestHistory.set(key, { ...rustProxyRateSampling.stamp(now), requests, snapshot, rate: continuous ? rate : 0 }); + return continuous ? rate : 0; +} +function backendExtraMetrics(pool, member, status, snapshot, now, seen) { + const vhost = status.vhost_metrics?.[pool.id] || {}, cacheTotals = status.vhost_cache_totals?.[pool.id] || null; + const classes = Array.isArray(vhost.status_classes) ? vhost.status_classes.map(value => Math.max(0, Number(value) || 0)) : [0, 0, 0, 0, 0]; + const classTotal = classes.reduce((sum, value) => sum + value, 0); + const hits = Number(cacheTotals?.hits) || 0, misses = Number(cacheTotals?.misses) || 0, bypasses = Number(cacheTotals?.bypasses) || 0, lookups = hits + misses + bypasses; + const cacheBytes = Math.max(0, Number(cacheTotals?.saved_body_bytes) || 0), outBytes = Math.max(cacheBytes, Number(vhost.bytes_out) || 0), originBytes = Math.max(0, outBytes - cacheBytes); + const p95 = Number(vhost.p95_latency_ms) || 0, p99 = Number(vhost.p99_latency_ms) || 0; + const probeOff = member.health_mode === "off", probeLatency = Number(member.latency_ms) || 0, probeAt = Number(member.last_probe_ms) || 0; + const causes = BO_CAUSES.map(([field, label]) => ({ label, value: Math.max(0, Number(member[field]) || 0) })).filter(cause => cause.value > 0).sort((a, b) => b.value - a.value); + const causeTotal = causes.filter(cause => cause.label !== "retry").reduce((sum, cause) => sum + cause.value, 0); + const h1 = Math.max(0, Number(member.upstream_protocol_http1_total) || 0), h2 = Math.max(0, Number(member.upstream_protocol_http2_total) || 0); + const protocol = member.upstream_protocol_observed === "h1" || member.upstream_protocol_observed === "h2" ? member.upstream_protocol_observed.toUpperCase() : ""; + const rate = backendRequestRate(`${pool.id}\u0000${member.id}`, Math.max(0, Number(member.requests) || 0), snapshot, now, seen); + const hitRatio = lookups ? hits / lookups : -1, errorShare = classTotal ? (classes[3] + classes[4]) / classTotal : -1; + return { + rate, classes, classTotal, hits, misses, bypasses, lookups, cacheBytes, originBytes, outBytes, p95, p99, probeOff, probeLatency, probeAt, lastProbeResult: member.last_result || "", causes, causeTotal, h1, h2, protocol, + sort: { rate, statuses: errorShare, p95, cache: hitRatio, probe: probeOff ? -1 : probeLatency, causes: causeTotal, protocol: h1 + h2 ? h2 / (h1 + h2) : -1 }, + text: { + rate: `${rate.toFixed(rate < 10 ? 1 : 0)}/s`, + statuses: classTotal ? ["2xx", "3xx", "4xx", "5xx"].map((label, index) => `${label} ${boPercent(classes[index + 1], classTotal)}`).join(" · ") : "-", + p95: p95 ? `${boMs(p95)} / ${boMs(p99)}` : "-", + cache: lookups || cacheBytes ? `HIT ${boPercent(hits, lookups)} · ${bytes(cacheBytes)} / ${bytes(originBytes)}` : "-", + probe: probeOff || !(probeLatency > 0) ? "-" : `${probeLatency.toFixed(1)} ms`, + causes: causes.length ? causes.map(cause => `${cause.label} ${cause.value}`).join(" · ") : "-", + protocol: protocol ? `${protocol}${h1 + h2 ? ` · H2 ${boPercent(h2, h1 + h2)}` : ""}` : "-", + }, + }; +} function boSeriesOf(item) { return item.series || { latency: [], inRate: [], outRate: [], requests: [] }; } function boAgo(ms) { const seconds = Math.max(0, Math.floor((Date.now() - Number(ms)) / 1000)); @@ -341,6 +391,67 @@ function renderBackendOverview(items, hasProxy, snapshot = null) { $("backend-overview-card")?.classList.toggle("is-no-proxy", !boHasProxy); applyBackendOverviewView(); } +function boStack(parts) { + const track = el("div", undefined, "bo-stack"), total = parts.reduce((sum, [value]) => sum + value, 0); + if (total > 0) for (const [value, kind] of parts) if (value > 0) { const part = el("i", undefined, `is-${kind}`); part.style.flexGrow = String(value); part.style.minWidth = "2px"; track.append(part); } + return track; +} +function boRing(ratio) { + const svg = document.createElementNS(BO_SVG, "svg"), radius = 8, length = 2 * Math.PI * radius; + svg.setAttribute("viewBox", "0 0 20 20"); svg.setAttribute("class", "bo-ring"); svg.setAttribute("aria-hidden", "true"); + for (const kind of ["track", "value"]) { + const circle = document.createElementNS(BO_SVG, "circle"); + circle.setAttribute("cx", "10"); circle.setAttribute("cy", "10"); circle.setAttribute("r", String(radius)); circle.setAttribute("class", `bo-ring-${kind}`); + if (kind === "value") { circle.setAttribute("stroke-dasharray", `${(Math.max(0, Math.min(1, ratio)) * length).toFixed(2)} ${length.toFixed(2)}`); circle.setAttribute("transform", "rotate(-90 10 10)"); } + svg.append(circle); + } + return svg; +} +const BO_EXTRA_CELLS = { + rate(cell, extra) { cell.className = "is-num"; cell.textContent = extra.text.rate; if (!(extra.rate > 0)) cell.classList.add("is-zero"); }, + statuses(cell, extra) { + cell.className = "bo-statuses"; cell.title = `${t("backendVhostScope")}\n${["1xx", "2xx", "3xx", "4xx", "5xx"].map((label, index) => `${label}: ${extra.classes[index]}`).join("\n")}`; + if (!extra.classTotal) { cell.append(el("span", "-", "is-zero")); return; } + const top = el("div", undefined, "bo-meter-top"), issues = el("small"); + top.append(el("span", `2xx ${boPercent(extra.classes[1], extra.classTotal)}`)); + for (const [index, kind] of [[3, "warn"], [4, "bad"]]) if (extra.classes[index] > 0) issues.append(el("b", `${index + 1}xx ${boPercent(extra.classes[index], extra.classTotal)}`, `is-${kind}`)); + if (!issues.children.length) issues.textContent = "4xx/5xx 0%"; + top.append(issues); cell.append(top, boStack([[extra.classes[0] + extra.classes[1], "ok"], [extra.classes[2], "info"], [extra.classes[3], "warn"], [extra.classes[4], "bad"]])); + }, + p95(cell, extra) { + cell.className = "bo-pair"; cell.title = `${t("backendVhostScope")} · ${t("backendBucketBound")}`; + if (!extra.p95) { cell.append(el("span", "-", "is-zero")); return; } + cell.append(el("span", boMs(extra.p95)), el("small", `p99 ${boMs(extra.p99)}`)); + }, + probe(cell, extra) { + cell.className = "bo-pair"; + if (extra.probeOff || !(extra.probeLatency > 0)) { cell.append(el("span", "-", "is-zero")); return; } + cell.title = extra.lastProbeResult; cell.append(el("span", `${extra.probeLatency.toFixed(1)} ms`)); + if (extra.probeAt > 0) cell.append(el("small", boAgo(extra.probeAt))); + }, + cache(cell, extra) { + cell.className = "bo-cache"; + cell.title = `${t("backendVhostScope")}\nHIT ${extra.hits} · MISS ${extra.misses} · BYPASS ${extra.bypasses}\nCACHE ${bytes(extra.cacheBytes)} (${boPercent(extra.cacheBytes, extra.outBytes)}) · ORIGIN ${bytes(extra.originBytes)}`; + if (!extra.lookups && !extra.cacheBytes) { cell.append(el("span", "-", "is-zero")); return; } + const wrap = el("div", undefined, "bo-chartcell"), hit = el("div", undefined, "bo-cache-hit"), text = el("div", undefined, "bo-traffic-text"); + hit.append(boRing(extra.lookups ? extra.hits / extra.lookups : 0), el("span", extra.lookups ? boPercent(extra.hits, extra.lookups) : "-")); + const line = (kind, label, value) => { const row = el("div", undefined, `bo-${kind}`); row.append(el("span", label), el("b", bytes(value), value > 0 ? "" : "is-zero")); return row; }; + text.append(line("hit", "CACHE", extra.cacheBytes), line("origin", "ORIGIN", extra.originBytes)); + wrap.append(hit, text); cell.append(wrap); + }, + causes(cell, extra) { + cell.className = "bo-causes"; cell.title = extra.text.causes; + if (!extra.causes.length) { cell.append(el("span", "-", "is-zero")); return; } + for (const cause of extra.causes.slice(0, 3)) { const chip = el("span", undefined, `bo-cause${cause.label === "retry" ? " is-soft" : ""}`); chip.append(cause.label, el("b", String(cause.value))); cell.append(chip); } + if (extra.causes.length > 3) cell.append(el("small", `+${extra.causes.length - 3}`)); + }, + protocol(cell, extra) { + cell.className = "bo-pair"; cell.title = `H1 ${extra.h1} · H2 ${extra.h2}`; + if (!extra.protocol) { cell.append(el("span", "-", "is-zero")); return; } + cell.append(el("span", extra.protocol)); + if (extra.h1 + extra.h2) cell.append(el("small", `H2 ${boPercent(extra.h2, extra.h1 + extra.h2)}`)); + }, +}; function applyBackendOverviewView() { const body = $("bo-rows"); if (!body) return; const visibleColumns = new Set(poolTablePrefs.columns); @@ -370,7 +481,9 @@ function applyBackendOverviewView() { const columns = [ expandable && ["toggle", ""], showIdentity && ["identity", `${visibleColumns.has("vhost") ? t("vhVhost") : ""}${visibleColumns.has("vhost") && visibleColumns.has("backend") ? " / " : ""}${visibleColumns.has("backend") ? t("backend") : ""}`], visibleColumns.has("status") && ["status", t("status")], visibleColumns.has("active") && ["active", t("vhActive"), true], visibleColumns.has("requests") && ["requests", t("vhRequests"), true], - visibleColumns.has("errors") && ["errors", t("vhErrors")], visibleColumns.has("latency") && ["latency", t("vhLatency")], visibleColumns.has("traffic") && ["traffic", t("vhTraffic"), true], showEvent && ["event", t("backendLastEvent")], + visibleColumns.has("rate") && ["rate", t("backendColRate"), true], visibleColumns.has("errors") && ["errors", t("vhErrors")], visibleColumns.has("statuses") && ["statuses", t("backendColStatuses")], visibleColumns.has("latency") && ["latency", t("vhLatency")], + visibleColumns.has("p95") && ["p95", t("backendColP95"), true], visibleColumns.has("probe") && ["probe", t("backendColProbe"), true], visibleColumns.has("cache") && ["cache", t("backendColCache")], visibleColumns.has("traffic") && ["traffic", t("vhTraffic"), true], + visibleColumns.has("causes") && ["causes", t("backendColCauses")], visibleColumns.has("protocol") && ["protocol", t("backendColProtocol")], showEvent && ["event", t("backendLastEvent")], ].filter(Boolean); const head = $("bo-head"); if (head) head.replaceChildren(...columns.map(([id, label, numeric]) => { const th = el("th", label, numeric ? "is-num" : ""); th.dataset.boColumn = id; if (id === "toggle") th.setAttribute("aria-label", t("backendDetailsToggle")); return th; })); const rows = []; @@ -405,6 +518,7 @@ function applyBackendOverviewView() { text.append(line("in", "IN", item.trafficIn || 0), line("out", "OUT", item.trafficOut || 0)); wrap.append(boTrafficChart(boSeriesOf(item).inRate, boSeriesOf(item).outRate), text); cell.append(wrap); } + else if (id in BO_EXTRA_CELLS) BO_EXTRA_CELLS[id](cell, item.extra); else if (id === "event") { cell.className = "bo-event"; if (item.lastFailureMs && item.lastFailureMs >= item.lastRecoveryMs) { const line = el("span", item.lastFailureClass || t("backendEventFailed"), "is-failed"); line.title = item.lastFailureDetail || ""; cell.append(line, el("small", boAgo(item.lastFailureMs))); } @@ -651,7 +765,7 @@ function renderStatus(status) { refreshVhostBackendPopup(status); form.querySelectorAll(".vh-member").forEach(row => row.syncLocalRisk?.()); const rows = [], summaries = new Map(), aggregate = new Map(), overviewItems = []; - const trafficSnapshot = status, trafficNow = performance.now(), seenTraffic = new Set(), seenVhostTraffic = new Set(); + const trafficSnapshot = status, trafficNow = performance.now(), seenTraffic = new Set(), seenVhostTraffic = new Set(), seenRequests = new Set(); const statusKeys = { up: "vhUp", down: "vhDown", checking: "vhChecking", draining: "vhDraining", disabled: "vhDisabled" }; const configuredVhosts = new Map((envelope?.config.vhosts || []).map(vhost => [vhost.id, vhost])); for (const pool of status.backend_pools || []) { @@ -659,6 +773,7 @@ function renderStatus(status) { let up = 0, active = 0, requests = 0, errors = 0, traffic = 0, weightedLatency = 0, latencyWeight = 0; for (const member of members) { const memberTraffic = backendTrafficRate(pool.id, member, trafficSnapshot, trafficNow, seenTraffic); + const extra = backendExtraMetrics(pool, member, status, trafficSnapshot, trafficNow, seenRequests); const label = vhostDisabled ? t("vhDisabled") : (member.health_mode === "off" && member.status === "up" ? t("vhHealthDisabled") : t(statusKeys[member.status] || "vhHealthUnknown")); const activeLimit = Number(member.max_concurrent_requests) > 0 ? `${member.active ?? 0}/${member.max_concurrent_requests}` : `${member.active ?? 0}/∞`; const lastFailure = Number(member.last_failure_at_ms) > 0 ? `${member.last_failure_class || "-"} · ${formatTime(member.last_failure_at_ms)}` : "-"; @@ -668,17 +783,17 @@ function renderStatus(status) { const generationAge = Math.max(0, Number(generationAgeRaw)); const generationAgeText = generationAgeRaw == null || !Number.isFinite(generationAge) ? "-" : generationAge >= 3600 ? `${Math.floor(generationAge / 3600)}h ${Math.floor((generationAge % 3600) / 60)}m` : generationAge >= 60 ? `${Math.floor(generationAge / 60)}m ${Math.floor(generationAge % 60)}s` : `${Math.floor(generationAge)}s`; const row = el("tr"); if (vhostDisabled) row.classList.add("pool-vhost-disabled"); const poolDomain = status.backend_pool_domains?.[pool.id]; const poolLabel = poolDomain ? `${pool.id} (${poolDomain})` : pool.id; const cells = [poolLabel, `${member.id}${member.backup ? " / " + t("vhBackup") : ""}`, label, activeLimit, member.requests, member.errors, - member.ewma_latency_ms > 0 ? `${member.ewma_latency_ms.toFixed(1)} ms` : "-", `${rateBytes(memberTraffic)}/s`, member.upstream_client_generation ?? "-", member.upstream_client_generation_requests ?? "-", generationAgeText, member.upstream_client_rotations_total ?? "-", lastFailure, lastRecovery]; - const sortValues = { vhost: poolLabel.toLowerCase(), backend: String(member.id || "").toLowerCase(), status: label.toLowerCase(), active: Number(member.active) || 0, requests: Number(member.requests) || 0, errors: Number(member.errors) || 0, latency: Number(member.ewma_latency_ms) || 0, traffic: memberTraffic, generation: Number(member.upstream_client_generation) || 0, generationRequests: Number(member.upstream_client_generation_requests) || 0, generationAge: Number.isFinite(generationAge) ? generationAge : -1, rotations: Number(member.upstream_client_rotations_total) || 0, lastFailure: Number(member.last_failure_at_ms) || 0, lastRecovery: Number(member.last_recovered_ms) || 0 }; + member.ewma_latency_ms > 0 ? `${member.ewma_latency_ms.toFixed(1)} ms` : "-", `${rateBytes(memberTraffic)}/s`, extra.text.rate, extra.text.statuses, extra.text.p95, extra.text.cache, extra.text.probe, extra.text.causes, extra.text.protocol, member.upstream_client_generation ?? "-", member.upstream_client_generation_requests ?? "-", generationAgeText, member.upstream_client_rotations_total ?? "-", lastFailure, lastRecovery]; + const sortValues = { vhost: poolLabel.toLowerCase(), backend: String(member.id || "").toLowerCase(), status: label.toLowerCase(), active: Number(member.active) || 0, requests: Number(member.requests) || 0, errors: Number(member.errors) || 0, latency: Number(member.ewma_latency_ms) || 0, traffic: memberTraffic, ...extra.sort, generation: Number(member.upstream_client_generation) || 0, generationRequests: Number(member.upstream_client_generation_requests) || 0, generationAge: Number.isFinite(generationAge) ? generationAge : -1, rotations: Number(member.upstream_client_rotations_total) || 0, lastFailure: Number(member.last_failure_at_ms) || 0, lastRecovery: Number(member.last_recovered_ms) || 0 }; row.poolSort = sortValues; row.poolSearchValue = [poolLabel, member.id, label, member.status, member.last_failure_class, member.last_failure_detail].filter(Boolean).join(" ").toLowerCase(); - cells.forEach((value, index) => { const cell = el("td", String(value ?? "-")); cell.dataset.poolColumn = poolColumns[index]; cell.title = index === 2 ? (vhostDisabled ? t("vhDisabled") : `${label} · ${healthDetail}`) : index === 12 ? (member.last_failure_detail || lastFailure) : String(value ?? "-"); if (index === 2 && !vhostDisabled) cell.className = member.status === "down" ? "status-error" : member.status === "up" ? "status-good" : ""; if (index === 12 && member.last_failure_at_ms) cell.classList.add("backend-last-failure"); row.append(cell); }); rows.push(row); + cells.forEach((value, index) => { const cell = el("td", String(value ?? "-")); const column = poolColumns[index]; cell.dataset.poolColumn = column; cell.title = column === "status" ? (vhostDisabled ? t("vhDisabled") : `${label} · ${healthDetail}`) : column === "lastFailure" ? (member.last_failure_detail || lastFailure) : String(value ?? "-"); if (column === "status" && !vhostDisabled) cell.className = member.status === "down" ? "status-error" : member.status === "up" ? "status-good" : ""; if (column === "lastFailure" && member.last_failure_at_ms) cell.classList.add("backend-last-failure"); row.append(cell); }); rows.push(row); const tone = vhostDisabled ? "muted" : member.health_mode === "off" && member.status === "up" ? "muted" : member.status === "up" ? "up" : member.status === "down" ? "down" : member.status === "checking" || member.status === "draining" ? "warn" : "muted"; overviewItems.push({ key: `${pool.id}\u0000${member.id}`, vhost: poolLabel, backend: `${member.id}${member.backup ? " / " + t("vhBackup") : ""}`, disabled: vhostDisabled, tone, group: tone === "up" ? "up" : tone === "down" ? "down" : "other", label, title: vhostDisabled ? t("vhDisabled") : `${label} · ${healthDetail}`, activeText: activeLimit, requests: Number(member.requests) || 0, errors: Number(member.errors) || 0, latency: Number(member.ewma_latency_ms) || 0, traffic: memberTraffic, ...(({ inRate, outRate }) => ({ trafficIn: inRate, trafficOut: outRate }))(backendTrafficSplit(pool.id, member)), generation: member.upstream_client_generation ?? "-", generationRequests: member.upstream_client_generation_requests ?? "-", generationAgeText, rotations: member.upstream_client_rotations_total ?? "-", lastFailureMs: Number(member.last_failure_at_ms) || 0, lastFailureClass: member.last_failure_class || "", lastFailureDetail: member.last_failure_detail || "", lastFailureText: lastFailure, lastRecoveryMs: Number(member.last_recovered_ms) || 0, lastRecoveryText: lastRecovery, - sort: sortValues, search: row.poolSearchValue, + extra, sort: sortValues, search: row.poolSearchValue, }); if (!vhostDisabled && member.status === "up") up++; active += Number(member.active) || 0; requests += Number(member.requests) || 0; errors += Number(member.errors) || 0; traffic += memberTraffic; const latency = Number(member.ewma_latency_ms || member.latency_ms) || 0; if (latency > 0) { const weight = Math.max(1, Number(member.requests) || 1); weightedLatency += latency * weight; latencyWeight += weight; } @@ -698,6 +813,7 @@ function renderStatus(status) { }); } for (const key of backendTrafficHistory.keys()) if (!seenTraffic.has(key)) backendTrafficHistory.delete(key); + for (const key of backendRequestHistory.keys()) if (!seenRequests.has(key)) backendRequestHistory.delete(key); for (const [id, vhost] of configuredVhosts) { const existing = aggregate.get(id) || {}; const metrics = status.vhost_metrics?.[id]; @@ -822,5 +938,5 @@ $("pool-open").addEventListener("click", () => setPanel("vhosts")); initPoolTableTools(); $("location-help-close")?.addEventListener("click", () => $("location-help-dialog")?.close()); window.rustProxyDrafts.register("vhost", { labelKey: "vhosts", isOpen: () => dialog.open, isDirty: () => dirty, suspend: suspendDraft, restore: restoreDraft, close: () => { dirty = false; if (dialog.open) dialog.close(); } }); -window.rustProxyVhosts = { render, renderStatus, applyPreferences, reset() { closeVhostBackendPopup(); dialog.close(); draftEpoch++; stagedCertificates = []; uploading = false; editing = null; revision = null; dirty = false; initialized = false; backendHealthEditorControl = null; poolTablePrefs = defaultPoolTablePrefs(); vhostListPrefs = defaultVhostListPrefs(); poolPreferenceSaveRunning = false; poolPreferenceSaveQueued = false; chartHistory.clear(); backendTrafficHistory.clear(); vhostTrafficHistory.clear(); container.replaceChildren(); $("vhost-search").value = ""; if ($("pool-search")) $("pool-search").value = ""; $("vhost-list").replaceChildren(); $("pool-rows").replaceChildren(); const sort = $("pool-sort"); if (sort) sort.value = poolTablePrefs.sort; const vhostSort = $("vhost-sort"); if (vhostSort) vhostSort.value = vhostListPrefs.sort; syncVhostSortDirection(); applyPoolTableView(); } }; +window.rustProxyVhosts = { render, renderStatus, applyPreferences, reset() { closeVhostBackendPopup(); dialog.close(); draftEpoch++; stagedCertificates = []; uploading = false; editing = null; revision = null; dirty = false; initialized = false; backendHealthEditorControl = null; poolTablePrefs = defaultPoolTablePrefs(); vhostListPrefs = defaultVhostListPrefs(); poolPreferenceSaveRunning = false; poolPreferenceSaveQueued = false; chartHistory.clear(); backendTrafficHistory.clear(); backendRequestHistory.clear(); vhostTrafficHistory.clear(); container.replaceChildren(); $("vhost-search").value = ""; if ($("pool-search")) $("pool-search").value = ""; $("vhost-list").replaceChildren(); $("pool-rows").replaceChildren(); const sort = $("pool-sort"); if (sort) sort.value = poolTablePrefs.sort; const vhostSort = $("vhost-sort"); if (vhostSort) vhostSort.value = vhostListPrefs.sort; syncVhostSortDirection(); applyPoolTableView(); } }; if (envelope) render(); if (latestStatus) renderStatus(latestStatus); diff --git a/src/cache/dashboard.rs b/src/cache/dashboard.rs index ce00649..78e7e07 100644 --- a/src/cache/dashboard.rs +++ b/src/cache/dashboard.rs @@ -275,6 +275,9 @@ impl Cache { } view } + pub fn scope_totals(&self) -> BTreeMap { + self.domain_stats.scope_totals() + } pub fn record(&self, scope: &str, domain: &str, outcome: &str, reason: &'static str) { self.domain_stats.record(scope, domain, outcome, reason); } diff --git a/src/cache/dashboard_tests.rs b/src/cache/dashboard_tests.rs index f9bd25a..c0d5582 100644 --- a/src/cache/dashboard_tests.rs +++ b/src/cache/dashboard_tests.rs @@ -574,6 +574,23 @@ mod dashboard_tests { assert_eq!(result.domains[0].counters.saved_body_bytes, 1040); } + #[test] + fn scope_totals_sum_domains_per_vhost() { + let cache = Cache::new(7); + cache.record("alpha", "a.example", "HIT", "cache_hit"); + cache.record("alpha", "b.example", "HIT", "cache_hit"); + cache.record("alpha", "b.example", "MISS", "cache_miss"); + cache.record("beta", "c.example", "BYPASS", "request_cookie"); + cache.domain_stats.saved_body_hit("alpha", "a.example", 300); + cache.domain_stats.saved_body_hit("alpha", "b.example", 700); + let totals = cache.scope_totals(); + assert_eq!( + totals["alpha"], + ScopeCacheTotals { hits: 2, misses: 1, bypasses: 0, saved_body_bytes: 1000 } + ); + assert_eq!(totals["beta"].bypasses, 1); + } + #[test] fn per_domain_counter_cardinality_is_bounded() { let cache = Cache::new(7); diff --git a/src/cache/model/entry.rs b/src/cache/model/entry.rs index 2c55beb..3c26e62 100644 --- a/src/cache/model/entry.rs +++ b/src/cache/model/entry.rs @@ -586,3 +586,10 @@ pub struct DomainCounters { pub saved_body_bytes: u64, pub reasons: BTreeMap, } +#[derive(Debug, Clone, Copy, Default, Serialize, PartialEq, Eq)] +pub struct ScopeCacheTotals { + pub hits: u64, + pub misses: u64, + pub bypasses: u64, + pub saved_body_bytes: u64, +} diff --git a/src/cache/stats.rs b/src/cache/stats.rs index 48a322f..2ff9c19 100644 --- a/src/cache/stats.rs +++ b/src/cache/stats.rs @@ -196,4 +196,25 @@ impl DomainStatsStore { } counters } + + fn scope_totals(&self) -> BTreeMap { + let published = self.domains.load(); + let load = |value: &AtomicU64| value.load(Ordering::Relaxed); + published + .index + .iter() + .map(|(scope, domains)| { + let mut totals = ScopeCacheTotals::default(); + for stats in domains.values() { + totals.hits = totals.hits.saturating_add(load(&stats.hits)); + totals.misses = totals.misses.saturating_add(load(&stats.misses)); + totals.bypasses = totals.bypasses.saturating_add(load(&stats.bypasses)); + totals.saved_body_bytes = totals + .saved_body_bytes + .saturating_add(load(&stats.saved_body_bytes)); + } + (scope.clone(), totals) + }) + .collect() + } } diff --git a/src/metrics/vhost/mod.rs b/src/metrics/vhost/mod.rs index 51e7493..7dd80ea 100644 --- a/src/metrics/vhost/mod.rs +++ b/src/metrics/vhost/mod.rs @@ -24,6 +24,9 @@ pub struct VhostMetricsView { pub bytes_out: u64, pub average_latency_ms: f64, pub ewma_latency_ms: f64, + pub status_classes: [u64; 5], + pub p95_latency_ms: Option, + pub p99_latency_ms: Option, } impl VhostMetrics { @@ -95,8 +98,32 @@ impl VhostMetrics { duration_us as f64 / completed as f64 / 1000.0 }, ewma_latency_ms: n(&self.ewma_duration_us) as f64 / 1000.0, + status_classes: std::array::from_fn(|index| n(&self.status[index])), + p95_latency_ms: self.latency_quantile_ms(95), + p99_latency_ms: self.latency_quantile_ms(99), } } + + pub fn latency_quantile_ms(&self, percent: u64) -> Option { + let counts: [u64; 11] = std::array::from_fn(|index| n(&self.buckets[index])); + let total = counts.iter().fold(0u64, |sum, value| sum.saturating_add(*value)); + if total == 0 { + return None; + } + let target = total.saturating_mul(percent).div_ceil(100).max(1); + let mut seen = 0u64; + for (index, count) in counts.iter().enumerate() { + seen = seen.saturating_add(*count); + if seen >= target { + let bound = DURATION_BUCKETS_US + .get(index) + .copied() + .unwrap_or(DURATION_BUCKETS_US[DURATION_BUCKETS_US.len() - 1] * 2); + return Some(bound as f64 / 1000.0); + } + } + None + } } #[cfg(test)] include!("tests.rs"); diff --git a/src/metrics/vhost/tests.rs b/src/metrics/vhost/tests.rs index a9473b2..d309519 100644 --- a/src/metrics/vhost/tests.rs +++ b/src/metrics/vhost/tests.rs @@ -69,4 +69,25 @@ mod compression_counter_tests { assert_eq!(view.compression_zstd, 1); assert_eq!(view.compression_deflate, 1); } + + #[test] + fn view_reports_status_classes_and_bucket_quantiles() { + let m = VhostMetrics::default(); + assert_eq!(m.view().p95_latency_ms, None); + m.active.store(100, Ordering::Relaxed); + for index in 0..100u64 { + let (status, millis) = match index { + 0..=89 => (200, 3), + 90..=95 => (404, 40), + 96..=98 => (302, 200), + _ => (502, 9_000), + }; + m.complete(status, false, std::time::Duration::from_millis(millis), 0, 0); + } + let view = m.view(); + assert_eq!(view.status_classes, [0, 90, 3, 6, 1]); + assert_eq!(view.p95_latency_ms, Some(50.0)); + assert_eq!(view.p99_latency_ms, Some(250.0)); + assert_eq!(m.latency_quantile_ms(100), Some(10_000.0)); + } } diff --git a/tests/panel_refresh_contract.js b/tests/panel_refresh_contract.js index 179e32b..ef6bb28 100644 --- a/tests/panel_refresh_contract.js +++ b/tests/panel_refresh_contract.js @@ -51,5 +51,5 @@ const events = openapi.paths["/api/events"].get; const logsFlag = (events.parameters || []).find(parameter => parameter.name === "logs"); assert.equal(logsFlag?.schema?.type, "boolean"); assert.equal(logsFlag?.schema?.default, true); -assert.equal(openapi.info.version, "0.36.10"); +assert.equal(openapi.info.version, "0.36.11"); console.log("Panel refresh/API contract checks: OK");