v0.33.0
This commit is contained in:
1 parent
bbfa8cbc3f
commit
2ccc598b12
49 files changed
+4749
-617
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
|
||||
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
|
||||
compatibility. Current source version: 0.32.8.
|
||||
compatibility. Current source version: 0.33.0.
|
||||
|
||||
## Delivery
|
||||
|
||||
|
||||
@@ -3,8 +3,38 @@
|
||||
> Operator-facing release history. Historical engineering notes are kept in
|
||||
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
|
||||
|
||||
## 0.33.0
|
||||
|
||||
- Move the Overview history sample selector to the connection-status header row
|
||||
(right-aligned, wrapping on narrow screens); hide it on other pages without
|
||||
changing temporary/persistent history settings or polling. Narrow the
|
||||
Web Server-only `PhpMetrics::record_web` visibility to match `ResponseKind`
|
||||
and remove Rust's `private_interfaces` warning.
|
||||
- Fix fatal authenticated JS bundle startup error (`rustProxyOverviewSamples.onChange`
|
||||
accessed before sample selector initialization). Ensure delayed DOM setup and
|
||||
preserve temporary/persistent 60/120/300/600 history choices.
|
||||
- Keep donuts compact with totals next to rings and legends.
|
||||
- Add twelve **hidden-by-default** Web Server Overview metric blocks and
|
||||
expandable per-Web-Service live metrics, using the existing status SSE path,
|
||||
shared cadence and user-controlled widget ordering/visibility.
|
||||
- Instrument Web Server request kinds, exact status subset, HTTP methods,
|
||||
pre-compression response body bytes, active header work, configured PHP
|
||||
concurrency, classified FastCGI failures, and per-service compression.
|
||||
- Expand fixed-cardinality Prometheus families and Grafana operations dashboard
|
||||
with Web Server, PHP-FPM, per-service filtering and cross-metric correlation.
|
||||
- Preserve API/configuration schema, previous user preferences and existing
|
||||
cache/transform sample controls; add targeted test regressions and document
|
||||
the outstanding Rust/live-runtime acceptance gate.
|
||||
|
||||
## 0.32.8
|
||||
|
||||
### Dashboard metrics visual follow-up
|
||||
|
||||
- Render bounded HOT cache RAM, COLD queue RAM and COLD disk budgets as progress bars with used/limit detail; use a meter for file descriptors, HTTP protocol shares and backend capacity as well. Missing or disabled limits never generate a fabricated percentage.
|
||||
- Show cache HIT/MISS distribution as compact, labeled donut charts, preserving the percentage and raw HIT/MISS counts.
|
||||
- Introduce full-size, four-series transformation and compression rate charts with per-series legends (transform applied/cache HIT/skipped/errors and gzip/Brotli/Zstd/deflate). Keep existing refresh cadence, authenticated status APIs, appearance presets and user card preferences.
|
||||
- Ship JS simulated-status regression checks and responsive Chromium layout checks; Rust build and live-server acceptance remain pending.
|
||||
|
||||
- Keep the Overview title on the left with actions aligned to the right, wrapping naturally on narrow screens instead of forcing three equally sized button columns.
|
||||
- Use existing quiet, outline and primary button styles in the Overview actions and visual editor; remove redundant Overview-specific button classes and obsolete mobile button overrides without changing the shared presets.
|
||||
- Remove the complete `tools/checks/` directory from the distributed source tree. Keep the source/upgrade preflight in `scripts/preflight.py` and run Rust tests directly from `dev.sh`.
|
||||
|
||||
Generated
+1
-1
@@ -1881,7 +1881,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-proxy"
|
||||
version = "0.32.8"
|
||||
version = "0.33.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rust-proxy"
|
||||
version = "0.32.8"
|
||||
version = "0.33.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.99"
|
||||
description = "RustProxy - independent reverse proxy with local administration"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# RustProxy 0.32.8
|
||||
# RustProxy 0.33.0
|
||||
|
||||
**0.31.0 panel/cache additions:** browser-negotiated Zstd/Brotli/gzip for HTML, JS and CSS (Settings → Administration); optional HTTP BAN/PURGE restricted to explicit IP/CIDR allowlist (Settings → Cache, disabled by default); System information Refresh button beside the API badge. See [Settings](docs/SETTINGS.md) and [Cache](docs/CACHE.md).
|
||||
|
||||
@@ -7,11 +7,52 @@ streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhos
|
||||
|
||||
**0.31.9 Certificate API management: inspect and edit credential settings without exposing/rotating the current token, or regenerate one existing token with one-time display. Rust/Cargo and live testing required.**
|
||||
|
||||
## 0.33.0: Web Server / PHP-FPM observability
|
||||
|
||||
- **Fixed dashboard startup:** initialize the shared Overview history selector before any
|
||||
bundled top-level change listener. This prevents `onChange` startup errors
|
||||
and allows the authenticated panel to load. The History selector supports
|
||||
60/120/300/600 samples, temporary edits and browser-persistent choices,
|
||||
in the header row beside the connection indicator (Overview only),
|
||||
independently of Cache/Transform, without additional status polling.
|
||||
- **Compilation cleanup:** restrict `record_web` visibility to the Web Server
|
||||
module, matching its `ResponseKind` argument and removing the
|
||||
`private_interfaces` diagnostic without changing request accounting.
|
||||
- **Compact donut layout:** move HIT/MISS totals beside the rings/legends rather
|
||||
than above them, preserving standard card-row heights and responsive behavior.
|
||||
- **Twelve opt-in WWW widgets** in the existing Overview layout registry: HTTP
|
||||
status split, static/PHP/other, PHP permit capacity, HTTP methods, recent 5xx
|
||||
percentage, PHP requests/s, requests/s, response-header latency, FastCGI
|
||||
failures, emitted-body byte rates, status-class rates, and top services.
|
||||
**Every new widget starts hidden** for both new accounts and upgrades from
|
||||
older saved layouts. Existing cards remain as configured.
|
||||
- **Web Server per-service live details:** expandable metrics in existing service
|
||||
cards, including RPS, 5xx, header latency, PHP concurrency permits, static
|
||||
versus PHP classification, and body bytes. The main live SSE/status snapshot
|
||||
is reused; no new API polling, SQL schema or per-request untrusted labels.
|
||||
- **Prometheus exporter:** bounded per-Web-Service response-kind/method/status,
|
||||
body byte and downstream compression counters, plus active headers, PHP
|
||||
concurrency limit/use/available gauges and classified FastCGI failures.
|
||||
Response byte counts are sampled from produced body frames **before** final
|
||||
downstream compression (not wire throughput). Response latency is time to
|
||||
headers (not complete delivery). PHP capacity is RustProxy's semaphore,
|
||||
**not** the actual PHP-FPM worker pool.
|
||||
- **Grafana:** retain the existing operations dashboard and extend it with
|
||||
Web Server, PHP-FPM, per-service breakdown and WWW/PHP correlation rows,
|
||||
all filterable by configured `web_service` IDs.
|
||||
|
||||
|
||||
## 0.32.8: Overview metric visualizations
|
||||
|
||||
- HOT cache RAM occupancy, COLD write-queue RAM occupancy and COLD disk occupancy appear as compact progress bars with used/limit details on the Overview. COLD cache objects live on disk; queue RAM and disk storage have separate budgets. Without a configured budget, the card shows bytes and an indeterminate meter rather than an invented percentage.
|
||||
- The main cache HIT ratio has a small HIT/MISS donut with counts and legend. The optional recent-window cache ratio uses the same format; bounded file-descriptor, HTTP protocol-share and backend-capacity widgets use progress bars.
|
||||
- Transformation and compression activity use two full-width, four-series live charts like the existing HTTP protocol mix. They show sampled per-second counts with named series; compression charts count gzip, Brotli, Zstd and deflate responses (not uncompressed ones).
|
||||
- These changes use existing status/SSE data without extra polling, API routes, database migrations or altered appearance presets. Existing account-specific Overview ordering/visibility preferences remain in effect; newly added cards are visible by default in factory layouts. Runtime integration testing remains required.
|
||||
|
||||
## 0.32.8: Overview action layout
|
||||
|
||||
- The Overview heading stays left-aligned while the refresh, arrangement and layout-lock actions flow to the right; small screens wrap buttons without squeezing them into three equal columns.
|
||||
- The inline layout actions and visual editor keep their existing IDs and behavior while sharing the standard button styles (including Compact mode).
|
||||
- The `tools/checks/` source-check directory is no longer shipped. `./dev.sh doctor` and `./dev.sh repair` use `scripts/preflight.py`; `./dev.sh test` runs Cargo checks and Rust tests.
|
||||
|
||||
## 0.32.7 development follow-up: Appearance and H2 diagnostics
|
||||
|
||||
@@ -760,7 +801,7 @@ See `scripts/README.md`, `tools/README.md` and `integrations/README.md`.
|
||||
./dev.sh check
|
||||
./dev.sh test
|
||||
# Source-only package, with no Git checkout required:
|
||||
python3 make_zip.py /tmp/RustProxy-0.32.8.zip
|
||||
python3 make_zip.py /tmp/RustProxy-0.33.0.zip
|
||||
```
|
||||
|
||||
## New management and test-origin guide
|
||||
|
||||
+13
-2
@@ -1,11 +1,22 @@
|
||||
# RustProxy roadmap
|
||||
|
||||
## 0.33.0 - Web Server observability (implementation complete; live acceptance pending)
|
||||
|
||||
- [x] Add one Overview 60/120/300/600 history selector on the connection-status header row, with temporary changes and explicit browser-persistent choice. Use the same source stream and leave the Cache and Transform selectors independent.
|
||||
- [x] Match the Web Server metric method visibility to `ResponseKind` (no `private_interfaces` warning).
|
||||
- [x] Align donut totals beside the ring, preserving the legend and compact tile heights.
|
||||
- [x] Add opt-in Web Server Overview cards, with all new metrics hidden by default and per-service drilldown.
|
||||
- [x] Extend Web Server/PHP diagnostics and bounded Prometheus metric families without untrusted labels or additional request-path database calls.
|
||||
- [x] Extend the packaged Grafana operations dashboard with Web Server/PHP panels and per-service filters.
|
||||
|
||||
## 0.32.8 - Overview action alignment (runtime acceptance pending)
|
||||
|
||||
- [x] Convert bounded cache RAM/queue/disk, file descriptor, HTTP protocol share and backend capacity cards into meter/progress-bar visuals, with clear unknown-limit behavior.
|
||||
- [x] Replace cache hit ratio sparklines with compact HIT/MISS donut visualizations and legends.
|
||||
- [x] Add large transformation/compression four-series live charts and Polish/English labels, preserving existing status sampling and layout/editor preferences.
|
||||
- [x] Add deterministic UI metric tests and Chromium responsive-layout regression checks.
|
||||
- [x] Replace mobile three-column Overview actions with natural wrapping: keep the heading at left, actions to the right, without shrinking buttons.
|
||||
- [x] Remove redundant button-only classes and outdated small-screen overrides while preserving existing common button variants, layout controls and editor handlers.
|
||||
- [x] Remove the complete `tools/checks/` directory and update affected scripts and docs; retain the upgrade-safety preflight.
|
||||
- [ ] Build and exercise RustProxy against a live backend and real administration session in a Rust-enabled environment.
|
||||
|
||||
## 0.32.7 development follow-up: Appearance and H2-close diagnostics
|
||||
|
||||
|
||||
+113
-104
@@ -1,13 +1,13 @@
|
||||
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
||||
238101ec7b2518840b166ef6e778a3e6ed835dd493b3a36e200bcdc6e6aa22a7 AGENTS.md
|
||||
ef8f6cbd5adf52cbd5f0c58263b4ea36e9ef0212a9579f43f4ad45d2c478be2f CHANGELOG.md
|
||||
1aa14f02650d326337f2acb5953cd2caaae963ac463db2c9e0d0f8d0689b0ae2 Cargo.lock
|
||||
daef03750129b52867850bd75bccef198ca45194c42b9d91d17e039e2e114a35 Cargo.toml
|
||||
12bc9d0f2444dc556ab922b534bd5b7c1602d804325cc0fb3d4d0d28a76cd4ce AGENTS.md
|
||||
09472318ab840fdffc9f1517f4a7a5e4baa46abfba1f96b7d921e69e8a9d25e9 CHANGELOG.md
|
||||
f6df6e080138068fecfe4e6ad7f271c450d158633315da8dd5f2e7377c3674b3 Cargo.lock
|
||||
bcdfe0d135824ad7b570b2070074b29783b449b4b7ff2cc001c29b36ac451168 Cargo.toml
|
||||
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
||||
0460e1ace69d57c8d1ec79634dfb55c8744e3ac01f158fadfcf6c0d9cb6d9f65 README.md
|
||||
9297fa45ba5df2490c2ec38da8fb6e659571e2b16363f3c9933c08c08d76c6e2 ROADMAP.md
|
||||
acca7f883bb48101b4385625b940e852d8a888ac3955160d978e84e59aa350c1 README.md
|
||||
5104ae23146a728c7561ff5d6aeaa787ee755b379b1ac1d4ed4bbf2e897c46b3 ROADMAP.md
|
||||
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
||||
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
|
||||
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
|
||||
@@ -51,25 +51,25 @@ e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND
|
||||
89a1af6c9fe10c572b211f9632ce256166e67990b5dacacf2ae07265d99a5172 docs/NPM_MIGRATION.md
|
||||
e68d5a5cf0991de9308f78afb0264e02979f7463a5f395eb7a6e9ec2bd3a3d54 docs/PHP_FPM.md
|
||||
bc068a176056f5ea4d3b637071e644ed58a2140494c233ef22aa66e9700e3299 docs/PHP_FPM_ISOLATION.md
|
||||
5cd028b7b13838fcf500e1660aea5324223329467f6b717573f47d09b366f1b5 docs/PROMETHEUS.md
|
||||
ad6e3f003b820a4f45e30e39577d120a69d167c70910bda4891971759458c3dd docs/PROMETHEUS.md
|
||||
bb15713876184682cf1907d8c5edd18e657735b6de6cc5f8ca976dbdec4dd936 docs/README.md
|
||||
6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md
|
||||
0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md
|
||||
d85c5567fc6a7714b96ad7cfe4ee9ff2085f84c4f31534a6484340d03a73e0f7 docs/RENEWAL.md
|
||||
72b95592b310ced607cdaf8a334e2732cb29341aed12f9024eaf95884737d947 docs/ROADMAP_HISTORY.md
|
||||
cd4ed3af26eb51c9ff6ef3dda9d52ff5120bcfc78c842bce34a639cf85b6aea9 docs/ROADMAP_HISTORY.md
|
||||
759bf2987f2b602488829d1856fa33d57c2a3b23bc681ef5a931b9633af3a533 docs/SECRET_STORE_BACKUP.md
|
||||
ec7e32fd19e6ba6ed1732f1f3dd92ac0860609e8c0bbd3c1998840cdfb24bf54 docs/SECURITY.md
|
||||
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
|
||||
007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md
|
||||
4dcf3243a13d9ae1827b5cefdbb9e27bd6310321c50d4d5934527d44437df6ef docs/SETTINGS.md
|
||||
8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md
|
||||
9e2d4de706f7fd9cbec425237ad44f8a18f985f9f98be10df5a52d612c3a8379 docs/TEST_REPORT.md
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 docs/TEST_REPORT.md
|
||||
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
|
||||
751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md
|
||||
fe3ec6ca16ff933239c12dacf39dd7b45717c0551a8b9f6ce601bf78136c9e37 docs/TRANSPORT.md
|
||||
fd1c187ea8eef101764670d4ebb0c554173412514e37b909c81ea3f22ba8e7ee docs/VHOSTS.md
|
||||
09b60fb7b40db4d0139fa2a9e69a2aa9f28d4e61b8643521cfc7e936d7df8774 docs/VHOST_ACCESS.md
|
||||
04a42e4629fdfb7224173dcf57cd0992cba2edd109204bbf503a255886605542 docs/WEB_SERVER.md
|
||||
a101fb3f3d8fa1ab50977418dbc16d33d4bf3590f966970e26044c9727045965 docs/WEB_SERVER.md
|
||||
c336f998027330c58d5afbbbb3a3a682fd44ae3997643882fe342633822f8bb3 docs/screenshots/README.md
|
||||
5502e8ce44276213803b66c58e3a4d804a1354f5ed5d83f47ee805e8c44ab088 examples/acl-headers.json
|
||||
2431dd88a2d9ad2cf3cd4b8fcdfa602563ab1013c612d8917fddbe7febfb2dc3 examples/cache-dashboard.json
|
||||
@@ -81,8 +81,8 @@ b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock-
|
||||
5a8f3a8a054abc89c6e78896b9fe2f903212199a24dc3d2d2bcbd0696a31bcde examples/transport.json
|
||||
d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json
|
||||
af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json
|
||||
4068b79ddd898af43b85617d8de9e9f48cfee09a4fb9ed26bf5b692571a30196 grafana/README.md
|
||||
0d97fbf31f60563bb41f2ea8d798bccebc3d4c57f10bad8b7faa17296722e36b grafana/rustproxy-overview.json
|
||||
1b8974af79a5026e0648ceab58a32f233e1760e21e7465a0d46e9c41fb09964f grafana/README.md
|
||||
1b9120b1d64eb07060bcb84df404c28ed375da9e0214d39cf01e167dd46e17f9 grafana/rustproxy-overview.json
|
||||
a05cbbf101b288bf9cf83fedfc5017925a8b360a78f313d31c1a70f15dc1dd22 install-debian13.sh
|
||||
909e9ab94617ace74a425f15084bf85f3086cc52ba5b28f2b441ff800abc6ef9 install.sh
|
||||
42f9f1d18c021fc7734b5bd020844bf678725a0e539e2fcbe05ec676f6239eae integrations/README.md
|
||||
@@ -95,8 +95,8 @@ cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/r
|
||||
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
|
||||
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
|
||||
13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md
|
||||
a1f873bfe87d090bac46962598aeb90ddfa62ae257b3b76ec1af011c74197bac langs/en_US.json
|
||||
ca95ffe6919c07699155d1c49fa711916f91f0fbe928e5a36be23f05519ffbbb langs/pl_pl.json
|
||||
b5922257df0009d3e0778d2a08c926952fed6db6c013049e04ca8b63eaa6df6d langs/en_US.json
|
||||
f195133531f91a3c139979df7f14814e87e008899e5ccd123bfbeadec447cee7 langs/pl_pl.json
|
||||
a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
|
||||
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
|
||||
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
|
||||
@@ -129,68 +129,77 @@ e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtim
|
||||
174c8059625e3315a3d9fefe74c17f78ed850ff25ee15bfdabc148107dd344eb scripts/start_stream_backends.sh
|
||||
046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/test_stream_tcp.py
|
||||
f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/test_stream_udp.py
|
||||
276e732fc2dfb0d39badb159e4fdcba0d7d73509c60ad5c9df49621c03046f1b scripts/tests/dashboard_metric_visuals_ui_test.mjs
|
||||
aa12d00acdd226602b3efc90221fbc936f69f73bcabbaa6c01dfb0e05d08c505 scripts/tests/overview_samples_0330_ui_test.mjs
|
||||
046b4282550f5cb4e53906665c91c7442be70fc68fd0743466fd832582f7c979 scripts/tests/test_stream_tcp.py
|
||||
f0b48ec0cc4411755cc0b4128eebce710375afa99344cb76198cf31074f8eb63 scripts/tests/test_stream_udp.py
|
||||
91bf803f05b97b4654a6e607de40fccdd0268882039d6d1c5d677edce9459006 scripts/tests/web_observability_0330_browser_test.py
|
||||
38ac7c7cc8c789f407cb2543a8a7dedcc8cbfce13eac89f5ec6537f8944d83f9 scripts/tests/web_observability_0330_source_test.py
|
||||
8b600edbe0aff817f871f2bf3821a35ca8d2d29870c0f6b6bf3c0cef2139e221 scripts/tests/web_server_0330_ui_test.py
|
||||
22e880e98c7b9d700c7a4c02b9b1d543e5dbd1f4ecdca42813a1083165b32a19 scripts/version.py
|
||||
4803e44de39b872a5b3b55f9ca9e81c066ba52cb8f6adb879fd57c5f94b93e89 src/acl/dynamic/tests.rs
|
||||
63efab1e95d41134fc30cc57320c32810bbae9f81a640bdb24b1be2059e63d8a src/acl/dynamic.rs
|
||||
a22e5ea6381a1eacafe650be0f5cd6e653773e31f6491a562fe0459a309fb7cc src/acl/tests.rs
|
||||
55485a513e8c4da2a58daa7abcb15b32ed408a5fc062cf23599fb83d16b90cfe src/acl.rs
|
||||
63efab1e95d41134fc30cc57320c32810bbae9f81a640bdb24b1be2059e63d8a src/acl/dynamic.rs
|
||||
4803e44de39b872a5b3b55f9ca9e81c066ba52cb8f6adb879fd57c5f94b93e89 src/acl/dynamic/tests.rs
|
||||
a22e5ea6381a1eacafe650be0f5cd6e653773e31f6491a562fe0459a309fb7cc src/acl/tests.rs
|
||||
6f5d3137bf24451da05c1db9f0f9e2a451305db17b6bc2590815ad54a6511829 src/acme.rs
|
||||
587097620b2f25ce7c6f59f907b47660184b095e3040c7615e74509a82e7cdd8 src/acme/jobs.rs
|
||||
3f158f46e80b403b658b4e9b1e3710e38c70e47b2d476b8223d9c394ffeeba95 src/acme/model.rs
|
||||
b9e57db4bd80ef37564dfc533a96897d8a494671d53acea9fb4bf28d23ba370d src/acme/providers/tests.rs
|
||||
c5334de6c732d76b2bea7bfc17b3ac305a250bbaad31a5420d6a7d7abb200449 src/acme/providers.rs
|
||||
b9e57db4bd80ef37564dfc533a96897d8a494671d53acea9fb4bf28d23ba370d src/acme/providers/tests.rs
|
||||
29ff0647417b0f1d50a4cc075ab51afecff9ddf47db3eb7948d51db5a38f33f3 src/acme/runtime_bootstrap.rs
|
||||
87f64198770ae026c03a242fa542db2590ca734b02e756f1e5c28d66236f753a src/acme/runtime_env.rs
|
||||
f8075a5ef9ae5651d72a33ac0f0fd5b44ad0ae30f6e2d2318412177e0ed837f5 src/acme/runtime_versions.rs
|
||||
e38331a1b186b49fe9d04051de52b603e1fa028def37e4e2f52e386b4d7b29ac src/acme/tests.rs
|
||||
6f5d3137bf24451da05c1db9f0f9e2a451305db17b6bc2590815ad54a6511829 src/acme.rs
|
||||
b6f38f3fd75d6685bbc882ed1077a38bf495cfcf01c397f69ed9b98be68b446f src/admin/access.rs
|
||||
b4a2a068acfa1143b0032b9cd072c19cf3fdda2580f0b932d11d6368cd2ab3ad src/admin/certificate_api.rs
|
||||
019f4aa33e3596bc9c11ef8cf5703bcfffe46c15288c0eba83c8dd9e38055fb2 src/admin/certificates.rs
|
||||
5e150d4ef0413a2f92db5b6631669933b84d4b4796633d01476c4b39188cd05c src/admin/documentation.rs
|
||||
958546c43c05bf4bed8121b24db3b3096f9243f3c56a71e1762592b755684013 src/admin/documentation/documents.rs
|
||||
67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs
|
||||
5e150d4ef0413a2f92db5b6631669933b84d4b4796633d01476c4b39188cd05c src/admin/documentation.rs
|
||||
1ce779b84aa8f47bdb3c34f83593c1f2f06d2235f0319847096cfcd035434168 src/admin/file_log_search.rs
|
||||
ab18acf6fb4ac86fb22635b9219a12dac76d24952c8f15538486267a9b4b7855 src/admin/file_logs.rs
|
||||
c6b552cd5acd9e743c28b1a5564420c214c619fe891ab0561b7d4499aae8d6c0 src/admin/file_logs_io.rs
|
||||
c720a7cad6a15c57b1f2ae7b3e5fbf30a97c64afb9137915f37b23f4e576bdc5 src/admin/file_logs_query.rs
|
||||
74636c6560618ee95757335452baf8db03394e1ad18acd54252f54b15746b8d5 src/admin/logging_api.rs
|
||||
33be50109497d0adb23c36a10894acb5508dd38f3c01136bd0438a4ce0ffe74b src/admin/mod.rs
|
||||
83490218da65fe4354052dcccce48b59e00869b16012c4cc7753d66f29426eba src/admin/openapi.json
|
||||
5b75274164d97b2c7a3acf5d82f494c6de8a41e357fa9e5b07df60bf0fcebe8f src/admin/openapi.json
|
||||
d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/operations.rs
|
||||
30895e05c5d5d0a26be29f667e0f6d72f16a96a5c8a98e09cd11d8ef2d7b4763 src/admin/panel_assets.rs
|
||||
75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs
|
||||
c10f0d277ca5f1fd84eea3705360a81bb0923e10b7e5f5d25378b55ee080de40 src/admin/preferences.rs
|
||||
cd0fda2b5a7b86a6619d6fa7cf5cef5a2598f033a8e9304475c4a05a169c958c src/admin/preferences_columns.rs
|
||||
c17ccba5202d375d4cc593f7e20979f2b796362d87660b898feca0af5a135bfc src/admin/preferences_tests.rs
|
||||
7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs
|
||||
d94640019bc1679c5ac39110ba90399d8228fe8261bafac456be46ec2c8781ab src/admin/preferences_tests.rs
|
||||
055642f1239346616788f3b1da52f814de5ba0649316352459b6d84b0710eeea src/admin/public.rs
|
||||
7a00e4ab943f74d2b1bdcbfab7f9cd3f584e9f59536b4aca6c4c31f5f5508d12 src/admin/public/tests.rs
|
||||
9926827131254677a848c178c80850b3ca1993c05b4bf913687dc0c7992ad048 src/admin/refresh_preferences.rs
|
||||
a9a8b9cce86990d5759eba2f9b29dce885fef056d4f81775bcbd429f4c00bba3 src/admin/router.rs
|
||||
ef82ece387c6a8987aafbc08ca04ccadf1043b9f9a49c02efb28b9df456da429 src/admin/router.rs
|
||||
a40adfbaa908da3ea726d7aa3d45394a70a6a55b6fc0063270ec4d2a2f2026a3 src/admin/session.rs
|
||||
e7355ef15e4377cd71e47d054c607079dd8e6711f1d86824f716b8003210a046 src/admin/status.rs
|
||||
479eca6162df5152ce536727b1090dd8e2e262775f2c8cf5bb2c8fa40cb2a600 src/admin/status_live.rs
|
||||
77fe317c15bf2e03301b22e0552518fe2d7e1ad8d80afaba4e16cd2bf8ddf10c src/admin/status.rs
|
||||
4b8a0483ede651d08489a4d6e5bb616f06423094bbbefcee70b8755d65c04398 src/admin/status_live.rs
|
||||
c97881b17b7a42511868b0e48e2f25aab6fb5b613d538f5e1f1967508454e486 src/admin/status_system.rs
|
||||
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
|
||||
18b778c2c4e723e2fe5cbaaef20effa6ea50fcc35d507ebde2361c9b6eda62b2 src/admin/tests.rs
|
||||
48fcd818c522fe80f2c7b2e218f87de6a7772ffd0a24e93502780b03ec78e335 src/admin/web/acme.js
|
||||
326e404ea67b23318a22cc6161ea42b93644b16c3897e5d72e95636a6ea2fccc src/admin/web/app/backend-dashboard.js
|
||||
4722d47fce7bdb45f0205cc364cb81e94c06abbdfd026d566907eb969ff7e2a2 src/admin/web/app/backend-dashboard.js
|
||||
d6c5448db12de34a7bb4924411b44d09c7144bc3e659bd3c8a7c0ebf1938da1c src/admin/web/app/config-history.js
|
||||
acb778341181553585b66ba35e7b71fda33246570f125ffce349ba2cf0394fa4 src/admin/web/app/core.js
|
||||
8aa205bc88530e4c4678f84ea324c6ecbc823b4ed681624102ff177f3867bc98 src/admin/web/app/core.js
|
||||
dec0b875086e3567486191231056bfc6c1edef006f7aad7c88f19ef37c034570 src/admin/web/app/dashboard-inline-layout.js
|
||||
dc097b9c6ec8ebd4fa53ed8b0a641c9446fe44184c74246b521cfe49cf5a32b3 src/admin/web/app/dashboard-layout-editor.js
|
||||
e5928664c259feab3e9cfd3be69d05571f3aa60445ca373560f50857ecc8bfc0 src/admin/web/app/overview-extra-metrics.js
|
||||
2f17473efa4147b04bcf8b0ad91e936a35ccf7c44771de95dec115d770f1ee42 src/admin/web/app/overview-extra-metrics.js
|
||||
fb0ba435beaa91e518537606f5fe55b0eea63c70efd290798c8d9754fa6771b2 src/admin/web/app/overview-masonry.js
|
||||
c4d59e129837db854f2851ac69130325f6d2f25fd1b67a009354b83da1dfbf02 src/admin/web/app/overview-wide-metrics.js
|
||||
7e0a4dc7274a1d1e4cc928e360a6b77c48a71d586b585a9e28c3bb2eeb17d46a src/admin/web/app/preferences.js
|
||||
5cf84000b52815177792fd1e0d64b0cac93ecb1ae72ec8a9fa391fbe11fd9794 src/admin/web/app/overview-samples.js
|
||||
fbac70623c8b16b577bcc79d499c739458d08b0ae7346d39c5bc0c9db38d3d39 src/admin/web/app/overview-wide-metrics.js
|
||||
4ead1e93be94e411aefc4b944355414f034d1b0edfb31b402328fc80cdaba6dd src/admin/web/app/preferences.js
|
||||
8e75b0915edfbadefc4388701431541f4d35fd5d717363297b63740b3afd34d2 src/admin/web/app/rate-sampling.js
|
||||
43f4dddd1c3ea7fa42eeb9d7b655a8db48b5ddcc6c68213e08dfc2fcfbe8b17d src/admin/web/app/refresh.js
|
||||
7eee9fd6f930288ee3f87dd7a2c64bddb24ae5326cf69600accc376fb015fc87 src/admin/web/app/runtime.js
|
||||
b97095ae0c94184693bcd3929724c456edf3d66119b32db4fcdad5ae5b7a84bc src/admin/web/app/runtime.js
|
||||
31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js
|
||||
86ac42bd4a6a84bb137d5a3c3dc3d1f4e404a543447845e6ea6e6e8210e268ce src/admin/web/app/settings.js
|
||||
fbd2c402828088695f5f5f4b210d1ea8da24ae6425f61248c82c5b07b14afd56 src/admin/web/app/stream-dashboard.js
|
||||
db25aeb84827b6c921519a1f17be607319c50add4b0f9ea57436a387829cf4bf src/admin/web/app/stream-dashboard.js
|
||||
0029667b44b904bd4f81be9885248ddc717ca7be9a12a0923b019706b571c5ba src/admin/web/app/web-overview.js
|
||||
0603dc8793a731a0e582cbbec53aa6d1cc6e007faafcec5d8843730b2cdc39e1 src/admin/web/cache-panel.js
|
||||
02a9f5a61379e68e1723e64ef4c589500e9a993adcb02fe352f1cf9456c90c52 src/admin/web/certificate-api.js
|
||||
da3683f3aa0cec4f86b0ff0fb105959caac4ce65c41a9391f5adb3c289e8ff1c src/admin/web/dashboard-widgets.json
|
||||
797e2935a34b974cb0e07a88056c31e6948cc11385bf2a076d902a332882d0a0 src/admin/web/dashboard-widgets.json
|
||||
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
|
||||
d01a2e48f344fe074c1e63ee0a9bbf7151dfe26140690418535197e13a2a16d7 src/admin/web/docker.js
|
||||
d4aef376cb44b2a13174dca1cb9383f64c04b0b93279f4f0b4805e76002548a1 src/admin/web/docs.css
|
||||
@@ -212,7 +221,7 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
|
||||
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
|
||||
7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js
|
||||
b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js
|
||||
3d67dfd471f5313a745939d4a98e72fbb97c96aec6dcb275068dcf1f4e8ef4f3 src/admin/web/index.html
|
||||
7fc17c09a1c24f7753a84202f64cd7db370e923418e59bd35720d432a06abad9 src/admin/web/index.html
|
||||
03ad9572bb14c34059636d612cf6b876d6361ce601817cff674c57b715dc65dd src/admin/web/integrations.js
|
||||
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
|
||||
cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html
|
||||
@@ -222,7 +231,7 @@ c2e5f554810b8e34fb47361fb90a01073a2e332ee26d82e030956fad3ec1f83e src/admin/web/
|
||||
c28f4254ddc1f720d62646ee0f0b590b60a89c0bf447e8ec74ff3dde0c7635ba src/admin/web/settings.js
|
||||
8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html
|
||||
c13cffe18a616fe5faa5ee945ab0d94d8d392ae8f32474c62fe41010e063ddb1 src/admin/web/streams.js
|
||||
12670b09844212757ff6ed101906fa5251502f0b07de41da4994c5c0971eec1f src/admin/web/style.css
|
||||
918c18132365e6522250c210e765df815f6adef06e2d411c08199e3c49cd34b0 src/admin/web/style.css
|
||||
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
|
||||
69f412bd897e45f4889b71e9982cfff56df9ef20ba2da109b454713a5ecd7c74 src/admin/web/transform-panel.js
|
||||
d816f2f70eef398c18091479320c36d07f9afe07278a250b840ebcdd7bc715f2 src/admin/web/vhosts/core.js
|
||||
@@ -232,18 +241,19 @@ b3239e67901f1796d7a974ade0796f2e3590ff8064628a5629b78c7f6ce2f32e src/admin/web/
|
||||
2170cfddf5c611672de34b0525d36ade8fcbe459cc21269281d6b615d8601b93 src/admin/web/vhosts/rates.js
|
||||
b991e81a98dd604a1d7c30158c0cd8cd825479bd2cd94b082b3550de1071bb58 src/admin/web/vhosts/render.js
|
||||
4dfdeb7c73c2775eacaca65fd8ef1dc80b275421b15522d319d338f5b96288cc src/admin/web/web-server-profiles.js
|
||||
355684980d1caf8918d1773ce17934b51e3a85a60f38491217d10035bf68ce9f src/admin/web/web-server.js
|
||||
b2f948bcc3dbd24952c7b1b02d41d06757f49f0c38a78caf51f8f8a687d8480d src/auth/files/tests.rs
|
||||
a273847795799679bf15540f7ce34347ff40c013969d09e9ffbf530776d13c0b src/admin/web/web-server.js
|
||||
53237ae8b08d261ce21e0aa6fd225bf4f3bb17a37a80529e2a4039d45b13f7c5 src/auth/files.rs
|
||||
b2f948bcc3dbd24952c7b1b02d41d06757f49f0c38a78caf51f8f8a687d8480d src/auth/files/tests.rs
|
||||
775178ee5781e64574bdd7c1fb7d64e7e283977fbd9c98c99896532a7f1df612 src/auth/mod.rs
|
||||
c96a87e82ff0c1c9ce3d0cd5135903bb53af78d1c4fb6ebfe5fea503bf5644ea src/auth/store.rs
|
||||
745226cabb007819370fc6aa680995a03a8a13fa057dacfab0fcc21d0c43bbe4 src/auth/tests.rs
|
||||
09c3a622a7d2169e49e3369ea5f0f816eab2f717eaf1675c625d518c8f34d367 src/backup.rs
|
||||
76aa2afe9e7302be39b3512f30d0448d9134300f8ae930d64ada40cf3ad4a8a3 src/backup/archive.rs
|
||||
732cd4f9ed38ae69f8bbae61b1db8baaa6f86b07ca21c41f24a206a30f0a96fc src/backup/ownership.rs
|
||||
076350506215faec3cfe7d9de4e60baf29672cc22a41eabe601dcc510c257799 src/backup/snapshot.rs
|
||||
a00571b409d305af86ab2a57a25434b5ab58706772df440d017fc1fec097f7b4 src/backup/tests.rs
|
||||
f5afae218f3a6f91bc2f1f5a90094630ce5b7ca574de61807b5a317b13937ec2 src/backup/validation.rs
|
||||
09c3a622a7d2169e49e3369ea5f0f816eab2f717eaf1675c625d518c8f34d367 src/backup.rs
|
||||
426a946b2c9e244f09f6cbc5f9664b8ad0737529a9bb5a3d549d27196a1f8043 src/balance.rs
|
||||
d3257fa5222dd4f70a82400f5060d3cbd33c13862a9320931361fe0e1c5bd452 src/balance/health.rs
|
||||
59d90e709bfc2be6cd48576f1c1d222891523853a46f2561f0562d74d01f60a5 src/balance/lease_runtime.rs
|
||||
aed9ed896a122d7adcbedf0897cfd5acf62be3c5b63a71a4a425c8fe8474c0eb src/balance/model.rs
|
||||
@@ -251,26 +261,26 @@ aed9ed896a122d7adcbedf0897cfd5acf62be3c5b63a71a4a425c8fe8474c0eb src/balance/mo
|
||||
477132dd381e23f1be197c32ff58a0925f41d3e6aa63fe77c10973cefb2b76f6 src/balance/pool_selection.rs
|
||||
8abb6048303ee8c022546a528d5693167766e7450efd8e20f37ec6fd0fa9ef5e src/balance/pool_view.rs
|
||||
f1a92d188628ab3fb587375e80956e1552301647f2732cafb55e1471c7f743e7 src/balance/tests.rs
|
||||
426a946b2c9e244f09f6cbc5f9664b8ad0737529a9bb5a3d549d27196a1f8043 src/balance.rs
|
||||
9a8858b7ff461a4d961ba701ca3ed6eaec15c3f7f05eef8e0e30edc63617605a src/basic_auth/tests.rs
|
||||
5049d2bd01a9c866f3bad2288d8d594df96bba6045dfe15cd22b4173562b7388 src/basic_auth.rs
|
||||
9a8858b7ff461a4d961ba701ca3ed6eaec15c3f7f05eef8e0e30edc63617605a src/basic_auth/tests.rs
|
||||
7def03ace07dc38a4806277d75b6ce673d33ddaaf1c4855fa0a3372be1cf9489 src/cache/cold.rs
|
||||
a9eeb93519aae2de7a1282296ced91d2eb8307cc966f8fbf6085af0fd21b0ad2 src/cache/cold/cleanup.rs
|
||||
e022003be144598100389cf38d76f509b5dbcc035d5bae556bb9b94cc8bd30ba src/cache/cold/demotion.rs
|
||||
9fc18368e2ce278ef0ad2b73ff016aa24936c2186be371ca4010a1f6f63bb0ed src/cache/cold/startup.rs
|
||||
ea319b6e200b0c6ca93da5e2af7a3f86e3667cef918716f54f82b8012111b9bd src/cache/cold/worker.rs
|
||||
7def03ace07dc38a4806277d75b6ce673d33ddaaf1c4855fa0a3372be1cf9489 src/cache/cold.rs
|
||||
d51fa4f320be56c572f767617428c3f3838543cb0eeb7d1f8ae791e1a1ce9a71 src/cache/collapse.rs
|
||||
27a35689fef639f1aea00d9f5a86adc981c0c304dc3e28e6da40494dffbb6bfa src/cache/dashboard.rs
|
||||
780c2654c67dbe176d8310678e6deec8022bcebfd85b95df7262069ed41f57b1 src/cache/fill.rs
|
||||
675d18afb53f25c56cba49bbb431fa39ecd9d7bdd192c7b39b0698225de7a2a2 src/cache/invalidation.rs
|
||||
c1357a13658e1ed5158084d3a41ba9ea906512774a5863ce9c56cb508d3fa6c8 src/cache/invalidation/ban.rs
|
||||
0cc31d8e70fb3774c4ce3713b94ed79c184a8d8f0d38ce66a97724410733e17e src/cache/invalidation/invalidate.rs
|
||||
675d18afb53f25c56cba49bbb431fa39ecd9d7bdd192c7b39b0698225de7a2a2 src/cache/invalidation.rs
|
||||
b74945c7460b76ed7afcb02a45ede09d76f08e0703c47920b9b9d794d4b74980 src/cache/lifecycle.rs
|
||||
bd831807ca4720b3a41a6ea1d9a6c0ed2d935225e2dacbd9313be064f2e4a20e src/cache/lookup.rs
|
||||
88f6210c03d7a8e7a35be1f123c9c15675af6e6a599c50db927d99866851b634 src/cache/lookup/hot.rs
|
||||
1779abcee77600c13050a2882f896a5b35625b1ae53737c2554179492ce0e40d src/cache/lookup/insert.rs
|
||||
55a39fbcb8c034a98eaad44d9efc092902cd591ef167b8d8e14a56fd1a9f1794 src/cache/lookup/tiered.rs
|
||||
bd831807ca4720b3a41a6ea1d9a6c0ed2d935225e2dacbd9313be064f2e4a20e src/cache/lookup.rs
|
||||
58c03fde09e5eab40968679793b364f4f1cf5fd832bd79ebcdc92bb23871ad82 src/cache/mod.rs
|
||||
0c8b0d550e94d4474dcdf991aa0340228dc5f2f904f39350e6e666a16f16fe87 src/cache/model.rs
|
||||
ba94226addfd6cdab67d666dc15b8412c4d117111e44639e503e89a95175d0a2 src/cache/model/cache.rs
|
||||
0ec4f6d0adea1ed80a3ca17c68c9c85502491da2904d9d690b4169ab9b025b21 src/cache/model/cold_metadata.rs
|
||||
29bfce96b2ffd102d9c848c7b717a45fedb9aa44511f62f7a1f28a53dafd063a src/cache/model/entry.rs
|
||||
@@ -278,21 +288,21 @@ ba94226addfd6cdab67d666dc15b8412c4d117111e44639e503e89a95175d0a2 src/cache/mode
|
||||
09a4417f82984eca8c040ea28eb82b3f129c347d951d4b4fab704bb194d9b985 src/cache/model/maintenance.rs
|
||||
5441fa3a384add28b6fdd15300cf2648f8237dbfc211cad9117754203be76911 src/cache/model/settings.rs
|
||||
55dcc0ad84d6b73689ba43e759d5685c406419c7733e23e78d5b072891ab17d7 src/cache/model/views.rs
|
||||
0c8b0d550e94d4474dcdf991aa0340228dc5f2f904f39350e6e666a16f16fe87 src/cache/model.rs
|
||||
e26dce02df9c043329ca164fed215d271781bce8fb03240f528331bf3df6b9d7 src/cache/policy.rs
|
||||
989df11e246123f760ef31689370f3500e0913fcc6b70d7ac0c2c95e2f412fb0 src/cache/policy/request.rs
|
||||
d426d341a7678f8ec70406d86052c97d0f49804bbcd8402951103d61a735b9da src/cache/policy/response.rs
|
||||
e26dce02df9c043329ca164fed215d271781bce8fb03240f528331bf3df6b9d7 src/cache/policy.rs
|
||||
7e2bcfbb2b57732cdc704d9df7a9298fb80e735e37984431bc936630e41e7f8a src/cache/range.rs
|
||||
7ad96b3baae36ec49c90d03b380e749944fb3d54abb045c90db3a35f6b89b1c1 src/cache/tests.rs
|
||||
981c33d8321c0d5482a00dc3cf0911e95fa69a29c68a2f5b6cd4cff27cec1cbd src/cache/tests/cold_tier_tests.rs
|
||||
22cb9bf20a0d25bee30c1e2213dfe8278ad7d0d5d294275d47d4a6ff9ac646ab src/cache/tests/dashboard_tests.rs
|
||||
d7a02eb7bad37e6a5524600aabb5e60ccf50e26c7a487e2806891c9d8acd48f3 src/cache/tests/http_invalidation_tests.rs
|
||||
1db70ea5f3cc1f1543504a68498ba23cafce5f9bb48c4616a4bf70d9a536790e src/cache/tests/scope_tests.rs
|
||||
9aa6bfdb88b5ea4ef0ecd72e8e3c6e82b5ab9b37051ad300b6a5dc12f8579ddd src/cache/tests/tests.rs
|
||||
444d9a364339248bdb86f2285de6d7e598b880502e25a70be0ee877ff3e52ea7 src/cache/tests/v022_cache_tests.rs
|
||||
7ad96b3baae36ec49c90d03b380e749944fb3d54abb045c90db3a35f6b89b1c1 src/cache/tests.rs
|
||||
a8a0ce446eb53ffdb8f8d97efa3e1344410dc3798b25f62b8817e8e160d28940 src/cache/tickets.rs
|
||||
e2f58279499b7c3b47c65d3a7814bb5c7cd6506544d939875fbb0f91fa9aa59d src/compression/tests.rs
|
||||
28f904cf4827294f2f759e6d3624608ed90059b64e42864e55dc9213824eb655 src/compression.rs
|
||||
e2f58279499b7c3b47c65d3a7814bb5c7cd6506544d939875fbb0f91fa9aa59d src/compression/tests.rs
|
||||
7bc8285cb2012506fd1a38a71dffa561f885f4ab65a9b2a7c8344319e3cd1cf6 src/config.rs
|
||||
34be288c858a037c7174f6c4dee23178b01b565aa9d65f0951f351e1685847df src/config/backend.rs
|
||||
beb831a208c4e5f7ebe8327686b1e0edc66608c7032f895cb4fc3366b189aae0 src/config/compile.rs
|
||||
490d05d84b2fe349ddde54eae44880993fab70811652484dd9ba38b5069d603b src/config/internal_routing.rs
|
||||
@@ -300,67 +310,66 @@ a19fc675720c7c2dbf4bba706e1b1c30c83c6df1da3f29e3cbb3b82442e7246a src/config/mod
|
||||
f66c2388f31b19142c12469df23d43451e0f7c48b158d4996920282c06d32076 src/config/routing.rs
|
||||
818545ad36af3ae8d56976f318349dd1f5264ef5665fbf94681a81b79003f8cb src/config/snapshot.rs
|
||||
76cbb4f1339de9f13d441561a3c4ee6bc5ff06e90ff0f019b7f0239c74f9542d src/config/tests.rs
|
||||
7bc8285cb2012506fd1a38a71dffa561f885f4ab65a9b2a7c8344319e3cd1cf6 src/config.rs
|
||||
c6db84438878accb8a0c96be13fa01a83da824af39c929dc3f6df498f393b7e0 src/db.rs
|
||||
30f0b7269224a64e3f1ec9e6fbe9e27ddb1d308dc84cc9882e50545cf612695d src/db/certificate_api.rs
|
||||
2ff744223934685439374572b83fd639bb3eabdbac168bea4a6501a6e9ce0ee0 src/db/history.rs
|
||||
c05a02618d476e4670b4f9930555f199cac776cdf09edb8fcc1a675695b8f99f src/db/tests.rs
|
||||
c6db84438878accb8a0c96be13fa01a83da824af39c929dc3f6df498f393b7e0 src/db.rs
|
||||
19ab6c3ae37009c544291cb0b65c1bb9c721e168f95c1ed3fa88155aca7463ad src/demo.rs
|
||||
e69031875ffec32c9f548c43bed5f1a5c4c626e9568c9173e3b093f9a8b73036 src/diagnostics.rs
|
||||
b45d6040810c93ae2acb04ef6b0a59c189751edf9008cd81a4521c9e04e7f07d src/docker.rs
|
||||
93a8a3f6ba12761ddcbd19f22f32fb2c0e6948b660fb774ddd7ebdba9b6562a5 src/docker/client.rs
|
||||
4c2c988191a97f81568a8df64af772463d010844f1abf327871901cdb32d4336 src/docker/model.rs
|
||||
5f4610c59b2d4ff6a205a2d9ae11bc84be689b2a68302459f546b3f43e53593c src/docker/reconcile.rs
|
||||
a12c47df4b90b6fc36c978a42477e6ddffbbbf69aed7252101c2fd829709bd6e src/docker/tests.rs
|
||||
b45d6040810c93ae2acb04ef6b0a59c189751edf9008cd81a4521c9e04e7f07d src/docker.rs
|
||||
057e652f216a1478638cbb53b1c64ac88c7f413a65825caacbf893c5af004a63 src/forwarding/tests.rs
|
||||
2a02ea2e010cde1b1fe99b2e6baa80ea344ea6b332dbb3c4be795b71541f64a7 src/forwarding.rs
|
||||
057e652f216a1478638cbb53b1c64ac88c7f413a65825caacbf893c5af004a63 src/forwarding/tests.rs
|
||||
af7a4e91c32ffbe4701c8c06e48ba3f8bbbe55e8b9e877aecb30ccdf858e9e8f src/geoip.rs
|
||||
80850f7acd1ed4c0048b065e9e81740869a8efbb0d9b5d2e83d8ec7fbc494703 src/geoip/mmdb.rs
|
||||
06aa056d789742cc11c8826c338d985e6a6fb22da6c3530937eec38e35478034 src/geoip/mmdb_tests.rs
|
||||
d3dc58628683472ef4d205fa5c42f0b7ef83cf90846146d96941cd3b36a40766 src/geoip/tests.rs
|
||||
70091bdf6f38f03b6df8c8c4407c300677e57f293130122503f936500963d37c src/geoip/update.rs
|
||||
c1b49bef28d87712e4c46138e70b52c4bcfb1a00196579031fac043d0b4aaa7b src/geoip/update_http.rs
|
||||
9a912dad0b8d1e10410fd95d0da3684656092ef85cf8b23eda4ffe7fb92921d8 src/geoip/update_tests.rs
|
||||
af7a4e91c32ffbe4701c8c06e48ba3f8bbbe55e8b9e877aecb30ccdf858e9e8f src/geoip.rs
|
||||
fbb6859780309f8cca1aeaba43fa67d13a1edf6f5a60c5c982d2067b71fb0846 src/headers/tests.rs
|
||||
13269be4ad8d3e4c91893753f654c3022e6f86d171c5d1ff21eda2d9185ec61f src/headers.rs
|
||||
fbb6859780309f8cca1aeaba43fa67d13a1edf6f5a60c5c982d2067b71fb0846 src/headers/tests.rs
|
||||
9e21edc67a14ef8a2ce3dda0ed18784d0fd0be305973e5d216058b7ac802c6cb src/http3.rs
|
||||
eb57117938a447d9a6c2ec1806ebd9fecc640a68c88950a8b35fa9bad14d0dc6 src/http3/body.rs
|
||||
ed25da264a2d25b40c4fcddc97c626a3a43d4f3a9be6c6d31af58b9e4925c198 src/http3/diagnostics.rs
|
||||
37f54961d8f52b79a75fb2be31e81f4fdb1e66b06350345c4483d6197b8a0bbc src/http3/identity.rs
|
||||
f2a8c2a98f4ccfb708f9d75a48ec7b351232be9b8a4364b7e63ba14d28eead7c src/http3/settings.rs
|
||||
a31db7eac12b578b1eb5a74fae98bbe36312da95b7d7173135dbc41fc454da21 src/http3/tests.rs
|
||||
9e21edc67a14ef8a2ce3dda0ed18784d0fd0be305973e5d216058b7ac802c6cb src/http3.rs
|
||||
68fb5ab6a889995a66ad12cb6acf88c5d04f0aca572e2ae15f1a9cd171a5f24b src/lib.rs
|
||||
2551189a3e5701371026c9abd2c5d44d37495f28fb67e220180421bfd315e3dd src/logging.rs
|
||||
3849a09a7d7402dc4d867b646cda153c574327ddd3c707578ddb4dc6f95b1563 src/logging/error_runtime.rs
|
||||
a16613114273c54b6d2a16a94fa8748550e91468585469642622bd2c86087baa src/logging/format.rs
|
||||
f5815d778cc1785723faed4abb15f9880f22b6524b5289fe1e7158905aac0986 src/logging/model.rs
|
||||
42fef80af50735cd566c6801dad86ba81be98cf4a8ec7d45c2ea26f53f556a22 src/logging/runtime.rs
|
||||
03c50bd14fecd421b0901a5f0af891dc277e35420e32068781e3a6d78c88c24b src/logging/tests.rs
|
||||
2551189a3e5701371026c9abd2c5d44d37495f28fb67e220180421bfd315e3dd src/logging.rs
|
||||
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
|
||||
fc8e99eac83c083d65101f6402aacbcc8392d66508c31566a50de3ebcb12e7da src/main/admin_runtime.rs
|
||||
42a9197475823f67c1ca762951ede5d7a2f10124bd6ed4d1a3365f4297be894c src/main/bootstrap.rs
|
||||
5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs
|
||||
0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs
|
||||
4dd7c1084a6214c0f2513ebf86fe8ef144470695557772bfe25979edd64491ab src/main/shutdown.rs
|
||||
0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs
|
||||
e5c2e9189777725f4afa1c3c052e4f75047ec3f2f486630d990f5d3c194bcc96 src/main/tasks.rs
|
||||
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
|
||||
201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs
|
||||
a54b5b34ea65f29723b89b81bf7968a238e8247b9c9760f3430d3504cb1017c9 src/metrics/vhost/tests.rs
|
||||
49b11df65b4fb0b5b727f9f867436584680b45e3f4fbd53f74bc0cab8b9a88ec src/metrics/vhost.rs
|
||||
49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs
|
||||
668840e7bd70590e2e13f59b6d5b9e43ca7c877f3d07c71907baea91b5bba6cb src/not_found/tests.rs
|
||||
201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs
|
||||
49b11df65b4fb0b5b727f9f867436584680b45e3f4fbd53f74bc0cab8b9a88ec src/metrics/vhost.rs
|
||||
a54b5b34ea65f29723b89b81bf7968a238e8247b9c9760f3430d3504cb1017c9 src/metrics/vhost/tests.rs
|
||||
a4702c95c7e5077c2ccff86e2e3216e1da6ff9ee4383ae199a0aff2f78db325f src/not_found.rs
|
||||
da40e8ece87245efc49a0c4e750526b1d19fd97565856bd5caf641fcc1dfe18d src/pages/tests.rs
|
||||
668840e7bd70590e2e13f59b6d5b9e43ca7c877f3d07c71907baea91b5bba6cb src/not_found/tests.rs
|
||||
d66936cc15fd5d56566d40fa8cab93f80d94b05f10ac44478a30f63572a95e32 src/pages.rs
|
||||
da40e8ece87245efc49a0c4e750526b1d19fd97565856bd5caf641fcc1dfe18d src/pages/tests.rs
|
||||
0dba7b6a33866cc7bbe05509b01961e545892645d1dad4eb88ac8a4bec65e3bf src/prometheus.rs
|
||||
cc6fb14351b009a16d07ed0f412c2bcc68cd7e551421aea03d8d97ceec9bee40 src/prometheus/core.rs
|
||||
c46882ed26742d93806a976257674326e105b8ca1fc23f5ca81e317b268057aa src/prometheus/docker_misc.rs
|
||||
d3c6b08088df0003144544936a55e2a3661aac85292821b594f00adfd19a2951 src/prometheus/model.rs
|
||||
064b2be7fed2f6acf526b1cce070c1bb1b943d36f08fa50bbfcbdf3cf1c9318c src/prometheus/tests.rs
|
||||
028e1dd9f2ec2afaf699a6b21c2d4b0d96e76f11f5e4ed009c233dadf0f3be51 src/prometheus/traffic.rs
|
||||
e03f0d6b3f35dae6b98c394837aa0d17e4d73a8306767b4c607868bc00c30286 src/prometheus/vhosts_backends.rs
|
||||
59095a04707c2e4714358f4462dcf21c41952474b9b815c42bbd14e96c573846 src/prometheus/web_php.rs
|
||||
0dba7b6a33866cc7bbe05509b01961e545892645d1dad4eb88ac8a4bec65e3bf src/prometheus.rs
|
||||
16b9d4db3b20b2ab14c47ac14fcd18d3d8ca77c3ad9a1f37866ebfa60f379862 src/proxy/body/tests.rs
|
||||
fb2d8d45cd2e947f783eb981b37fa418c39fcb327a3af17a648fadea7f37f888 src/prometheus/web_php.rs
|
||||
b5856fd38a957b2ca9c7e3606d84d13d5cfcf3a45b487b15ae1d2dc248cbda57 src/proxy/body.rs
|
||||
16b9d4db3b20b2ab14c47ac14fcd18d3d8ca77c3ad9a1f37866ebfa60f379862 src/proxy/body/tests.rs
|
||||
3d2c8462fd259c299ed26493cb2316d0b7349bd32fedc9b0c11e77b21d6faefc src/proxy/cache_helpers.rs
|
||||
aa49c9928bb91e3d5bae0d340d88396a0417b9edec6192d41e1403533319a897 src/proxy/cache_http_invalidation.rs
|
||||
771cc1a38927f96e593b144dbab66cd64a5dc0ed42d54b1615c359845b2b7223 src/proxy/error_fallback.rs
|
||||
@@ -373,119 +382,119 @@ ce782dce2f7fe65ef93d08f88eb8277162ad9b975c1391da9c45869511196621 src/proxy/forw
|
||||
b05c4f5c009c5e410f472ceaa999fa3a9c7fb60364d5b62de219f0924c9d524e src/proxy/forward/web_source.rs
|
||||
a9c011061585c067f04f7885215d53e6c40474bdced15d2be5a5ba65190910a0 src/proxy/forward_entry.rs
|
||||
9b25f5e3ea4443a63ee6f45d12e1957dd316fe8e85325fc00ea416faaab1025a src/proxy/h2_graceful.rs
|
||||
5cb7522ceb9a9f083a15596ac4898559295e38d1138c80099577211de8a868ec src/proxy/headers/tests.rs
|
||||
c438066616527a3108f2ad53e3698ae9b2a80961714394cdd0a450c92f99d992 src/proxy/headers.rs
|
||||
5cb7522ceb9a9f083a15596ac4898559295e38d1138c80099577211de8a868ec src/proxy/headers/tests.rs
|
||||
8d17b97d90fd6a749ba8d37a8d28c94e6d7302b6c86905f073e2dc595506961a src/proxy/mod.rs
|
||||
55133ceace2e5262d9c54fa41bea099a27b817d1e09c1cd0e2493fd9cee4da38 src/proxy/redirect_helpers.rs
|
||||
ad701033f005c9c8c4b160f9b9b97bcabc17e936d8ade273a111937c483a125a src/proxy/tests/authority_tests.rs
|
||||
0f2eacadf1447db8aa1c1a78005aa262e0f747096bfe7e0a0e41a2cd31a0d17f src/proxy/tests/h3_navigation_tests.rs
|
||||
d134b0eb617f82f9971b3f3461379ea37c57bd0079bf54748d0c434b2ef1b475 src/proxy/tests/http_invalidation_tests.rs
|
||||
591b47fc40cbcb36d7c71998933dc873340dfcf2657e16c561ab64361c4dbab6 src/proxy/types.rs
|
||||
9999f717492755d62fc05cd5c1aff802ff0da901fe3e6b928bd8a000833c8799 src/proxy/upstream_helpers.rs
|
||||
d7b4b809b12ee89293df45e9a64b1c9981fb3f89deaab3762f4a3ad3992dd7b1 src/proxy/web_cache_revalidation/tests.rs
|
||||
1b58f39045e82b1a290435502f5bf8a52c208c283bfb0d13618e0a7d150bb6b1 src/proxy/upstream_helpers.rs
|
||||
2d9142e3ae95895b5271b4009ab48236a3a2074ce51179c021193b35777560e4 src/proxy/web_cache_revalidation.rs
|
||||
d7b4b809b12ee89293df45e9a64b1c9981fb3f89deaab3762f4a3ad3992dd7b1 src/proxy/web_cache_revalidation/tests.rs
|
||||
69f775f6746b4c6d729dc10961dbef54168846f4ce9cf7fe453ab5a2b879c9b8 src/proxy/websocket.rs
|
||||
d866e7e72dee6ff54c2418c34e9899cbcd7e4e027d9cbe2b625044dc9062e4ee src/proxy/websocket/handshake.rs
|
||||
7e9c4d4c207badabf90630223964bb2ad641f06d4149843cab751f192b8b4adf src/proxy/websocket/tests.rs
|
||||
a44421714aab37f7ceda19e3ec2512c645d2a3ce5dcb3da5bc7a0d87a442d6a8 src/proxy/websocket/tunnel.rs
|
||||
15fabd38576b61f650c039acdbf7ac5a5bd24330062475b94315285b2dc258b6 src/proxy/websocket/validation.rs
|
||||
69f775f6746b4c6d729dc10961dbef54168846f4ce9cf7fe453ab5a2b879c9b8 src/proxy/websocket.rs
|
||||
e7e3a898e2c769b1ef11e58a13c1053104ada93dd694f8a03ea4b647c44c7d69 src/rate_limit/tests.rs
|
||||
545247eb00b0116efbd4cc913cc251195a55feded1530e0d8d8945abbae78039 src/rate_limit.rs
|
||||
811c83ae9fd0f5a13d35b5443cd37843f82b9d600bee4bb136ba9b87732a5347 src/redirect/tests.rs
|
||||
e7e3a898e2c769b1ef11e58a13c1053104ada93dd694f8a03ea4b647c44c7d69 src/rate_limit/tests.rs
|
||||
d40ad63939097d2b91339127d5835a74b618d83b7ba31db0c512f05a82107d6e src/redirect.rs
|
||||
811c83ae9fd0f5a13d35b5443cd37843f82b9d600bee4bb136ba9b87732a5347 src/redirect/tests.rs
|
||||
d0939e1b0829c25356a0b67ecb3a178c74936dd07baa15fee24eb32544db6cad src/runtime.rs
|
||||
8d0d7b01871c8c588c40a54330a1053c85024655fbe38516ca6c94c3c19a8956 src/runtime/certificates.rs
|
||||
8d2443df8dab1ce6f11878e991b4928046b7362c417c5703f6d1f3a977646cbe src/runtime/constructors.rs
|
||||
92b4b284f1b69dca58e4e3f76199f0aecc397eed26d10123b427af1bb8a4a57e src/runtime/docker.rs
|
||||
6b2b57888863a396461fff202bfdaa6d8e869b2628cc171c0823f31f388e5178 src/runtime/tests.rs
|
||||
4036b48925f5b25de70e21dfbb278488c20b39248dcda4cad4d19d5d7aed2b8d src/runtime/types.rs
|
||||
e1d172acd2b230b8f4dd671cae8862a76f28416cbf0e43f86214a796edd90fab src/runtime/updates.rs
|
||||
d0939e1b0829c25356a0b67ecb3a178c74936dd07baa15fee24eb32544db6cad src/runtime.rs
|
||||
6dca8da072a3bd610704a391bb6e25c9042971f4ebc60edb47665ce1d74cbc58 src/secret_store/tests.rs
|
||||
208a378b14ca80e1412ffa19ef1b140594182971e2e797ee0bb95bcdb8404da7 src/secret_store.rs
|
||||
0e5fc2aff856bf6fd959cba12b5ddd1e3b3cbeb0fbaadde906c811cdfafdaae7 src/security/tests.rs
|
||||
6dca8da072a3bd610704a391bb6e25c9042971f4ebc60edb47665ce1d74cbc58 src/secret_store/tests.rs
|
||||
6fa1d8c352bce7a16ff0208bc4cdbe80404715df0aff4ed26910a5df57cbbcd4 src/security.rs
|
||||
0e5fc2aff856bf6fd959cba12b5ddd1e3b3cbeb0fbaadde906c811cdfafdaae7 src/security/tests.rs
|
||||
53a4bed4d1233aa434712b946857ea2050acaaf5de011c6dadca5b5c22155a83 src/server.rs
|
||||
f2f6196632b2fdb2444189624f961edaf5bdfff57f700b0207b57e2491c1daa5 src/server/http.rs
|
||||
8c6ca5eaffb9acab57199d0349722703b903cfff3752baad2cf2596bd51188de src/server/listeners.rs
|
||||
e9e478cf45b88994eaffd115f51764b6b07852ee3c941cafdabb20584d3dabc0 src/server/serve/tests.rs
|
||||
71899a9b1897fa22ab9e8eb6a670054a6d382bad78714006b9b218d07ba99902 src/server/serve.rs
|
||||
e9e478cf45b88994eaffd115f51764b6b07852ee3c941cafdabb20584d3dabc0 src/server/serve/tests.rs
|
||||
048f2c7a8ef698a172ba7ed844558f4118b367b2cf340748c5c28da7723d8e85 src/server/tls_runtime.rs
|
||||
53a4bed4d1233aa434712b946857ea2050acaaf5de011c6dadca5b5c22155a83 src/server.rs
|
||||
73ec453ac2850f6963f5bc33401cb8c92aa26d5123f15d68e6633860d91f265e src/settings.rs
|
||||
240ecdd70e8662ff3b84c21830dc0cbbbacadd755d56146e54214bc2887f068c src/settings/public_origin.rs
|
||||
1f1121c3562c8f8f3fdee2113be2afffb8849be2d303e4c4409f87478b6e3307 src/settings/tests.rs
|
||||
73ec453ac2850f6963f5bc33401cb8c92aa26d5123f15d68e6633860d91f265e src/settings.rs
|
||||
9dbf814870b8636770f579c3d36d16f5b6afe8bc66adfad356c26d7ae7dbce45 src/stream.rs
|
||||
254840126be2310426f6332f5efbe98147d9d8de1999731f40a3d13325c1dc6c src/stream/health.rs
|
||||
bdc56be7dae85a97a4d5b8d46ef31ec4177292b1cadd507fe423b6d4ee1403bd src/stream/model.rs
|
||||
de138a4ac68bcddba3a4f17ceba008bab643359d76a68726a755069f19334bed src/stream/pool.rs
|
||||
6ec42b8647c95a0c2025462e0be29c71481ce1c3b1f4d4193926471f1cf06be0 src/stream/runtime.rs
|
||||
9346cbd71e006e41a7188f28eb86f9b00bac822e4cb31c76c8d6d2ec5d4c4711 src/stream/tests.rs
|
||||
9dbf814870b8636770f579c3d36d16f5b6afe8bc66adfad356c26d7ae7dbce45 src/stream.rs
|
||||
d9123c623e2a97d09e41edaa29993b4b2c8cba39a3abdf2b48a205aa9a9c2939 src/system_stats/linux/tests.rs
|
||||
3070d78a7b88c2e47ed59960fa35e7d220a4cd7d78e48f6c6d2bcc5ff77f9620 src/system_stats/linux.rs
|
||||
d9123c623e2a97d09e41edaa29993b4b2c8cba39a3abdf2b48a205aa9a9c2939 src/system_stats/linux/tests.rs
|
||||
751d59582fb92c84e0c0cc7c0855c9d0a545805c9ba3be61826e50fb96c297ed src/system_stats/mod.rs
|
||||
050aa831b1012c4aaade63863f433f01bfacf5d3f95787e57975572116a30ab2 src/system_stats/tests.rs
|
||||
0e1d87554db55e724f30c41ec4a5c80bdb8fc9c8cba8700ac509864d4abf8871 src/tinyauth/tests.rs
|
||||
7467d83066bb6986d8c9103597f7a7617ac677579ed46d573c573ff6ca57bbd9 src/tinyauth.rs
|
||||
0e1d87554db55e724f30c41ec4a5c80bdb8fc9c8cba8700ac509864d4abf8871 src/tinyauth/tests.rs
|
||||
1084cccd9de28acd6cb0f0813de7c0953910f51726f73f4acdb2e3ada4593976 src/tls.rs
|
||||
4972a436f8878dec5f9eb8ff17eb82670b0bfd9e94e821c516575479f5fc1919 src/tls/certificates.rs
|
||||
436575144aa352498a4696f12c2258bef3a898f3f50e6fbc6d67c3b93afa099f src/tls/external_paths.rs
|
||||
3e6947dde167f82a5efc407ccf9bd4c9f5b26cc8633d1418a10284bc1a1503d4 src/tls/server_config.rs
|
||||
31c0f51a3a0bab433f438c3ba1b1cdaf3c63fae6086e7d99b3464a537f75a5e1 src/tls/tests.rs
|
||||
2af2164ecdf2b7fd9eef7c6e358dec053dfac9aedaa501dbb75df742d18638b2 src/tls/tests/acme.rs
|
||||
93b359e9defd62506729bc90e56c96783e2f14dd477dd67f337ca83149da7168 src/tls/tests/external_paths.rs
|
||||
31c0f51a3a0bab433f438c3ba1b1cdaf3c63fae6086e7d99b3464a537f75a5e1 src/tls/tests.rs
|
||||
10c4a187e3aeb0c72a8c8968ea0cc8bfae4e3cb2f3d834ebd3da1056c4171ec5 src/tls/types.rs
|
||||
1084cccd9de28acd6cb0f0813de7c0953910f51726f73f4acdb2e3ada4593976 src/tls.rs
|
||||
36301bcbd20b557dd3d58e77a9175f3581947f577c0bb0b972899cec6786b03f src/traffic/tests.rs
|
||||
c2f802d95da4332fb8480e83f0c7502334457b3408141c1c4516c9ba320a0463 src/traffic.rs
|
||||
36301bcbd20b557dd3d58e77a9175f3581947f577c0bb0b972899cec6786b03f src/traffic/tests.rs
|
||||
d88c92f5a2a7773bc3e715d24039db35da4df53881f1a0859eec819b96f06d3b src/transform/apply.rs
|
||||
87f8af5261d79072f8215f1fb8a90715a58e7626544fd8176ce840e3111bdb92 src/transform/compile.rs
|
||||
b2c5236589093454ec60984b785ad6a4af6149d47f6db37d8e08c17fec7a2c1b src/transform/css.rs
|
||||
aa99b0d3029ef92c78371cb90e11f2026cce890d1be8e10ebd7fa9bc5cb13287 src/transform/html/tests.rs
|
||||
5478670cef87777b14a2dab308f5a9182df56d7ea2f5625bdc94aa7e3d661503 src/transform/html.rs
|
||||
cf5195ab6bacccae0082da7b3f1bd0382b73fdeb3607f4ae569ca6daab9d10d0 src/transform/js/tests.rs
|
||||
aa99b0d3029ef92c78371cb90e11f2026cce890d1be8e10ebd7fa9bc5cb13287 src/transform/html/tests.rs
|
||||
a1d2fec07617c8a8dc425bfaeaf77c844152a0a174568a88a5baf4c317cc6c5c src/transform/js.rs
|
||||
216e24e29ea87be4a124f95f898e155f127fd959d7d5d9b30f8cd0b2b5018f30 src/transform/json/tests.rs
|
||||
cf5195ab6bacccae0082da7b3f1bd0382b73fdeb3607f4ae569ca6daab9d10d0 src/transform/js/tests.rs
|
||||
226ae561dc7b0243c04e55e28cdfd2b76acd48cf614d1919b81c196660b03cfb src/transform/json.rs
|
||||
216e24e29ea87be4a124f95f898e155f127fd959d7d5d9b30f8cd0b2b5018f30 src/transform/json/tests.rs
|
||||
6a8c28be93fb590942493c075b29ace1527981542bd183f232aa82b62e951eca src/transform/mod.rs
|
||||
fde2fa3aa94b30d8536afec1c11348cfbd922d0b5ff89e91d34266e8e3863e26 src/transform/model.rs
|
||||
0f53c64f442390ee20a59087a10f8f9ffb58053263e9aa848ddabfae1a6f3f51 src/transform/svg/tests.rs
|
||||
a4e1d2aa2913587c250f251356431097b76995aee6e8bf9e35c4978a95491de1 src/transform/svg.rs
|
||||
0f53c64f442390ee20a59087a10f8f9ffb58053263e9aa848ddabfae1a6f3f51 src/transform/svg/tests.rs
|
||||
8ed4baeaf79edfa2a84912cc00f1d13c564e1b997efe6888d2e4b5a16aaa8905 src/transform/tests.rs
|
||||
c42d000d1d576721af3ffe930589388693001b8113554a7658f6dcbc665b5d1b src/update_check/tests.rs
|
||||
6146c59f36cf419623747d497b3de685da6c84964df43317bebdcb21deb9ee09 src/update_check.rs
|
||||
c42d000d1d576721af3ffe930589388693001b8113554a7658f6dcbc665b5d1b src/update_check/tests.rs
|
||||
92efcdaafb9f5a8514daf63d7fcde29be977d7bdcd90162eda0ce00d6967ffbd src/upstream.rs
|
||||
ca695e246bee5389b3757fe8588e48bff5d0ad473bc8588ad4ba7b52c99d7dfa src/upstream/client.rs
|
||||
ad29f3fc15c9b627b5d752af5adc9f99ee1364fec2df63f1e4509cb223988efb src/upstream/client_tests.rs
|
||||
75fef0a488d3f3dabfa03c2eabc5995442dfb3c9a1fde8eb6653a124dda25d43 src/upstream/protocol_tests.rs
|
||||
8a112154b9631f62a54584302a3ff6a64e4f177ce79aba678b4cb23e466c6ea3 src/upstream/tests.rs
|
||||
92efcdaafb9f5a8514daf63d7fcde29be977d7bdcd90162eda0ce00d6967ffbd src/upstream.rs
|
||||
6d7cb73de7d8c6f3780736480f5f81ef08813f4626fd5ed494aca0552cf214a1 src/user_agent/tests.rs
|
||||
a6b4905c245a22eafb1aa535d303d0fa24e4353db94e1d67fd42b825bf8c5adb src/user_agent.rs
|
||||
6d7cb73de7d8c6f3780736480f5f81ef08813f4626fd5ed494aca0552cf214a1 src/user_agent/tests.rs
|
||||
3aa1b07cd26d4a5b9e5e94d1d6a712ef1397d827e7f1e8c01f7195b98d2c5f67 src/vhost.rs
|
||||
6678192de56bc10c5cf82bdff8da14eece7685f409592450c07478333aa3b99c src/vhost/error_fallback.rs
|
||||
63e9fc7f7be78dc3e16a89f015a56a2da6ba3aff0197b70ff48321dfb17529bb src/vhost/model.rs
|
||||
35a0aca3eee8d47d1bab27c83d4532cd572fc3e17b5f0d4a8d4515a8f85b3d96 src/vhost/redirect.rs
|
||||
9b16b5307e140db97363f56c9fac8319e56df846bb1c919d3bc06091ab6ce69c src/vhost/tests.rs
|
||||
b7d0f7f5d0bbd74add3fda9b1308350c4f70f8b6debf979fb977a4f2f659c32f src/vhost/validation.rs
|
||||
3aa1b07cd26d4a5b9e5e94d1d6a712ef1397d827e7f1e8c01f7195b98d2c5f67 src/vhost.rs
|
||||
1fae9d57dfee80edcc3073438353254db5b6afdae1d9c1d6173cc6de5f66b2e6 src/web_server/config/compile.rs
|
||||
7853f6ec127a91163a4418ca45b93a92f424d5c618f27b29f9d526598a26f7ef src/web_server/config/model.rs
|
||||
42a613234af7015a0b14f125bb88be1dcc30a47e9064b12e3fbb5b71ce6f7caf src/web_server/config/validation.rs
|
||||
8d1eff4b24f01d137414ef5419f75dd281879228adb70409645b11b3c78c20e9 src/web_server/config.rs
|
||||
7608f18d9a093a7cdd7019bd9a6cc2e4cf29bc941ab760ecaa86ab095958c497 src/web_server/fastcgi/protocol.rs
|
||||
1e10cabc08e1f71bcd281fd01c3fef28c0f2b8b294e4fc4adf5b19f3235a2368 src/web_server/fastcgi/response.rs
|
||||
aa38e89e84f928bd1952d3e2cb67e48ef2e92c5f444c095b6288034adc5e3d3d src/web_server/config/compile.rs
|
||||
7853f6ec127a91163a4418ca45b93a92f424d5c618f27b29f9d526598a26f7ef src/web_server/config/model.rs
|
||||
d9178aa934ee1a7236cb04d864fa842faf64156e9103786e3103c92bc4e18fde src/web_server/config/validation.rs
|
||||
86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs
|
||||
200cba8cb3482e49db73d2c60cb60786175177c7d4be2a566cd281fec7ba7fca src/web_server/fastcgi/protocol.rs
|
||||
cef44ccdf882d84892a64bdbfdc1ba01380fce5e5f86d2829b0f9613f738b964 src/web_server/fastcgi/response.rs
|
||||
8186363315bd998919c6c783e6f7501528f257a5c0da77c5d60f423e8670db5e src/web_server/filesystem.rs
|
||||
0aafd7f37c4fe784b047646e4ce5ddcb5fe821231a41ed0a9f519529385288a8 src/web_server/htaccess.rs
|
||||
013830a79a78ce803ce776a9b333de406367751e0cea7e33c3f6a98e6600c243 src/web_server/htaccess/model.rs
|
||||
76de1d11df75f4a64556b2821fd0a1774d26c9f0ca0e5f2b5fcfad0722d43d47 src/web_server/htaccess/parse.rs
|
||||
66260d159986d41eab0dfde2cb9f0713fc1cc209c2fb47e9023bd24fd37919e8 src/web_server/htaccess/rewrite.rs
|
||||
0aafd7f37c4fe784b047646e4ce5ddcb5fe821231a41ed0a9f519529385288a8 src/web_server/htaccess.rs
|
||||
ddc329309a3c75c095aede58495b7f698929237e1d08467921a971577ca8405d src/web_server/metrics.rs
|
||||
93988a6e2f6c8a84ee03ddee8c474d516e6be6404ad14d4f23a35bdb57a9cf67 src/web_server/mod.rs
|
||||
3341264f6e8041ac5a5d3c4b1601a69787386116019b5febc1a5e22bdb1186f9 src/web_server/metrics.rs
|
||||
7b945065e636ca74147048aa53cc5b34028cced72a780fbaf0aa2499b00718fa src/web_server/mod.rs
|
||||
67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs
|
||||
87a0dd815d92688bbc173ca1b2a28487c4dc580cf04318273ac1d460791f6788 src/web_server/response/file.rs
|
||||
20509f9302b79d3a7be76dda6edaf5711826d3ddf47d19ebe07cb289427ca328 src/web_server/response/headers.rs
|
||||
c5cb561b0f276e8082f904b9446fa5885f6b3c0608f25ea2ef425061e9f7f6b4 src/web_server/response.rs
|
||||
cb461fb68c2740270edb9030a63e9d8c70c3ab241ba20d92f1a6e5b83915996e src/web_server/response/file.rs
|
||||
20509f9302b79d3a7be76dda6edaf5711826d3ddf47d19ebe07cb289427ca328 src/web_server/response/headers.rs
|
||||
9b58d6bb3ff1520060fc5aa86a3594f99192b1a8f8183255be1fb3ff35b4bb35 src/web_server/routing.rs
|
||||
400e1c8aa2fe857d96036a64499fbc3de44c1e3a53811c5b25101fb843359792 src/web_server/security.rs
|
||||
518856a221161430cb0ab3e411fa81cab07e1a6e52edd470f026d25c5307943e src/web_server/serve.rs
|
||||
2283548f7a6a1f5e0e4025f498f14f3402b4f741ae4a9ac61c60e3debf5ca39a src/web_server/tests.rs
|
||||
a082e1d32aeff7b64976c943251b0c6487409f191883bc822ac4a60bb4d33e7d src/web_server/serve.rs
|
||||
6b3ade894f02fb68f3507e1ecc578f173050d97100d41261467faeb9fd10c31b src/web_server/tests.rs
|
||||
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
|
||||
1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs
|
||||
347a4ff9e028a1e999ad129590c355b5734cf928dd81e5099aea0c8aca8d5908 tests/http3.rs
|
||||
|
||||
@@ -56,6 +56,57 @@ Only `GET` on the exact configured path without a query string is accepted. A
|
||||
wrong path is `404`, wrong method `405`, disallowed peer `403`, invalid/missing
|
||||
required bearer `401`, and exporter concurrency exhaustion `429`.
|
||||
|
||||
## Web Server and PHP-FPM metrics (0.33.0)
|
||||
|
||||
Prometheus already exports core Web Server/PHP response counters by configured
|
||||
`web_service` ID. The 0.33.0 additions below are enabled with the existing
|
||||
Prometheus exporter (no new listener, credentials, options or open routes).
|
||||
|
||||
| Family | Type | Labels | Meaning |
|
||||
|---|---|---|---|
|
||||
| `rust_proxy_web_server_requests_total` | counter | `web_service` | Header-ready WWW responses, including PHP |
|
||||
| `rust_proxy_web_server_responses_total` | counter | `web_service`, `class` | HTTP 2xx/3xx/4xx/5xx |
|
||||
| `rust_proxy_web_server_response_header_duration_microseconds_total` | counter | `web_service` | Cumulative time until headers |
|
||||
| `rust_proxy_web_server_requests_active` | gauge | `web_service` | Requests still awaiting headers |
|
||||
| `rust_proxy_web_server_request_kind_total` | counter | `web_service`, `kind` | `static`, `php`, `other` handler |
|
||||
| `rust_proxy_web_server_methods_total` | counter | `web_service`, `method` | `GET`, `POST`, `PUT`, `DELETE`, `other` |
|
||||
| `rust_proxy_web_server_status_code_total` | counter | `web_service`, `status` | Selected 200/206/304/403/404/500/502/503 |
|
||||
| `rust_proxy_web_server_response_body_bytes_total` | counter | `web_service` | Body bytes delivered before final compression |
|
||||
| `rust_proxy_web_server_compression_total` | counter | `web_service`, `encoding` | Downstream gzip/br/zstd/deflate operations |
|
||||
| `rust_proxy_php_requests_total` | counter | `web_service` | PHP response headers processed |
|
||||
| `rust_proxy_php_responses_total` | counter | `web_service`, `class` | PHP response status classes |
|
||||
| `rust_proxy_php_response_header_duration_microseconds_total` | counter | `web_service` | Cumulative PHP time until headers |
|
||||
| `rust_proxy_php_response_body_bytes_total` | counter | `web_service` | PHP body bytes emitted before compression |
|
||||
| `rust_proxy_php_concurrency_available` | gauge | `web_service` | Free RustProxy PHP request permits |
|
||||
| `rust_proxy_php_concurrency_limit` | gauge | `web_service` | Configured concurrent PHP request limit |
|
||||
| `rust_proxy_php_concurrency_in_use` | gauge | `web_service` | Permits currently held by PHP requests |
|
||||
| `rust_proxy_php_failures_total` | counter | `web_service`, `reason` | Fixed FastCGI/PHP failure classes |
|
||||
|
||||
PHP `reason` is one of `capacity`, `connect_timeout`, `connect_error`,
|
||||
`request_timeout`, `request_io`, `response_timeout`, `response_io`,
|
||||
`invalid_response`, `body_io`. PHP concurrency is the admission semaphore in
|
||||
RustProxy, not external PHP worker health or PHP-FPM child count. Status-code
|
||||
labels are deliberately restricted to a stable, useful subset; arbitrary code,
|
||||
request paths, IP addresses and untrusted method strings are not labels.
|
||||
The exported byte counters are measured **before the final dynamic response
|
||||
encoder**, so treat them as payload sizes rather than measured network transfer.
|
||||
Precompressed static file delivery is not counted in the downstream compression
|
||||
family. All time-to-headers metrics exclude streamed transfer duration.
|
||||
|
||||
Useful Grafana/PromQL examples:
|
||||
|
||||
```promql
|
||||
sum(rate(rust_proxy_web_server_requests_total[5m]))
|
||||
sum by (class) (rate(rust_proxy_web_server_responses_total[5m]))
|
||||
sum by (kind) (rate(rust_proxy_web_server_request_kind_total[5m]))
|
||||
100 * sum(rust_proxy_php_concurrency_in_use) / clamp_min(sum(rust_proxy_php_concurrency_limit), 1)
|
||||
1e-3 * sum(rate(rust_proxy_web_server_response_header_duration_microseconds_total[5m])) / clamp_min(sum(rate(rust_proxy_web_server_requests_total[5m])), 1e-9)
|
||||
sum by (reason) (rate(rust_proxy_php_failures_total[5m]))
|
||||
```
|
||||
|
||||
Import `grafana/rustproxy-overview.json` and use the **Web service** variable
|
||||
for the new Web Server, PHP-FPM, per-service and correlation sections.
|
||||
|
||||
## Coverage audit in 0.26.0
|
||||
|
||||
The exporter represents all scalar fields currently sampled by `diagnostics::View` when available. The per-interface `network_interfaces` list added in 0.18.6 is intentionally dashboard-only so kernel-created interface names cannot create an uncontrolled Prometheus label set. RustProxy also exports the bounded operational data already available from traffic metrics, cache, vhosts, backend pools, streams, logging, GeoIP and ACME renewal observations.
|
||||
|
||||
@@ -19,7 +19,6 @@ Current direction: 0.24.10 -> 0.25.0
|
||||
- [x] Add unit/source-contract coverage, OpenAPI/UI language coverage and Web Server documentation.
|
||||
- [x] Clean 0.25 README/Docs/CHANGELOG release material so current user-facing changes are separated from historical audits.
|
||||
- [x] Add an authenticated `/docs` Markdown Reader with a separate API/OpenAPI tab, canonical docs index, 0.25 specification audit and open-work TODO.
|
||||
- [ ] Run compiler-enabled `./dev.sh fmt`, `./dev.sh check`, `./dev.sh test` plus live H1/H2/H3/static-file acceptance on the release host.
|
||||
|
||||
## 0.24.10
|
||||
|
||||
@@ -29,7 +28,6 @@ Current direction: 0.24.9 -> 0.24.10
|
||||
- [x] Remove the redundant Error File help sentence while retaining JSON/RAW controls and live apply.
|
||||
- [x] Keep response-time history permanently expanded and add an accessible information popover matching Process.
|
||||
- [x] Extend UI/API/source regressions and update release identity to 0.24.10.
|
||||
- [ ] Run compiler-enabled `./dev.sh fmt`, `./dev.sh check`, `./dev.sh test` and browser acceptance on the release host.
|
||||
|
||||
## 0.24.9
|
||||
|
||||
@@ -39,7 +37,6 @@ Current direction: 0.24.8 -> 0.24.9
|
||||
- [x] Add password-encrypted full backup/restore with manifest validation, integrity checks, staging and rollback-safe activation.
|
||||
- [x] Add the optional bounded asynchronous ERROR file sink with JSON/RAW formats, shared secret redaction and external logrotate integration.
|
||||
- [x] Extend offline source/release contracts and update release identity to 0.24.9.
|
||||
- [ ] Run compiler-enabled `./dev.sh fmt`, `./dev.sh check`, `./dev.sh test` and end-to-end backup/restore/error-log acceptance on the release host.
|
||||
|
||||
## 0.24.8
|
||||
|
||||
@@ -63,7 +60,6 @@ Current direction: 0.24.6 -> 0.24.7
|
||||
- [x] Clean externally reported cache/CSS compiler warnings without lint suppression.
|
||||
- [x] Rotate cache compatibility identity for global transform representation/eligibility changes but not resource-only capacity metadata.
|
||||
- [x] Extend OpenAPI, SQLite persistence, memory-contract and release/source regressions and update release identity to 0.24.7.
|
||||
- [ ] Run compiler-enabled `./dev.sh fmt`, `./dev.sh check`, `./dev.sh test` and sustained RSS/heap acceptance for every resource preset on the release host.
|
||||
|
||||
## 0.24.6
|
||||
|
||||
@@ -75,7 +71,6 @@ Current direction: 0.24.5 -> 0.24.6
|
||||
- [x] Bound stale revalidation, proactive refresh scheduling, COLD cleanup selection/concurrency and delete fallback tasks.
|
||||
- [x] Audit the remaining long-lived/background workers for queue/task cardinality and cancellation ownership.
|
||||
- [x] Add Rust-independent memory contract regression checks and update release identity to 0.24.6.
|
||||
- [ ] Run compiler-enabled `./dev.sh fmt`, `./dev.sh check`, `./dev.sh test` and sustained RSS/heap acceptance on the release host.
|
||||
|
||||
## 0.24.5
|
||||
|
||||
@@ -89,7 +84,6 @@ Current direction: 0.24.4 -> 0.24.5
|
||||
- [x] Make global cache disable authoritative over vhost overrides and remove the artificial 1 GiB HOT/object ceiling.
|
||||
- [x] Add host/cgroup-aware conservative/recommended/performance cache presets and non-blocking memory/headroom warnings in Settings.
|
||||
- [x] Update source/package/UI/OpenAPI identity and offline release checks to 0.24.5.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, the H1/H2/H3 method matrix, and cache RSS/HOT/COLD stress acceptance on the release host.
|
||||
|
||||
## 0.24.4
|
||||
|
||||
@@ -98,7 +92,6 @@ Current direction: 0.24.3 -> 0.24.4
|
||||
- [x] Make the Linux THP mitigation process-local with `PR_SET_THP_DISABLE` before Tokio worker creation.
|
||||
- [x] Preserve the existing glibc arena limit and allocator-trim behavior.
|
||||
- [x] Add source/release regression coverage for THP opt-out ordering and release identity.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and a sustained production RSS comparison with host THP returned to its normal policy.
|
||||
|
||||
## 0.24.3
|
||||
|
||||
@@ -110,7 +103,6 @@ Current direction: 0.24.2 -> 0.24.3
|
||||
- [x] Rename Proxy Process to Process and hide unsupported platform diagnostics dynamically.
|
||||
- [x] Remove the stale unreferenced per-BSD parser source from the release archive.
|
||||
- [x] Update source/package/UI/OpenAPI identity and Rust-independent release gates to 0.24.3.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and sustained cache/transform RSS acceptance on the release host.
|
||||
|
||||
## 0.24.2
|
||||
|
||||
@@ -132,7 +124,6 @@ Current direction: 0.24.0 -> 0.24.1
|
||||
- [x] Remove artificial hostname-count ceilings from certificate bindings/issuance and hostname-list configuration while retaining syntax, payload and resource-safety bounds.
|
||||
- [x] Update source/package/UI/OpenAPI identity and source-side regression contracts to 0.24.1.
|
||||
- [ ] Generate and commit a resolver-produced `Cargo.lock`, then run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on the release host.
|
||||
- [ ] Run native FreeBSD/OpenBSD/NetBSD diagnostics acceptance and final H2/H3/WebSocket/proxy-statistics regression.
|
||||
|
||||
The 0.24.1-era notes are summarized in the retained [technical changelog archive](CHANGELOG_TECHNICAL_ARCHIVE.md).
|
||||
|
||||
@@ -159,7 +150,6 @@ Current direction: 0.23.2 -> 0.23.3
|
||||
- [x] Tolerate old `101` response framing while keeping the WebSocket accept proof and subprotocol validation strict.
|
||||
- [x] Add dashboard/OpenAPI/language-pack controls and source regressions.
|
||||
- [x] Update package/source identity and Rust-independent release gates to 0.23.3.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and live iLO4/legacy WebSocket acceptance on the release host.
|
||||
|
||||
The 0.23.3-era notes are summarized in the retained [technical changelog archive](CHANGELOG_TECHNICAL_ARCHIVE.md).
|
||||
|
||||
@@ -172,7 +162,6 @@ Current direction: 0.23.1 -> 0.23.2
|
||||
- [x] Fix local CLI administration transport detection so HTTPS administration no longer traps `--livelogs` in a 308 reconnect loop.
|
||||
- [x] Pin local CLI HTTPS to certificates from the private RustProxy certificate directory and reuse the transport for config/TLS CLI calls.
|
||||
- [x] Update package/source identity and Rust-independent release gates to 0.23.2.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and live HTTPS `--livelogs` acceptance on the release host.
|
||||
|
||||
The 0.23.2-era notes are summarized in the retained [technical changelog archive](CHANGELOG_TECHNICAL_ARCHIVE.md).
|
||||
|
||||
@@ -185,7 +174,6 @@ Current direction: 0.23.0 -> 0.23.1
|
||||
- [x] Plot selected-scope Transform applied/cache-HIT/skipped/error rates from the existing runtime metrics endpoint.
|
||||
- [x] Reuse the dashboard SVG/chart styling, live 3-second refresh and language-pack model.
|
||||
- [x] Update package/source identity and Rust-independent release gates to 0.23.1.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and browser acceptance with sustained Cache/Transform traffic on the release host.
|
||||
|
||||
The 0.23.1-era notes are summarized in the retained [technical changelog archive](CHANGELOG_TECHNICAL_ARCHIVE.md).
|
||||
|
||||
@@ -198,8 +186,6 @@ Current direction: 0.22.3 -> 0.23.0
|
||||
- [x] Add Settings → Certificate API UX and preserve direct production binary commands for local deploy/reload.
|
||||
- [x] Add `rust-proxy --livelogs` with a bounded live stream, interactive structured filters, detailed request inspection and explicit lag reporting.
|
||||
- [x] Split Settings → Cache into General, Resilience, Cold cache and Static & diagnostics sub-tabs without changing the atomic cache settings document.
|
||||
- [ ] Run compiler-enabled `rust-proxy`/Cargo check, tests and release build plus live token-auth certificate rotation and sustained high-rate live-log acceptance on the release host.
|
||||
- [ ] Upstream mTLS/client certificate remains deliberately deferred.
|
||||
|
||||
The 0.23.0-era notes are summarized in the retained [technical changelog archive](CHANGELOG_TECHNICAL_ARCHIVE.md).
|
||||
|
||||
@@ -213,8 +199,6 @@ Current direction: 0.22.2 -> 0.22.3
|
||||
- [x] Add dedicated certificate hot reload without mutating desired configuration or revision.
|
||||
- [x] Add external certificate deployment for Certbot/manual issuers with immutable generation staging and atomic multi-vhost switch by certificate id.
|
||||
- [x] Add API/CLI/panel support plus audit/OpenAPI/documentation for certificate reload/deploy.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test`, release build and live H1 -> h2c Upgrade -> H2 plus TLS/H2/H3 certificate-rotation acceptance on the release host.
|
||||
- [ ] Upstream mTLS/client certificate remains deliberately deferred.
|
||||
|
||||
The 0.22.3-era notes are summarized in the retained [technical changelog archive](CHANGELOG_TECHNICAL_ARCHIVE.md).
|
||||
|
||||
@@ -228,8 +212,6 @@ Current direction: 0.22.1 -> 0.22.2
|
||||
- [x] Add detailed upstream failure classification plus last failure and health ejection/recovery timestamps.
|
||||
- [x] Consolidate backend operations data in the existing dashboard table: health, active/limit, requests/errors, latency, traffic, last failure and last recovery.
|
||||
- [x] Parse and expose public-certificate issuer/SAN/validity/remaining-days/expiry status plus renewal status/last attempt/error.
|
||||
- [ ] Add a distinct maximum TCP connection/socket limit per backend; request concurrency is implemented but is intentionally not presented as a socket limit.
|
||||
- [ ] Run compiler-enabled `./dev.sh check` / `./dev.sh test` and live H1/H2/H3 plus H2 -> proxy -> H2 acceptance on the release host.
|
||||
|
||||
The 0.22.2-era notes are summarized in the retained [technical changelog archive](CHANGELOG_TECHNICAL_ARCHIVE.md).
|
||||
|
||||
@@ -245,7 +227,6 @@ Current direction: 0.22.0 -> 0.22.1
|
||||
- [x] Split WebSocket tunnel statistics by H1 / H2 RFC 8441 / H3 RFC 9220 on the main dashboard and Prometheus.
|
||||
- [x] Preserve the 0.22.0 cache feature set and `Accept-Ranges` policy.
|
||||
- [x] Update 0.22.1 source/package identity and Rust-independent release gates.
|
||||
- [ ] Run the compiler-enabled `./dev.sh check` / `./dev.sh test` gate plus live H1/H2/H3/h2c/RFC8441 acceptance on the release host.
|
||||
|
||||
The 0.22.1-era notes are summarized in the retained [technical changelog archive](CHANGELOG_TECHNICAL_ARCHIVE.md).
|
||||
|
||||
@@ -260,7 +241,6 @@ Current direction: 0.20.0 -> 0.21.0
|
||||
- [x] Make Passive Backend Health explicit opt-in for ordinary and Docker vhosts while preserving independent active health checks and unconditional observability.
|
||||
- [x] Keep route cache override tri-state and expose effective route/cache/transform diagnostics.
|
||||
- [x] Close 0.21.0 with the current conservative JS/CSS transformers; defer parser-backed replacements to a later development cycle.
|
||||
- [ ] Release-host gate: run Rust 1.88+ fmt/check/test/clippy/release-build and live H1/H2/H3/cache/compression/restart acceptance before deployment.
|
||||
- [x] Finish release polish: backend timing header rename, human-readable uptime, transformed Live traffic series, Docker Passive Backend Health layout fix, warning cleanup and cache module split.
|
||||
- [x] Keep package/source identity and OpenAPI at 0.21.0 and package the completed feature scope as the 0.21.0 release source.
|
||||
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
# RustProxy 0.32.8 - verification report
|
||||
|
||||
## Scope
|
||||
|
||||
UI-only Overview header/editor button alignment, deletion of the complete
|
||||
`tools/checks/` source directory, retained source-upgrade preflight and the
|
||||
0.32.8 version bump. No proxy request path, backend behavior, preferences
|
||||
schema or dashboard JavaScript event handlers were changed.
|
||||
|
||||
## Checks performed
|
||||
|
||||
- **PASS** - Chromium headless checked the actual Overview HTML and CSS at
|
||||
viewport widths 320, 360, 375, 390, 430, 480, 600, 700, 768, 1024 and
|
||||
1440 px, with Normal and Compact button styles and four visibility/editing
|
||||
scenarios: 88 combinations. Title stays left, available actions align right;
|
||||
no overlapping buttons or page/header horizontal overflow was observed.
|
||||
This is a layout fixture, not a running API/backend acceptance test.
|
||||
- **PASS** - All Overview and editor controls use the original semantic
|
||||
`quiet`, `outline` and `primary` classes. No CSS class names were added.
|
||||
Removed obsolete dedicated button selectors. The Overview/core/refresh/layout
|
||||
JavaScript files are byte-identical to 0.32.7, so existing event handling,
|
||||
save/cancel and editor locking implementations were not changed.
|
||||
- **PASS** - Before removing the old QA directory, the 0.32.7 UI simulation
|
||||
suites `dashboard_layout_editor_0323_ui_test.mjs` and
|
||||
`dashboard_inline_layout_0323_ui_test.mjs` passed. The original
|
||||
`appearance_density_buttons_0327.py` source check also passed. These tests
|
||||
exercised simulated UI behavior, not a live RustProxy server.
|
||||
- **PASS** - `python3 scripts/preflight.py`, `./dev.sh doctor`,
|
||||
`./dev.sh repair`, shell syntax of `dev.sh` and the dependency-resolution
|
||||
script, all bundled language JSON files, version consistency and source
|
||||
class/handler comparisons.
|
||||
- **PASS** - Source-upgrade rehearsal from the supplied 0.32.7 release tree
|
||||
to 0.32.8 removed `tools/checks/` from the installed working tree, retained
|
||||
the previous sources under `.upgrade-backup/`, preserved `.env` and left
|
||||
`./dev.sh doctor` working. No live server/database was used in this rehearsal.
|
||||
- **PASS** - All eight bundled SQL migration scripts applied twice in an
|
||||
in-memory SQLite database; `PRAGMA integrity_check` returned `ok`.
|
||||
|
||||
## Limits and outstanding acceptance
|
||||
|
||||
- Rust/Cargo was **not available** in the verification environment, so
|
||||
`./dev.sh check`, `./dev.sh test`, binary build and live proxy/UI regression
|
||||
checks were not run. These remain required before production acceptance.
|
||||
- An unrelated legacy `dashboard_startup_0310_ui_test.mjs` assertion was already
|
||||
failing on the supplied 0.32.7 tree due to its expected
|
||||
`prefers-reduced-motion: reduce` CSS rule being absent. That test was removed
|
||||
with `tools/checks/`, not claimed as passing or fixed by this patch.
|
||||
- The intentionally removed `tools/checks/` suites are no longer executed by
|
||||
`dev.sh`; `./dev.sh test` now runs Cargo validation and the Rust test suite.
|
||||
Independent API, stress, browser and real-runtime acceptance tests must be
|
||||
run externally when required.
|
||||
|
||||
## Test-suite maintenance follow-up (0.32.8)
|
||||
|
||||
This maintenance change affects test code and duplicate test helpers only. No
|
||||
production Rust or Go source logic was changed.
|
||||
|
||||
- Renamed the invalid Rust test identifier beginning with `304_` to
|
||||
`response_304_with_private_policy_discards_cached_representation`. The
|
||||
revalidation regression itself is retained.
|
||||
- Placed four passive-health regression tests back inside the existing
|
||||
`src/balance/tests.rs` `mod tests` scope, so they can use its private
|
||||
`settings(HealthMode)` fixture. No test assertions were removed.
|
||||
- Removed two byte-for-byte duplicate files under `scripts/tests/`:
|
||||
`test_stream_tcp.py` and `test_stream_udp.py`. The canonical scripts under
|
||||
`scripts/` remain, including their documented commands and self-tests.
|
||||
- Statically enumerated 655 Rust `#[test]`/`#[tokio::test]` cases after the
|
||||
edit, without duplicate test names or remaining numeric-leading test names.
|
||||
A name containing an old version or `legacy` is not evidence that its
|
||||
compatibility/regression coverage is obsolete.
|
||||
|
||||
**Checks passing in the available environment:**
|
||||
|
||||
- `python3 scripts/preflight.py`, `bash dev.sh doctor`, `bash dev.sh repair`.
|
||||
- `go test ./...` in `docker-agent/`.
|
||||
- `python3 scripts/test_stream_tcp.py selftest` and
|
||||
`python3 scripts/test_stream_udp.py selftest` (mock-peer harness checks only;
|
||||
these do not exercise RustProxy).
|
||||
- Python AST parsing of all 14 scripts under `scripts/`, shell syntax checks
|
||||
for `dev.sh` and the Stream/dependency helper scripts.
|
||||
- All eight bundled SQLite migrations executed twice in order on an in-memory
|
||||
database, with `PRAGMA integrity_check = ok` after both passes.
|
||||
|
||||
**Not verified:** The environment does not have `cargo` or `rustc`, and DNS
|
||||
resolution is unavailable for downloading a toolchain. Consequently,
|
||||
`cargo check --locked --all-targets` and `cargo test --locked --all-targets`
|
||||
have not passed here. This maintenance does not close the live-runtime
|
||||
acceptance gate or establish that every remaining test is passing.
|
||||
@@ -20,6 +20,74 @@ A Web Service can be reused by several vhosts and one vhost can mix Web Server a
|
||||
|
||||
The Web Server implementation is split under `src/web_server/` by responsibility. `mod.rs` is only the public facade; `config.rs` owns persisted/compiled service configuration, `filesystem.rs` owns document-root confinement and directory reads, `security.rs` owns Web Service hardening, `response.rs` owns static HTTP response/validator/range/listing behavior, `routing.rs` owns rewrites/`try_files`/precompressed selection, `fastcgi.rs` owns PHP-FPM/FastCGI, `serve.rs` owns request dispatch, and `htaccess.rs` owns the compatibility layer. Regression tests live in `tests.rs`. This keeps filesystem/security and FastCGI changes reviewable without growing another single Web Server source file.
|
||||
|
||||
## Live Web Service diagnostics (0.33.0)
|
||||
|
||||
Each configured Web Service card now contains an **expandable Live metrics**
|
||||
section with completed requests, responses/s, HTTP 5xx count, mean elapsed time
|
||||
until response headers, PHP request permits in use/limit, static/PHP/other response
|
||||
counts, delivered response body bytes and a compact per-service rate history.
|
||||
The section reuses the existing authenticated live status stream and is closed
|
||||
by default. Unconfigured/non-PHP services do not claim PHP worker capacity.
|
||||
|
||||
**Overview** provides 12 additional opt-in Web Server blocks under
|
||||
**Settings -> Appearance -> Dashboard layout -> Overview cards**. All 12 are
|
||||
hidden by default, including when opening an older saved dashboard layout.
|
||||
They follow normal widget reordering, theme, card sizing and SSE updates.
|
||||
Sample history length is selectable as 60/120/300/600 points (temporary or
|
||||
browser-persistent), independently of Cache and Transform.
|
||||
|
||||
The authenticated `/api/status` and compact `/api/status?view=live` payloads
|
||||
include `web_server_metrics`, keyed only by *configured Web Service ID*:
|
||||
|
||||
```json
|
||||
{
|
||||
"web_server_metrics": {
|
||||
"site": {
|
||||
"web": {
|
||||
"requests": 120, "status_2xx": 100, "status_3xx": 5,
|
||||
"status_4xx": 12, "status_5xx": 3,
|
||||
"static_requests": 90, "php_requests": 25, "other_requests": 5,
|
||||
"response_headers_us": 1500000, "response_bytes": 98000,
|
||||
"active_headers": 0,
|
||||
"methods": [110, 8, 0, 0, 2],
|
||||
"selected_status": [95, 1, 3, 4, 8, 0, 2, 1],
|
||||
"failures": [0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
"compression": [12, 1, 0, 0]
|
||||
},
|
||||
"php": {
|
||||
"metrics": {
|
||||
"requests": 25, "response_headers_us": 750000,
|
||||
"response_bytes": 24000,
|
||||
"failures": [0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
},
|
||||
"capacity_limit": 32, "capacity_available": 24,
|
||||
"capacity_used": 8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The example is illustrative; the actual `php` object is `null` when PHP is
|
||||
not configured. Array orders: methods `GET, POST, PUT, DELETE, other`;
|
||||
selected statuses `200, 206, 304, 403, 404, 500, 502, 503`;
|
||||
FastCGI failures `capacity, connect_timeout, connect_error, request_timeout,
|
||||
request_io, response_timeout, response_io, invalid_response, body_io`;
|
||||
compression `gzip, br, zstd, deflate`.
|
||||
|
||||
**Accounting semantics:** The response-kind counters describe the handler
|
||||
that produced the response; `other` covers early rejections not dispatched
|
||||
to a static file or PHP. `active_headers` means requests still awaiting their
|
||||
response headers, *not* live streaming body counts. Mean duration is until
|
||||
response headers are ready. Body-byte counters advance only when body frames
|
||||
are polled, **before final dynamic compression**; these are not network wire
|
||||
bytes. The compression counter records only the downstream encoder, not static
|
||||
precompressed `.gz`/`.br` sidecar selection. Failed PHP requests may count as
|
||||
both PHP responses and FastCGI failures. PHP permit capacity is RustProxy's
|
||||
configured request semaphore, *not* the number of processes in the external
|
||||
PHP-FPM pool. All counters are process/snapshot lifetime observations and may
|
||||
restart on a config publication.
|
||||
|
||||
## Dashboard configuration
|
||||
|
||||
The Web Service editor is split into stable configuration tabs so later features do not create one long form:
|
||||
|
||||
+12
-1
@@ -12,6 +12,16 @@ latency, backend health/EWMA latency/active work, traffic, probe age and upstrea
|
||||
variables **Docker agent** and **Docker service** filter only this section, which keeps
|
||||
multi-agent installations readable without changing the ordinary vhost panels.
|
||||
|
||||
The **0.33.0 Web Server** row adds header-ready request rates, status-class and
|
||||
static/PHP/other donut breakdowns, error percentage, response-header latency,
|
||||
payload byte rates, and downstream compression by codec. **PHP-FPM / FastCGI**
|
||||
adds request activity, RustProxy PHP permit capacity, and errors grouped by
|
||||
finite reason. **Per Web Service** shows top service rates, HTTP methods and
|
||||
selected status codes, while **WWW / PHP correlation** compares PHP load and
|
||||
Web Server errors with cache activity. Use the **Web service** variable to
|
||||
filter the new panels by configured Web Service ID. All new panels are
|
||||
added to the existing dashboard without changing the legacy rows.
|
||||
|
||||
The **Traffic analytics and Web Server** row uses the `rust_proxy_traffic_*` families.
|
||||
The **Traffic vhost** variable filters that row and exposes Web Server request rate,
|
||||
serving-source rate, top countries, fixed User-Agent families, HTTP methods, status
|
||||
@@ -19,7 +29,8 @@ classes, cache outcomes, capture-state gauges and configured-route traffic. Raw
|
||||
are intentionally absent from Prometheus labels.
|
||||
|
||||
Import the JSON in Grafana and select a Prometheus datasource when prompted. The
|
||||
dashboard expects RustProxy 0.29.2 metric names and a scrape interval around 15s.
|
||||
dashboard expects RustProxy 0.33.0 for its newest Web Server metric names
|
||||
(and also retains the older panels); a scrape interval around 15s is suitable.
|
||||
Collectors can be disabled in RustProxy settings; panels for disabled collectors
|
||||
will simply show no series.
|
||||
|
||||
|
||||
+1114
-3
File diff suppressed because it is too large.
Load diff
+67
-1
@@ -2638,6 +2638,72 @@
|
||||
"aclRuleIp": "IP / CIDR",
|
||||
"aclRuleDns": "DNS / DDNS",
|
||||
"aclDnsAllowOnly": "DNS/DDNS addresses only support allow rules. Use IP/CIDR for deny.",
|
||||
"aclDnsHelp": "DNS/DDNS allow rules resolve A/AAAA every 60 seconds. DNS errors remove access (fail closed). Enter a hostname such as office.example.ddns.net; no URL or wildcard. Select Allow exceptions with default Deny. Up to 128 unique hostnames, 32 addresses per hostname."
|
||||
"aclDnsHelp": "DNS/DDNS allow rules resolve A/AAAA every 60 seconds. DNS errors remove access (fail closed). Enter a hostname such as office.example.ddns.net; no URL or wildcard. Select Allow exceptions with default Deny. Up to 128 unique hostnames, 32 addresses per hostname.",
|
||||
"overviewExtraColdQueueMemory": "COLD queue RAM utilization",
|
||||
"overviewExtraColdQueueMemoryDescription": "RAM used by the COLD write queue relative to its configured RAM budget, not the disk cache.",
|
||||
"overviewExtraColdStorage": "COLD disk utilization",
|
||||
"overviewExtraColdStorageDescription": "Persistent COLD objects on disk compared with the configured storage budget.",
|
||||
"overviewWideTransformActivity": "Transformation activity",
|
||||
"overviewWideTransformActivityDescription": "Per-second responses transformed, served from transformed cache, skipped, and errors.",
|
||||
"overviewWideCompressionActivity": "Compression activity",
|
||||
"overviewWideCompressionActivityDescription": "Responses per second encoded as gzip, Brotli, Zstd or deflate; identity responses are not counted.",
|
||||
"overviewTransformApplied": "Applied",
|
||||
"overviewTransformCacheHits": "Cache HIT",
|
||||
"overviewTransformSkipped": "Skipped",
|
||||
"overviewTransformErrors": "Errors",
|
||||
"overviewMeterNoLimit": "No configured limit",
|
||||
"overviewCacheHitsMisses": "HIT {hits}, MISS {misses}",
|
||||
"overviewSamplesRemember": "Remember choice",
|
||||
"overviewSamplesSaved": "Saved default",
|
||||
"overviewSamplesTemporary": "For this visit only",
|
||||
"overviewSamplesStorageUnavailable": "Could not save in this browser",
|
||||
"overviewSamplesTitle": "Overview chart samples",
|
||||
"webMetricStatus": "WWW HTTP status",
|
||||
"webMetricStatusHelp": "Completed Web Server status classes, all-time since reload.",
|
||||
"webMetricKind": "Static vs PHP",
|
||||
"webMetricKindHelp": "Static file, PHP and other/early rejected requests.",
|
||||
"webMetricCapacity": "PHP request capacity",
|
||||
"webMetricCapacityHelp": "Busy/maximum RustProxy FastCGI permits; not PHP worker utilization.",
|
||||
"webMetricMethods": "WWW HTTP methods",
|
||||
"webMetricMethodsHelp": "GET/POST/PUT/DELETE and other methods.",
|
||||
"webMetricErrorRate": "WWW 5xx error rate",
|
||||
"webMetricErrorRateHelp": "5xx share of the latest interval, not all-time.",
|
||||
"webMetricPhpRequests": "PHP requests/s",
|
||||
"webMetricPhpRequestsHelp": "PHP responses per second.",
|
||||
"webMetricRequests": "WWW requests/s",
|
||||
"webMetricRequestsHelp": "Web Server request rates including PHP.",
|
||||
"webMetricLatency": "WWW header latency",
|
||||
"webMetricLatencyHelp": "Mean time to response headers in the latest interval; excludes streamed body.",
|
||||
"webMetricFailures": "PHP-FPM failures/s",
|
||||
"webMetricFailuresHelp": "Failures by fixed reasons; generated HTTP error responses still count as requests.",
|
||||
"webMetricTraffic": "WWW body bytes/s",
|
||||
"webMetricTrafficHelp": "Body bytes before downstream compression; not wire bandwidth.",
|
||||
"webMetricTopServices": "Top Web Services",
|
||||
"webMetricTopServicesHelp": "Most active configured Web Services by requests per second.",
|
||||
"webMetricStatusTrend": "WWW status/s",
|
||||
"webMetricStatusTrendHelp": "Status-class rates in Web Server responses.",
|
||||
"webMetricPerSecond": "Responses per second",
|
||||
"webMetricSeriesAll": "Total",
|
||||
"webMetricSeriesPhp": "PHP",
|
||||
"webMetricSeriesWeb": "WWW",
|
||||
"webMetricSeriesTimeout": "Timeout",
|
||||
"webMetricSeriesConnection": "I/O + connect",
|
||||
"webMetricSeriesProtocol": "Protocol",
|
||||
"webMetricSeriesCapacity": "Capacity",
|
||||
"webMetricSeriesNonphp": "Non-PHP",
|
||||
"webMetricSeriesTwoxx": "2xx",
|
||||
"webMetricSeriesThreexx": "3xx",
|
||||
"webMetricSeriesFourxx": "4xx",
|
||||
"webMetricSeriesFivexx": "5xx",
|
||||
"webLiveMetrics": "Live metrics",
|
||||
"webLiveNoData": "Waiting for Web Server metrics",
|
||||
"webLiveRequests": "Responses total",
|
||||
"webLiveRate": "Rate",
|
||||
"webLiveErrors": "5xx",
|
||||
"webLiveLatency": "Header latency",
|
||||
"webLivePhpCapacity": "PHP capacity",
|
||||
"webLiveKinds": "Static / PHP / other",
|
||||
"webLiveBytes": "Body delivered",
|
||||
"webLiveNoPhp": "PHP disabled"
|
||||
}
|
||||
}
|
||||
+67
-1
@@ -2638,6 +2638,72 @@
|
||||
"aclRuleIp": "IP / CIDR",
|
||||
"aclRuleDns": "DNS / DDNS",
|
||||
"aclDnsAllowOnly": "Reguły DNS/DDNS obsługują tylko allow. Do deny użyj IP/CIDR.",
|
||||
"aclDnsHelp": "Reguły DNS/DDNS typu allow odświeżają rekordy A/AAAA co 60 sekund. Błąd DNS usuwa zezwolenie (fail closed). Podaj nazwę, np. biuro.example.ddns.net, bez URL i gwiazdek. Wybierz wyjątki zezwalające z domyślną blokadą. Maksymalnie 128 nazw i 32 adresy na nazwę."
|
||||
"aclDnsHelp": "Reguły DNS/DDNS typu allow odświeżają rekordy A/AAAA co 60 sekund. Błąd DNS usuwa zezwolenie (fail closed). Podaj nazwę, np. biuro.example.ddns.net, bez URL i gwiazdek. Wybierz wyjątki zezwalające z domyślną blokadą. Maksymalnie 128 nazw i 32 adresy na nazwę.",
|
||||
"overviewExtraColdQueueMemory": "Wykorzystanie RAM kolejki COLD",
|
||||
"overviewExtraColdQueueMemoryDescription": "RAM używany przez kolejkę zapisu COLD w stosunku do jej limitu; nie jest to pamięć dyskowa.",
|
||||
"overviewExtraColdStorage": "Wykorzystanie dysku COLD",
|
||||
"overviewExtraColdStorageDescription": "Rozmiar trwałego cache COLD na dysku w stosunku do skonfigurowanego limitu.",
|
||||
"overviewWideTransformActivity": "Aktywność transformacji",
|
||||
"overviewWideTransformActivityDescription": "Tempo transformacji, trafień w transformowany cache, pominięć i błędów.",
|
||||
"overviewWideCompressionActivity": "Aktywność kompresji",
|
||||
"overviewWideCompressionActivityDescription": "Tempo kodowania odpowiedzi gzip, Brotli, Zstd lub deflate, bez odpowiedzi nieskompresowanych.",
|
||||
"overviewTransformApplied": "Wykonane",
|
||||
"overviewTransformCacheHits": "Cache HIT",
|
||||
"overviewTransformSkipped": "Pominięte",
|
||||
"overviewTransformErrors": "Błędy",
|
||||
"overviewMeterNoLimit": "Brak skonfigurowanego limitu",
|
||||
"overviewCacheHitsMisses": "HIT {hits}, MISS {misses}",
|
||||
"overviewSamplesRemember": "Zapamiętaj wybór",
|
||||
"overviewSamplesSaved": "Ustawienie zapisane",
|
||||
"overviewSamplesTemporary": "Tylko do odświeżenia strony",
|
||||
"overviewSamplesStorageUnavailable": "Nie można zapisać w przeglądarce",
|
||||
"overviewSamplesTitle": "Próbki wykresów na stronie głównej",
|
||||
"webMetricStatus": "Statusy HTTP WWW",
|
||||
"webMetricStatusHelp": "Klasy odpowiedzi serwera WWW od ostatniego resetu.",
|
||||
"webMetricKind": "Static / PHP",
|
||||
"webMetricKindHelp": "Pliki statyczne, PHP i inne/wczesne odrzucenia.",
|
||||
"webMetricCapacity": "Limit żądań PHP",
|
||||
"webMetricCapacityHelp": "Zajęte/łączne sloty FastCGI RustProxy, nie wykorzystanie workerów PHP.",
|
||||
"webMetricMethods": "Metody HTTP WWW",
|
||||
"webMetricMethodsHelp": "GET/POST/PUT/DELETE i pozostałe metody.",
|
||||
"webMetricErrorRate": "Błędy WWW 5xx",
|
||||
"webMetricErrorRateHelp": "Odsetek 5xx w ostatnim przedziale, nie od startu.",
|
||||
"webMetricPhpRequests": "Żądania PHP/s",
|
||||
"webMetricPhpRequestsHelp": "Odpowiedzi PHP na sekundę.",
|
||||
"webMetricRequests": "Żądania WWW/s",
|
||||
"webMetricRequestsHelp": "Ruch Web Server z uwzględnieniem PHP.",
|
||||
"webMetricLatency": "Czas nagłówków WWW",
|
||||
"webMetricLatencyHelp": "Średni czas do nagłówków odpowiedzi w ostatnim przedziale; bez treści.",
|
||||
"webMetricFailures": "Błędy PHP-FPM/s",
|
||||
"webMetricFailuresHelp": "Błędy według przyczyn; odpowiedzi błędów też są liczone.",
|
||||
"webMetricTraffic": "Dane WWW/s",
|
||||
"webMetricTrafficHelp": "Bajty treści przed kompresją końcową; nie ruch sieciowy.",
|
||||
"webMetricTopServices": "Najaktywniejsze Web Services",
|
||||
"webMetricTopServicesHelp": "Najaktywniejsze usługi WWW według żądań na sekundę.",
|
||||
"webMetricStatusTrend": "Statusy WWW/s",
|
||||
"webMetricStatusTrendHelp": "Odpowiedzi WWW na sekundę wg klas HTTP.",
|
||||
"webMetricPerSecond": "Odpowiedzi na sekundę",
|
||||
"webMetricSeriesAll": "Razem",
|
||||
"webMetricSeriesPhp": "PHP",
|
||||
"webMetricSeriesWeb": "WWW",
|
||||
"webMetricSeriesTimeout": "Timeout",
|
||||
"webMetricSeriesConnection": "I/O + połączenia",
|
||||
"webMetricSeriesProtocol": "Protokół",
|
||||
"webMetricSeriesCapacity": "Limit",
|
||||
"webMetricSeriesNonphp": "Bez PHP",
|
||||
"webMetricSeriesTwoxx": "2xx",
|
||||
"webMetricSeriesThreexx": "3xx",
|
||||
"webMetricSeriesFourxx": "4xx",
|
||||
"webMetricSeriesFivexx": "5xx",
|
||||
"webLiveMetrics": "Metryki na żywo",
|
||||
"webLiveNoData": "Oczekiwanie na metryki WWW",
|
||||
"webLiveRequests": "Odpowiedzi łącznie",
|
||||
"webLiveRate": "Tempo",
|
||||
"webLiveErrors": "5xx",
|
||||
"webLiveLatency": "Czas do nagłówków",
|
||||
"webLivePhpCapacity": "Limit PHP",
|
||||
"webLiveKinds": "Statyczne / PHP / inne",
|
||||
"webLiveBytes": "Wysłana treść",
|
||||
"webLiveNoPhp": "PHP wyłączone"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// Deterministic Overview widget regression; simulated DOM/status, no running API.
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import vm from 'node:vm';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const html = fs.readFileSync(path.join(root, 'src/admin/web/index.html'), 'utf8');
|
||||
const registry = JSON.parse(fs.readFileSync(path.join(root, 'src/admin/web/dashboard-widgets.json'), 'utf8'));
|
||||
const lang = JSON.parse(fs.readFileSync(path.join(root, 'langs/en_US.json'), 'utf8')).translations;
|
||||
const code = file => fs.readFileSync(path.join(root, 'src/admin/web', file), 'utf8');
|
||||
const widgets = new Map(), ids = new Map();
|
||||
class Element {
|
||||
constructor() { this.hidden = false; this.attributes = new Map(); this.style = {}; this.textContent = '-'; }
|
||||
setAttribute(key, value) { this.attributes.set(key, String(value)); }
|
||||
removeAttribute(key) { this.attributes.delete(key); }
|
||||
getAttribute(key) { return this.attributes.get(key) ?? null; }
|
||||
}
|
||||
for (const [, id] of html.matchAll(/\bid="([^"]+)"/g)) {
|
||||
assert.ok(!ids.has(id), `duplicate DOM id: ${id}`);
|
||||
ids.set(id, new Element());
|
||||
}
|
||||
for (const [, id] of html.matchAll(/data-dashboard-widget="([^"]+)"/g)) widgets.set(id, new Element());
|
||||
assert.equal(registry.length, widgets.size, 'registry and DOM widget count must match');
|
||||
for (const entry of registry) assert.ok(widgets.has(entry.id), `registry references missing DOM card ${entry.id}`);
|
||||
let now = 1000;
|
||||
const document = {
|
||||
hidden: false,
|
||||
addEventListener() {},
|
||||
querySelector(query) { return widgets.get(query.match(/^\[data-dashboard-widget="([^"]+)"\]$/)?.[1]) ?? null; },
|
||||
};
|
||||
const window = { addEventListener() {}, rustProxyRefresh: { current: () => ({ overview_seconds: 3 }) } };
|
||||
const context = vm.createContext({
|
||||
window, document, performance: { now: () => now },
|
||||
currentPanel: 'overview', $: id => ids.get(id) ?? null,
|
||||
fmt: (v, digits = 0) => Number(v).toFixed(digits),
|
||||
fmtBytes: v => `${Number(v)} B`,
|
||||
t: key => lang[key] || key,
|
||||
console,
|
||||
});
|
||||
for (const script of ['app/rate-sampling.js', 'app/overview-extra-metrics.js', 'app/overview-wide-metrics.js']) {
|
||||
vm.runInContext(code(script), context, { filename: script });
|
||||
}
|
||||
const extra = window.rustProxyExtraOverview, wide = window.rustProxyWideOverview;
|
||||
const status = {
|
||||
revision: 9,
|
||||
system: { open_fds: 60, max_fds: 300 },
|
||||
cache: { hits: 100, misses: 50, resident_bytes: 150, hot_limit_bytes: 300,
|
||||
cold_queue_bytes: 25, cold_queue_limit_bytes: 100, cold_bytes: 400, cold_limit_bytes: 1000 },
|
||||
metrics: { requests: 300, http1_requests: 200, http2_requests: 80, http3_requests: 20,
|
||||
compression_gzip: 10, compression_br: 10, compression_zstd: 10, compression_deflate: 1 },
|
||||
transform_metrics: { applied: 10, cache_hits: 10, skipped: 10, errors: 1 },
|
||||
};
|
||||
extra.render(status);
|
||||
wide.render(status);
|
||||
assert.equal(ids.get('overview-extra-extra_cache_memory-meter-fill').style.width, '50%');
|
||||
assert.equal(ids.get('overview-extra-extra_cold_queue_memory-meter-fill').style.width, '25%');
|
||||
assert.equal(ids.get('overview-extra-extra_cold_storage-meter-fill').style.width, '40%');
|
||||
widgets.get('extra_cache_memory').hidden = false;
|
||||
widgets.get('extra_cache_hit_rate').hidden = false;
|
||||
widgets.get('extra_h2_share').hidden = false;
|
||||
widgets.get('extra_h1_share').hidden = false;
|
||||
widgets.get('extra_h3_share').hidden = false;
|
||||
widgets.get('extra_process_fds').hidden = false;
|
||||
extra.render(status);
|
||||
assert.equal(ids.get('overview-extra-extra_process_fds-meter-fill').style.width, '20%');
|
||||
assert.equal(ids.get('overview-extra-extra_cache_hit_rate').textContent, '-'); // first counter sample has no rate
|
||||
assert.equal(ids.get('overview-wide-wide_transform_activity-summary').textContent, '-');
|
||||
|
||||
now = 4000;
|
||||
status.cache.hits += 30;
|
||||
status.cache.misses += 10;
|
||||
status.metrics.requests += 40;
|
||||
status.metrics.http1_requests += 10;
|
||||
status.metrics.http2_requests += 20;
|
||||
status.metrics.http3_requests += 10;
|
||||
status.metrics.compression_gzip += 3;
|
||||
status.metrics.compression_br += 6;
|
||||
status.metrics.compression_zstd += 9;
|
||||
status.metrics.compression_deflate += 12;
|
||||
status.transform_metrics.applied += 15;
|
||||
status.transform_metrics.cache_hits += 3;
|
||||
status.transform_metrics.skipped += 6;
|
||||
status.transform_metrics.errors += 1;
|
||||
extra.render(status);
|
||||
wide.render(status);
|
||||
assert.equal(ids.get('overview-extra-extra_cache_hit_rate').textContent, '75.0%');
|
||||
assert.equal(ids.get('extra-cache-ratio-hit-arc').getAttribute('stroke-dasharray'), '75 25');
|
||||
assert.equal(ids.get('extra-cache-ratio-miss-arc').getAttribute('stroke-dashoffset'), '-75');
|
||||
assert.equal(ids.get('extra-cache-ratio-hit-count').textContent, '30');
|
||||
assert.equal(ids.get('extra-cache-ratio-miss-count').textContent, '10');
|
||||
assert.equal(ids.get('overview-extra-extra_h2_share-meter-fill').style.width, '50%');
|
||||
assert.equal(ids.get('overview-wide-wide_transform_activity-summary').textContent, '8.3/s');
|
||||
assert.equal(ids.get('overview-wide-wide_compression_activity-summary').textContent, '10.0/s');
|
||||
for (const name of ['applied', 'cache_hits', 'skipped', 'errors'])
|
||||
assert.ok(ids.get(`overview-wide-wide_transform_activity-${name}-line`).getAttribute('d')?.length);
|
||||
for (const name of ['gzip', 'br', 'zstd', 'deflate'])
|
||||
assert.ok(ids.get(`overview-wide-wide_compression_activity-${name}-line`).getAttribute('d')?.length);
|
||||
|
||||
// No hidden-tab spike or fake zero when a long interval is skipped.
|
||||
now = 100000;
|
||||
status.cache.hits += 300;
|
||||
status.cache.misses += 200;
|
||||
status.metrics.compression_gzip += 600;
|
||||
extra.render(status);
|
||||
wide.render(status);
|
||||
assert.equal(ids.get('overview-extra-extra_cache_hit_rate').textContent, '-');
|
||||
assert.equal(ids.get('overview-wide-wide_compression_activity-summary').textContent, '-');
|
||||
now = 103000;
|
||||
status.cache.hits += 5;
|
||||
status.cache.misses += 5;
|
||||
status.metrics.compression_gzip += 3;
|
||||
extra.render(status);
|
||||
wide.render(status);
|
||||
assert.equal(ids.get('overview-extra-extra_cache_hit_rate').textContent, '50.0%');
|
||||
assert.equal(ids.get('overview-wide-wide_compression_activity-summary').textContent, '1.0/s');
|
||||
|
||||
// Bounded metric with no configured budget remains unknown, not a made-up percentage.
|
||||
status.cache.cold_limit_bytes = 0;
|
||||
extra.render(status);
|
||||
assert.equal(ids.get('overview-extra-extra_cold_storage').textContent, '400 B');
|
||||
assert.equal(ids.get('overview-extra-extra_cold_storage-meter').getAttribute('aria-valuenow'), null);
|
||||
assert.equal(ids.get('overview-extra-extra_cold_storage-meter-fill').style.width, '0%');
|
||||
|
||||
extra.donut('cache-ratio', 75, 25);
|
||||
assert.equal(ids.get('cache-ratio-hit-arc').getAttribute('stroke-dasharray'), '75 25');
|
||||
assert.equal(ids.get('cache-ratio-chart').getAttribute('aria-label'), 'HIT 75, MISS 25');
|
||||
extra.donut('cache-ratio', 0, 0);
|
||||
assert.equal(ids.get('cache-ratio-hit-arc').getAttribute('stroke-dasharray'), '0 100');
|
||||
assert.ok(code('app/runtime.js').includes('rustProxyExtraOverview?.donut?.("cache-ratio"'),
|
||||
'main HIT/MISS tile must share tested donut rendering');
|
||||
console.log('PASS: bounded meters, COLD tiers, HIT/MISS rings, transform and compression rates, pause/no-limit');
|
||||
@@ -0,0 +1,114 @@
|
||||
// Simulated browser/status contract: this does not exercise the Rust server.
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import vm from 'node:vm';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const read = file => fs.readFileSync(path.join(root, file), 'utf8');
|
||||
const html = read('src/admin/web/index.html');
|
||||
const controllerSource = read('src/admin/web/app/overview-samples.js');
|
||||
const store = new Map();
|
||||
|
||||
assert.match(html, /id="overview-layout-unlock"/);
|
||||
// History controls share the connected-core eyebrow row (not a separate
|
||||
// dashboard row), and navigation shows them only on Overview.
|
||||
const metaStart = html.indexOf('class="page-intro-meta-row"');
|
||||
const metaEnd = html.indexOf('class="page-intro-title-row"');
|
||||
const statusPos = html.indexOf('id="connection-label"');
|
||||
const selectPos = html.indexOf('id="overview-live-samples"');
|
||||
assert.ok(metaStart < statusPos && statusPos < selectPos && selectPos < metaEnd);
|
||||
assert.ok(selectPos < html.indexOf('id="panel-overview"'));
|
||||
assert.match(html, /id="overview-samples-toolbar" hidden role="group"/);
|
||||
assert.match(read('src/admin/web/app/core.js'), /\$\("overview-samples-toolbar"\)\.hidden = name !== "overview"/);
|
||||
assert.ok(html.indexOf('id="overview-live-samples"') < html.indexOf('id="overview-layout-unlock"'));
|
||||
assert.ok(html.indexOf('id="overview-live-samples"') < html.indexOf('id="dashboard-overview-grid"'));
|
||||
for (const sample of [60, 120, 300, 600]) assert.match(html, new RegExp(`<option value="${sample}">`));
|
||||
|
||||
class Element {
|
||||
constructor() { this.listeners = {}; this.value = ''; this.dataset = {}; this.disabled = false; this.attributes = new Map(); this.textContent = ''; this.hidden = false; }
|
||||
addEventListener(name, callback) { this.listeners[name] = callback; }
|
||||
fire(name) { this.listeners[name](); }
|
||||
setAttribute(name, value) { this.attributes.set(name, String(value)); }
|
||||
getAttribute(name) { return this.attributes.get(name); }
|
||||
}
|
||||
const nodes = new Map();
|
||||
for (const id of ['overview-live-samples', 'overview-samples-save', 'overview-samples-state']) nodes.set(id, new Element());
|
||||
const widgets = new Map([['wide_compression_activity', new Element()]]);
|
||||
const document = {
|
||||
readyState: 'complete', getElementById: id => nodes.get(id) || null,
|
||||
querySelector: query => widgets.get(query.match(/data-dashboard-widget="([^"]+)"/)?.[1]) || null,
|
||||
};
|
||||
const localStorage = {
|
||||
getItem: key => store.get(key) ?? null,
|
||||
setItem: (key, value) => { store.set(key, value); },
|
||||
};
|
||||
let now = 1000;
|
||||
const window = { rustProxyRefresh: { current: () => ({ overview_seconds: 3 }) } };
|
||||
const context = vm.createContext({ document, localStorage, window, queueMicrotask: callback => callback(),
|
||||
t: key => key, performance: { now: () => now },
|
||||
currentPanel: 'overview', latestStatus: null, $: id => { if (!nodes.has(id)) nodes.set(id, new Element()); return nodes.get(id); },
|
||||
fmt: n => String(n), fmtBytes: n => `${n} B`,
|
||||
});
|
||||
vm.runInContext(controllerSource, context);
|
||||
const samples = window.rustProxyOverviewSamples;
|
||||
const select = document.getElementById('overview-live-samples');
|
||||
const save = document.getElementById('overview-samples-save');
|
||||
const state = document.getElementById('overview-samples-state');
|
||||
assert.equal(samples.getLimit(), 60);
|
||||
assert.equal(save.disabled, true);
|
||||
select.value = '600'; select.fire('change');
|
||||
assert.equal(samples.getLimit(), 600);
|
||||
assert.equal(state.dataset.i18n, 'overviewSamplesTemporary');
|
||||
assert.equal(store.size, 0, 'temporary samples must not touch localStorage');
|
||||
assert.equal(save.disabled, false);
|
||||
const range = Array.from({ length: 700 }, (_, i) => i);
|
||||
let calls = 0;
|
||||
samples.onChange(() => { samples.trim(range); calls++; });
|
||||
select.value = '120'; select.fire('change');
|
||||
assert.equal(calls, 1);
|
||||
assert.equal(range.length, 120);
|
||||
assert.equal(range[0], 580);
|
||||
assert.equal(samples.setLimit(999), false);
|
||||
assert.equal(samples.getLimit(), 120);
|
||||
save.fire('click');
|
||||
assert.equal(store.get('rustproxy.overview.liveChartSamples'), '120');
|
||||
assert.equal(state.dataset.i18n, 'overviewSamplesSaved');
|
||||
assert.equal(save.disabled, true);
|
||||
|
||||
// Fresh load reads only the saved choice, not a temporary override.
|
||||
select.value = '300'; select.fire('change');
|
||||
assert.equal(samples.getLimit(), 300);
|
||||
const freshDocument = { readyState: 'loading', addEventListener() {} };
|
||||
const fresh = vm.createContext({ document: freshDocument, localStorage, window: {}, t: key => key });
|
||||
vm.runInContext(controllerSource, fresh);
|
||||
assert.equal(fresh.window.rustProxyOverviewSamples.getLimit(), 120);
|
||||
// Permanently saved choice survives a fresh load.
|
||||
save.fire('click');
|
||||
const again = vm.createContext({ document: freshDocument, localStorage, window: {}, t: key => key });
|
||||
vm.runInContext(controllerSource, again);
|
||||
assert.equal(again.window.rustProxyOverviewSamples.getLimit(), 300);
|
||||
|
||||
// Wide Overview charts must respect the same limit and redraw immediately when
|
||||
// the limit shrinks, without recording an extra point or hitting the network.
|
||||
vm.runInContext(read('src/admin/web/app/rate-sampling.js'), context);
|
||||
vm.runInContext(read('src/admin/web/app/overview-wide-metrics.js'), context);
|
||||
select.value = '600'; select.fire('change');
|
||||
const status = {
|
||||
revision: 1, metrics: { compression_gzip: 0, compression_br: 0, compression_zstd: 0, compression_deflate: 0 },
|
||||
};
|
||||
context.latestStatus = status;
|
||||
for (let i = 0; i <= 650; i++) {
|
||||
now += 3000;
|
||||
for (const key of ['compression_gzip', 'compression_br', 'compression_zstd', 'compression_deflate']) status.metrics[key] += i % 3 + 1;
|
||||
window.rustProxyWideOverview.render(status);
|
||||
}
|
||||
const line = () => nodes.get('overview-wide-wide_compression_activity-gzip-line').getAttribute('d');
|
||||
assert.equal((line().match(/ L/g) || []).length, 599, 'maximum 600 samples must be retained');
|
||||
select.value = '120'; select.fire('change');
|
||||
assert.equal((line().match(/ L/g) || []).length, 119, 'changing selection must trim and redraw without a new sample');
|
||||
assert.equal(store.get('rustproxy.overview.liveChartSamples'), '300', 'temporary choice must preserve the saved limit');
|
||||
assert.ok(!read('src/admin/web/app/overview-samples.js').includes('fetch('));
|
||||
assert.ok(!read('src/admin/web/app/overview-samples.js').includes('setInterval('));
|
||||
console.log('PASS: Overview samples 60/120/300/600, temporary vs persistent, immediate trim/redraw, no extra requests');
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
from pathlib import Path
|
||||
import re, json
|
||||
from playwright.sync_api import sync_playwright
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
router=(root/'src/admin/router.rs').read_text()
|
||||
|
||||
def concatenate(section):
|
||||
pattern=r'fn '+section+r'\([^)]*\)[^{]*\{'
|
||||
m=re.search(pattern,router)
|
||||
assert m, section
|
||||
text=router[m.end():]
|
||||
# The array contents are bounded by ].join("\\n") before the next function.
|
||||
text=text[:re.search(r'\]\s*\.join\("\\n"\)', text).start()]
|
||||
result=[]
|
||||
for line in text.splitlines():
|
||||
line=line.strip()
|
||||
if 'include_str!("' in line:
|
||||
path=re.search(r'include_str!\("([^"]+)"\)',line).group(1)
|
||||
result.append((root/'src/admin'/path).read_text())
|
||||
elif line=='admin_vhosts_javascript(),':
|
||||
result.append('(() => {\n'+''.join((root/'src/admin/web/vhosts'/f'{part}.js').read_text()+'\n' for part in ['core','protocol','health','rates','editor','render'])+'})();')
|
||||
elif line.startswith('"') and line.endswith('",'):
|
||||
result.append(json.loads(line[:-1]))
|
||||
return '\n'.join(result)
|
||||
bundle=concatenate('admin_app_javascript')
|
||||
assert 'window.rustProxyOverviewSamples =' in bundle
|
||||
assert bundle.index('window.rustProxyOverviewSamples =') < bundle.index('window.rustProxyOverviewSamples.onChange(')
|
||||
css=(root/'src/admin/web/style.css').read_text()
|
||||
html=(root/'src/admin/web/index.html').read_text()
|
||||
with sync_playwright() as p:
|
||||
browser=p.chromium.launch(headless=True, executable_path='/usr/bin/chromium',args=['--no-sandbox','--allow-file-access-from-files'])
|
||||
for width in [320,375,800,1440]:
|
||||
page=browser.new_page(viewport={'width':width,'height':900})
|
||||
errors=[]
|
||||
page.on('pageerror',lambda e: errors.append(str(e)))
|
||||
# Execute in a fully populated DOM without disallowed filesystem/network navigation.
|
||||
page.set_content(html.replace('/assets/app___RUSTPROXY_ASSET_ID__.js','').replace('/assets/style___RUSTPROXY_ASSET_ID__.css',''))
|
||||
page.add_style_tag(content=css)
|
||||
page.add_script_tag(content=bundle)
|
||||
page.wait_for_timeout(550)
|
||||
state=page.evaluate('''() => ({ controller:typeof window.rustProxyOverviewSamples,
|
||||
handle:typeof window.rustProxyOverviewSamples?.onChange,
|
||||
web:typeof window.rustProxyWebOverview, services:typeof window.rustProxyWebServer,
|
||||
registry: document.querySelectorAll('[data-dashboard-widget^="www_"]').length,
|
||||
htmlWidth:document.documentElement.scrollWidth,windowWidth:window.innerWidth })''')
|
||||
print(width,state,'errors',errors)
|
||||
assert state['controller']=='object' and state['handle']=='function' and state['web']=='object'
|
||||
assert state['registry']==12
|
||||
assert not [e for e in errors if 'onChange' in e or 'ReferenceError' in e or 'SyntaxError' in e or 'TypeError' in e],errors
|
||||
assert state['htmlWidth'] <= state['windowWidth'] + 2
|
||||
# The selector belongs to the same header row as the core connection;
|
||||
# hiding it on other panels must not leave an empty dashboard row.
|
||||
layout = page.evaluate('''() => {
|
||||
document.getElementById('workspace').hidden = false;
|
||||
setPanel('overview', false);
|
||||
const status = document.getElementById('connection-label');
|
||||
const sample = document.getElementById('overview-samples-toolbar');
|
||||
const row = document.querySelector('.page-intro-meta-row');
|
||||
const rect = node => { const r = node.getBoundingClientRect();
|
||||
return {x:r.x,y:r.y,width:r.width,height:r.height,right:r.right}; };
|
||||
const overview = { status:rect(status),sample:rect(sample),row:rect(row),
|
||||
visible: !sample.hidden,inside: row.contains(sample) && row.contains(status),
|
||||
overflow: document.documentElement.scrollWidth > innerWidth + 2 };
|
||||
setPanel('web-server', false);
|
||||
const notOverview = sample.hidden && !status.hidden;
|
||||
setPanel('overview', false);
|
||||
return {overview,notOverview,visibleAgain:!sample.hidden};
|
||||
}''')
|
||||
assert layout['overview']['inside'] and layout['overview']['visible']
|
||||
assert layout['notOverview'] and layout['visibleAgain']
|
||||
assert not layout['overview']['overflow'], (width, layout)
|
||||
if width >= 800:
|
||||
assert abs(layout['overview']['sample']['y'] - layout['overview']['status']['y']) <= 20, (width, layout)
|
||||
assert layout['overview']['sample']['right'] <= layout['overview']['row']['right'] + 2, (width, layout)
|
||||
# A genuinely sampled WWW metrics payload (not a screenshot-only fixture).
|
||||
report = page.evaluate('''() => {
|
||||
Object.defineProperty(window.performance, "now", {configurable:true, value:()=>window.__clock});
|
||||
window.__clock = 1000;
|
||||
currentPanel = "overview";
|
||||
document.getElementById('workspace').hidden = false;
|
||||
document.getElementById('login').hidden = true;
|
||||
document.getElementById('panel-overview').hidden = false;
|
||||
for (const n of document.querySelectorAll('[data-dashboard-widget^="www_"]')) n.hidden = false;
|
||||
function snap(k) {
|
||||
return {revision:7, web_server_metrics:{
|
||||
"public-site": {web: {requests:100+k*30, status_2xx:86+k*24, status_3xx:3+k,
|
||||
status_4xx:8+k*2, status_5xx:3+k*3, response_headers_us:4000000+k*1300000,
|
||||
response_bytes:100000+k*20000, static_requests:70+k*20, php_requests:25+k*7,
|
||||
other_requests:5+k*3, methods:[90+k*25,8+k*4,0,0,2+k],
|
||||
selected_status:[80+k*18,2,2,1,5+k,0,0,0], active_headers:0},
|
||||
php: {metrics:{requests:25+k*7, response_headers_us:2000000+k*700000,
|
||||
response_bytes:30000+k*5000,failures:[1+k,1+k,1+k,0,0,0,0,0,0]},
|
||||
capacity_limit:32, capacity_used:8, capacity_available:24}}}
|
||||
};
|
||||
}
|
||||
const before = snap(0), after = snap(1);
|
||||
// Saved custom layout from before 0.33.0 must not enable new cards.
|
||||
const savedLayout = applyDashboardLayout(['requests', 'errors'],
|
||||
normalizeHiddenDashboardWidgets([], ['requests', 'errors']));
|
||||
const newCardsHidden = [...document.querySelectorAll('[data-dashboard-widget^="www_"]')]
|
||||
.every(card=>card.hidden);
|
||||
for (const n of document.querySelectorAll('[data-dashboard-widget^="www_"]')) n.hidden = false;
|
||||
window.rustProxyWebOverview.render(before, true);
|
||||
window.__clock = 4000;
|
||||
window.rustProxyWebOverview.render(after, true);
|
||||
let ids = ['www_requests-all-line', 'www_requests-php-line',
|
||||
'www_latency-web-line', 'www_traffic-php-line', 'www_status_trend-5xx-line'];
|
||||
const linePaths = ids.map(id=>document.getElementById(id)?.getAttribute('d'));
|
||||
const statusText = document.getElementById('www_status-value').textContent;
|
||||
const capacityText = document.getElementById('www_capacity-value').textContent;
|
||||
const rateText = document.getElementById('www_php_requests-value').textContent;
|
||||
const bars = document.querySelectorAll('#www_top_services-bars .www-top-row').length;
|
||||
window.rustProxyOverviewSamples.setLimit(120);
|
||||
const historyLimit = window.rustProxyOverviewSamples.getLimit();
|
||||
currentPanel = "web-server";
|
||||
envelope = {config:{web_services:[{id:'public-site', name:'Public site',document_root:'/srv/www/public'}],vhosts:[]}};
|
||||
window.rustProxyWebServer.render();
|
||||
const details = document.querySelector('.www-service-panel');
|
||||
details.open = true;
|
||||
window.__clock = 7000;
|
||||
window.rustProxyWebServer.renderLive(before, true);
|
||||
window.__clock = 10000;
|
||||
window.rustProxyWebServer.renderLive(after, true);
|
||||
const value = details.querySelector('[data-web-live-field="requests"]')?.textContent;
|
||||
const phpCapacity = details.querySelector('[data-web-live-field="php"]')?.textContent;
|
||||
return {linePaths,statusText,capacityText,rateText,bars,historyLimit,value,phpCapacity,newCardsHidden,
|
||||
width:document.documentElement.scrollWidth};
|
||||
}''')
|
||||
print('sampled WWW',width,report)
|
||||
assert all(report['linePaths']), report
|
||||
assert report['statusText'] != '-' and report['capacityText'] == '8 / 32', report
|
||||
assert report['rateText'] not in ('-', '0.0/s'), report
|
||||
assert report['bars'] == 1 and report['historyLimit'] == 120 and report['newCardsHidden'], report
|
||||
assert report['value'] and report['phpCapacity'] == '8 / 32', report
|
||||
assert report['width'] <= width + 2, report
|
||||
assert not [e for e in errors if any(x in e for x in ('ReferenceError', 'TypeError', 'SyntaxError'))], errors
|
||||
page.close()
|
||||
browser.close()
|
||||
print('PASS: actual composed admin bundle starts without a JS TypeError')
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Static contracts for 0.33.0 WWW observability (not Rust compilation)."""
|
||||
from pathlib import Path
|
||||
import json
|
||||
import re
|
||||
import tomllib
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
read = lambda file: (ROOT / file).read_text()
|
||||
manifest = tomllib.loads(read('Cargo.toml'))
|
||||
lock = tomllib.loads(read('Cargo.lock'))
|
||||
version = manifest['package']['version']
|
||||
assert version == '0.33.0'
|
||||
assert [p['version'] for p in lock['package'] if p['name'] == 'rust-proxy'] == [version]
|
||||
|
||||
registry = json.loads(read('src/admin/web/dashboard-widgets.json'))
|
||||
widgets = [w for w in registry if w['id'].startswith('www_')]
|
||||
assert len(widgets) == 12
|
||||
assert all(w['default_hidden'] is True for w in widgets)
|
||||
assert len({w['id'] for w in registry}) == len(registry)
|
||||
html = read('src/admin/web/index.html')
|
||||
english = json.loads(read('langs/en_US.json'))['translations']
|
||||
polish = json.loads(read('langs/pl_pl.json'))['translations']
|
||||
for widget in widgets:
|
||||
assert f'data-dashboard-widget="{widget["id"]}" hidden' in html, widget['id']
|
||||
assert widget['label_key'] in english, widget['label_key']
|
||||
assert widget['label_key'] in polish, widget['label_key']
|
||||
if widget['description_key']:
|
||||
assert widget['description_key'] in english
|
||||
assert widget['description_key'] in polish
|
||||
|
||||
# Safely assemble the admin app before core.js: the earlier release crashed on
|
||||
# registering onChange callbacks before rustProxyOverviewSamples was defined.
|
||||
router = read('src/admin/router.rs')
|
||||
assert router.index('include_str!("web/app/overview-samples.js")') < router.index('include_str!("web/app/core.js")')
|
||||
assert '"web/app/web-overview.js"' in router
|
||||
assert '"web_server_metrics"' in read('src/admin/status_live.rs')
|
||||
assert 'web_server_metrics_payload(&snapshot)' in read('src/admin/status.rs')
|
||||
# record_web is only consumed by web_server::serve; expose no wider than the
|
||||
# ResponseKind enum argument. This is a static visibility contract, not rustc.
|
||||
metrics = read('src/web_server/metrics.rs')
|
||||
assert 'pub(super) enum ResponseKind' in metrics
|
||||
assert 'pub(super) fn record_web(' in metrics
|
||||
assert 'pub(crate) fn record_web(' not in metrics
|
||||
assert 'service.web_metrics.record_web(' in read('src/web_server/serve.rs')
|
||||
|
||||
families = read('src/prometheus/web_php.rs')
|
||||
required = {
|
||||
'rust_proxy_web_server_requests_total', 'rust_proxy_web_server_responses_total',
|
||||
'rust_proxy_web_server_request_kind_total', 'rust_proxy_web_server_methods_total',
|
||||
'rust_proxy_web_server_status_code_total', 'rust_proxy_web_server_response_header_duration_microseconds_total',
|
||||
'rust_proxy_web_server_response_body_bytes_total', 'rust_proxy_web_server_requests_active',
|
||||
'rust_proxy_web_server_compression_total', 'rust_proxy_php_requests_total',
|
||||
'rust_proxy_php_responses_total', 'rust_proxy_php_response_header_duration_microseconds_total',
|
||||
'rust_proxy_php_response_body_bytes_total', 'rust_proxy_php_concurrency_available',
|
||||
'rust_proxy_php_concurrency_limit', 'rust_proxy_php_concurrency_in_use', 'rust_proxy_php_failures_total'
|
||||
}
|
||||
assert all(f'"{name}"' in families for name in required), required
|
||||
assert 'PHP_FAILURE_NAMES' in families
|
||||
assert re.search(r'\["GET", "POST", "PUT", "DELETE", "other"\]', families)
|
||||
assert 'let wl = labels(&[("web_service", service.id.as_str())]);' in families
|
||||
|
||||
j = json.loads(read('grafana/rustproxy-overview.json'))
|
||||
assert j['title'].startswith('RustProxy 0.33.0')
|
||||
ids = [p['id'] for p in j['panels']]
|
||||
assert len(ids) == len(set(ids))
|
||||
assert any(v.get('name') == 'web_service' and v.get('multi') and v.get('includeAll') for v in j['templating']['list'])
|
||||
for title in ('Web Server 0.33.0', 'PHP-FPM / FastCGI', 'Per Web Service', 'WWW / PHP correlation'):
|
||||
assert any(p['title'] == title and p['type'] == 'row' for p in j['panels'])
|
||||
extra = [p for p in j['panels'] if p['id'] >= 89]
|
||||
for i, p in enumerate(extra):
|
||||
a = p['gridPos']
|
||||
assert 0 <= a['x'] and a['w'] > 0 and a['x'] + a['w'] <= 24
|
||||
for q in extra[i + 1:]:
|
||||
b = q['gridPos']
|
||||
overlap = a['x'] < b['x'] + b['w'] and b['x'] < a['x'] + a['w'] and a['y'] < b['y'] + b['h'] and b['y'] < a['y'] + a['h']
|
||||
assert not overlap, (p['id'], q['id'])
|
||||
|
||||
production = [p for p in (ROOT / 'src').rglob('*.rs') if p.name != 'tests.rs' and 'tests' not in p.parts]
|
||||
large = [str(p.relative_to(ROOT)) for p in production if len(p.read_text().splitlines()) > 500]
|
||||
assert not large, large
|
||||
print('PASS RustProxy 0.33.0 source version, opt-in widgets, localizations, status API,')
|
||||
print(' exporter families, Grafana geometry/filtering and Rust module size (STATIC ONLY)')
|
||||
Executable
+134
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Simulated Web Server telemetry against the production admin JS/CSS bundle.
|
||||
|
||||
Browser-only test: does not assert live RustProxy, PHP-FPM, or network behavior.
|
||||
Run: python3 scripts/tests/web_server_0330_ui_test.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
router = (ROOT / 'src/admin/router.rs').read_text()
|
||||
match = re.search(r'fn admin_app_javascript\([^)]*\)[^{]*\{', router)
|
||||
assert match, 'admin_app_javascript definition missing'
|
||||
portion = router[match.end():]
|
||||
portion = portion[:re.search(r'\]\s*\.join\("\\n"\)', portion).start()]
|
||||
fragments = []
|
||||
for line in portion.splitlines():
|
||||
line = line.strip()
|
||||
if 'include_str!("' in line:
|
||||
path = re.search(r'include_str!\("([^"]+)"\)', line).group(1)
|
||||
fragments.append((ROOT / 'src/admin' / path).read_text())
|
||||
elif line == 'admin_vhosts_javascript(),':
|
||||
fragments.append('(() => {\n' + '\n'.join((ROOT / 'src/admin/web/vhosts' / f'{part}.js').read_text() for part in ['core', 'protocol', 'health', 'rates', 'editor', 'render']) + '\n})();')
|
||||
elif line.startswith('"') and line.endswith('",'):
|
||||
fragments.append(json.loads(line[:-1]))
|
||||
BUNDLE = '\n'.join(fragments)
|
||||
assert BUNDLE.index('window.rustProxyOverviewSamples =') < BUNDLE.index('window.rustProxyOverviewSamples.onChange(')
|
||||
|
||||
WEB = {
|
||||
'requests': 100, 'status_2xx': 90, 'status_3xx': 2, 'status_4xx': 5, 'status_5xx': 3,
|
||||
'response_headers_us': 2_500_000, 'response_bytes': 10_000, 'active_headers': 2,
|
||||
'static_requests': 70, 'php_requests': 25, 'other_requests': 5,
|
||||
'methods': [80, 10, 5, 2, 3], 'selected_status': [75, 5, 2, 0, 5, 1, 1, 1],
|
||||
'failures': [0] * 9, 'compression': [5, 3, 2, 1],
|
||||
}
|
||||
PHP = {
|
||||
'requests': 25, 'status_2xx': 22, 'status_3xx': 0, 'status_4xx': 1, 'status_5xx': 2,
|
||||
'response_headers_us': 1_000_000, 'response_bytes': 2_000,
|
||||
'failures': [1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
}
|
||||
|
||||
def observation(factor: int):
|
||||
web = {key: ([factor * v for v in value] if isinstance(value, list) else factor * value)
|
||||
for key, value in WEB.items()}
|
||||
php = {key: ([factor * v for v in value] if isinstance(value, list) else factor * value)
|
||||
for key, value in PHP.items()}
|
||||
web['active_headers'] = 2
|
||||
return {'revision': 10, 'web_server_metrics': {
|
||||
'site-A': {'web': web, 'php': {'metrics': php, 'capacity_limit': 32, 'capacity_used': 18}}
|
||||
}}
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox'])
|
||||
for width in (320, 375, 800, 1440):
|
||||
page = browser.new_page(viewport={'width': width, 'height': 900})
|
||||
errors = []
|
||||
page.on('pageerror', lambda err: errors.append(str(err)))
|
||||
page.set_content((ROOT / 'src/admin/web/index.html').read_text())
|
||||
page.add_style_tag(content=(ROOT / 'src/admin/web/style.css').read_text())
|
||||
page.add_script_tag(content=BUNDLE)
|
||||
page.evaluate("""() => {
|
||||
document.querySelector('#workspace').hidden = false;
|
||||
document.querySelector('#panel-overview').hidden = false;
|
||||
document.querySelectorAll('[data-dashboard-widget^="www_"]').forEach(n => n.hidden = false);
|
||||
currentPanel = 'overview';
|
||||
window.__clock = 1000;
|
||||
Object.defineProperty(performance, 'now', { configurable: true, value: () => window.__clock });
|
||||
}""")
|
||||
first, second = observation(1), observation(2)
|
||||
state = page.evaluate("""([one, two]) => {
|
||||
const api = window.rustProxyWebOverview;
|
||||
api.render(one);
|
||||
window.__clock = 4000;
|
||||
api.render(two);
|
||||
return {
|
||||
rps: document.getElementById('www_requests-all-value').textContent,
|
||||
php: document.getElementById('www_requests-php-value').textContent,
|
||||
latency: document.getElementById('www_latency-web-value').textContent,
|
||||
capacity: document.getElementById('www_capacity-value').textContent,
|
||||
types: document.getElementById('www_kind-value').textContent,
|
||||
status: document.getElementById('www_status-value').textContent,
|
||||
bytes: document.getElementById('www_traffic-value').textContent,
|
||||
top: document.getElementById('www_top_services-value').textContent,
|
||||
layoutWidth: document.documentElement.scrollWidth,
|
||||
screenWidth: window.innerWidth,
|
||||
children: document.querySelectorAll('[data-dashboard-widget^="www_"]').length,
|
||||
sampleCount: document.getElementById('www_requests-all-line').getAttribute('d'),
|
||||
limit: window.rustProxyOverviewSamples.getLimit(),
|
||||
};
|
||||
}""", [first, second])
|
||||
assert state['children'] == 12, state
|
||||
assert state['rps'] != '-' and state['php'] != '-', state
|
||||
assert 'ms' in state['latency'] and '18' in state['capacity'], state
|
||||
assert state['types'].endswith('%') and state['status'].endswith('%'), state
|
||||
assert '/s' in state['top'], state
|
||||
assert state['layoutWidth'] <= state['screenWidth'] + 2, (width, state)
|
||||
assert not errors, (width, errors)
|
||||
# Exercise the Web Server per-service accordion with the same status,
|
||||
# without any real HTTP/PHP request or extra polling.
|
||||
details = page.evaluate("""(status) => {
|
||||
envelope = { revision: 10, config: {
|
||||
web_services: [{ id: 'site-A', name: 'Example', document_root: '/var/www/site-A',
|
||||
index: ['index.html'], php: { enabled: true } }],
|
||||
vhosts: [], php_fpm_profiles: []
|
||||
}};
|
||||
currentPanel = 'web-server';
|
||||
window.rustProxyWebServer.render();
|
||||
const detail = document.querySelector('#web-service-list .www-service-panel');
|
||||
if (!detail) return { missing: true };
|
||||
detail.open = true;
|
||||
window.rustProxyWebServer.renderLive(status, false);
|
||||
return { text: detail.textContent, rows: detail.querySelectorAll('[data-web-live-field]').length };
|
||||
}""", second)
|
||||
assert not details.get('missing') and details['rows'] == 7, (width, details)
|
||||
assert '18 / 32' in details['text'], (width, details)
|
||||
assert not errors, (width, errors)
|
||||
old = page.evaluate("""() => {
|
||||
currentPanel = 'overview';
|
||||
window.rustProxyWebOverview.render({ revision: 11, web_server_metrics: {} });
|
||||
return {
|
||||
rate: document.getElementById('www_requests-value').textContent,
|
||||
status: document.getElementById('www_status-value').textContent,
|
||||
top: document.getElementById('www_top_services-bars').childElementCount,
|
||||
};
|
||||
}""")
|
||||
assert old == {'rate': '-', 'status': '-', 'top': 0}, (width, old)
|
||||
assert not errors, (width, errors)
|
||||
print(f'PASS simulated Web Server cards and service accordion at {width}px: {state["rps"]}, {state["php"]}, layout {state["layoutWidth"]}px')
|
||||
page.close()
|
||||
browser.close()
|
||||
print('PASS production JS bundle + all twelve visible WWW widgets (simulated data only)')
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.32.8",
|
||||
"version": "0.33.0",
|
||||
"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."
|
||||
},
|
||||
"servers": [
|
||||
|
||||
@@ -142,6 +142,28 @@ fn old_widget_preferences_preserve_order_opt_in_and_survive_unknown_ids() {
|
||||
.dashboard_hidden_widgets
|
||||
.contains(&"extra_process_cpu".into()));
|
||||
assert!(prefs.dashboard_widgets.contains(&"backend_table".into()));
|
||||
// 0.33.0 WWW cards stay opt-in when upgrading an old saved custom layout.
|
||||
for id in [
|
||||
"www_status",
|
||||
"www_kind",
|
||||
"www_capacity",
|
||||
"www_methods",
|
||||
"www_error_rate",
|
||||
"www_php_requests",
|
||||
"www_requests",
|
||||
"www_latency",
|
||||
"www_failures",
|
||||
"www_traffic",
|
||||
"www_top_services",
|
||||
"www_status_trend",
|
||||
] {
|
||||
assert!(prefs.dashboard_widgets.contains(&id.into()), "missing {id}");
|
||||
assert!(
|
||||
prefs.dashboard_hidden_widgets.contains(&id.into()),
|
||||
"visible on upgrade: {id}"
|
||||
);
|
||||
}
|
||||
|
||||
assert!(!prefs
|
||||
.dashboard_hidden_widgets
|
||||
.contains(&"backend_table".into()));
|
||||
|
||||
@@ -53,6 +53,10 @@ use std::{
|
||||
fn admin_app_javascript() -> &'static str {
|
||||
static ASSET: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
|
||||
[
|
||||
"\"use strict\";",
|
||||
// The samples controller is needed by top-level listeners in core.js.
|
||||
// Define it first; DOM initialization and translations happen later.
|
||||
include_str!("web/app/overview-samples.js"),
|
||||
include_str!("web/app/core.js"),
|
||||
include_str!("web/app/session-resume.js"),
|
||||
include_str!("web/app/refresh.js"),
|
||||
@@ -67,6 +71,7 @@ fn admin_app_javascript() -> &'static str {
|
||||
include_str!("web/app/backend-dashboard.js"),
|
||||
include_str!("web/app/overview-extra-metrics.js"),
|
||||
include_str!("web/app/overview-wide-metrics.js"),
|
||||
include_str!("web/app/web-overview.js"),
|
||||
include_str!("web/app/overview-masonry.js"),
|
||||
include_str!("web/app/config-history.js"),
|
||||
include_str!("web/app/runtime.js"),
|
||||
|
||||
@@ -211,6 +211,7 @@ fn status_payload(state: &App) -> Value {
|
||||
.map(|h| h.routes.iter().filter(|r| r.enabled).count())
|
||||
.sum::<usize>());
|
||||
payload["metrics"] = json!(state.metrics.view());
|
||||
payload["web_server_metrics"] = web_server_metrics_payload(&snapshot);
|
||||
payload["transform_metrics"] = json!(state.transform_metrics.total());
|
||||
payload["logs_dropped"] = json!(state.logs.dropped.load(Ordering::Relaxed));
|
||||
payload["software_update"] = json!(state.software_update.snapshot());
|
||||
|
||||
@@ -14,11 +14,39 @@ fn live_chart_system_payload(state: &App) -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
// Snapshot already holds bounded, configured Web Service IDs. Do not expose
|
||||
// paths, FastCGI addresses or PHP INI parameters in a status poll.
|
||||
fn web_server_metrics_payload(snapshot: &crate::config::Snapshot) -> Value {
|
||||
let mut services = serde_json::Map::new();
|
||||
for service in &snapshot.envelope.config.web_services {
|
||||
let Some(compiled) = snapshot.web_php_services.get(&service.id) else {
|
||||
continue;
|
||||
};
|
||||
let php = compiled.php.as_ref().map(|handler| {
|
||||
let available = handler.permits.available_permits();
|
||||
json!({"metrics": handler.metrics.view(), "capacity_limit": handler.max_concurrent,
|
||||
"capacity_available": available, "capacity_used": handler.max_concurrent.saturating_sub(available)})
|
||||
});
|
||||
services.insert(
|
||||
service.id.clone(),
|
||||
json!({"web": compiled.web_metrics.view(), "php": php}),
|
||||
);
|
||||
}
|
||||
Value::Object(services)
|
||||
}
|
||||
|
||||
fn live_status_payload(state: &App) -> Value {
|
||||
let snapshot = state.config.load_full();
|
||||
let stream_metrics = state.active_streams.iter().filter_map(|stream| {
|
||||
state.stream_metrics.get(&stream.id).map(|metrics| (stream.id.clone(), metrics.view()))
|
||||
}).collect::<std::collections::BTreeMap<_, _>>();
|
||||
let stream_metrics = state
|
||||
.active_streams
|
||||
.iter()
|
||||
.filter_map(|stream| {
|
||||
state
|
||||
.stream_metrics
|
||||
.get(&stream.id)
|
||||
.map(|metrics| (stream.id.clone(), metrics.view()))
|
||||
})
|
||||
.collect::<std::collections::BTreeMap<_, _>>();
|
||||
json!({
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
"uptime_seconds": state.started.elapsed().as_secs(),
|
||||
@@ -29,6 +57,7 @@ fn live_status_payload(state: &App) -> Value {
|
||||
"restart_required": snapshot.listeners != state.active_listeners
|
||||
},
|
||||
"metrics": state.metrics.view(),
|
||||
"web_server_metrics": web_server_metrics_payload(&snapshot),
|
||||
"live_system": live_chart_system_payload(state),
|
||||
"cache": state.cache.view(),
|
||||
"transform_metrics": state.transform_metrics.total(),
|
||||
@@ -47,11 +76,17 @@ struct StatusQuery {
|
||||
view: Option<String>,
|
||||
}
|
||||
|
||||
async fn status(State(state): State<Arc<App>>, Query(query): Query<StatusQuery>) -> Result<Json<Value>, ApiError> {
|
||||
async fn status(
|
||||
State(state): State<Arc<App>>,
|
||||
Query(query): Query<StatusQuery>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
match query.view.as_deref() {
|
||||
None => Ok(Json(status_payload(&state))),
|
||||
Some("live") => Ok(Json(live_status_payload(&state))),
|
||||
_ => Err(ApiError(StatusCode::BAD_REQUEST, "invalid status view".into())),
|
||||
_ => Err(ApiError(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"invalid status view".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// Optional HTTP origin diagnostics. Reuse status.backend_pools already present
|
||||
// in live SSE/polling; never issue one request per tile or per member.
|
||||
window.rustProxyBackendDashboard = (() => {
|
||||
const ids = ["backend_health", "backend_activity", "backend_errors", "backend_latency", "backend_traffic", "backend_protocols", "backend_requests", "backend_capacity", "backend_http5xx", "backend_timeouts", "backend_network", "backend_retries", "backend_queue", "backend_rotations"];
|
||||
const charts = new Set(["backend_activity", "backend_errors", "backend_latency", "backend_traffic", "backend_requests", "backend_capacity", "backend_http5xx", "backend_timeouts", "backend_network", "backend_retries", "backend_queue"]);
|
||||
@@ -34,7 +32,7 @@ window.rustProxyBackendDashboard = (() => {
|
||||
}
|
||||
function push(name, value) {
|
||||
samples[name].push(Math.max(0, value));
|
||||
if (samples[name].length > 30) samples[name].shift();
|
||||
if (samples[name].length > window.rustProxyOverviewSamples.getLimit()) samples[name].shift();
|
||||
}
|
||||
function unavailable() {
|
||||
for (const id of ids) {
|
||||
@@ -47,6 +45,7 @@ window.rustProxyBackendDashboard = (() => {
|
||||
$(id)?.setAttribute("points", "0,55 300,55");
|
||||
const bar = $("backend-mode-h1");
|
||||
if (bar) for (const item of bar.parentElement.children) item.style.flexGrow = "0";
|
||||
window.rustProxyExtraOverview?.meter?.("backend-dashboard-capacity", null);
|
||||
reset();
|
||||
}
|
||||
// Counter deltas are matched to stable (pool, member) IDs, not aggregate
|
||||
@@ -177,7 +176,7 @@ window.rustProxyBackendDashboard = (() => {
|
||||
set("backend-dashboard-capacity", capacityPercent === null ? "-" : `${fmt(capacityPercent, 1)}%`);
|
||||
set("backend-dashboard-capacity-detail", capacityPercent === null ? t("backendWidgetCapacityNone") :
|
||||
`${t("backendWidgetCapacityLimits")}: ${limited.length} · ${capacityUsed}/${capacityLimit} · ${t("backendWidgetCapacityRejects")}: ${fmt(capacityRejects)}`);
|
||||
paintSeries("capacity", "backend-dashboard-capacity-line", Math.max(100, ...samples.capacity));
|
||||
window.rustProxyExtraOverview?.meter?.("backend-dashboard-capacity", capacityPercent);
|
||||
}
|
||||
if (visible("backend_errors")) {
|
||||
set("backend-dashboard-errors", fmt(totalErrors));
|
||||
@@ -218,5 +217,9 @@ window.rustProxyBackendDashboard = (() => {
|
||||
paintSeries(name, lineId, Math.max(1, ...samples[name]));
|
||||
}
|
||||
}
|
||||
window.rustProxyOverviewSamples.onChange(() => {
|
||||
for (const values of Object.values(samples)) window.rustProxyOverviewSamples.trim(values);
|
||||
if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview") render(latestStatus, null, false);
|
||||
});
|
||||
return { syncVisibility, render, reset };
|
||||
})();
|
||||
+44
-49
@@ -27,8 +27,6 @@ let samples = [], protocolSamples = { h1: [], h2: [], h3: [] }, websocketProtoco
|
||||
let liveEvents = null, liveConnected = false, liveFallback = false, liveInterval = null, authEpoch = 0, pendingAuthDestination = null;
|
||||
let lastApiLatencyMs = null, sessionCheckPromise = null;
|
||||
let adminAuthChannel = null; try { adminAuthChannel = new BroadcastChannel("rustproxy-admin-session"); adminAuthChannel.addEventListener("message", event => { if (event.data === "locked" && authenticated) lock(false); }); } catch { }
|
||||
// Language content is discovered by build.rs from langs/*.json, never compiled
|
||||
// into a two-language switch here. Locale metadata also drives Intl and RTL.
|
||||
let languagePacks = new Map(), defaultLanguage = null;
|
||||
function activePack() { return languagePacks.get(language) || languagePacks.get(defaultLanguage); }
|
||||
function locale() { try { return Intl.getCanonicalLocales(activePack()?.meta.locale || "und")[0]; } catch { return "und"; } }
|
||||
@@ -59,15 +57,13 @@ function translate() {
|
||||
window.rustProxyDrafts?.refresh();
|
||||
window.rustProxyPreferences?.refreshAppearanceUi?.();
|
||||
$("connection-label").textContent = t(connected ? "connected" : "disconnected");
|
||||
if (envelope) renderConfig(); if (latestStatus) { renderFooter(latestStatus); renderDiagnostics(latestStatus, false); window.rustProxyVhosts?.renderStatus(latestStatus); window.rustProxyStreams?.renderStatus(latestStatus); window.rustProxyStreamDashboard?.render(latestStatus); window.rustProxyBackendDashboard?.render(latestStatus, null, false); window.rustProxyExtraOverview?.render(latestStatus, false); window.rustProxyWideOverview?.render(latestStatus, false); } if (lastLogs) renderLogs(lastLogs);
|
||||
if (envelope) renderConfig(); if (latestStatus) { renderFooter(latestStatus); renderDiagnostics(latestStatus, false); window.rustProxyVhosts?.renderStatus(latestStatus); window.rustProxyStreams?.renderStatus(latestStatus); window.rustProxyStreamDashboard?.render(latestStatus); window.rustProxyBackendDashboard?.render(latestStatus, null, false); window.rustProxyExtraOverview?.render(latestStatus, false); window.rustProxyWideOverview?.render(latestStatus, false); window.rustProxyWebOverview?.render(latestStatus, false); } if (lastLogs) renderLogs(lastLogs);
|
||||
syncSettingsDirtyIndicators();
|
||||
}
|
||||
function hideToast() { const node = $("toast"); if (typeof node.hidePopover === "function" && node.matches(":popover-open")) node.hidePopover(); node.hidden = true; }
|
||||
function raiseToast() {
|
||||
const node = $("toast"); if (node.hidden) return;
|
||||
const modal = [...document.querySelectorAll("dialog[open]")].at(-1), parent = modal || document.body;
|
||||
// A popover outside a modal is still inert. Keep it inside the active dialog,
|
||||
// then lift it to the top layer so its backdrop cannot blur or cover it.
|
||||
if (typeof node.hidePopover === "function" && node.matches(":popover-open")) node.hidePopover();
|
||||
if (node.parentElement !== parent) parent.append(node);
|
||||
if (typeof node.showPopover === "function") node.showPopover();
|
||||
@@ -154,19 +150,16 @@ function setPanel(name, navigate = true) {
|
||||
const previousPanel = currentPanel; if (name !== "overview" && previousPanel === "overview") window.rustProxyInlineLayout?.cancel?.();
|
||||
currentPanel = name;
|
||||
$("overview-refresh").hidden = name !== "overview";
|
||||
$("overview-samples-toolbar").hidden = name !== "overview";
|
||||
for (const id of ["overview-layout-open", "overview-layout-unlock"]) if ($(id)) $(id).hidden = name !== "overview";
|
||||
document.querySelectorAll(".nav").forEach(e => e.classList.toggle("active", e.dataset.panel === name));
|
||||
document.querySelectorAll(".panel").forEach(e => e.hidden = e.id !== `panel-${name}`);
|
||||
$("page-label").textContent = t(name); $("page-title").textContent = t(`${name}Title`); $("page-description").textContent = t(`${name}Description`);
|
||||
if (navigate && typeof location !== "undefined" && ["http:", "https:"].includes(location.protocol) && location.pathname !== PANEL_PATHS[name]) history.pushState({ panel: name }, "", PANEL_PATHS[name]);
|
||||
if (name === "web-server" && latestStatus) window.rustProxyWebServer?.renderLive?.(latestStatus, false);
|
||||
if (name === "settings") window.rustProxySettings?.syncLocation?.();
|
||||
window.rustProxyRefresh?.setPanel(name);
|
||||
// Paint Stream tiles immediately on return from Appearance using cached status.
|
||||
// Do not create an extra sample/poll on navigation.
|
||||
if (name === "overview" && previousPanel !== name && typeof latestStatus !== "undefined" && latestStatus)
|
||||
{ window.rustProxyStreamDashboard?.render?.(latestStatus, null, false); window.rustProxyBackendDashboard?.render?.(latestStatus, null, false); window.rustProxyExtraOverview?.render?.(latestStatus, false); window.rustProxyWideOverview?.render?.(latestStatus, false); }
|
||||
// Read the cached Docker status on each visit, independent of the Live-view toggle.
|
||||
// This is GET /api/docker, not the POST that triggers an agent poll.
|
||||
if (name === "overview" && previousPanel !== name && typeof latestStatus !== "undefined" && latestStatus) { window.rustProxyStreamDashboard?.render?.(latestStatus, null, false); window.rustProxyBackendDashboard?.render?.(latestStatus, null, false); window.rustProxyExtraOverview?.render?.(latestStatus, false); window.rustProxyWideOverview?.render?.(latestStatus, false); window.rustProxyWebOverview?.render?.(latestStatus, false); }
|
||||
if (name === "docker-vhosts" && previousPanel !== name && authenticated && envelope) void window.rustProxyRefresh?.manual("docker");
|
||||
if (name === "tls") window.rustProxyAcme?.syncLocation?.();
|
||||
if (name === "filelogs") void window.rustProxyFileLogs?.refresh();
|
||||
@@ -203,9 +196,35 @@ function enhanceEnabledControl(input, hintKey = "entityEnableHint") {
|
||||
}
|
||||
window.rustProxyEnableControl = enhanceEnabledControl;
|
||||
function action(label, fn) { const b = el("button", label, "quiet"); b.type = "button"; b.addEventListener("click", fn); return b; }
|
||||
function redrawOverviewHistoryCharts() {
|
||||
const connectionPeak = Math.max(1, ...connectionOpenSamples, ...connectionExchangeSamples);
|
||||
const connectionPoints = values => values.map((value, index) => `${index * 300 / Math.max(1, values.length - 1)},${95 - value / connectionPeak * 85}`).join(" ");
|
||||
$("connection-open-sparkline").setAttribute("points", connectionPoints(connectionOpenSamples));
|
||||
$("connection-exchange-sparkline").setAttribute("points", connectionPoints(connectionExchangeSamples));
|
||||
const peak = Math.max(1, ...samples, ...transformSamples);
|
||||
const points = values => values.map((n, i) => `${i * 600 / Math.max(1, values.length - 1)},${120 - n / peak * 105}`).join(" ");
|
||||
$("sparkline").setAttribute("points", points(samples));
|
||||
$("sparkline-h1").setAttribute("points", points(protocolSamples.h1));
|
||||
$("sparkline-h2").setAttribute("points", points(protocolSamples.h2));
|
||||
$("sparkline-h3").setAttribute("points", points(protocolSamples.h3));
|
||||
$("sparkline-transform").setAttribute("points", points(transformSamples));
|
||||
for (const key of ["h1", "h2", "h3"]) {
|
||||
$("http-protocol-" + key).setAttribute("points", sparklinePoints(protocolSamples[key], 300, 60, 5));
|
||||
$("ws-protocol-" + key).setAttribute("points", sparklinePoints(websocketProtocolSamples[key], 300, 60, 5));
|
||||
}
|
||||
$("chart-peak").textContent = `max ${Math.max(0, ...samples).toFixed(1)} req/s`;
|
||||
}
|
||||
window.rustProxyOverviewSamples.onChange(() => {
|
||||
for (const history of [samples, transformSamples, connectionOpenSamples, connectionExchangeSamples,
|
||||
...Object.values(protocolSamples), ...Object.values(websocketProtocolSamples)]) {
|
||||
window.rustProxyOverviewSamples.trim(history);
|
||||
}
|
||||
redrawOverviewHistoryCharts();
|
||||
});
|
||||
function renderStatus(s, systemSample = false) {
|
||||
const firstStatus = latestStatus === null;
|
||||
latestStatus = { ...(latestStatus || {}), ...s,
|
||||
latestStatus = {
|
||||
...(latestStatus || {}), ...s,
|
||||
streams: s.streams ? { ...(latestStatus?.streams || {}), ...s.streams } : latestStatus?.streams,
|
||||
listeners: s.listeners ? { ...(latestStatus?.listeners || {}), ...s.listeners } : latestStatus?.listeners,
|
||||
logging: s.logging ? { ...(latestStatus?.logging || {}), ...s.logging } : latestStatus?.logging
|
||||
@@ -220,12 +239,8 @@ function renderStatus(s, systemSample = false) {
|
||||
$("metric-h2").textContent = m.http2_requests || 0; $("metric-h2-active").textContent = m.http2_active_connections || 0; $("metric-tls-errors").textContent = m.tls_handshake_errors || 0;
|
||||
$("metric-requests").textContent = m.requests.toLocaleString(locale()); $("metric-active").textContent = m.active; $("metric-bytes").textContent = bytes(m.bytes_in + m.bytes_out); $("metric-bytes-down").textContent = bytes(m.bytes_out); $("metric-bytes-up").textContent = bytes(m.bytes_in); $("metric-errors").textContent = m.errors;
|
||||
const openConnections = Math.max(0, Number(m.active_connections) || 0), activeExchanges = Math.max(0, Number(m.active) || 0);
|
||||
connectionOpenSamples.push(openConnections); connectionOpenSamples = connectionOpenSamples.slice(-30);
|
||||
connectionExchangeSamples.push(activeExchanges); connectionExchangeSamples = connectionExchangeSamples.slice(-30);
|
||||
const connectionPeak = Math.max(1, ...connectionOpenSamples, ...connectionExchangeSamples);
|
||||
const connectionPoints = values => values.map((value, index) => `${index * 300 / Math.max(1, values.length - 1)},${95 - value / connectionPeak * 85}`).join(" ");
|
||||
$("connection-open-sparkline").setAttribute("points", connectionPoints(connectionOpenSamples));
|
||||
$("connection-exchange-sparkline").setAttribute("points", connectionPoints(connectionExchangeSamples));
|
||||
connectionOpenSamples.push(openConnections); connectionOpenSamples = connectionOpenSamples.slice(-window.rustProxyOverviewSamples.getLimit());
|
||||
connectionExchangeSamples.push(activeExchanges); connectionExchangeSamples = connectionExchangeSamples.slice(-window.rustProxyOverviewSamples.getLimit());
|
||||
$("connection-current").textContent = fmt(openConnections); $("connection-open-value").textContent = fmt(openConnections); $("connection-exchange-value").textContent = fmt(activeExchanges); $("connection-backends").textContent = fmt(s.backends);
|
||||
const now = performance.now();
|
||||
const transformed = s.transform_metrics || {};
|
||||
@@ -234,38 +249,25 @@ function renderStatus(s, systemSample = false) {
|
||||
const pausedRate = previous !== null && rateSample.deltas === null; previous = rateSample.next;
|
||||
const rates = Object.fromEntries(keys.map((key, i) => [key, rateSample.deltas ? rateSample.deltas[i] / rateSample.seconds : 0]));
|
||||
if (!pausedRate) {
|
||||
samples.push(rates.total); samples = samples.slice(-30);
|
||||
for (const key of ["h1", "h2", "h3"]) { protocolSamples[key].push(rates[key]); protocolSamples[key] = protocolSamples[key].slice(-30); }
|
||||
samples.push(rates.total); samples = samples.slice(-window.rustProxyOverviewSamples.getLimit());
|
||||
for (const key of ["h1", "h2", "h3"]) { protocolSamples[key].push(rates[key]); protocolSamples[key] = protocolSamples[key].slice(-window.rustProxyOverviewSamples.getLimit()); }
|
||||
}
|
||||
websocketProtocolSamples.h1.push(Number(m.websocket_h1_active) || 0); websocketProtocolSamples.h1 = websocketProtocolSamples.h1.slice(-30);
|
||||
websocketProtocolSamples.h2.push(Number(m.websocket_h2_active) || 0); websocketProtocolSamples.h2 = websocketProtocolSamples.h2.slice(-30);
|
||||
websocketProtocolSamples.h3.push(Number(m.websocket_h3_active) || 0); websocketProtocolSamples.h3 = websocketProtocolSamples.h3.slice(-30);
|
||||
if (!pausedRate) { transformSamples.push(rates.transform); transformSamples = transformSamples.slice(-30); }
|
||||
const peak = Math.max(1, ...samples, ...transformSamples);
|
||||
const points = values => values.map((n, i) => `${i * 600 / Math.max(1, values.length - 1)},${120 - n / peak * 105}`).join(" ");
|
||||
$("sparkline").setAttribute("points", points(samples));
|
||||
$("sparkline-h1").setAttribute("points", points(protocolSamples.h1));
|
||||
$("sparkline-h2").setAttribute("points", points(protocolSamples.h2));
|
||||
$("sparkline-h3").setAttribute("points", points(protocolSamples.h3));
|
||||
$("sparkline-transform").setAttribute("points", points(transformSamples));
|
||||
websocketProtocolSamples.h1.push(Number(m.websocket_h1_active) || 0); websocketProtocolSamples.h1 = websocketProtocolSamples.h1.slice(-window.rustProxyOverviewSamples.getLimit());
|
||||
websocketProtocolSamples.h2.push(Number(m.websocket_h2_active) || 0); websocketProtocolSamples.h2 = websocketProtocolSamples.h2.slice(-window.rustProxyOverviewSamples.getLimit());
|
||||
websocketProtocolSamples.h3.push(Number(m.websocket_h3_active) || 0); websocketProtocolSamples.h3 = websocketProtocolSamples.h3.slice(-window.rustProxyOverviewSamples.getLimit());
|
||||
if (!pausedRate) { transformSamples.push(rates.transform); transformSamples = transformSamples.slice(-window.rustProxyOverviewSamples.getLimit()); }
|
||||
redrawOverviewHistoryCharts();
|
||||
const rateText = key => pausedRate ? "-" : `${rates[key].toFixed(1)}/s`;
|
||||
$("request-rate").textContent = pausedRate ? "- req/s" : `${rates.total.toFixed(1)} req/s`;
|
||||
$("rate-total").textContent = rateText("total"); $("rate-h1").textContent = rateText("h1"); $("rate-h2").textContent = rateText("h2"); $("rate-h3").textContent = rateText("h3"); $("rate-transform").textContent = rateText("transform");
|
||||
$("metric-http-proto-total").textContent = rateText("total"); $("metric-http-proto-h1").textContent = rateText("h1");
|
||||
$("metric-http-proto-h2").textContent = rateText("h2"); $("metric-http-proto-h3").textContent = rateText("h3");
|
||||
$("http-protocol-h1").setAttribute("points", sparklinePoints(protocolSamples.h1, 300, 60, 5));
|
||||
$("http-protocol-h2").setAttribute("points", sparklinePoints(protocolSamples.h2, 300, 60, 5));
|
||||
$("http-protocol-h3").setAttribute("points", sparklinePoints(protocolSamples.h3, 300, 60, 5));
|
||||
const wsTotalActive = (Number(m.websocket_h1_active) || 0) + (Number(m.websocket_h2_active) || 0) + (Number(m.websocket_h3_active) || 0);
|
||||
$("metric-ws-proto-total").textContent = wsTotalActive.toLocaleString(locale());
|
||||
$("metric-ws-proto-h1").textContent = (Number(m.websocket_h1_active) || 0).toLocaleString(locale());
|
||||
$("metric-ws-proto-h2").textContent = (Number(m.websocket_h2_active) || 0).toLocaleString(locale());
|
||||
$("metric-ws-proto-h3").textContent = (Number(m.websocket_h3_active) || 0).toLocaleString(locale());
|
||||
$("ws-protocol-h1").setAttribute("points", sparklinePoints(websocketProtocolSamples.h1, 300, 60, 5));
|
||||
$("ws-protocol-h2").setAttribute("points", sparklinePoints(websocketProtocolSamples.h2, 300, 60, 5));
|
||||
$("ws-protocol-h3").setAttribute("points", sparklinePoints(websocketProtocolSamples.h3, 300, 60, 5));
|
||||
$("chart-peak").textContent = `max ${Math.max(0, ...samples).toFixed(1)} req/s`;
|
||||
renderFooter(s); renderDiagnostics(s, true, systemSample); window.rustProxyVhosts?.renderStatus(s); window.rustProxyStreams?.renderStatus(s); window.rustProxyStreamDashboard?.render(s); window.rustProxyBackendDashboard?.render(s); window.rustProxyExtraOverview?.render(s); window.rustProxyWideOverview?.render(s);
|
||||
renderFooter(s); renderDiagnostics(s, true, systemSample); window.rustProxyVhosts?.renderStatus(s); window.rustProxyStreams?.renderStatus(s); window.rustProxyStreamDashboard?.render(s); window.rustProxyBackendDashboard?.render(s); window.rustProxyExtraOverview?.render(s); window.rustProxyWideOverview?.render(s); window.rustProxyWebOverview?.render(s); window.rustProxyWebServer?.renderLive?.(s);
|
||||
}
|
||||
function renderFooter(s) {
|
||||
const apiLatency = lastApiLatencyMs == null ? "API -" : `API ${lastApiLatencyMs.toFixed(lastApiLatencyMs < 10 ? 1 : 0)} ms`;
|
||||
@@ -370,17 +372,16 @@ function authReturnDestination() {
|
||||
const tab = window.rustProxySettings?.current?.() || new URLSearchParams(location.search).get("tab");
|
||||
return tab ? `/settings?tab=${encodeURIComponent(tab)}` : "/settings";
|
||||
}
|
||||
function lock(notify = true) { window.rustProxyInlineLayout?.cancel?.();
|
||||
function lock(notify = true) {
|
||||
window.rustProxyInlineLayout?.cancel?.();
|
||||
window.rustProxyRefresh?.stop();
|
||||
const destination = pendingAuthDestination || authReturnDestination(), navigating = typeof location !== "undefined" && (location.protocol === "http:" || location.protocol === "https:");
|
||||
pendingAuthDestination = destination; if (notify) adminAuthChannel?.postMessage("locked"); $("api-docs").hidden = true; $("logs-menu").hidden = true; resetAdminPreferences(); window.rustProxyCertificates?.reset(); window.rustProxySettings?.reset(); window.rustProxyTrafficPanel?.reset(); window.rustProxyCachePanel?.reset(); window.rustProxyTransformPanel?.reset(); window.rustProxyAcme?.reset(); window.rustProxyIntegrations?.reset(); window.rustProxyLogs?.reset(); window.rustProxyFileLogs?.reset(); window.rustProxyDrafts?.reset(); $("logs-open").hidden = true; $("file-logs-open").hidden = true; window.rustProxyAclProfiles?.reset(); window.rustProxyVhosts?.reset(); window.rustProxyWebServer?.reset(); window.rustProxyDocker?.reset(); window.rustProxyRedirects?.reset(); window.rustProxyNotFound?.reset(); window.rustProxyStreams?.reset(); window.rustProxyStreamDashboard?.reset(); window.rustProxyBackendDashboard?.reset(); window.rustProxyExtraOverview?.reset(); window.rustProxyWideOverview?.reset(); authEpoch++; stopLive(); csrf = ""; authenticated = false; sessionInfo = null; envelope = null; certificateInventory = new Map(); previous = null; samples = []; protocolSamples = { h1: [], h2: [], h3: [] }; transformSamples = []; systemSamples = []; connectionOpenSamples = []; connectionExchangeSamples = []; networkInterfaceSamples.clear(); lastLogs = null; latestStatus = null; clearSettingsDirty(); connected = false; busy = false; $("workspace").hidden = true; $("login").hidden = navigating; $("logout").hidden = true; $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("password-form").reset(); $("config-editor").value = ""; $("logs").replaceChildren(); $("audit-rows").replaceChildren(); authMessage("");
|
||||
pendingAuthDestination = destination; if (notify) adminAuthChannel?.postMessage("locked"); $("api-docs").hidden = true; $("logs-menu").hidden = true; resetAdminPreferences(); window.rustProxyCertificates?.reset(); window.rustProxySettings?.reset(); window.rustProxyTrafficPanel?.reset(); window.rustProxyCachePanel?.reset(); window.rustProxyTransformPanel?.reset(); window.rustProxyAcme?.reset(); window.rustProxyIntegrations?.reset(); window.rustProxyLogs?.reset(); window.rustProxyFileLogs?.reset(); window.rustProxyDrafts?.reset(); $("logs-open").hidden = true; $("file-logs-open").hidden = true; window.rustProxyAclProfiles?.reset(); window.rustProxyVhosts?.reset(); window.rustProxyWebServer?.reset(); window.rustProxyDocker?.reset(); window.rustProxyRedirects?.reset(); window.rustProxyNotFound?.reset(); window.rustProxyStreams?.reset(); window.rustProxyStreamDashboard?.reset(); window.rustProxyBackendDashboard?.reset(); window.rustProxyExtraOverview?.reset(); window.rustProxyWideOverview?.reset(); window.rustProxyWebOverview?.reset(); authEpoch++; stopLive(); csrf = ""; authenticated = false; sessionInfo = null; envelope = null; certificateInventory = new Map(); previous = null; samples = []; protocolSamples = { h1: [], h2: [], h3: [] }; websocketProtocolSamples = { h1: [], h2: [], h3: [] }; transformSamples = []; systemSamples = []; connectionOpenSamples = []; connectionExchangeSamples = []; networkInterfaceSamples.clear(); lastLogs = null; latestStatus = null; clearSettingsDirty(); connected = false; busy = false; $("workspace").hidden = true; $("login").hidden = navigating; $("logout").hidden = true; $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("password-form").reset(); $("config-editor").value = ""; $("logs").replaceChildren(); $("audit-rows").replaceChildren(); authMessage("");
|
||||
for (const id of ["certificate-list", "host-policy-list"]) { $(id)?.replaceChildren(); }
|
||||
$("workspace").querySelectorAll("form").forEach(form => form.reset());
|
||||
$("workspace").querySelectorAll("input:not([type=checkbox]):not([type=radio]),textarea").forEach(input => { input.value = ""; });
|
||||
document.querySelectorAll("dialog").forEach(dialog => { dialog.close(); dialog.querySelectorAll("input:not([type=checkbox]):not([type=radio]),textarea").forEach(input => input.value = ""); });
|
||||
renderSession(); syncAuthForm();
|
||||
// Replace the authenticated document with the minimal public login document.
|
||||
// Keep the exact panel/settings tab so re-authentication resumes in place.
|
||||
if (navigating) { document.documentElement.classList.add("app-loading"); location.replace(destination); }
|
||||
}
|
||||
|
||||
@@ -392,8 +393,6 @@ async function activate(info) {
|
||||
for (const id of ["password", "password-confirm", "setup-token"]) $(id).value = "";
|
||||
$("login").hidden = true; $("workspace").hidden = false; $("logout").hidden = false;
|
||||
renderSession();
|
||||
// Paint a usable shell immediately. Preferences, configuration and initial
|
||||
// telemetry are network requests and must not block the first content.
|
||||
document.documentElement.classList.remove("app-loading");
|
||||
document.documentElement.classList.remove("app-startup-failed");
|
||||
rustProxyRefresh.start();
|
||||
@@ -415,8 +414,6 @@ async function activate(info) {
|
||||
}
|
||||
}
|
||||
if (!authenticated || epoch !== authEpoch) return false;
|
||||
// A single full snapshot seeds non-live diagnostics; subsequent updates
|
||||
// continue over SSE rather than polling full status on a timer.
|
||||
void refresh(true).catch(error => {
|
||||
if (authenticated && epoch === authEpoch && error?.status !== 401) toast(error.message, true);
|
||||
});
|
||||
@@ -470,8 +467,6 @@ function setMobileMenu(open) {
|
||||
}
|
||||
mobileMenuToggle?.addEventListener("click", event => { event.stopPropagation(); setMobileMenu(!dashboardSidebar.classList.contains("mobile-nav-open")); });
|
||||
document.querySelectorAll(".nav").forEach(button => button.addEventListener("click", () => { setPanel(button.dataset.panel); setMobileMenu(false); }));
|
||||
// The brand is a Home navigation action, not a hard reload of the HTTP/3
|
||||
// document. Preserve href="/" as the no-JS and modified-click fallback.
|
||||
document.querySelector('.brand[href="/"]')?.addEventListener("click", event => {
|
||||
if (!authenticated || event.defaultPrevented || event.button !== 0 ||
|
||||
event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
|
||||
|
||||
@@ -1,25 +1,69 @@
|
||||
// Opt-in compact metrics and bounded visualizations. All readings reuse the
|
||||
// existing Overview live-status payload (no additional requests or timers).
|
||||
window.rustProxyExtraOverview = (() => {
|
||||
const limit = 30;
|
||||
const limit = () => window.rustProxyOverviewSamples?.getLimit?.() ?? 60;
|
||||
const names = [
|
||||
"extra_process_cpu", "extra_process_rss", "extra_process_fds", "extra_open_connections",
|
||||
"extra_websocket_active", "extra_tls_error_rate", "extra_request_error_rate",
|
||||
"extra_cache_hit_rate", "extra_cache_memory", "extra_h2_share",
|
||||
"extra_cache_hit_rate", "extra_cache_memory", "extra_cold_queue_memory", "extra_cold_storage", "extra_h2_share",
|
||||
"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",
|
||||
];
|
||||
const histories = new Map();
|
||||
const lastCounter = new Map();
|
||||
const lastValue = new Map();
|
||||
const lastUnits = new Map();
|
||||
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",
|
||||
]);
|
||||
const histories = new Map(), lastCounter = new Map(), lastValue = new Map();
|
||||
const lastUnits = new Map(), lastSegments = new Map();
|
||||
const positive = value => value != null && Number.isFinite(Number(value)) && Number(value) >= 0 ? Number(value) : null;
|
||||
const node = id => document.querySelector(`[data-dashboard-widget="${id}"]`);
|
||||
function meter(id, percent) {
|
||||
const track = $(`${id}-meter`), fill = $(`${id}-meter-fill`);
|
||||
if (!track || !fill) return;
|
||||
const valid = positive(percent), boundedPercent = valid === null ? 0 : Math.min(100, valid);
|
||||
fill.style.width = `${boundedPercent}%`;
|
||||
if (valid === null) {
|
||||
track.removeAttribute("aria-valuenow");
|
||||
track.setAttribute("aria-valuetext", "-");
|
||||
} else {
|
||||
track.setAttribute("aria-valuenow", String(boundedPercent));
|
||||
track.setAttribute("aria-valuetext", `${fmt(valid, 1)}%`);
|
||||
}
|
||||
}
|
||||
function donut(prefix, hits, misses) {
|
||||
const hit = positive(hits), miss = positive(misses);
|
||||
const total = hit !== null && miss !== null ? hit + miss : 0;
|
||||
const share = total > 0 ? hit / total * 100 : 0;
|
||||
const hitArc = $(`${prefix}-hit-arc`), missArc = $(`${prefix}-miss-arc`);
|
||||
if (hitArc) hitArc.setAttribute("stroke-dasharray", `${share} ${100 - share}`);
|
||||
if (missArc) {
|
||||
missArc.setAttribute("stroke-dasharray", `${100 - share} ${share}`);
|
||||
missArc.setAttribute("stroke-dashoffset", String(-share));
|
||||
}
|
||||
const hitCount = $(`${prefix}-hit-count`), missCount = $(`${prefix}-miss-count`);
|
||||
if (hitCount) hitCount.textContent = hit === null ? "-" : fmt(hit);
|
||||
if (missCount) missCount.textContent = miss === null ? "-" : fmt(miss);
|
||||
const graphic = $(`${prefix}-chart`);
|
||||
if (graphic) graphic.setAttribute("aria-label", t("overviewCacheHitsMisses")
|
||||
.replace("{hits}", hit === null ? "-" : fmt(hit))
|
||||
.replace("{misses}", miss === null ? "-" : fmt(miss)));
|
||||
}
|
||||
function elapsedDelta(id, counters, now) {
|
||||
const old = lastCounter.get(id);
|
||||
const result = rustProxyRateSampling.difference(old, counters, now, rustProxyRateSampling.cadence("overview_seconds"));
|
||||
const result = rustProxyRateSampling.difference(lastCounter.get(id), counters, now,
|
||||
rustProxyRateSampling.cadence("overview_seconds"));
|
||||
lastCounter.set(id, result.next);
|
||||
return result.deltas ? result : null;
|
||||
}
|
||||
function ramBudget(bytes, budget) {
|
||||
const used = positive(bytes), maximum = positive(budget);
|
||||
const percent = maximum > 0 && used !== null ? used / maximum * 100 : null;
|
||||
return {
|
||||
value: percent === null ? used : percent,
|
||||
unit: percent === null ? "bytes" : "percent", meterPercent: percent,
|
||||
detail: used === null ? "" : `${fmtBytes(used)} / ${maximum > 0 ? fmtBytes(maximum) : t("overviewMeterNoLimit")}`,
|
||||
};
|
||||
}
|
||||
function read(id, status, now, record) {
|
||||
const m = status.metrics || {}, d = status.system || {}, live = status.live_system || d, c = status.cache || {};
|
||||
const direct = {
|
||||
@@ -32,33 +76,28 @@ window.rustProxyExtraOverview = (() => {
|
||||
extra_active_exchanges: m.active,
|
||||
};
|
||||
if (Object.hasOwn(direct, id)) {
|
||||
const numeric = positive(direct[id]);
|
||||
const numeric = positive(direct[id]), fdLimit = positive(d.max_fds);
|
||||
return {
|
||||
value: numeric, unit: id === "extra_process_cpu" ? "percent" : id === "extra_process_rss" ? "bytes" : id === "extra_response_latency" ? "ms" : "count",
|
||||
detail: id === "extra_process_fds" && positive(d.max_fds) ? `${t("overviewExtraFdLimit")}: ${fmt(d.max_fds)}` : ""
|
||||
value: numeric, unit: id === "extra_process_cpu" ? "percent" : id === "extra_process_rss" ? "bytes"
|
||||
: id === "extra_response_latency" ? "ms" : "count",
|
||||
meterPercent: id === "extra_process_fds" && fdLimit > 0 && numeric !== null ? numeric / fdLimit * 100 : null,
|
||||
detail: id === "extra_process_fds"
|
||||
? (fdLimit > 0 ? `${t("overviewExtraFdLimit")}: ${fmt(fdLimit)}` : t("overviewMeterNoLimit")) : "",
|
||||
};
|
||||
}
|
||||
if (id === "extra_backend_active") {
|
||||
const members = Array.isArray(status.backend_pools) ? status.backend_pools.flatMap(pool => pool.members || []) : null;
|
||||
return {
|
||||
value: members ? members.reduce((sum, member) => sum + (positive(member.active) ?? 0), 0) : null,
|
||||
unit: "count", detail: ""
|
||||
};
|
||||
return { value: members ? members.reduce((sum, member) => sum + (positive(member.active) ?? 0), 0) : null,
|
||||
unit: "count", detail: "" };
|
||||
}
|
||||
if (id === "extra_backend_unhealthy") {
|
||||
return {
|
||||
value: Array.isArray(status.backend_pools) ? status.backend_pools.reduce((sum, pool) =>
|
||||
sum + (pool.members || []).filter(member => member.status === "down").length, 0) : null, unit: "count", detail: ""
|
||||
};
|
||||
}
|
||||
if (id === "extra_cache_memory") {
|
||||
const bytes = positive(c.resident_bytes), budget = positive(c.hot_limit_bytes);
|
||||
const percent = budget && bytes !== null ? bytes / budget * 100 : null;
|
||||
return {
|
||||
value: percent === null ? bytes : percent, unit: percent === null ? "bytes" : "percent",
|
||||
detail: bytes === null ? "" : `${fmtBytes(bytes)}${budget ? ` / ${fmtBytes(budget)}` : ""}`
|
||||
};
|
||||
return { value: Array.isArray(status.backend_pools) ? status.backend_pools.reduce((sum, pool) =>
|
||||
sum + (pool.members || []).filter(member => member.status === "down").length, 0) : null,
|
||||
unit: "count", detail: "" };
|
||||
}
|
||||
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"].includes(id))
|
||||
return { value: null, unit: "count", detail: "" };
|
||||
@@ -74,14 +113,18 @@ window.rustProxyExtraOverview = (() => {
|
||||
})[id].map(positive);
|
||||
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); return { value: null, unit, detail: "" }; }
|
||||
if (!record) return { value: lastValue.get(id) ?? null, unit, detail: "" };
|
||||
if (fields.some(v => v === null)) {
|
||||
lastCounter.delete(id);
|
||||
return { value: null, unit, detail: "", slices: null };
|
||||
}
|
||||
if (!record) return { value: lastValue.get(id) ?? null, unit, detail: "", slices: lastSegments.get(id) || null };
|
||||
const result = elapsedDelta(id, fields, now);
|
||||
if (!result) return { value: null, unit, detail: "" };
|
||||
if (!result) return { value: null, unit, detail: "", slices: null };
|
||||
const { deltas, seconds } = result;
|
||||
if (unit === "rate") return { value: deltas[0] / seconds, unit, detail: "" };
|
||||
const sum = id === "extra_cache_hit_rate" ? deltas[0] + deltas[1] : deltas[1];
|
||||
return { value: sum ? Math.min(100, deltas[0] / sum * 100) : null, unit, detail: "" };
|
||||
return { value: sum > 0 ? Math.min(100, deltas[0] / sum * 100) : null, unit, detail: "",
|
||||
slices: id === "extra_cache_hit_rate" ? [deltas[0], deltas[1]] : null };
|
||||
}
|
||||
function format(value, unit) {
|
||||
if (value == null) return "-";
|
||||
@@ -108,20 +151,35 @@ window.rustProxyExtraOverview = (() => {
|
||||
histories.delete(id);
|
||||
lastUnits.set(id, metric.unit);
|
||||
}
|
||||
if (!bounded.has(id) && id !== "extra_cache_hit_rate") {
|
||||
const history = histories.get(id) || [];
|
||||
if (record && value !== null) {
|
||||
history.push(value);
|
||||
if (history.length > limit) history.shift();
|
||||
if (history.length > limit()) history.shift();
|
||||
histories.set(id, history);
|
||||
}
|
||||
if (record) lastValue.set(id, value);
|
||||
const field = $(`overview-extra-${id}`);
|
||||
if (field) field.textContent = format(value, metric.unit);
|
||||
const detail = $(`overview-extra-${id}-detail`);
|
||||
if (detail) detail.textContent = metric.detail || t("overviewExtraRecentSamples");
|
||||
$(`overview-extra-${id}-line`)?.setAttribute("points", points(history, metric.unit));
|
||||
}
|
||||
if (record) {
|
||||
lastValue.set(id, value);
|
||||
if (id === "extra_cache_hit_rate") lastSegments.set(id, metric.slices || null);
|
||||
}
|
||||
function reset() { histories.clear(); lastCounter.clear(); lastValue.clear(); lastUnits.clear(); }
|
||||
return { render, reset };
|
||||
const field = $(`overview-extra-${id}`);
|
||||
if (field) field.textContent = format(value, metric.unit);
|
||||
if (bounded.has(id)) {
|
||||
meter(`overview-extra-${id}`, metric.meterPercent ?? (metric.unit === "percent" ? value : null));
|
||||
} else if (id === "extra_cache_hit_rate") {
|
||||
const slices = metric.slices || (record ? null : lastSegments.get(id));
|
||||
donut("extra-cache-ratio", slices?.[0], slices?.[1]);
|
||||
}
|
||||
const detail = $(`overview-extra-${id}-detail`);
|
||||
if (detail && id !== "extra_cache_hit_rate") detail.textContent = metric.detail || t("overviewExtraRecentSamples");
|
||||
}
|
||||
}
|
||||
function reset() { histories.clear(); lastCounter.clear(); lastValue.clear(); lastUnits.clear(); lastSegments.clear(); }
|
||||
window.rustProxyOverviewSamples?.onChange?.(() => {
|
||||
for (const history of histories.values()) window.rustProxyOverviewSamples.trim(history);
|
||||
if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview") render(latestStatus, false);
|
||||
});
|
||||
return { render, reset, meter, donut };
|
||||
})();
|
||||
@@ -0,0 +1,58 @@
|
||||
window.rustProxyOverviewSamples = (() => {
|
||||
const allowed = new Set([60, 120, 300, 600]);
|
||||
const storageKey = "rustproxy.overview.liveChartSamples";
|
||||
let saved = 60;
|
||||
try {
|
||||
const stored = Number(localStorage.getItem(storageKey));
|
||||
if (allowed.has(stored)) saved = stored;
|
||||
} catch { /* Private browsing or storage policy: temporary selection still works. */ }
|
||||
let limit = saved;
|
||||
const callbacks = new Set();
|
||||
const getLimit = () => limit;
|
||||
const trim = array => {
|
||||
if (Array.isArray(array) && array.length > limit) array.splice(0, array.length - limit);
|
||||
return array;
|
||||
};
|
||||
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) {
|
||||
state.dataset.i18n = messageKey;
|
||||
state.textContent = t(messageKey);
|
||||
}
|
||||
const save = document.getElementById("overview-samples-save");
|
||||
if (save) save.disabled = limit === saved;
|
||||
}
|
||||
function setLimit(value, persist = false) {
|
||||
const count = Number(value);
|
||||
if (!allowed.has(count)) return false;
|
||||
if (persist) {
|
||||
try {
|
||||
localStorage.setItem(storageKey, String(count));
|
||||
saved = count;
|
||||
} catch {
|
||||
const changed = count !== limit;
|
||||
limit = count;
|
||||
if (changed) for (const callback of callbacks) callback(limit);
|
||||
sync("overviewSamplesStorageUnavailable");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const changed = count !== limit;
|
||||
limit = count;
|
||||
if (changed) for (const callback of callbacks) callback(limit);
|
||||
sync();
|
||||
return true;
|
||||
}
|
||||
function init() {
|
||||
const select = document.getElementById("overview-live-samples");
|
||||
const save = document.getElementById("overview-samples-save");
|
||||
select?.addEventListener("change", () => setLimit(select.value));
|
||||
save?.addEventListener("click", () => setLimit(select.value, true));
|
||||
sync();
|
||||
}
|
||||
if (document.readyState !== "complete") document.addEventListener("DOMContentLoaded", init, { once: true });
|
||||
else queueMicrotask(init);
|
||||
return { getLimit, trim, setLimit, onChange: callback => callbacks.add(callback) };
|
||||
})();
|
||||
@@ -1,5 +1,5 @@
|
||||
window.rustProxyWideOverview = (() => {
|
||||
const limit = 60;
|
||||
const limit = () => window.rustProxyOverviewSamples?.getLimit?.() ?? 60;
|
||||
const configurations = {
|
||||
wide_protocol_rates: {
|
||||
unit: "rate", names: ["h1", "h2", "h3"], counters: s =>
|
||||
@@ -39,6 +39,16 @@ window.rustProxyWideOverview = (() => {
|
||||
probes.length ? probes.reduce((total, m) => total + Number(m.latency_ms), 0) / probes.length : null];
|
||||
}
|
||||
},
|
||||
wide_transform_activity: {
|
||||
unit: "rate", names: ["applied", "cache_hits", "skipped", "errors"], total: true,
|
||||
counters: s => [s.transform_metrics?.applied, s.transform_metrics?.cache_hits,
|
||||
s.transform_metrics?.skipped, s.transform_metrics?.errors]
|
||||
},
|
||||
wide_compression_activity: {
|
||||
unit: "rate", names: ["gzip", "br", "zstd", "deflate"], total: true,
|
||||
counters: s => [s.metrics?.compression_gzip, s.metrics?.compression_br,
|
||||
s.metrics?.compression_zstd, s.metrics?.compression_deflate]
|
||||
},
|
||||
};
|
||||
const history = new Map(), baselines = new Map(), last = new Map();
|
||||
let revision = null;
|
||||
@@ -81,7 +91,7 @@ window.rustProxyWideOverview = (() => {
|
||||
if (record) {
|
||||
values.forEach((value, i) => {
|
||||
series[i].push(value);
|
||||
if (series[i].length > limit) series[i].shift();
|
||||
if (series[i].length > limit()) series[i].shift();
|
||||
});
|
||||
history.set(id, series);
|
||||
last.set(id, values);
|
||||
@@ -95,9 +105,15 @@ window.rustProxyWideOverview = (() => {
|
||||
$(`overview-wide-${id}-${key}-line`)?.setAttribute("d", path(series[i], peak));
|
||||
});
|
||||
const summary = $(`overview-wide-${id}-summary`);
|
||||
if (summary) summary.textContent = format(values[0], config.unit);
|
||||
if (summary) summary.textContent = config.total
|
||||
? format(values.every(value => value !== null) ? values.reduce((sum, value) => sum + value, 0) : null, config.unit)
|
||||
: format(values[0], config.unit);
|
||||
}
|
||||
}
|
||||
function reset() { history.clear(); baselines.clear(); last.clear(); revision = null; }
|
||||
window.rustProxyOverviewSamples?.onChange?.(() => {
|
||||
for (const series of history.values()) for (const values of series) window.rustProxyOverviewSamples.trim(values);
|
||||
if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview") render(latestStatus, false);
|
||||
});
|
||||
return { render, reset };
|
||||
})();
|
||||
@@ -103,7 +103,7 @@ function applyDashboardLayout(order, hidden) {
|
||||
window.rustProxyStreamDashboard?.syncVisibility?.([...hiddenSet]);
|
||||
window.rustProxyBackendDashboard?.syncVisibility?.([...hiddenSet]);
|
||||
window.rustProxyMasonry?.refresh?.();
|
||||
if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview") { window.rustProxyStreamDashboard?.render?.(latestStatus, [...hiddenSet], false); window.rustProxyBackendDashboard?.render?.(latestStatus, [...hiddenSet], false); window.rustProxyExtraOverview?.render?.(latestStatus, false); window.rustProxyWideOverview?.render?.(latestStatus, false); }
|
||||
if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview") { window.rustProxyStreamDashboard?.render?.(latestStatus, [...hiddenSet], false); window.rustProxyBackendDashboard?.render?.(latestStatus, [...hiddenSet], false); window.rustProxyExtraOverview?.render?.(latestStatus, false); window.rustProxyWideOverview?.render?.(latestStatus, false); window.rustProxyWebOverview?.render?.(latestStatus, false); }
|
||||
return { order: normalizedOrder, hidden: [...hiddenSet] };
|
||||
}
|
||||
function dashboardWidgetLabel(id) {
|
||||
|
||||
@@ -222,8 +222,8 @@ function renderNetworkInterfaceCharts(interfaces, appendSample) {
|
||||
if (!item?.name) continue;
|
||||
const history = networkInterfaceSamples.get(item.name) || { rx: [], tx: [] };
|
||||
const rx = Number(item.rx_bytes_per_second), tx = Number(item.tx_bytes_per_second);
|
||||
if (Number.isFinite(rx)) { history.rx.push(rx); history.rx = history.rx.slice(-60); }
|
||||
if (Number.isFinite(tx)) { history.tx.push(tx); history.tx = history.tx.slice(-60); }
|
||||
if (Number.isFinite(rx)) { history.rx.push(rx); history.rx = history.rx.slice(-window.rustProxyOverviewSamples.getLimit()); }
|
||||
if (Number.isFinite(tx)) { history.tx.push(tx); history.tx = history.tx.slice(-window.rustProxyOverviewSamples.getLimit()); }
|
||||
networkInterfaceSamples.set(item.name, history);
|
||||
}
|
||||
}
|
||||
@@ -306,8 +306,10 @@ function renderNetworkInterfaceCharts(interfaces, appendSample) {
|
||||
function renderDiagnostics(status, appendSample = true, systemSample = appendSample) {
|
||||
const d = status.system || {}, live = status.live_system || d, c = status.cache || {}, m = status.metrics || {};
|
||||
$("metric-h3").textContent = fmt(m.http3_requests);
|
||||
$("metric-cache-ratio").textContent = c.hit_ratio == null ? "-" : `${fmt(c.hit_ratio * 100, 1)}%`;
|
||||
$("cache-summary").textContent = `${fmt(c.hits)} HIT / ${fmt(c.misses)} MISS`;
|
||||
const cacheHits = c.hits == null ? null : Number(c.hits), cacheMisses = c.misses == null ? null : Number(c.misses);
|
||||
const cacheEvents = cacheHits !== null && cacheMisses !== null ? cacheHits + cacheMisses : 0;
|
||||
$("metric-cache-ratio").textContent = cacheEvents > 0 ? `${fmt(cacheHits / cacheEvents * 100, 1)}%` : "-";
|
||||
window.rustProxyExtraOverview?.donut?.("cache-ratio", cacheHits, cacheMisses);
|
||||
if (systemSample) {
|
||||
if (d.process_rss_bytes != null) $("metric-rss").textContent = fmtBytes(d.process_rss_bytes);
|
||||
$("process-summary").hidden = d.process_threads == null;
|
||||
@@ -325,7 +327,7 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
||||
setOptionalMetric("system-load", loadSupported, loadSupported ? d.load_average.map(n => fmt(n, 2)).join(" / ") : "");
|
||||
setOptionalMetric("system-uptime", d.system_uptime_seconds != null, d.system_uptime_seconds == null ? "" : uptime(d.system_uptime_seconds));
|
||||
}
|
||||
$("system-chart").hidden = live.cpu_percent == null; if (appendSample && live.cpu_percent != null) { systemSamples.push(Number(live.cpu_percent)); systemSamples = systemSamples.slice(-30); }
|
||||
$("system-chart").hidden = live.cpu_percent == null; if (appendSample && live.cpu_percent != null) { systemSamples.push(Number(live.cpu_percent)); systemSamples = systemSamples.slice(-window.rustProxyOverviewSamples.getLimit()); }
|
||||
$("system-sparkline").setAttribute("points", systemSamples.map((n, i) => `${i * 300 / Math.max(1, systemSamples.length - 1)},${95 - Math.max(0, Math.min(100, n)) * .9}`).join(" "));
|
||||
$("cache-state").textContent = t(envelope?.config.cache?.enabled ? "enabled" : "notEnabled");
|
||||
$("cache-counters").textContent = [c.hits, c.misses, c.bypasses].map(n => fmt(n)).join(" / ");
|
||||
@@ -339,28 +341,28 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
||||
: t("notEnabled");
|
||||
$("cache-evictions").textContent = `${fmt(c.evictions)} / ${fmt(c.expired)}`;
|
||||
if (appendSample) {
|
||||
if (c.resident_bytes != null) { cacheMemorySamples.push(Number(c.resident_bytes)); cacheMemorySamples = cacheMemorySamples.slice(-60); }
|
||||
if (c.transient_bytes != null) { cacheTransientSamples.push(Number(c.transient_bytes)); cacheTransientSamples = cacheTransientSamples.slice(-60); }
|
||||
if (c.fill_bytes_active != null) { cacheFillSamples.push(Number(c.fill_bytes_active)); cacheFillSamples = cacheFillSamples.slice(-60); }
|
||||
if (c.cold_queue_bytes != null) { cacheColdQueueSamples.push(Number(c.cold_queue_bytes)); cacheColdQueueSamples = cacheColdQueueSamples.slice(-60); }
|
||||
if (c.resident_bytes != null) { cacheMemorySamples.push(Number(c.resident_bytes)); cacheMemorySamples = cacheMemorySamples.slice(-window.rustProxyOverviewSamples.getLimit()); }
|
||||
if (c.transient_bytes != null) { cacheTransientSamples.push(Number(c.transient_bytes)); cacheTransientSamples = cacheTransientSamples.slice(-window.rustProxyOverviewSamples.getLimit()); }
|
||||
if (c.fill_bytes_active != null) { cacheFillSamples.push(Number(c.fill_bytes_active)); cacheFillSamples = cacheFillSamples.slice(-window.rustProxyOverviewSamples.getLimit()); }
|
||||
if (c.cold_queue_bytes != null) { cacheColdQueueSamples.push(Number(c.cold_queue_bytes)); cacheColdQueueSamples = cacheColdQueueSamples.slice(-window.rustProxyOverviewSamples.getLimit()); }
|
||||
const coldIoBytes = (Number(c.cold_write_bytes_active) || 0) + (Number(c.cold_read_bytes_active) || 0);
|
||||
if (c.cold_write_bytes_active != null || c.cold_read_bytes_active != null) { cacheColdIoSamples.push(coldIoBytes); cacheColdIoSamples = cacheColdIoSamples.slice(-60); }
|
||||
if (c.cold_bytes != null) { cacheColdStorageSamples.push(Number(c.cold_bytes)); cacheColdStorageSamples = cacheColdStorageSamples.slice(-60); }
|
||||
if (c.cold_write_bytes_active != null || c.cold_read_bytes_active != null) { cacheColdIoSamples.push(coldIoBytes); cacheColdIoSamples = cacheColdIoSamples.slice(-window.rustProxyOverviewSamples.getLimit()); }
|
||||
if (c.cold_bytes != null) { cacheColdStorageSamples.push(Number(c.cold_bytes)); cacheColdStorageSamples = cacheColdStorageSamples.slice(-window.rustProxyOverviewSamples.getLimit()); }
|
||||
const currentCounters = [c.hits, c.misses, c.bypasses].map(Number);
|
||||
if (currentCounters.every(Number.isFinite)) {
|
||||
const delta = rustProxyRateSampling.difference(lastCacheCounters, currentCounters, performance.now(), rustProxyRateSampling.cadence("overview_seconds"));
|
||||
const values = delta.deltas || lastCacheOutcomeRates || (!lastCacheCounters ? [0, 0, 0] : null);
|
||||
if (values) {
|
||||
cacheHitSamples.push(values[0]); cacheMissSamples.push(values[1]); cacheBypassSamples.push(values[2]);
|
||||
cacheHitSamples = cacheHitSamples.slice(-60); cacheMissSamples = cacheMissSamples.slice(-60); cacheBypassSamples = cacheBypassSamples.slice(-60);
|
||||
cacheHitSamples = cacheHitSamples.slice(-window.rustProxyOverviewSamples.getLimit()); cacheMissSamples = cacheMissSamples.slice(-window.rustProxyOverviewSamples.getLimit()); cacheBypassSamples = cacheBypassSamples.slice(-window.rustProxyOverviewSamples.getLimit());
|
||||
lastCacheOutcomeRates = [...values];
|
||||
}
|
||||
lastCacheCounters = delta.next;
|
||||
}
|
||||
}
|
||||
if (appendSample) {
|
||||
if (live.process_cpu_percent != null) { processCpuSamples.push(Number(live.process_cpu_percent)); processCpuSamples = processCpuSamples.slice(-60); }
|
||||
if (live.process_rss_bytes != null) { processRssSamples.push(Number(live.process_rss_bytes)); processRssSamples = processRssSamples.slice(-60); }
|
||||
if (live.process_cpu_percent != null) { processCpuSamples.push(Number(live.process_cpu_percent)); processCpuSamples = processCpuSamples.slice(-window.rustProxyOverviewSamples.getLimit()); }
|
||||
if (live.process_rss_bytes != null) { processRssSamples.push(Number(live.process_rss_bytes)); processRssSamples = processRssSamples.slice(-window.rustProxyOverviewSamples.getLimit()); }
|
||||
}
|
||||
const cacheOutcomePeak = Math.max(1, ...cacheHitSamples, ...cacheMissSamples, ...cacheBypassSamples);
|
||||
$("cache-hit-sparkline").setAttribute("points", sparklinePoints(cacheHitSamples, 300, 60, 5, cacheOutcomePeak));
|
||||
@@ -427,16 +429,15 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
||||
if (appendSample) {
|
||||
if (networkRateSupported) {
|
||||
networkRxSamples.push(Number(live.network_rx_bytes_per_second)); networkTxSamples.push(Number(live.network_tx_bytes_per_second));
|
||||
networkRxSamples = networkRxSamples.slice(-60); networkTxSamples = networkTxSamples.slice(-60);
|
||||
networkRxSamples = networkRxSamples.slice(-window.rustProxyOverviewSamples.getLimit()); networkTxSamples = networkTxSamples.slice(-window.rustProxyOverviewSamples.getLimit());
|
||||
}
|
||||
if (m.mean_duration_ms != null) { responseLatencySamples.push(Number(m.mean_duration_ms)); responseLatencySamples = responseLatencySamples.slice(-60); }
|
||||
if (m.mean_duration_ms != null) { responseLatencySamples.push(Number(m.mean_duration_ms)); responseLatencySamples = responseLatencySamples.slice(-window.rustProxyOverviewSamples.getLimit()); }
|
||||
}
|
||||
const networkPeak = Math.max(1, ...networkRxSamples, ...networkTxSamples);
|
||||
$("network-rx-sparkline").setAttribute("points", sparklinePoints(networkRxSamples, 300, 60, 5, networkPeak));
|
||||
$("network-tx-sparkline").setAttribute("points", sparklinePoints(networkTxSamples, 300, 60, 5, networkPeak));
|
||||
setChartText($("network-chart-value"), networkRateSupported ? `${fmtBytes(live.network_rx_bytes_per_second)} / ${fmtBytes(live.network_tx_bytes_per_second)}` : "");
|
||||
$("network-rx-sparkline").closest(".mini-live-chart").hidden = !networkRateSupported;
|
||||
// Cache-sampled OS chart rates are independent from slow System details.
|
||||
renderNetworkInterfaceCharts(live.network_interfaces || [], appendSample);
|
||||
$("latency-sparkline").setAttribute("points", sparklinePoints(responseLatencySamples));
|
||||
setChartText($("latency-chart-value"), m.mean_duration_ms == null ? "" : `${fmt(m.mean_duration_ms, 2)} ms`);
|
||||
@@ -493,3 +494,19 @@ function collectHeaderRules(direction) {
|
||||
return op === "remove" ? { op, name } : { op, name, value };
|
||||
});
|
||||
}
|
||||
|
||||
window.rustProxyOverviewSamples.onChange(() => {
|
||||
for (const history of [systemSamples, cacheMemorySamples, cacheTransientSamples,
|
||||
cacheFillSamples, cacheColdQueueSamples, cacheColdIoSamples, cacheColdStorageSamples,
|
||||
cacheHitSamples, cacheMissSamples, cacheBypassSamples, processCpuSamples,
|
||||
processRssSamples, networkRxSamples, networkTxSamples, responseLatencySamples]) {
|
||||
window.rustProxyOverviewSamples.trim(history);
|
||||
}
|
||||
for (const history of networkInterfaceSamples.values()) {
|
||||
window.rustProxyOverviewSamples.trim(history.rx);
|
||||
window.rustProxyOverviewSamples.trim(history.tx);
|
||||
}
|
||||
if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview") {
|
||||
renderDiagnostics(latestStatus, false, false);
|
||||
}
|
||||
});
|
||||
@@ -95,7 +95,7 @@ window.rustProxyStreamDashboard = (() => {
|
||||
const tx = sample.deltas ? sample.deltas[1] / sample.seconds : 0;
|
||||
if (!pausedRate) {
|
||||
history.rx.push(rx); history.tx.push(tx);
|
||||
if (history.rx.length > 60) { history.rx.shift(); history.tx.shift(); }
|
||||
if (history.rx.length > window.rustProxyOverviewSamples.getLimit()) { history.rx.shift(); history.tx.shift(); }
|
||||
}
|
||||
set("stream-dashboard-traffic", pausedRate ? "-" : `${bytes(Math.round(rx))}/s`);
|
||||
trafficRates(pausedRate ? "-" : `${bytes(Math.round(rx))}/s`, pausedRate ? "-" : `${bytes(Math.round(tx))}/s`);
|
||||
@@ -103,5 +103,10 @@ window.rustProxyStreamDashboard = (() => {
|
||||
$("stream-dashboard-tx")?.setAttribute("points", line(history.tx));
|
||||
}
|
||||
function reset() { clearTraffic(); wasVisible = false; }
|
||||
window.rustProxyOverviewSamples.onChange(() => {
|
||||
window.rustProxyOverviewSamples.trim(history.rx);
|
||||
window.rustProxyOverviewSamples.trim(history.tx);
|
||||
if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview") render(latestStatus, null, false);
|
||||
});
|
||||
return { syncVisibility, render, reset };
|
||||
})();
|
||||
@@ -0,0 +1,225 @@
|
||||
window.rustProxyWebOverview = (() => {
|
||||
const valid = n => n != null && Number.isFinite(Number(n)) && Number(n) >= 0 ? Number(n) : null;
|
||||
const show = (id, text) => { const el = $(id); if (el) el.textContent = text; };
|
||||
const fmtRate = n => n == null ? "-" : `${fmt(n, 1)}/s`;
|
||||
const tile = id => document.querySelector(`[data-dashboard-widget="${id}"]`);
|
||||
const seriesNames = {
|
||||
www_requests: ["all", "php"], www_latency: ["web", "php"],
|
||||
www_failures: ["timeout", "connection", "protocol", "capacity"],
|
||||
www_traffic: ["nonphp", "php"],
|
||||
www_status_trend: ["2xx", "3xx", "4xx", "5xx"]
|
||||
};
|
||||
const histories = new Map(), serviceBaselines = new Map();
|
||||
let baseline = null, revision = null, last = null;
|
||||
function aggregate(s) {
|
||||
const services = s?.web_server_metrics;
|
||||
if (!services || typeof services !== "object") return null;
|
||||
const web = {
|
||||
requests: 0, response_headers_us: 0, response_bytes: 0,
|
||||
status: [0, 0, 0, 0], kinds: [0, 0, 0], methods: [0, 0, 0, 0, 0],
|
||||
selected: Array(8).fill(0), active: 0
|
||||
};
|
||||
const php = {
|
||||
requests: 0, response_headers_us: 0, response_bytes: 0,
|
||||
failures: Array(9).fill(0), capacity_limit: 0, capacity_used: 0, count: 0
|
||||
};
|
||||
const perService = {};
|
||||
for (const [id, entry] of Object.entries(services)) {
|
||||
const w = entry?.web;
|
||||
if (!w || valid(w.requests) === null) continue;
|
||||
for (const key of ["requests", "response_headers_us", "response_bytes"])
|
||||
web[key] += valid(w[key]) || 0;
|
||||
for (const [i, key] of ["status_2xx", "status_3xx", "status_4xx", "status_5xx"].entries())
|
||||
web.status[i] += valid(w[key]) || 0;
|
||||
for (const [i, key] of ["static_requests", "php_requests", "other_requests"].entries())
|
||||
web.kinds[i] += valid(w[key]) || 0;
|
||||
["methods", "selected_status"].forEach((field, index) => {
|
||||
const output = index ? web.selected : web.methods;
|
||||
output.forEach((_, i) => { output[i] += valid(w[field]?.[i]) || 0; });
|
||||
});
|
||||
web.active += valid(w.active_headers) || 0;
|
||||
perService[id] = valid(w.requests);
|
||||
if (entry.php?.metrics) {
|
||||
php.count++;
|
||||
const pm = entry.php.metrics;
|
||||
for (const key of ["requests", "response_headers_us", "response_bytes"])
|
||||
php[key] += valid(pm[key]) || 0;
|
||||
php.failures.forEach((_, i) => { php.failures[i] += valid(pm.failures?.[i]) || 0; });
|
||||
php.capacity_limit += valid(entry.php.capacity_limit) || 0;
|
||||
php.capacity_used += valid(entry.php.capacity_used) || 0;
|
||||
}
|
||||
}
|
||||
return Object.keys(perService).length ? { web, php, perService } : null;
|
||||
}
|
||||
function compact(a) {
|
||||
return [a.web.requests, ...a.web.status, ...a.web.kinds, ...a.web.methods,
|
||||
a.web.response_headers_us, a.web.response_bytes, a.php.requests,
|
||||
a.php.response_headers_us, a.php.response_bytes, ...a.php.failures];
|
||||
}
|
||||
function rates(a, record) {
|
||||
if (!record) return last;
|
||||
const sample = rustProxyRateSampling.difference(baseline, compact(a), performance.now(),
|
||||
rustProxyRateSampling.cadence("overview_seconds"));
|
||||
baseline = sample.next;
|
||||
if (!sample.deltas) { last = null; return null; }
|
||||
const d = sample.deltas, seconds = sample.seconds;
|
||||
const perSec = value => value / seconds;
|
||||
last = {
|
||||
rps: [perSec(d[0]), perSec(d[15])],
|
||||
status: d.slice(1, 5).map(perSec),
|
||||
latency: [d[0] > 0 ? d[13] / d[0] / 1000 : null,
|
||||
d[15] > 0 ? d[16] / d[15] / 1000 : null],
|
||||
traffic: [perSec(Math.max(0, d[14] - d[17])), perSec(d[17])],
|
||||
failures: [perSec(d[19] + d[21] + d[23]),
|
||||
perSec(d[20] + d[22] + d[24]), perSec(d[25] + d[26]), perSec(d[18])],
|
||||
errorRate: d[0] > 0 ? d[4] / d[0] * 100 : null,
|
||||
phpRps: perSec(d[15]),
|
||||
};
|
||||
return last;
|
||||
}
|
||||
function chartPath(values, peak) {
|
||||
let output = "", painting = false;
|
||||
values.forEach((value, i) => {
|
||||
if (value == null) { painting = false; return; }
|
||||
output += `${painting ? " L" : "M"}${Math.round(i * 600 / Math.max(1, values.length - 1))} ${Math.round(137 - 120 * value / peak)}`;
|
||||
painting = true;
|
||||
});
|
||||
return output;
|
||||
}
|
||||
function smallPath(values) {
|
||||
const peak = Math.max(1, ...values.filter(v => v != null));
|
||||
return values.map((value, i) => `${i * 300 / Math.max(1, values.length - 1)},${55 - (value || 0) / peak * 50}`).join(" ");
|
||||
}
|
||||
function ring(id, amounts) {
|
||||
const total = amounts.reduce((n, value) => n + value, 0);
|
||||
let offset = 0;
|
||||
amounts.forEach((amount, i) => {
|
||||
const share = total ? amount / total * 100 : 0;
|
||||
const arc = $(`${id}-arc-${i}`);
|
||||
arc?.setAttribute("stroke-dasharray", `${share} ${100 - share}`);
|
||||
arc?.setAttribute("stroke-dashoffset", String(-offset));
|
||||
offset += share;
|
||||
show(`${id}-count-${i}`, fmt(amount));
|
||||
});
|
||||
show(`${id}-value`, total ? `${fmt(amounts[0] / total * 100, 1)}%` : "-");
|
||||
$(`${id}-chart`)?.setAttribute("aria-label", `${t(id === "www_status" ? "webMetricStatus" : "webMetricKind")}: ${amounts.join(" / ")}`);
|
||||
}
|
||||
function meter(id, percent) {
|
||||
window.rustProxyExtraOverview?.meter?.(id, percent);
|
||||
}
|
||||
function clearUnavailable() {
|
||||
for (const id of ["www_requests", "www_latency", "www_failures", "www_traffic",
|
||||
"www_status_trend", "www_php_requests", "www_capacity", "www_error_rate",
|
||||
"www_methods", "www_top_services"]) show(`${id}-value`, "-");
|
||||
for (const [id, names] of Object.entries(seriesNames)) {
|
||||
for (const name of names) {
|
||||
show(`${id}-${name}-value`, "-");
|
||||
$(`${id}-${name}-line`)?.setAttribute("d", "");
|
||||
}
|
||||
}
|
||||
$("www_php_requests-line")?.setAttribute("points", "");
|
||||
$("www_top_services-bars")?.replaceChildren();
|
||||
show("www_methods-detail", "-");
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const segment = $(`www_methods-segment-${i}`);
|
||||
if (segment) { segment.style.width = "0%"; segment.title = ""; }
|
||||
}
|
||||
show("www_capacity-detail", t("webLiveNoPhp"));
|
||||
show("www_error_rate-detail", t("webMetricErrorRateHelp"));
|
||||
meter("www_capacity", null);
|
||||
meter("www_error_rate", null);
|
||||
ring("www_status", [0, 0, 0, 0]);
|
||||
ring("www_kind", [0, 0, 0]);
|
||||
}
|
||||
function render(s, record = true) {
|
||||
const a = aggregate(s);
|
||||
if (revision !== s?.revision) { reset(); revision = s?.revision; }
|
||||
if (!a) { reset(); revision = s?.revision; clearUnavailable(); return; }
|
||||
if (currentPanel !== "overview") { baseline = null; return; }
|
||||
const r = rates(a, record);
|
||||
ring("www_status", a.web.status);
|
||||
ring("www_kind", a.web.kinds);
|
||||
const totalMethods = a.web.methods.reduce((n, v) => n + v, 0);
|
||||
show("www_methods-value", totalMethods ? fmt(totalMethods) : "-");
|
||||
a.web.methods.forEach((value, i) => {
|
||||
const segment = $(`www_methods-segment-${i}`);
|
||||
if (segment) { segment.style.width = `${totalMethods ? value / totalMethods * 100 : 0}%`; segment.title = `${["GET", "POST", "PUT", "DELETE", "Other"][i]}: ${fmt(value)}`; }
|
||||
});
|
||||
show("www_methods-detail", ["GET", "POST", "PUT", "DELETE", "Other"].map((name, i) => `${name} ${fmt(a.web.methods[i])}`).join(" · "));
|
||||
const { capacity_limit: max, capacity_used: used } = a.php;
|
||||
show("www_capacity-value", max ? `${fmt(used)} / ${fmt(max)}` : "-");
|
||||
meter("www_capacity", max ? used / max * 100 : null);
|
||||
show("www_capacity-detail", max ? `${fmt(used / max * 100, 1)}%` : t("webLiveNoPhp"));
|
||||
show("www_error_rate-value", r?.errorRate == null ? "-" : `${fmt(r.errorRate, 1)}%`);
|
||||
meter("www_error_rate", r?.errorRate);
|
||||
show("www_error_rate-detail", t("webMetricErrorRateHelp"));
|
||||
show("www_php_requests-value", fmtRate(r?.phpRps));
|
||||
if (record && !tile("www_php_requests")?.hidden) {
|
||||
const values = histories.get("www_php_requests") || [];
|
||||
values.push(r?.phpRps ?? null); window.rustProxyOverviewSamples.trim(values);
|
||||
histories.set("www_php_requests", values);
|
||||
}
|
||||
$(`www_php_requests-line`)?.setAttribute("points", smallPath(histories.get("www_php_requests") || []));
|
||||
const set = {
|
||||
www_requests: r?.rps, www_latency: r?.latency, www_failures: r?.failures,
|
||||
www_traffic: r?.traffic, www_status_trend: r?.status,
|
||||
};
|
||||
for (const [id, names] of Object.entries(seriesNames)) {
|
||||
const series = histories.get(id) || names.map(() => []);
|
||||
if (record && !tile(id)?.hidden) {
|
||||
names.forEach((_, i) => { series[i].push(set[id]?.[i] ?? null); window.rustProxyOverviewSamples.trim(series[i]); });
|
||||
histories.set(id, series);
|
||||
}
|
||||
const peak = Math.max(1, ...series.flat().filter(v => v !== null));
|
||||
names.forEach((name, i) => {
|
||||
const value = set[id]?.[i];
|
||||
const label = id === "www_latency" ? (value == null ? "-" : `${fmt(value, 1)} ms`)
|
||||
: id === "www_traffic" ? (value == null ? "-" : `${fmtBytes(value)}/s`) : fmtRate(value);
|
||||
show(`${id}-${name}-value`, label);
|
||||
$(`${id}-${name}-line`)?.setAttribute("d", chartPath(series[i], peak));
|
||||
});
|
||||
const summary = id === "www_latency" ? (set[id]?.[0] == null ? "-" : `${fmt(set[id][0], 1)} ms`)
|
||||
: id === "www_traffic" ? (set[id] ? `${fmtBytes(set[id][0] + set[id][1])}/s` : "-")
|
||||
: fmtRate(set[id]?.[0]);
|
||||
show(`${id}-value`, summary);
|
||||
}
|
||||
if (record) {
|
||||
const currentIds = new Set(Object.keys(a.perService));
|
||||
for (const id of serviceBaselines.keys()) if (!currentIds.has(id)) serviceBaselines.delete(id);
|
||||
const rows = Object.entries(a.perService).map(([id, total]) => {
|
||||
const old = serviceBaselines.get(id);
|
||||
const delta = rustProxyRateSampling.difference(old, [total], performance.now(),
|
||||
rustProxyRateSampling.cadence("overview_seconds"));
|
||||
serviceBaselines.set(id, delta.next);
|
||||
return { id, value: delta.deltas ? delta.deltas[0] / delta.seconds : null };
|
||||
});
|
||||
if (rows.some(item => item.value !== null)) {
|
||||
rows.sort((x, y) => (y.value ?? -1) - (x.value ?? -1));
|
||||
const top = rows.filter(item => item.value !== null).slice(0, 6);
|
||||
const container = $("www_top_services-bars");
|
||||
if (container && !tile("www_top_services")?.hidden) {
|
||||
const peak = Math.max(1, ...top.map(item => item.value));
|
||||
container.replaceChildren(...top.map(item => {
|
||||
const row = document.createElement("div"); row.className = "www-top-row";
|
||||
const name = document.createElement("span"); name.textContent = item.id;
|
||||
const meter = document.createElement("div"); meter.className = "www-top-meter";
|
||||
const fill = document.createElement("span"); fill.style.width = `${item.value / peak * 100}%`;
|
||||
meter.append(fill);
|
||||
const value = document.createElement("b"); value.textContent = fmtRate(item.value);
|
||||
row.append(name, meter, value); return row;
|
||||
}));
|
||||
}
|
||||
show("www_top_services-value", fmtRate(top.reduce((n, item) => n + item.value, 0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
function reset() { histories.clear(); serviceBaselines.clear(); baseline = null; last = null; revision = null; }
|
||||
window.rustProxyOverviewSamples.onChange(() => {
|
||||
for (const history of histories.values()) {
|
||||
if (Array.isArray(history[0])) history.forEach(window.rustProxyOverviewSamples.trim);
|
||||
else window.rustProxyOverviewSamples.trim(history);
|
||||
}
|
||||
if (latestStatus && currentPanel === "overview") render(latestStatus, false);
|
||||
});
|
||||
return { render, reset, aggregate };
|
||||
})();
|
||||
@@ -425,7 +425,29 @@
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": true,
|
||||
"default_hidden": false,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "extra_cold_queue_memory",
|
||||
"label_key": "overviewExtraColdQueueMemory",
|
||||
"description_key": "overviewExtraColdQueueMemoryDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"extra_cold_queue_memory\"]",
|
||||
"data_source": "cache_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": false,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "extra_cold_storage",
|
||||
"label_key": "overviewExtraColdStorage",
|
||||
"description_key": "overviewExtraColdStorageDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"extra_cold_storage\"]",
|
||||
"data_source": "cache_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": false,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
@@ -604,6 +626,28 @@
|
||||
"default_hidden": true,
|
||||
"display_type": "chart"
|
||||
},
|
||||
{
|
||||
"id": "wide_transform_activity",
|
||||
"label_key": "overviewWideTransformActivity",
|
||||
"description_key": "overviewWideTransformActivityDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"wide_transform_activity\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "panels",
|
||||
"default_hidden": false,
|
||||
"display_type": "chart"
|
||||
},
|
||||
{
|
||||
"id": "wide_compression_activity",
|
||||
"label_key": "overviewWideCompressionActivity",
|
||||
"description_key": "overviewWideCompressionActivityDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"wide_compression_activity\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "panels",
|
||||
"default_hidden": false,
|
||||
"display_type": "chart"
|
||||
},
|
||||
{
|
||||
"id": "traffic_chart",
|
||||
"label_key": "requestsPerSecond",
|
||||
@@ -680,5 +724,137 @@
|
||||
"category": "panels",
|
||||
"default_hidden": false,
|
||||
"display_type": "table"
|
||||
},
|
||||
{
|
||||
"id": "www_status",
|
||||
"label_key": "webMetricStatus",
|
||||
"description_key": "webMetricStatusHelp",
|
||||
"element_selector": "[data-dashboard-widget=\"www_status\"]",
|
||||
"data_source": "web_server_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "web_server",
|
||||
"default_hidden": true,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "www_kind",
|
||||
"label_key": "webMetricKind",
|
||||
"description_key": "webMetricKindHelp",
|
||||
"element_selector": "[data-dashboard-widget=\"www_kind\"]",
|
||||
"data_source": "web_server_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "web_server",
|
||||
"default_hidden": true,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "www_capacity",
|
||||
"label_key": "webMetricCapacity",
|
||||
"description_key": "webMetricCapacityHelp",
|
||||
"element_selector": "[data-dashboard-widget=\"www_capacity\"]",
|
||||
"data_source": "web_server_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "web_server",
|
||||
"default_hidden": true,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "www_methods",
|
||||
"label_key": "webMetricMethods",
|
||||
"description_key": "webMetricMethodsHelp",
|
||||
"element_selector": "[data-dashboard-widget=\"www_methods\"]",
|
||||
"data_source": "web_server_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "web_server",
|
||||
"default_hidden": true,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "www_error_rate",
|
||||
"label_key": "webMetricErrorRate",
|
||||
"description_key": "webMetricErrorRateHelp",
|
||||
"element_selector": "[data-dashboard-widget=\"www_error_rate\"]",
|
||||
"data_source": "web_server_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "web_server",
|
||||
"default_hidden": true,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "www_php_requests",
|
||||
"label_key": "webMetricPhpRequests",
|
||||
"description_key": "webMetricPhpRequestsHelp",
|
||||
"element_selector": "[data-dashboard-widget=\"www_php_requests\"]",
|
||||
"data_source": "web_server_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "web_server",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "www_requests",
|
||||
"label_key": "webMetricRequests",
|
||||
"description_key": "webMetricRequestsHelp",
|
||||
"element_selector": "[data-dashboard-widget=\"www_requests\"]",
|
||||
"data_source": "web_server_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "web_server",
|
||||
"default_hidden": true,
|
||||
"display_type": "chart"
|
||||
},
|
||||
{
|
||||
"id": "www_latency",
|
||||
"label_key": "webMetricLatency",
|
||||
"description_key": "webMetricLatencyHelp",
|
||||
"element_selector": "[data-dashboard-widget=\"www_latency\"]",
|
||||
"data_source": "web_server_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "web_server",
|
||||
"default_hidden": true,
|
||||
"display_type": "chart"
|
||||
},
|
||||
{
|
||||
"id": "www_failures",
|
||||
"label_key": "webMetricFailures",
|
||||
"description_key": "webMetricFailuresHelp",
|
||||
"element_selector": "[data-dashboard-widget=\"www_failures\"]",
|
||||
"data_source": "web_server_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "web_server",
|
||||
"default_hidden": true,
|
||||
"display_type": "chart"
|
||||
},
|
||||
{
|
||||
"id": "www_traffic",
|
||||
"label_key": "webMetricTraffic",
|
||||
"description_key": "webMetricTrafficHelp",
|
||||
"element_selector": "[data-dashboard-widget=\"www_traffic\"]",
|
||||
"data_source": "web_server_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "web_server",
|
||||
"default_hidden": true,
|
||||
"display_type": "chart"
|
||||
},
|
||||
{
|
||||
"id": "www_top_services",
|
||||
"label_key": "webMetricTopServices",
|
||||
"description_key": "webMetricTopServicesHelp",
|
||||
"element_selector": "[data-dashboard-widget=\"www_top_services\"]",
|
||||
"data_source": "web_server_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "web_server",
|
||||
"default_hidden": true,
|
||||
"display_type": "chart"
|
||||
},
|
||||
{
|
||||
"id": "www_status_trend",
|
||||
"label_key": "webMetricStatusTrend",
|
||||
"description_key": "webMetricStatusTrendHelp",
|
||||
"element_selector": "[data-dashboard-widget=\"www_status_trend\"]",
|
||||
"data_source": "web_server_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "web_server",
|
||||
"default_hidden": true,
|
||||
"display_type": "chart"
|
||||
}
|
||||
]
|
||||
+405
-48
@@ -113,8 +113,27 @@
|
||||
<div hidden="" id="workspace">
|
||||
<section class="page-intro">
|
||||
<div class="page-intro-copy">
|
||||
<div class="eyebrow"><i class="signal" id="signal"></i><span id="connection-label">CONNECTED TO
|
||||
<div class="page-intro-meta-row">
|
||||
<div class="eyebrow"><i class="signal" id="signal"></i><span id="connection-label">CONNECTED
|
||||
TO
|
||||
LOCAL CORE</span></div>
|
||||
<div class="overview-samples-toolbar" id="overview-samples-toolbar" hidden role="group"
|
||||
aria-label="Overview chart samples" data-i18n-aria="overviewSamplesTitle">
|
||||
<label class="function-live-chart-samples" for="overview-live-samples"><span
|
||||
data-i18n="liveChartSamples">Samples</span>
|
||||
<select id="overview-live-samples" aria-describedby="overview-samples-state">
|
||||
<option value="60">60</option>
|
||||
<option value="120">120</option>
|
||||
<option value="300">300</option>
|
||||
<option value="600">600</option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="quiet" id="overview-samples-save" type="button"
|
||||
data-i18n="overviewSamplesRemember">Remember choice</button>
|
||||
<small class="fine" id="overview-samples-state" role="status" aria-live="polite"
|
||||
data-i18n="overviewSamplesSaved">Saved default</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-intro-title-row">
|
||||
<h1 id="page-title">Traffic, in focus.</h1>
|
||||
<button class="quiet" data-i18n="refreshNow" id="overview-refresh" type="button"
|
||||
@@ -159,9 +178,29 @@
|
||||
data-i18n="errors">Errors</span><strong id="metric-errors">0</strong><small
|
||||
data-i18n="errorDescription">5xx / body /
|
||||
cancelled</small></article>
|
||||
<article class="stat" data-dashboard-widget="cache"><span data-i18n="cacheHitRatio">Cache hit
|
||||
ratio</span><strong id="metric-cache-ratio">-</strong><small
|
||||
id="cache-summary">-</small></article>
|
||||
<article class="stat stat-donut" data-dashboard-widget="cache"><span
|
||||
data-i18n="cacheHitRatio">Cache hit
|
||||
ratio</span>
|
||||
<div class="overview-donut-row">
|
||||
<svg class="overview-donut-svg" id="cache-ratio-chart" role="img"
|
||||
aria-label="Cache HIT and MISS distribution" viewBox="0 0 64 64">
|
||||
<circle class="overview-donut-track" cx="32" cy="32" r="24"></circle>
|
||||
<circle class="overview-donut-hit" id="cache-ratio-hit-arc" cx="32" cy="32" r="24"
|
||||
pathLength="100" stroke-dasharray="0 100"></circle>
|
||||
<circle class="overview-donut-miss" id="cache-ratio-miss-arc" cx="32" cy="32" r="24"
|
||||
pathLength="100" stroke-dasharray="0 100"></circle>
|
||||
</svg>
|
||||
<div class="overview-donut-info">
|
||||
<strong id="metric-cache-ratio">-</strong>
|
||||
<small class="overview-donut-legend" id="cache-summary">
|
||||
<span class="overview-donut-hit-key"><i></i>HIT <b
|
||||
id="cache-ratio-hit-count">0</b></span>
|
||||
<span class="overview-donut-miss-key"><i></i>MISS <b
|
||||
id="cache-ratio-miss-count">0</b></span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="memory"><span data-i18n="processMemory">Process
|
||||
memory</span><strong id="metric-rss">-</strong><small id="process-summary">-</small>
|
||||
</article>
|
||||
@@ -315,14 +354,14 @@
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="backend-dashboard-requests-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_capacity" hidden>
|
||||
<article class="stat stat-meter" data-dashboard-widget="backend_capacity" hidden>
|
||||
<span data-i18n="backendWidgetCapacity">Backend capacity</span><strong
|
||||
id="backend-dashboard-capacity">-</strong>
|
||||
<svg data-i18n-aria="backendWidgetCapacity" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-capacity-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="backend-dashboard-capacity-detail">-</small>
|
||||
<div class="overview-meter" id="backend-dashboard-capacity-meter" role="progressbar"
|
||||
data-i18n-aria="backendWidgetCapacity" aria-valuemin="0" aria-valuemax="100"
|
||||
aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="backend-dashboard-capacity-meter-fill"></span>
|
||||
</div><small id="backend-dashboard-capacity-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_http5xx" hidden>
|
||||
<span data-i18n="backendWidgetHttp5xx">backendWidgetHttp5xx</span><strong
|
||||
@@ -392,14 +431,15 @@
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_process_rss-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_process_fds" hidden>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_process_fds" hidden>
|
||||
<span data-i18n="overviewExtraProcessFds">overviewExtraProcessFds</span><strong
|
||||
id="overview-extra-extra_process_fds">-</strong>
|
||||
<svg data-i18n-aria="overviewExtraProcessFds" 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_process_fds-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_process_fds-detail">-</small>
|
||||
<div class="overview-meter" id="overview-extra-extra_process_fds-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraProcessFds" aria-valuemin="0" aria-valuemax="100"
|
||||
aria-valuetext="-">
|
||||
<span class="overview-meter-fill"
|
||||
id="overview-extra-extra_process_fds-meter-fill"></span>
|
||||
</div><small id="overview-extra-extra_process_fds-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_open_connections" hidden>
|
||||
<span data-i18n="overviewExtraOpenConnections">overviewExtraOpenConnections</span><strong
|
||||
@@ -437,32 +477,69 @@
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_request_error_rate-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_cache_hit_rate" hidden>
|
||||
<span data-i18n="overviewExtraCacheHitRate">overviewExtraCacheHitRate</span><strong
|
||||
id="overview-extra-extra_cache_hit_rate">-</strong>
|
||||
<svg data-i18n-aria="overviewExtraCacheHitRate" 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_cache_hit_rate-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_cache_hit_rate-detail">-</small>
|
||||
<article class="stat stat-donut" data-dashboard-widget="extra_cache_hit_rate" hidden>
|
||||
<span data-i18n="overviewExtraCacheHitRate">Cache hits share (recent)</span>
|
||||
<div class="overview-donut-row">
|
||||
<svg class="overview-donut-svg" id="extra-cache-ratio-chart" role="img"
|
||||
aria-label="Recent cache HIT and MISS distribution" viewBox="0 0 64 64">
|
||||
<circle class="overview-donut-track" cx="32" cy="32" r="24"></circle>
|
||||
<circle class="overview-donut-hit" id="extra-cache-ratio-hit-arc" cx="32" cy="32"
|
||||
r="24" pathLength="100" stroke-dasharray="0 100"></circle>
|
||||
<circle class="overview-donut-miss" id="extra-cache-ratio-miss-arc" cx="32" cy="32"
|
||||
r="24" pathLength="100" stroke-dasharray="0 100"></circle>
|
||||
</svg>
|
||||
<div class="overview-donut-info">
|
||||
<strong id="overview-extra-extra_cache_hit_rate">-</strong>
|
||||
<small class="overview-donut-legend"
|
||||
id="overview-extra-extra_cache_hit_rate-detail">
|
||||
<span class="overview-donut-hit-key"><i></i>HIT <b
|
||||
id="extra-cache-ratio-hit-count">0</b></span>
|
||||
<span class="overview-donut-miss-key"><i></i>MISS <b
|
||||
id="extra-cache-ratio-miss-count">0</b></span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_cache_memory" hidden>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_cache_memory">
|
||||
<span data-i18n="overviewExtraCacheMemory">overviewExtraCacheMemory</span><strong
|
||||
id="overview-extra-extra_cache_memory">-</strong>
|
||||
<svg data-i18n-aria="overviewExtraCacheMemory" 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_cache_memory-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_cache_memory-detail">-</small>
|
||||
<div class="overview-meter" id="overview-extra-extra_cache_memory-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraCacheMemory" aria-valuemin="0" aria-valuemax="100"
|
||||
aria-valuetext="-">
|
||||
<span class="overview-meter-fill"
|
||||
id="overview-extra-extra_cache_memory-meter-fill"></span>
|
||||
</div><small id="overview-extra-extra_cache_memory-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_h2_share" hidden>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_cold_queue_memory">
|
||||
<span data-i18n="overviewExtraColdQueueMemory">COLD queue RAM utilization</span>
|
||||
<strong id="overview-extra-extra_cold_queue_memory">-</strong>
|
||||
<div class="overview-meter" id="overview-extra-extra_cold_queue_memory-meter"
|
||||
role="progressbar" data-i18n-aria="overviewExtraColdQueueMemory" aria-valuemin="0"
|
||||
aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill"
|
||||
id="overview-extra-extra_cold_queue_memory-meter-fill"></span>
|
||||
</div>
|
||||
<small id="overview-extra-extra_cold_queue_memory-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_cold_storage">
|
||||
<span data-i18n="overviewExtraColdStorage">COLD disk utilization</span>
|
||||
<strong id="overview-extra-extra_cold_storage">-</strong>
|
||||
<div class="overview-meter" id="overview-extra-extra_cold_storage-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraColdStorage" aria-valuemin="0" aria-valuemax="100"
|
||||
aria-valuetext="-">
|
||||
<span class="overview-meter-fill"
|
||||
id="overview-extra-extra_cold_storage-meter-fill"></span>
|
||||
</div>
|
||||
<small id="overview-extra-extra_cold_storage-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_h2_share" hidden>
|
||||
<span data-i18n="overviewExtraH2Share">overviewExtraH2Share</span><strong
|
||||
id="overview-extra-extra_h2_share">-</strong>
|
||||
<svg data-i18n-aria="overviewExtraH2Share" 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_h2_share-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_h2_share-detail">-</small>
|
||||
<div class="overview-meter" id="overview-extra-extra_h2_share-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraH2Share" aria-valuemin="0" aria-valuemax="100"
|
||||
aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_h2_share-meter-fill"></span>
|
||||
</div><small id="overview-extra-extra_h2_share-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_response_latency" hidden>
|
||||
<span data-i18n="overviewExtraResponseLatency">overviewExtraResponseLatency</span><strong
|
||||
@@ -502,24 +579,24 @@
|
||||
</svg><small id="overview-extra-extra_active_exchanges-detail"
|
||||
data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_h1_share" hidden>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_h1_share" hidden>
|
||||
<span data-i18n="overviewExtraH1Share">HTTP/1.1 share</span><strong
|
||||
id="overview-extra-extra_h1_share">-</strong>
|
||||
<svg viewBox="0 0 300 60" role="img" aria-label="HTTP/1.1 share">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_h1_share-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_h1_share-detail"
|
||||
<div class="overview-meter" id="overview-extra-extra_h1_share-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraH1Share" aria-valuemin="0" aria-valuemax="100"
|
||||
aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_h1_share-meter-fill"></span>
|
||||
</div><small id="overview-extra-extra_h1_share-detail"
|
||||
data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_h3_share" hidden>
|
||||
<article class="stat stat-meter" data-dashboard-widget="extra_h3_share" hidden>
|
||||
<span data-i18n="overviewExtraH3Share">HTTP/3 share</span><strong
|
||||
id="overview-extra-extra_h3_share">-</strong>
|
||||
<svg viewBox="0 0 300 60" role="img" aria-label="HTTP/3 share">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_h3_share-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_h3_share-detail"
|
||||
<div class="overview-meter" id="overview-extra-extra_h3_share-meter" role="progressbar"
|
||||
data-i18n-aria="overviewExtraH3Share" aria-valuemin="0" aria-valuemax="100"
|
||||
aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="overview-extra-extra_h3_share-meter-fill"></span>
|
||||
</div><small id="overview-extra-extra_h3_share-detail"
|
||||
data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_cache_miss_rate" hidden>
|
||||
@@ -700,6 +777,286 @@
|
||||
id="overview-wide-wide_latency-probe-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_transform_activity">
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideTransformActivity">Transformation activity</h2>
|
||||
<span class="overview-wide-summary"
|
||||
id="overview-wide-wide_transform_activity-summary">-</span>
|
||||
</div>
|
||||
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
data-i18n-aria="overviewWideTransformActivity" aria-label="Transformation activity">
|
||||
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0"
|
||||
id="overview-wide-wide_transform_activity-applied-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1"
|
||||
id="overview-wide-wide_transform_activity-cache_hits-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-2"
|
||||
id="overview-wide-wide_transform_activity-skipped-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-3"
|
||||
id="overview-wide-wide_transform_activity-errors-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="overviewTransformApplied">Applied</span><b
|
||||
id="overview-wide-wide_transform_activity-applied-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span
|
||||
data-i18n="overviewTransformCacheHits">Cache HIT</span><b
|
||||
id="overview-wide-wide_transform_activity-cache_hits-value">-</b></span>
|
||||
<span><i class="overview-wide-key-2"></i><span
|
||||
data-i18n="overviewTransformSkipped">Skipped</span><b
|
||||
id="overview-wide-wide_transform_activity-skipped-value">-</b></span>
|
||||
<span><i class="overview-wide-key-3"></i><span
|
||||
data-i18n="overviewTransformErrors">Errors</span><b
|
||||
id="overview-wide-wide_transform_activity-errors-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_compression_activity">
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideCompressionActivity">Compression activity</h2>
|
||||
<span class="overview-wide-summary"
|
||||
id="overview-wide-wide_compression_activity-summary">-</span>
|
||||
</div>
|
||||
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
data-i18n-aria="overviewWideCompressionActivity" aria-label="Compression activity">
|
||||
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0"
|
||||
id="overview-wide-wide_compression_activity-gzip-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1"
|
||||
id="overview-wide-wide_compression_activity-br-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-2"
|
||||
id="overview-wide-wide_compression_activity-zstd-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-3"
|
||||
id="overview-wide-wide_compression_activity-deflate-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span>gzip</span><b
|
||||
id="overview-wide-wide_compression_activity-gzip-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span>Brotli</span><b
|
||||
id="overview-wide-wide_compression_activity-br-value">-</b></span>
|
||||
<span><i class="overview-wide-key-2"></i><span>Zstd</span><b
|
||||
id="overview-wide-wide_compression_activity-zstd-value">-</b></span>
|
||||
<span><i class="overview-wide-key-3"></i><span>deflate</span><b
|
||||
id="overview-wide-wide_compression_activity-deflate-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stat stat-donut" data-dashboard-widget="www_status" hidden>
|
||||
<span data-i18n="webMetricStatus">webMetricStatus</span>
|
||||
<div class="overview-donut-row">
|
||||
<svg class="overview-donut-svg" id="www_status-chart" role="img" viewBox="0 0 64 64"
|
||||
aria-label="WWW status" data-i18n-aria="webMetricStatus">
|
||||
<circle class="overview-donut-track" cx="32" cy="32" r="24"></circle>
|
||||
<circle class="www-ring-arc www-ring-0" id="www_status-arc-0" cx="32" cy="32" r="24"
|
||||
pathLength="100" stroke-dasharray="0 100" />
|
||||
<circle class="www-ring-arc www-ring-1" id="www_status-arc-1" cx="32" cy="32" r="24"
|
||||
pathLength="100" stroke-dasharray="0 100" />
|
||||
<circle class="www-ring-arc www-ring-2" id="www_status-arc-2" cx="32" cy="32" r="24"
|
||||
pathLength="100" stroke-dasharray="0 100" />
|
||||
<circle class="www-ring-arc www-ring-3" id="www_status-arc-3" cx="32" cy="32" r="24"
|
||||
pathLength="100" stroke-dasharray="0 100" />
|
||||
</svg>
|
||||
<div class="overview-donut-info"><strong id="www_status-value">-</strong><small
|
||||
class="overview-donut-legend www-ring-legend"><span><i class="www-dot-0"></i>2xx
|
||||
<b id="www_status-count-0">0</b></span>
|
||||
<span><i class="www-dot-1"></i>3xx <b id="www_status-count-1">0</b></span>
|
||||
<span><i class="www-dot-2"></i>4xx <b id="www_status-count-2">0</b></span>
|
||||
<span><i class="www-dot-3"></i>5xx <b
|
||||
id="www_status-count-3">0</b></span></small></div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stat stat-donut" data-dashboard-widget="www_kind" hidden>
|
||||
<span data-i18n="webMetricKind">webMetricKind</span>
|
||||
<div class="overview-donut-row">
|
||||
<svg class="overview-donut-svg" id="www_kind-chart" role="img" viewBox="0 0 64 64"
|
||||
aria-label="Static / PHP" data-i18n-aria="webMetricKind">
|
||||
<circle class="overview-donut-track" cx="32" cy="32" r="24"></circle>
|
||||
<circle class="www-ring-arc www-ring-0" id="www_kind-arc-0" cx="32" cy="32" r="24"
|
||||
pathLength="100" stroke-dasharray="0 100" />
|
||||
<circle class="www-ring-arc www-ring-1" id="www_kind-arc-1" cx="32" cy="32" r="24"
|
||||
pathLength="100" stroke-dasharray="0 100" />
|
||||
<circle class="www-ring-arc www-ring-2" id="www_kind-arc-2" cx="32" cy="32" r="24"
|
||||
pathLength="100" stroke-dasharray="0 100" />
|
||||
</svg>
|
||||
<div class="overview-donut-info"><strong id="www_kind-value">-</strong><small
|
||||
class="overview-donut-legend www-ring-legend"><span><i
|
||||
class="www-dot-0"></i>Static <b id="www_kind-count-0">0</b></span>
|
||||
<span><i class="www-dot-1"></i>PHP <b id="www_kind-count-1">0</b></span>
|
||||
<span><i class="www-dot-2"></i>Other <b
|
||||
id="www_kind-count-2">0</b></span></small></div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="www_capacity" hidden>
|
||||
<span data-i18n="webMetricCapacity">webMetricCapacity</span><strong
|
||||
id="www_capacity-value">-</strong>
|
||||
<div class="overview-meter" id="www_capacity-meter" role="progressbar" aria-valuemin="0"
|
||||
aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="www_capacity-meter-fill"></span>
|
||||
</div><small id="www_capacity-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-meter" data-dashboard-widget="www_error_rate" hidden>
|
||||
<span data-i18n="webMetricErrorRate">webMetricErrorRate</span><strong
|
||||
id="www_error_rate-value">-</strong>
|
||||
<div class="overview-meter" id="www_error_rate-meter" role="progressbar" aria-valuemin="0"
|
||||
aria-valuemax="100" aria-valuetext="-">
|
||||
<span class="overview-meter-fill" id="www_error_rate-meter-fill"></span>
|
||||
</div><small id="www_error_rate-detail">-</small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="www_methods" hidden>
|
||||
<span data-i18n="webMetricMethods">HTTP Methods</span><strong
|
||||
id="www_methods-value">-</strong>
|
||||
<div class="www-segmented" id="www_methods-bar" role="img" aria-label="HTTP method share"
|
||||
data-i18n-aria="webMetricMethods"><span class="www-segment-0"
|
||||
id="www_methods-segment-0"></span><span class="www-segment-1"
|
||||
id="www_methods-segment-1"></span><span class="www-segment-2"
|
||||
id="www_methods-segment-2"></span><span class="www-segment-3"
|
||||
id="www_methods-segment-3"></span><span class="www-segment-4"
|
||||
id="www_methods-segment-4"></span></div>
|
||||
<small class="www-method-legend" id="www_methods-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="www_php_requests" hidden>
|
||||
<span data-i18n="webMetricPhpRequests">PHP requests/s</span><strong
|
||||
id="www_php_requests-value">-</strong>
|
||||
<svg role="img" viewBox="0 0 300 60" aria-label="PHP requests per second"
|
||||
data-i18n-aria="webMetricPhpRequests">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="www_php_requests-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg>
|
||||
<small data-i18n="webMetricPerSecond">Requests per second</small>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="www_requests" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="webMetricRequests">webMetricRequests</h2><span
|
||||
class="overview-wide-summary" id="www_requests-value">-</span>
|
||||
</div>
|
||||
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
aria-label="Web Server request rate" data-i18n-aria="webMetricRequests">
|
||||
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="www_requests-all-line"
|
||||
d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="www_requests-php-line"
|
||||
d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend"><span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="webMetricSeriesAll">all</span><b
|
||||
id="www_requests-all-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span
|
||||
data-i18n="webMetricSeriesPhp">php</span><b
|
||||
id="www_requests-php-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="www_latency" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="webMetricLatency">webMetricLatency</h2><span
|
||||
class="overview-wide-summary" id="www_latency-value">-</span>
|
||||
</div>
|
||||
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
aria-label="Web Server header latency" data-i18n-aria="webMetricLatency">
|
||||
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="www_latency-web-line"
|
||||
d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="www_latency-php-line"
|
||||
d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend"><span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="webMetricSeriesWeb">web</span><b
|
||||
id="www_latency-web-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span
|
||||
data-i18n="webMetricSeriesPhp">php</span><b
|
||||
id="www_latency-php-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="www_failures" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="webMetricFailures">webMetricFailures</h2><span
|
||||
class="overview-wide-summary" id="www_failures-value">-</span>
|
||||
</div>
|
||||
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
aria-label="PHP FastCGI failures" data-i18n-aria="webMetricFailures">
|
||||
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="www_failures-timeout-line"
|
||||
d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1"
|
||||
id="www_failures-connection-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-2"
|
||||
id="www_failures-protocol-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-3"
|
||||
id="www_failures-capacity-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend"><span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="webMetricSeriesTimeout">timeout</span><b
|
||||
id="www_failures-timeout-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span
|
||||
data-i18n="webMetricSeriesConnection">connection</span><b
|
||||
id="www_failures-connection-value">-</b></span>
|
||||
<span><i class="overview-wide-key-2"></i><span
|
||||
data-i18n="webMetricSeriesProtocol">protocol</span><b
|
||||
id="www_failures-protocol-value">-</b></span>
|
||||
<span><i class="overview-wide-key-3"></i><span
|
||||
data-i18n="webMetricSeriesCapacity">capacity</span><b
|
||||
id="www_failures-capacity-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="www_traffic" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="webMetricTraffic">webMetricTraffic</h2><span
|
||||
class="overview-wide-summary" id="www_traffic-value">-</span>
|
||||
</div>
|
||||
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
aria-label="Web Server body byte rate" data-i18n-aria="webMetricTraffic">
|
||||
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="www_traffic-nonphp-line"
|
||||
d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="www_traffic-php-line"
|
||||
d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend"><span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="webMetricSeriesNonphp">nonphp</span><b
|
||||
id="www_traffic-nonphp-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span
|
||||
data-i18n="webMetricSeriesPhp">php</span><b
|
||||
id="www_traffic-php-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="www_status_trend" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="webMetricStatusTrend">webMetricStatusTrend</h2><span
|
||||
class="overview-wide-summary" id="www_status_trend-value">-</span>
|
||||
</div>
|
||||
<svg class="overview-wide-svg" viewBox="0 0 600 150" role="img"
|
||||
aria-label="Web Server HTTP statuses" data-i18n-aria="webMetricStatusTrend">
|
||||
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
|
||||
<path class="overview-wide-series overview-wide-series-0" id="www_status_trend-2xx-line"
|
||||
d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-1" id="www_status_trend-3xx-line"
|
||||
d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-2" id="www_status_trend-4xx-line"
|
||||
d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-3" id="www_status_trend-5xx-line"
|
||||
d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend"><span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="webMetricSeriesTwoxx">2xx</span><b
|
||||
id="www_status_trend-2xx-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span
|
||||
data-i18n="webMetricSeriesThreexx">3xx</span><b
|
||||
id="www_status_trend-3xx-value">-</b></span>
|
||||
<span><i class="overview-wide-key-2"></i><span
|
||||
data-i18n="webMetricSeriesFourxx">4xx</span><b
|
||||
id="www_status_trend-4xx-value">-</b></span>
|
||||
<span><i class="overview-wide-key-3"></i><span
|
||||
data-i18n="webMetricSeriesFivexx">5xx</span><b
|
||||
id="www_status_trend-5xx-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="www_top_services" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="webMetricTopServices">Top Web Services</h2>
|
||||
<span class="overview-wide-summary" id="www_top_services-value">-</span>
|
||||
</div>
|
||||
<div id="www_top_services-bars" class="www-top-bars"></div>
|
||||
<small class="fine" data-i18n="webMetricTopServicesHelp">Requests per second by Web
|
||||
Service.</small>
|
||||
</article>
|
||||
<article class="card activity-card" data-dashboard-widget="traffic_chart">
|
||||
<div class="card-heading">
|
||||
<div><span class="small-label" data-i18n="liveTraffic">LIVE TRAFFIC</span>
|
||||
|
||||
@@ -11236,3 +11236,392 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
|
||||
overscroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
|
||||
#panel-overview .stat-meter>.overview-meter {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
margin: 9px 0 8px;
|
||||
overflow: hidden;
|
||||
border-radius: var(--rp-radius-control, 5px);
|
||||
background: var(--line);
|
||||
}
|
||||
|
||||
#panel-overview .overview-meter-fill {
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: var(--chart-accent);
|
||||
transition: width 180ms ease;
|
||||
}
|
||||
|
||||
#panel-overview .stat-meter>small {
|
||||
display: block;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
#panel-overview .stat-donut .overview-donut-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
margin: 4px 0 2px;
|
||||
}
|
||||
|
||||
#panel-overview .stat-donut .overview-donut-svg {
|
||||
display: block;
|
||||
width: 62px;
|
||||
height: 62px;
|
||||
flex: 0 0 62px;
|
||||
overflow: visible;
|
||||
filter: var(--chart-svg-filter);
|
||||
}
|
||||
|
||||
#panel-overview .overview-donut-svg circle {
|
||||
fill: none;
|
||||
stroke-width: 9;
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: 32px 32px;
|
||||
}
|
||||
|
||||
#panel-overview .overview-donut-track {
|
||||
stroke: var(--line);
|
||||
}
|
||||
|
||||
#panel-overview .overview-donut-hit {
|
||||
stroke: var(--chart-accent);
|
||||
}
|
||||
|
||||
#panel-overview .overview-donut-miss {
|
||||
stroke: var(--chart-secondary);
|
||||
}
|
||||
|
||||
#panel-overview .stat-donut .overview-donut-legend {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#panel-overview .overview-donut-legend>span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
flex-wrap: wrap;
|
||||
line-height: 1.25;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
#panel-overview .overview-donut-legend i {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
flex: 0 0 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--chart-accent);
|
||||
}
|
||||
|
||||
#panel-overview .overview-donut-miss-key i {
|
||||
background: var(--chart-secondary);
|
||||
}
|
||||
|
||||
#panel-overview .overview-donut-legend b {
|
||||
color: var(--text);
|
||||
font-family: var(--mono);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#panel-overview .overview-wide-series-3 {
|
||||
stroke: var(--chart-quaternary);
|
||||
}
|
||||
|
||||
#panel-overview .overview-wide-legend .overview-wide-key-3 {
|
||||
background: var(--chart-quaternary);
|
||||
}
|
||||
|
||||
@container overview-canvas (max-width: 700px) {
|
||||
#panel-overview .stat-donut .overview-donut-row {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
#panel-overview .stat-donut .overview-donut-svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex-basis: 48px;
|
||||
}
|
||||
|
||||
#panel-overview .stat-donut .overview-donut-legend {
|
||||
gap: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
#panel-overview .overview-meter-fill {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.page-intro .page-intro-meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 16px;
|
||||
min-width: 0;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.page-intro .page-intro-meta-row .eyebrow {
|
||||
margin-bottom: 0;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.page-intro .overview-samples-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 6px 12px;
|
||||
flex-wrap: wrap;
|
||||
max-width: 100%;
|
||||
margin: 0 0 0 auto;
|
||||
}
|
||||
|
||||
.page-intro .overview-samples-toolbar .function-live-chart-samples select {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.page-intro .overview-samples-toolbar button {
|
||||
width: auto;
|
||||
flex: none;
|
||||
min-height: 28px;
|
||||
padding: 5px 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.page-intro .overview-samples-toolbar .fine {
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.page-intro .overview-samples-toolbar {
|
||||
margin-left: 0;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
#panel-overview .stat-donut .overview-donut-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
#panel-overview .stat-donut .overview-donut-info>strong {
|
||||
display: block;
|
||||
line-height: 1.1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#panel-overview .stat-donut .overview-donut-info>.overview-donut-legend {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
#panel-overview .www-ring-legend {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
column-gap: 7px;
|
||||
row-gap: 5px;
|
||||
}
|
||||
|
||||
#panel-overview .www-ring-legend span {
|
||||
white-space: nowrap;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
#panel-overview .www-ring-arc {
|
||||
stroke-width: 9;
|
||||
}
|
||||
|
||||
#panel-overview .www-ring-0,
|
||||
.www-dot-0 {
|
||||
stroke: var(--chart-accent);
|
||||
}
|
||||
|
||||
#panel-overview .www-ring-1,
|
||||
.www-dot-1 {
|
||||
stroke: var(--chart-secondary);
|
||||
}
|
||||
|
||||
#panel-overview .www-ring-2,
|
||||
.www-dot-2 {
|
||||
stroke: var(--chart-tertiary);
|
||||
}
|
||||
|
||||
#panel-overview .www-ring-3,
|
||||
.www-dot-3 {
|
||||
stroke: var(--chart-quaternary);
|
||||
}
|
||||
|
||||
#panel-overview .www-ring-legend i {
|
||||
background: var(--chart-accent);
|
||||
}
|
||||
|
||||
#panel-overview .www-ring-legend .www-dot-1 {
|
||||
background: var(--chart-secondary);
|
||||
}
|
||||
|
||||
#panel-overview .www-ring-legend .www-dot-2 {
|
||||
background: var(--chart-tertiary);
|
||||
}
|
||||
|
||||
#panel-overview .www-ring-legend .www-dot-3 {
|
||||
background: var(--chart-quaternary);
|
||||
}
|
||||
|
||||
#panel-overview .www-segmented {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
margin: 9px 0 8px;
|
||||
background: var(--line);
|
||||
border-radius: var(--rp-radius-control, 5px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#panel-overview .www-segmented span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
transition: width 180ms ease;
|
||||
}
|
||||
|
||||
#panel-overview .www-segment-0 {
|
||||
background: var(--chart-accent);
|
||||
}
|
||||
|
||||
#panel-overview .www-segment-1 {
|
||||
background: var(--chart-secondary);
|
||||
}
|
||||
|
||||
#panel-overview .www-segment-2 {
|
||||
background: var(--chart-tertiary);
|
||||
}
|
||||
|
||||
#panel-overview .www-segment-3 {
|
||||
background: var(--chart-quaternary);
|
||||
}
|
||||
|
||||
#panel-overview .www-segment-4 {
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
#panel-overview .www-method-legend {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
#panel-overview .www-top-bars {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
min-height: 90px;
|
||||
}
|
||||
|
||||
#panel-overview .www-top-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(60px, 1fr) minmax(30px, 2fr) auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
font: 11px var(--mono);
|
||||
}
|
||||
|
||||
#panel-overview .www-top-row>span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#panel-overview .www-top-meter {
|
||||
height: 7px;
|
||||
background: var(--line);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#panel-overview .www-top-meter span {
|
||||
display: block;
|
||||
background: var(--chart-accent);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#panel-overview .www-top-row b {
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
#panel-web-server .www-service-panel {
|
||||
border-top: 1px solid var(--line);
|
||||
margin-top: 10px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
#panel-web-server .www-service-panel summary {
|
||||
cursor: pointer;
|
||||
color: var(--chart-accent);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#panel-web-server .www-service-live {
|
||||
padding: 12px 0 3px;
|
||||
}
|
||||
|
||||
#panel-web-server .www-service-values {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#panel-web-server .www-service-values>div {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#panel-web-server .www-service-values small {
|
||||
color: var(--muted);
|
||||
font: 10px var(--mono);
|
||||
}
|
||||
|
||||
#panel-web-server .www-service-values strong {
|
||||
font: 500 14px var(--mono);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
#panel-web-server .www-service-live svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-height: 86px;
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
#panel-web-server .www-service-live polyline {
|
||||
fill: none;
|
||||
stroke: var(--chart-accent);
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
#panel-web-server .www-service-split {
|
||||
color: var(--muted);
|
||||
font: 11px var(--mono);
|
||||
overflow-wrap: anywhere;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
#panel-overview .www-segmented span {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -392,11 +392,70 @@
|
||||
item.append(el("span", t(key), "small-label"), el("b", value));
|
||||
details.append(item);
|
||||
}
|
||||
box.append(details); return box;
|
||||
const live = el("details", undefined, "www-service-panel");
|
||||
const summary = el("summary", t("webLiveMetrics")); live.append(summary);
|
||||
const panel = el("div", undefined, "www-service-live"); panel.dataset.webLiveId = service.id;
|
||||
const figures = el("div", undefined, "www-service-values");
|
||||
for (const [field, key] of [
|
||||
["requests", "webLiveRequests"], ["rate", "webLiveRate"],
|
||||
["errors", "webLiveErrors"], ["latency", "webLiveLatency"],
|
||||
["php", "webLivePhpCapacity"], ["bytes", "webLiveBytes"]]) {
|
||||
const item = el("div");
|
||||
const name = el("small", t(key));
|
||||
name.dataset.i18n = key;
|
||||
const value = el("strong", "-"); value.dataset.webLiveField = field;
|
||||
item.append(name, value); figures.append(item);
|
||||
}
|
||||
const chart = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
chart.setAttribute("viewBox", "0 0 300 60"); chart.setAttribute("role", "img");
|
||||
chart.setAttribute("aria-label", t("webLiveRate"));
|
||||
const line = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
|
||||
line.setAttribute("points", "0,55 300,55"); chart.append(line);
|
||||
const split = el("p", "-", "www-service-split"); split.dataset.webLiveField = "split";
|
||||
panel.append(figures, chart, split); live.append(panel);
|
||||
live.addEventListener("toggle", () => { if (live.open && latestStatus) renderLive(latestStatus, false); });
|
||||
box.append(details, live); return box;
|
||||
});
|
||||
$("web-service-list").replaceChildren(...(cards.length ? cards : [el("p", t(query ? "webServiceSearchEmpty" : "webServiceEmpty"), "fine")]));
|
||||
}
|
||||
|
||||
const serviceHistory = new Map();
|
||||
const serviceLast = new Map();
|
||||
function renderLive(status, record = true) {
|
||||
if (currentPanel !== "web-server") return;
|
||||
const services = status?.web_server_metrics || {};
|
||||
for (const panel of document.querySelectorAll("#web-service-list [data-web-live-id]")) {
|
||||
if (!panel.closest("details")?.open) continue;
|
||||
const id = panel.dataset.webLiveId, values = services[id], web = values?.web;
|
||||
const lookup = key => panel.querySelector(`[data-web-live-field="${key}"]`);
|
||||
const set = (key, value) => { const node = lookup(key); if (node) node.textContent = value; };
|
||||
if (!web) { for (const key of ["requests", "rate", "errors", "latency", "php", "bytes", "split"]) set(key, "-"); continue; }
|
||||
const n = value => value == null || !Number.isFinite(Number(value)) ? 0 : Number(value);
|
||||
const total = n(web.requests), php = values.php, last = serviceLast.get(id);
|
||||
const sample = rustProxyRateSampling.difference(record ? last : null,
|
||||
[total, n(web.response_headers_us), n(web.status_5xx)], performance.now(),
|
||||
rustProxyRateSampling.cadence("overview_seconds"));
|
||||
if (record) serviceLast.set(id, sample.next);
|
||||
const deltas = record ? sample.deltas : null;
|
||||
const rate = deltas ? deltas[0] / sample.seconds : null;
|
||||
const previous = serviceHistory.get(id) || [];
|
||||
if (record) { previous.push(rate); window.rustProxyOverviewSamples.trim(previous); serviceHistory.set(id, previous); }
|
||||
const peak = Math.max(1, ...previous.filter(x => x != null));
|
||||
const points = previous.map((value, i) => `${300 * i / Math.max(1, previous.length - 1)},${55 - (value || 0) / peak * 50}`).join(" ");
|
||||
panel.querySelector("polyline")?.setAttribute("points", points || "0,55 300,55");
|
||||
set("requests", fmt(total)); set("rate", rate == null ? "-" : `${fmt(rate, 1)}/s`);
|
||||
set("errors", fmt(n(web.status_5xx)));
|
||||
set("latency", deltas?.[0] > 0 ? `${fmt(deltas[1] / deltas[0] / 1000, 1)} ms` : "-");
|
||||
set("php", php ? `${fmt(n(php.capacity_used))} / ${fmt(n(php.capacity_limit))}` : t("webLiveNoPhp"));
|
||||
set("bytes", fmtBytes(n(web.response_bytes)));
|
||||
set("split", `${t("webLiveKinds")}: ${fmt(n(web.static_requests))} / ${fmt(n(web.php_requests))} / ${fmt(n(web.other_requests))}`);
|
||||
}
|
||||
}
|
||||
window.rustProxyOverviewSamples.onChange(() => {
|
||||
for (const history of serviceHistory.values()) window.rustProxyOverviewSamples.trim(history);
|
||||
if (latestStatus && currentPanel === "web-server") renderLive(latestStatus, false);
|
||||
});
|
||||
|
||||
form.addEventListener("submit", async event => {
|
||||
event.preventDefault(); if (!envelope || busy) return;
|
||||
const invalid = [...form.elements].find(input => input.willValidate && !input.checkValidity()); if (invalid) { activateTab(invalid.closest("[data-web-service-page]")?.dataset.webServicePage || "general"); invalid.reportValidity(); return; }
|
||||
@@ -491,6 +550,6 @@
|
||||
$("web-service-search").addEventListener("input", render);
|
||||
dialog.addEventListener("cancel", event => { event.preventDefault(); closeEditor(); });
|
||||
window.rustProxyDrafts.register("web-server", { labelKey: "webServer", isOpen: () => dialog.open, isDirty: () => dirty, suspend: suspendDraft, restore: restoreDraft, close: clearEditor });
|
||||
window.rustProxyWebServer = { render, reset: clearEditor };
|
||||
window.rustProxyWebServer = { render, renderLive, reset() { clearEditor(); serviceHistory.clear(); serviceLast.clear(); } };
|
||||
if (envelope) render();
|
||||
})();
|
||||
+138
-13
@@ -1,28 +1,153 @@
|
||||
// Only configured Web Service IDs and bounded enumerated classes/reasons are labels.
|
||||
fn render_web_php(snapshot: &crate::config::Snapshot, f: &mut Families) {
|
||||
use crate::web_server::metrics::PHP_FAILURE_NAMES;
|
||||
let config = &snapshot.envelope.config;
|
||||
for service in &config.web_services {
|
||||
let Some(compiled) = snapshot.web_php_services.get(&service.id) else { continue };
|
||||
let Some(compiled) = snapshot.web_php_services.get(&service.id) else {
|
||||
continue;
|
||||
};
|
||||
let web = &compiled.web_metrics;
|
||||
let wl = labels(&[("web_service", service.id.as_str())]);
|
||||
f.add("rust_proxy_web_server_requests_total", "counter", "Web Server requests completed through response headers, including PHP requests.", &wl, web.requests.load(Ordering::Relaxed));
|
||||
f.add(
|
||||
"rust_proxy_web_server_requests_total",
|
||||
"counter",
|
||||
"Web Server responses through headers, including PHP.",
|
||||
&wl,
|
||||
web.requests.load(Ordering::Relaxed),
|
||||
);
|
||||
for (class, count) in [
|
||||
("2xx", &web.status_2xx), ("3xx", &web.status_3xx),
|
||||
("4xx", &web.status_4xx), ("5xx", &web.status_5xx),
|
||||
("2xx", &web.status_2xx),
|
||||
("3xx", &web.status_3xx),
|
||||
("4xx", &web.status_4xx),
|
||||
("5xx", &web.status_5xx),
|
||||
] {
|
||||
f.add("rust_proxy_web_server_responses_total", "counter", "Web Server responses by status class.", &format!("{wl},class=\"{class}\""), count.load(Ordering::Relaxed));
|
||||
f.add(
|
||||
"rust_proxy_web_server_responses_total",
|
||||
"counter",
|
||||
"Web Server response status classes.",
|
||||
&format!("{wl},class=\"{class}\""),
|
||||
count.load(Ordering::Relaxed),
|
||||
);
|
||||
}
|
||||
f.add(
|
||||
"rust_proxy_web_server_response_header_duration_microseconds_total",
|
||||
"counter",
|
||||
"Cumulative response-header latency, not full streamed response latency.",
|
||||
&wl,
|
||||
web.response_headers_us.load(Ordering::Relaxed),
|
||||
);
|
||||
f.add("rust_proxy_web_server_response_body_bytes_total", "counter", "Web Server body frame bytes observed before downstream compression, not network traffic.", &wl, web.response_bytes.load(Ordering::Relaxed));
|
||||
f.add(
|
||||
"rust_proxy_web_server_requests_active",
|
||||
"gauge",
|
||||
"Web Server requests still waiting for response headers.",
|
||||
&wl,
|
||||
web.in_flight_headers.load(Ordering::Relaxed),
|
||||
);
|
||||
for (kind, count) in [
|
||||
("static", &web.static_requests),
|
||||
("php", &web.php_requests),
|
||||
("other", &web.other_requests),
|
||||
] {
|
||||
f.add(
|
||||
"rust_proxy_web_server_request_kind_total",
|
||||
"counter",
|
||||
"Web Server responses by resolved handler (other includes early rejects).",
|
||||
&format!("{wl},kind=\"{kind}\""),
|
||||
count.load(Ordering::Relaxed),
|
||||
);
|
||||
}
|
||||
for (method, count) in ["GET", "POST", "PUT", "DELETE", "other"]
|
||||
.iter()
|
||||
.zip(&web.methods)
|
||||
{
|
||||
f.add(
|
||||
"rust_proxy_web_server_methods_total",
|
||||
"counter",
|
||||
"Web Server requests grouped by HTTP method.",
|
||||
&format!("{wl},method=\"{method}\""),
|
||||
count.load(Ordering::Relaxed),
|
||||
);
|
||||
}
|
||||
for (status, count) in ["200", "206", "304", "403", "404", "500", "502", "503"]
|
||||
.iter()
|
||||
.zip(&web.selected_status)
|
||||
{
|
||||
f.add(
|
||||
"rust_proxy_web_server_status_code_total",
|
||||
"counter",
|
||||
"Selected exact HTTP status codes (other codes are in class totals).",
|
||||
&format!("{wl},status=\"{status}\""),
|
||||
count.load(Ordering::Relaxed),
|
||||
);
|
||||
}
|
||||
for (encoding, count) in ["gzip", "br", "zstd", "deflate"]
|
||||
.iter()
|
||||
.zip(&web.compression)
|
||||
{
|
||||
f.add("rust_proxy_web_server_compression_total", "counter", "Web Server routed responses compressed by RustProxy's downstream encoder (excludes precompressed source files).", &format!("{wl},encoding=\"{encoding}\""), count.load(Ordering::Relaxed));
|
||||
}
|
||||
f.add("rust_proxy_web_server_response_header_duration_microseconds_total", "counter", "Web Server response header latency, excluding streamed body transfer.", &wl, web.response_headers_us.load(Ordering::Relaxed));
|
||||
let Some(php) = &compiled.php else { continue };
|
||||
let l = labels(&[("web_service", service.id.as_str())]);
|
||||
let m = &php.metrics;
|
||||
f.add("rust_proxy_php_requests_total", "counter", "PHP-FPM requests completed through response headers.", &l, m.requests.load(Ordering::Relaxed));
|
||||
f.add(
|
||||
"rust_proxy_php_requests_total",
|
||||
"counter",
|
||||
"PHP requests completed through response headers.",
|
||||
&wl,
|
||||
m.requests.load(Ordering::Relaxed),
|
||||
);
|
||||
for (class, count) in [
|
||||
("2xx", &m.status_2xx), ("3xx", &m.status_3xx),
|
||||
("4xx", &m.status_4xx), ("5xx", &m.status_5xx),
|
||||
("2xx", &m.status_2xx),
|
||||
("3xx", &m.status_3xx),
|
||||
("4xx", &m.status_4xx),
|
||||
("5xx", &m.status_5xx),
|
||||
] {
|
||||
f.add("rust_proxy_php_responses_total", "counter", "PHP response headers by status class.", &format!("{l},class=\"{class}\""), count.load(Ordering::Relaxed));
|
||||
f.add(
|
||||
"rust_proxy_php_responses_total",
|
||||
"counter",
|
||||
"PHP response headers by status class.",
|
||||
&format!("{wl},class=\"{class}\""),
|
||||
count.load(Ordering::Relaxed),
|
||||
);
|
||||
}
|
||||
f.add(
|
||||
"rust_proxy_php_response_header_duration_microseconds_total",
|
||||
"counter",
|
||||
"Cumulative PHP response-header latency, excludes streamed body.",
|
||||
&wl,
|
||||
m.response_headers_us.load(Ordering::Relaxed),
|
||||
);
|
||||
f.add(
|
||||
"rust_proxy_php_response_body_bytes_total",
|
||||
"counter",
|
||||
"PHP response body bytes emitted before downstream compression, not network traffic.",
|
||||
&wl,
|
||||
m.response_bytes.load(Ordering::Relaxed),
|
||||
);
|
||||
let available = php.permits.available_permits();
|
||||
f.add(
|
||||
"rust_proxy_php_concurrency_available",
|
||||
"gauge",
|
||||
"Available PHP-FPM request permits in RustProxy for this service.",
|
||||
&wl,
|
||||
available,
|
||||
);
|
||||
f.add("rust_proxy_php_concurrency_limit", "gauge", "Configured PHP request concurrency limit within RustProxy (not PHP-FPM process capacity).", &wl, php.max_concurrent);
|
||||
f.add(
|
||||
"rust_proxy_php_concurrency_in_use",
|
||||
"gauge",
|
||||
"Permits acquired for requests to PHP-FPM (includes body streaming).",
|
||||
&wl,
|
||||
php.max_concurrent.saturating_sub(available),
|
||||
);
|
||||
for (reason, count) in PHP_FAILURE_NAMES.iter().zip(&m.failures) {
|
||||
f.add(
|
||||
"rust_proxy_php_failures_total",
|
||||
"counter",
|
||||
"PHP connection, request and response failures by fixed reason.",
|
||||
&format!("{wl},reason=\"{reason}\""),
|
||||
count.load(Ordering::Relaxed),
|
||||
);
|
||||
}
|
||||
f.add("rust_proxy_php_response_header_duration_microseconds_total", "counter", "Cumulative PHP response header latency in microseconds, excluding streamed body time.", &l, m.response_headers_us.load(Ordering::Relaxed));
|
||||
f.add("rust_proxy_php_concurrency_available", "gauge", "Available PHP-FPM request permits in this Web Service.", &l, php.permits.available_permits());
|
||||
}
|
||||
}
|
||||
@@ -327,6 +327,11 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
.copied()
|
||||
{
|
||||
state.metrics.record_compression(encoding);
|
||||
if let Some(route) = effective_route_context {
|
||||
if let crate::config::CompiledSource::WebServer(service) = &route.source {
|
||||
service.web_metrics.record_encoding(encoding);
|
||||
}
|
||||
}
|
||||
}
|
||||
if response
|
||||
.extensions()
|
||||
|
||||
@@ -26,8 +26,7 @@ impl Php {
|
||||
"Web Service PHP request body limit must be 0..8589934592 bytes"
|
||||
);
|
||||
ensure!(
|
||||
self.max_response_header_bytes >= 4096
|
||||
&& self.max_response_header_bytes <= 1024 * 1024,
|
||||
self.max_response_header_bytes >= 4096 && self.max_response_header_bytes <= 1024 * 1024,
|
||||
"Web Service PHP response header limit must be 4096..1048576 bytes"
|
||||
);
|
||||
ensure!(
|
||||
@@ -37,7 +36,13 @@ impl Php {
|
||||
self.custom_params = normalize_fastcgi_params(std::mem::take(&mut self.custom_params))?;
|
||||
self.php_values = normalize_php_values(std::mem::take(&mut self.php_values))?;
|
||||
self.php_admin_values = normalize_php_values(std::mem::take(&mut self.php_admin_values))?;
|
||||
ensure!(!self.php_values.keys().any(|key| self.php_admin_values.contains_key(key)), "PHP_VALUE and PHP_ADMIN_VALUE must not define the same directive");
|
||||
ensure!(
|
||||
!self
|
||||
.php_values
|
||||
.keys()
|
||||
.any(|key| self.php_admin_values.contains_key(key)),
|
||||
"PHP_VALUE and PHP_ADMIN_VALUE must not define the same directive"
|
||||
);
|
||||
let php_custom_bytes = self
|
||||
.custom_params
|
||||
.iter()
|
||||
@@ -50,7 +55,10 @@ impl Php {
|
||||
"Web Service PHP custom FastCGI/PHP_VALUE data must be at most 128 KiB"
|
||||
);
|
||||
if self.enabled {
|
||||
ensure!(!self.extensions.is_empty(), "PHP-FPM needs executable extensions");
|
||||
ensure!(
|
||||
!self.extensions.is_empty(),
|
||||
"PHP-FPM needs executable extensions"
|
||||
);
|
||||
FastCgiEndpoint::parse(&self.endpoint)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -59,10 +67,17 @@ impl Php {
|
||||
|
||||
impl PhpFpmProfile {
|
||||
pub fn validate(&mut self) -> Result<()> {
|
||||
ensure!(crate::config::valid_id(&self.id), "invalid PHP-FPM profile id");
|
||||
ensure!(
|
||||
crate::config::valid_id(&self.id),
|
||||
"invalid PHP-FPM profile id"
|
||||
);
|
||||
self.name = self.name.trim().to_owned();
|
||||
ensure!(!self.name.is_empty() && self.name.len() <= MAX_SERVICE_NAME_BYTES
|
||||
&& !self.name.chars().any(char::is_control), "PHP-FPM profile name is invalid");
|
||||
ensure!(
|
||||
!self.name.is_empty()
|
||||
&& self.name.len() <= MAX_SERVICE_NAME_BYTES
|
||||
&& !self.name.chars().any(char::is_control),
|
||||
"PHP-FPM profile name is invalid"
|
||||
);
|
||||
self.php.validate()
|
||||
}
|
||||
}
|
||||
@@ -98,7 +113,10 @@ impl Service {
|
||||
&& item != "..",
|
||||
"Web Service index entries must be plain file names"
|
||||
);
|
||||
ensure!(seen.insert(item.clone()), "duplicate Web Service index file");
|
||||
ensure!(
|
||||
seen.insert(item.clone()),
|
||||
"duplicate Web Service index file"
|
||||
);
|
||||
}
|
||||
ensure!(
|
||||
self.mime_types.len() <= MAX_MIME_TYPES,
|
||||
@@ -106,7 +124,10 @@ impl Service {
|
||||
);
|
||||
let mut normalized_mime_types = BTreeMap::new();
|
||||
for (extension, value) in std::mem::take(&mut self.mime_types) {
|
||||
let extension = extension.trim().trim_start_matches('.').to_ascii_lowercase();
|
||||
let extension = extension
|
||||
.trim()
|
||||
.trim_start_matches('.')
|
||||
.to_ascii_lowercase();
|
||||
ensure!(
|
||||
!extension.is_empty()
|
||||
&& extension.len() <= MAX_MIME_EXTENSION_BYTES
|
||||
@@ -160,10 +181,7 @@ impl Service {
|
||||
.matcher
|
||||
.strip_prefix("ext:")
|
||||
.is_some_and(valid_extension)
|
||||
|| rule
|
||||
.matcher
|
||||
.strip_prefix("mime:")
|
||||
.is_some_and(|value| {
|
||||
|| rule.matcher.strip_prefix("mime:").is_some_and(|value| {
|
||||
!value.is_empty()
|
||||
&& value.len() <= MAX_MIME_VALUE_BYTES
|
||||
&& !value.chars().any(char::is_control)
|
||||
@@ -192,8 +210,8 @@ impl Service {
|
||||
!name.is_empty() && name.len() <= MAX_HEADER_NAME_BYTES,
|
||||
"Web Service response header name must be 1..128 bytes"
|
||||
);
|
||||
let header_name = HeaderName::from_str(&name)
|
||||
.context("invalid Web Service response header name")?;
|
||||
let header_name =
|
||||
HeaderName::from_str(&name).context("invalid Web Service response header name")?;
|
||||
ensure!(
|
||||
!protected_service_header(&header_name),
|
||||
"Web Service response header cannot override framing/transport headers"
|
||||
@@ -203,8 +221,7 @@ impl Service {
|
||||
value.len() <= MAX_HEADER_VALUE_BYTES,
|
||||
"Web Service response header value must be at most 8192 bytes"
|
||||
);
|
||||
HeaderValue::from_str(&value)
|
||||
.context("invalid Web Service response header value")?;
|
||||
HeaderValue::from_str(&value).context("invalid Web Service response header value")?;
|
||||
ensure!(
|
||||
normalized_headers.insert(name, value).is_none(),
|
||||
"duplicate Web Service response header"
|
||||
@@ -277,7 +294,10 @@ impl Service {
|
||||
|
||||
self.php.validate()?;
|
||||
if let Some(id) = &self.php_profile_id {
|
||||
ensure!(crate::config::valid_id(id), "invalid PHP-FPM profile reference");
|
||||
ensure!(
|
||||
crate::config::valid_id(id),
|
||||
"invalid PHP-FPM profile reference"
|
||||
);
|
||||
}
|
||||
htaccess::validate(&mut self.htaccess)?;
|
||||
|
||||
@@ -320,7 +340,10 @@ impl Service {
|
||||
);
|
||||
#[cfg(unix)]
|
||||
let root_handle = Arc::new(open_root_directory(&root).with_context(|| {
|
||||
format!("Web Service {} document root could not be opened safely", self.id)
|
||||
format!(
|
||||
"Web Service {} document root could not be opened safely",
|
||||
self.id
|
||||
)
|
||||
})?);
|
||||
let htaccess = htaccess::compile(&root, &self.htaccess)?;
|
||||
if let Some(compiled) = &htaccess {
|
||||
@@ -337,10 +360,7 @@ impl Service {
|
||||
.response_headers
|
||||
.iter()
|
||||
.map(|(name, value)| {
|
||||
Ok::<_, anyhow::Error>((
|
||||
HeaderName::from_str(name)?,
|
||||
HeaderValue::from_str(value)?,
|
||||
))
|
||||
Ok::<_, anyhow::Error>((HeaderName::from_str(name)?, HeaderValue::from_str(value)?))
|
||||
})
|
||||
.collect::<std::result::Result<Vec<_>, anyhow::Error>>()?;
|
||||
let cache_rules = self
|
||||
@@ -367,12 +387,20 @@ impl Service {
|
||||
Ok(CompiledRewriteRule {
|
||||
pattern: Regex::new(&rule.pattern)?,
|
||||
replacement: rule.replacement.clone(),
|
||||
status: rule.status.and_then(|value| StatusCode::from_u16(value).ok()),
|
||||
status: rule
|
||||
.status
|
||||
.and_then(|value| StatusCode::from_u16(value).ok()),
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let mut script_extensions: HashSet<String> = self.php.extensions.iter().cloned().collect();
|
||||
script_extensions.extend(["php", "phtml", "phar", "php3", "php4", "php5", "php7", "php8"].into_iter().map(str::to_owned));
|
||||
script_extensions.extend(
|
||||
[
|
||||
"php", "phtml", "phar", "php3", "php4", "php5", "php7", "php8",
|
||||
]
|
||||
.into_iter()
|
||||
.map(str::to_owned),
|
||||
);
|
||||
let php = if self.php.enabled {
|
||||
Some(CompiledPhp {
|
||||
extensions: self.php.extensions.iter().cloned().collect(),
|
||||
@@ -381,6 +409,7 @@ impl Service {
|
||||
connect_timeout: Duration::from_millis(self.php.connect_timeout_ms),
|
||||
request_timeout: Duration::from_millis(self.php.request_timeout_ms),
|
||||
permits: Arc::new(Semaphore::new(self.php.max_concurrent_requests)),
|
||||
max_concurrent: self.php.max_concurrent_requests,
|
||||
pass_authorization: self.php.pass_authorization,
|
||||
max_request_body_bytes: self.php.max_request_body_bytes,
|
||||
max_response_header_bytes: self.php.max_response_header_bytes,
|
||||
@@ -403,10 +432,9 @@ impl Service {
|
||||
mime_types: self
|
||||
.mime_types
|
||||
.iter()
|
||||
.map(|(extension, value)| {
|
||||
Ok((extension.clone(), HeaderValue::from_str(value)?))
|
||||
})
|
||||
.collect::<std::result::Result<BTreeMap<_, _>, http::header::InvalidHeaderValue>>()?,
|
||||
.map(|(extension, value)| Ok((extension.clone(), HeaderValue::from_str(value)?)))
|
||||
.collect::<std::result::Result<BTreeMap<_, _>, http::header::InvalidHeaderValue>>(
|
||||
)?,
|
||||
default_mime_type: HeaderValue::from_str(&self.default_mime_type)?,
|
||||
cache_control: self
|
||||
.cache_control
|
||||
|
||||
@@ -31,6 +31,7 @@ pub(crate) struct CompiledPhp {
|
||||
pub(super) connect_timeout: Duration,
|
||||
pub(super) request_timeout: Duration,
|
||||
pub(crate) permits: Arc<Semaphore>,
|
||||
pub(crate) max_concurrent: usize,
|
||||
pub(super) pass_authorization: bool,
|
||||
pub(super) max_request_body_bytes: u64,
|
||||
pub(super) max_response_header_bytes: usize,
|
||||
|
||||
@@ -338,6 +338,7 @@ async fn drain_fastcgi_body(
|
||||
initial: Vec<u8>,
|
||||
send_body: bool,
|
||||
stderr_log_bytes: usize,
|
||||
metrics: std::sync::Arc<crate::web_server::metrics::PhpMetrics>,
|
||||
_permit: OwnedSemaphorePermit,
|
||||
) {
|
||||
if send_body && !initial.is_empty() && tx.send(Ok(Bytes::from(initial))).await.is_err() {
|
||||
@@ -361,6 +362,7 @@ async fn drain_fastcgi_body(
|
||||
Ok((FCGI_END_REQUEST, _)) => break,
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
metrics.failure(8);
|
||||
if send_body {
|
||||
let _ = tx.send(Err(error)).await;
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ pub(super) async fn php_response(
|
||||
context: RequestContext,
|
||||
) -> Response {
|
||||
let start = Instant::now();
|
||||
let response = php_response_inner( service, php, method, headers, original_uri, script_path, path_info, query, body, context).await;
|
||||
let mut response = php_response_inner(service, php, method, headers, original_uri, script_path, path_info, query, body, context).await;
|
||||
php.metrics.record(response.status(), start.elapsed().as_micros().min(u64::MAX as u128) as u64);
|
||||
response.extensions_mut().insert(crate::web_server::metrics::ResponseKind::Php);
|
||||
response
|
||||
}
|
||||
|
||||
@@ -42,6 +43,7 @@ async fn php_response_inner(
|
||||
let permit = match php.permits.clone().try_acquire_owned() {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => {
|
||||
php.metrics.failure(0);
|
||||
let mut response = builtin_error_response(StatusCode::SERVICE_UNAVAILABLE, &method);
|
||||
response
|
||||
.headers_mut()
|
||||
@@ -52,10 +54,11 @@ async fn php_response_inner(
|
||||
let mut io = match tokio::time::timeout(php.connect_timeout, connect_fastcgi(&php.endpoint)).await {
|
||||
Ok(Ok(io)) => io,
|
||||
Ok(Err(error)) => {
|
||||
php.metrics.failure(2);
|
||||
tracing::warn!(%error, service = %service.id, "PHP-FPM connection failed");
|
||||
return builtin_error_response(StatusCode::BAD_GATEWAY, &method);
|
||||
}
|
||||
Err(_) => return builtin_error_response(StatusCode::GATEWAY_TIMEOUT, &method),
|
||||
Err(_) => { php.metrics.failure(1); return builtin_error_response(StatusCode::GATEWAY_TIMEOUT, &method); },
|
||||
};
|
||||
let params = match fastcgi_params(
|
||||
service,
|
||||
@@ -71,6 +74,7 @@ async fn php_response_inner(
|
||||
Ok(params) => params,
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, service = %service.id, "PHP-FPM parameter preparation failed");
|
||||
php.metrics.failure(4);
|
||||
return builtin_error_response(StatusCode::BAD_GATEWAY, &method);
|
||||
}
|
||||
};
|
||||
@@ -109,10 +113,11 @@ async fn php_response_inner(
|
||||
return builtin_error_response(StatusCode::PAYLOAD_TOO_LARGE, &method);
|
||||
}
|
||||
Ok(Err(FastCgiRequestWriteError::Io(error))) => {
|
||||
php.metrics.failure(4);
|
||||
tracing::warn!(%error, service = %service.id, "PHP-FPM request write failed");
|
||||
return builtin_error_response(StatusCode::BAD_GATEWAY, &method);
|
||||
}
|
||||
Err(_) => return builtin_error_response(StatusCode::GATEWAY_TIMEOUT, &method),
|
||||
Err(_) => { php.metrics.failure(3); return builtin_error_response(StatusCode::GATEWAY_TIMEOUT, &method); },
|
||||
}
|
||||
|
||||
let mut stdout = Vec::new();
|
||||
@@ -122,10 +127,12 @@ async fn php_response_inner(
|
||||
let (record_type, content) = match record {
|
||||
Ok(value) => value,
|
||||
Err(error) if error.kind() == io::ErrorKind::TimedOut => {
|
||||
php.metrics.failure(5);
|
||||
return builtin_error_response(StatusCode::GATEWAY_TIMEOUT, &method)
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, service = %service.id, "PHP-FPM response read failed");
|
||||
php.metrics.failure(6);
|
||||
return builtin_error_response(StatusCode::BAD_GATEWAY, &method);
|
||||
}
|
||||
};
|
||||
@@ -133,6 +140,7 @@ async fn php_response_inner(
|
||||
FCGI_STDOUT => {
|
||||
stdout.extend_from_slice(&content);
|
||||
if stdout.len() > php.max_response_header_bytes {
|
||||
php.metrics.failure(7);
|
||||
return builtin_error_response(StatusCode::BAD_GATEWAY, &method);
|
||||
}
|
||||
if let Some((position, separator_len)) = find_cgi_header_end(&stdout) {
|
||||
@@ -142,6 +150,7 @@ async fn php_response_inner(
|
||||
Ok((status, headers)) => break (status, headers, body),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, service = %service.id, "PHP-FPM CGI headers rejected");
|
||||
php.metrics.failure(7);
|
||||
return builtin_error_response(StatusCode::BAD_GATEWAY, &method);
|
||||
}
|
||||
}
|
||||
@@ -152,6 +161,7 @@ async fn php_response_inner(
|
||||
stderr.extend_from_slice(&content[..content.len().min(room)]);
|
||||
}
|
||||
FCGI_END_REQUEST => {
|
||||
php.metrics.failure(7);
|
||||
if !stderr.is_empty() {
|
||||
tracing::warn!(stderr = %String::from_utf8_lossy(&stderr), service = %service.id, "PHP-FPM FastCGI stderr");
|
||||
}
|
||||
@@ -175,6 +185,7 @@ async fn php_response_inner(
|
||||
initial_body,
|
||||
send_body,
|
||||
php.stderr_log_bytes,
|
||||
php.metrics.clone(),
|
||||
permit,
|
||||
));
|
||||
let mut response = Response::new(if send_body {
|
||||
|
||||
+117
-4
@@ -1,6 +1,26 @@
|
||||
//! Fixed-cardinality PHP counters. Values are per compiled Web Service snapshot.
|
||||
//! Fixed-cardinality, per-compiled-Web-Service counters. No request-derived labels.
|
||||
use http::{Method, StatusCode};
|
||||
use serde_json::{json, Value};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(super) enum ResponseKind {
|
||||
Static,
|
||||
Php,
|
||||
}
|
||||
|
||||
pub(crate) const PHP_FAILURE_NAMES: [&str; 9] = [
|
||||
"capacity",
|
||||
"connect_timeout",
|
||||
"connect_error",
|
||||
"request_timeout",
|
||||
"request_io",
|
||||
"response_timeout",
|
||||
"response_io",
|
||||
"invalid_response",
|
||||
"body_io",
|
||||
];
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct PhpMetrics {
|
||||
pub(crate) requests: AtomicU64,
|
||||
@@ -9,10 +29,32 @@ pub(crate) struct PhpMetrics {
|
||||
pub(crate) status_4xx: AtomicU64,
|
||||
pub(crate) status_5xx: AtomicU64,
|
||||
pub(crate) response_headers_us: AtomicU64,
|
||||
pub(crate) response_bytes: AtomicU64, // observed uncompressed body frames, not wire bytes
|
||||
pub(crate) in_flight_headers: AtomicU64,
|
||||
pub(crate) static_requests: AtomicU64,
|
||||
pub(crate) php_requests: AtomicU64,
|
||||
pub(crate) other_requests: AtomicU64,
|
||||
pub(crate) methods: [AtomicU64; 5], // GET POST PUT DELETE other
|
||||
pub(crate) selected_status: [AtomicU64; 8], // 200 206 304 403 404 500 502 503
|
||||
pub(crate) failures: [AtomicU64; 9],
|
||||
pub(crate) compression: [AtomicU64; 4], // gzip Brotli zstd deflate (post-route encoder only)
|
||||
}
|
||||
|
||||
pub(crate) struct HeadersInFlight<'a>(&'a AtomicU64);
|
||||
impl<'a> HeadersInFlight<'a> {
|
||||
pub(crate) fn new(counter: &'a AtomicU64) -> Self {
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
Self(counter)
|
||||
}
|
||||
}
|
||||
impl Drop for HeadersInFlight<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.0.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
impl PhpMetrics {
|
||||
pub fn record(&self, status: http::StatusCode, elapsed_us: u64) {
|
||||
pub(crate) fn record(&self, status: StatusCode, elapsed_us: u64) {
|
||||
self.requests.fetch_add(1, Ordering::Relaxed);
|
||||
let counter = match status.as_u16() / 100 {
|
||||
2 => Some(&self.status_2xx),
|
||||
@@ -21,7 +63,78 @@ impl PhpMetrics {
|
||||
5 => Some(&self.status_5xx),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(counter) = counter { counter.fetch_add(1, Ordering::Relaxed); }
|
||||
self.response_headers_us.fetch_add(elapsed_us, Ordering::Relaxed);
|
||||
if let Some(counter) = counter {
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
self.response_headers_us
|
||||
.fetch_add(elapsed_us, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(super) fn record_web(
|
||||
&self,
|
||||
status: StatusCode,
|
||||
elapsed_us: u64,
|
||||
method: &Method,
|
||||
kind: Option<ResponseKind>,
|
||||
) {
|
||||
self.record(status, elapsed_us);
|
||||
let method_index = if method == Method::GET {
|
||||
0
|
||||
} else if method == Method::POST {
|
||||
1
|
||||
} else if method == Method::PUT {
|
||||
2
|
||||
} else if method == Method::DELETE {
|
||||
3
|
||||
} else {
|
||||
4
|
||||
};
|
||||
self.methods[method_index].fetch_add(1, Ordering::Relaxed);
|
||||
match kind {
|
||||
Some(ResponseKind::Static) => &self.static_requests,
|
||||
Some(ResponseKind::Php) => &self.php_requests,
|
||||
None => &self.other_requests,
|
||||
}
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
let status_index = [200, 206, 304, 403, 404, 500, 502, 503]
|
||||
.iter()
|
||||
.position(|code| *code == status.as_u16());
|
||||
if let Some(index) = status_index {
|
||||
self.selected_status[index].fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn failure(&self, index: usize) {
|
||||
if let Some(counter) = self.failures.get(index) {
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn record_encoding(&self, encoding: crate::compression::AppliedEncoding) {
|
||||
let index = match encoding {
|
||||
crate::compression::AppliedEncoding::Gzip => 0,
|
||||
crate::compression::AppliedEncoding::Brotli => 1,
|
||||
crate::compression::AppliedEncoding::Zstd => 2,
|
||||
crate::compression::AppliedEncoding::Deflate => 3,
|
||||
};
|
||||
self.compression[index].fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub(crate) fn view(&self) -> Value {
|
||||
let n = |counter: &AtomicU64| counter.load(Ordering::Relaxed);
|
||||
json!({
|
||||
"requests": n(&self.requests),
|
||||
"status_2xx": n(&self.status_2xx), "status_3xx": n(&self.status_3xx),
|
||||
"status_4xx": n(&self.status_4xx), "status_5xx": n(&self.status_5xx),
|
||||
"response_headers_us": n(&self.response_headers_us),
|
||||
"response_bytes": n(&self.response_bytes),
|
||||
"active_headers": n(&self.in_flight_headers),
|
||||
"static_requests": n(&self.static_requests),
|
||||
"php_requests": n(&self.php_requests), "other_requests": n(&self.other_requests),
|
||||
"methods": self.methods.iter().map(n).collect::<Vec<_>>(),
|
||||
"selected_status": self.selected_status.iter().map(n).collect::<Vec<_>>(),
|
||||
"failures": self.failures.iter().map(n).collect::<Vec<_>>(),
|
||||
"compression": self.compression.iter().map(n).collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ mod config;
|
||||
mod fastcgi;
|
||||
mod filesystem;
|
||||
mod htaccess;
|
||||
mod metrics;
|
||||
pub(crate) mod metrics;
|
||||
mod request;
|
||||
mod response;
|
||||
mod routing;
|
||||
@@ -16,7 +16,8 @@ mod security;
|
||||
mod serve;
|
||||
|
||||
pub use config::{
|
||||
CacheRule, CompiledService, Hardening, Php, PhpFpmProfile, Precompressed, RewriteRule, Service, SymlinkPolicy,
|
||||
CacheRule, CompiledService, Hardening, Php, PhpFpmProfile, Precompressed, RewriteRule, Service,
|
||||
SymlinkPolicy,
|
||||
};
|
||||
pub use htaccess::Config as HtaccessConfig;
|
||||
pub use request::RequestContext;
|
||||
|
||||
@@ -5,6 +5,22 @@ pub(super) async fn file_response(
|
||||
method: &Method,
|
||||
headers: &HeaderMap,
|
||||
content_encoding: Option<&'static str>,
|
||||
) -> Response {
|
||||
let mut response =
|
||||
file_response_inner(service, opened, path, method, headers, content_encoding).await;
|
||||
response
|
||||
.extensions_mut()
|
||||
.insert(crate::web_server::metrics::ResponseKind::Static);
|
||||
response
|
||||
}
|
||||
|
||||
async fn file_response_inner(
|
||||
service: &CompiledService,
|
||||
opened: Opened,
|
||||
path: &Path,
|
||||
method: &Method,
|
||||
headers: &HeaderMap,
|
||||
content_encoding: Option<&'static str>,
|
||||
) -> Response {
|
||||
let length = opened.metadata.len();
|
||||
let (etag, modified) = metadata_validators(&opened.metadata);
|
||||
@@ -65,9 +81,15 @@ pub(super) async fn file_response(
|
||||
};
|
||||
|
||||
let multipart = ranges.as_ref().is_some_and(|ranges| ranges.len() > 1);
|
||||
let single_range = ranges.as_ref().and_then(|ranges| (ranges.len() == 1).then_some(ranges[0]));
|
||||
let single_range = ranges
|
||||
.as_ref()
|
||||
.and_then(|ranges| (ranges.len() == 1).then_some(ranges[0]));
|
||||
let (status, start, body_length) = match single_range {
|
||||
Some(range) => (StatusCode::PARTIAL_CONTENT, range.start, range.end - range.start + 1),
|
||||
Some(range) => (
|
||||
StatusCode::PARTIAL_CONTENT,
|
||||
range.start,
|
||||
range.end - range.start + 1,
|
||||
),
|
||||
None if multipart => (StatusCode::PARTIAL_CONTENT, 0, 0),
|
||||
None => (StatusCode::OK, 0, length),
|
||||
};
|
||||
@@ -131,7 +153,10 @@ pub(super) async fn file_response(
|
||||
let mut file = tokio::fs::File::from_std(opened.file);
|
||||
if let Some(boundary) = boundary {
|
||||
let ranges = ranges.expect("multipart ranges");
|
||||
let mime_text = mime.to_str().unwrap_or("application/octet-stream").to_owned();
|
||||
let mime_text = mime
|
||||
.to_str()
|
||||
.unwrap_or("application/octet-stream")
|
||||
.to_owned();
|
||||
let (mut writer, reader) = tokio::io::duplex(64 * 1024);
|
||||
tokio::spawn(async move {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
@@ -141,7 +166,10 @@ pub(super) async fn file_response(
|
||||
range.start, range.end
|
||||
);
|
||||
if writer.write_all(header.as_bytes()).await.is_err()
|
||||
|| file.seek(std::io::SeekFrom::Start(range.start)).await.is_err()
|
||||
|| file
|
||||
.seek(std::io::SeekFrom::Start(range.start))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -152,7 +180,9 @@ pub(super) async fn file_response(
|
||||
return;
|
||||
}
|
||||
}
|
||||
let _ = writer.write_all(format!("--{boundary}--\r\n").as_bytes()).await;
|
||||
let _ = writer
|
||||
.write_all(format!("--{boundary}--\r\n").as_bytes())
|
||||
.await;
|
||||
});
|
||||
*response.body_mut() = Body::from_stream(ReaderStream::new(reader));
|
||||
return response;
|
||||
|
||||
+91
-30
@@ -1,5 +1,7 @@
|
||||
use super::{
|
||||
fastcgi::{method_not_allowed_response, php_for_path, php_response, protected_script_without_handler},
|
||||
fastcgi::{
|
||||
method_not_allowed_response, php_for_path, php_response, protected_script_without_handler,
|
||||
},
|
||||
filesystem::{list_directory_sync, path_components},
|
||||
htaccess,
|
||||
response::{
|
||||
@@ -59,7 +61,10 @@ async fn serve_inner(
|
||||
context: RequestContext,
|
||||
) -> Response {
|
||||
if crate::acl::validate_decoded_routing_path(path).is_err() {
|
||||
return status_response(StatusCode::BAD_REQUEST, "RustProxy: invalid Web Server path\n");
|
||||
return status_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid Web Server path\n",
|
||||
);
|
||||
}
|
||||
if hardening_denied_method(service, &method) || hardening_denied_path(service, path) {
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
@@ -68,8 +73,11 @@ async fn serve_inner(
|
||||
return method_not_allowed_response();
|
||||
}
|
||||
|
||||
if path_components(path).ok().is_some_and(|components| components.iter().any(|component| htaccess::protected_component(service.htaccess.as_ref(), component)))
|
||||
|| htaccess::denied(service.htaccess.as_ref(), path)
|
||||
if path_components(path).ok().is_some_and(|components| {
|
||||
components
|
||||
.iter()
|
||||
.any(|component| htaccess::protected_component(service.htaccess.as_ref(), component))
|
||||
}) || htaccess::denied(service.htaccess.as_ref(), path)
|
||||
{
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
}
|
||||
@@ -93,15 +101,30 @@ async fn serve_inner(
|
||||
&context,
|
||||
&rewritten_path,
|
||||
rewritten_query.as_deref(),
|
||||
).await {
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(htaccess::RewriteOutcome::Redirect(response)) => return response,
|
||||
Ok(htaccess::RewriteOutcome::Forbidden) => return builtin_error_response(StatusCode::FORBIDDEN, &method),
|
||||
Ok(htaccess::RewriteOutcome::Forbidden) => {
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method)
|
||||
}
|
||||
Ok(htaccess::RewriteOutcome::Internal { path, query }) => (path, query),
|
||||
Err(_) => return status_response(StatusCode::BAD_REQUEST, "RustProxy: invalid .htaccess rewrite target\n"),
|
||||
Err(_) => {
|
||||
return status_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"RustProxy: invalid .htaccess rewrite target\n",
|
||||
)
|
||||
}
|
||||
};
|
||||
if crate::acl::validate_decoded_routing_path(&rewritten_path).is_err()
|
||||
|| hardening_denied_path(service, &rewritten_path)
|
||||
|| path_components(&rewritten_path).ok().is_some_and(|components| components.iter().any(|component| htaccess::protected_component(service.htaccess.as_ref(), component)))
|
||||
|| path_components(&rewritten_path)
|
||||
.ok()
|
||||
.is_some_and(|components| {
|
||||
components.iter().any(|component| {
|
||||
htaccess::protected_component(service.htaccess.as_ref(), component)
|
||||
})
|
||||
})
|
||||
|| htaccess::denied(service.htaccess.as_ref(), &rewritten_path)
|
||||
{
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
@@ -115,7 +138,9 @@ async fn serve_inner(
|
||||
if let Some((script_path, path_info)) = resolve_php_path_info(service, &components).await {
|
||||
let script_components = path_components(&script_path).unwrap_or_default();
|
||||
if hardening_denied_path(service, &script_path)
|
||||
|| script_components.iter().any(|component| htaccess::protected_component(service.htaccess.as_ref(), component))
|
||||
|| script_components.iter().any(|component| {
|
||||
htaccess::protected_component(service.htaccess.as_ref(), component)
|
||||
})
|
||||
|| htaccess::denied(service.htaccess.as_ref(), &script_path)
|
||||
{
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
@@ -138,13 +163,8 @@ async fn serve_inner(
|
||||
}
|
||||
}
|
||||
|
||||
let (effective_path, effective_query) = match resolve_try_files(
|
||||
service,
|
||||
&rewritten_path,
|
||||
rewritten_query.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
let (effective_path, effective_query) =
|
||||
match resolve_try_files(service, &rewritten_path, rewritten_query.as_deref()).await {
|
||||
Ok(TryFilesOutcome::Path { path, query }) => (path, query),
|
||||
Ok(TryFilesOutcome::Status(status)) => return builtin_error_response(status, &method),
|
||||
Err(_) => {
|
||||
@@ -160,7 +180,9 @@ async fn serve_inner(
|
||||
Err(error) => return io_response(&error),
|
||||
};
|
||||
if hardening_denied_path(service, &effective_path)
|
||||
|| components.iter().any(|component| htaccess::protected_component(service.htaccess.as_ref(), component))
|
||||
|| components
|
||||
.iter()
|
||||
.any(|component| htaccess::protected_component(service.htaccess.as_ref(), component))
|
||||
|| htaccess::denied(service.htaccess.as_ref(), &effective_path)
|
||||
{
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
@@ -169,12 +191,19 @@ async fn serve_inner(
|
||||
let opened = match open_components(service, components.clone()).await {
|
||||
Ok(opened) => opened,
|
||||
Err(error)
|
||||
if matches!(error.kind(), io::ErrorKind::NotFound | io::ErrorKind::NotADirectory) =>
|
||||
if matches!(
|
||||
error.kind(),
|
||||
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
|
||||
) =>
|
||||
{
|
||||
if let Some((script_path, path_info)) =
|
||||
resolve_php_path_info(service, &components).await
|
||||
{
|
||||
if let Some((script_path, path_info)) = resolve_php_path_info(service, &components).await {
|
||||
let script_components = path_components(&script_path).unwrap_or_default();
|
||||
if hardening_denied_path(service, &script_path)
|
||||
|| script_components.iter().any(|component| htaccess::protected_component(service.htaccess.as_ref(), component))
|
||||
|| script_components.iter().any(|component| {
|
||||
htaccess::protected_component(service.htaccess.as_ref(), component)
|
||||
})
|
||||
|| htaccess::denied(service.htaccess.as_ref(), &script_path)
|
||||
{
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
@@ -290,14 +319,7 @@ async fn serve_inner(
|
||||
)
|
||||
.await;
|
||||
}
|
||||
return file_response(
|
||||
service,
|
||||
index_opened,
|
||||
&public_path,
|
||||
&method,
|
||||
&headers,
|
||||
None,
|
||||
)
|
||||
return file_response(service, index_opened, &public_path, &method, &headers, None)
|
||||
.await;
|
||||
}
|
||||
Ok(_) => continue,
|
||||
@@ -316,7 +338,11 @@ async fn serve_inner(
|
||||
if method != Method::GET && method != Method::HEAD {
|
||||
return method_not_allowed_response();
|
||||
}
|
||||
if !htaccess::directory_listing(service.htaccess.as_ref(), &components, service.directory_listing) {
|
||||
if !htaccess::directory_listing(
|
||||
service.htaccess.as_ref(),
|
||||
&components,
|
||||
service.directory_listing,
|
||||
) {
|
||||
return builtin_error_response(StatusCode::FORBIDDEN, &method);
|
||||
}
|
||||
let service_for_list = service.clone();
|
||||
@@ -369,6 +395,9 @@ async fn serve_inner(
|
||||
.headers_mut()
|
||||
.insert(CACHE_CONTROL, HeaderValue::from_static("no-store"));
|
||||
response
|
||||
.extensions_mut()
|
||||
.insert(crate::web_server::metrics::ResponseKind::Static);
|
||||
response
|
||||
}
|
||||
|
||||
/// Serve a decoded, validated routing path from a compiled Web Service.
|
||||
@@ -384,11 +413,43 @@ pub async fn serve(
|
||||
body: Body,
|
||||
context: RequestContext,
|
||||
) -> Response {
|
||||
use std::sync::atomic::Ordering;
|
||||
let start = std::time::Instant::now();
|
||||
let _in_flight =
|
||||
crate::web_server::metrics::HeadersInFlight::new(&service.web_metrics.in_flight_headers);
|
||||
let request_method = method.clone();
|
||||
let mut response = serve_inner(service, method, headers, uri, path, body, context).await;
|
||||
add_service_headers(&mut response, service);
|
||||
htaccess::apply_response(service.htaccess.as_ref(), &mut response, path);
|
||||
strip_unnecessary_headers(&mut response, service);
|
||||
service.web_metrics.record(response.status(), start.elapsed().as_micros().min(u64::MAX as u128) as u64);
|
||||
let kind = response
|
||||
.extensions()
|
||||
.get::<crate::web_server::metrics::ResponseKind>()
|
||||
.copied();
|
||||
service.web_metrics.record_web(
|
||||
response.status(),
|
||||
start.elapsed().as_micros().min(u64::MAX as u128) as u64,
|
||||
&request_method,
|
||||
kind,
|
||||
);
|
||||
// Count bytes actually yielded by the Web Server body before downstream compression;
|
||||
// never infer transferred bytes from Content-Length or count an unpolled response.
|
||||
let web = service.web_metrics.clone();
|
||||
let php = if matches!(kind, Some(crate::web_server::metrics::ResponseKind::Php)) {
|
||||
service.php.as_ref().map(|p| p.metrics.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let body = std::mem::replace(response.body_mut(), Body::empty());
|
||||
*response.body_mut() = Body::new(http_body_util::BodyExt::map_frame(body, move |frame| {
|
||||
if let Some(data) = frame.data_ref() {
|
||||
let size = data.len() as u64;
|
||||
web.response_bytes.fetch_add(size, Ordering::Relaxed);
|
||||
if let Some(metrics) = php.as_ref() {
|
||||
metrics.response_bytes.fetch_add(size, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
frame
|
||||
}));
|
||||
response
|
||||
}
|
||||
+509
-134
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user