From ba79df596644fae5718d3ce76ec10972d4a5071d Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Tue, 3 Feb 2026 21:46:01 +0000 Subject: [PATCH] Add grafana --- server-rs/Cargo.lock | 346 +++++++++++++++++++++++++++++++++++++-- server-rs/Cargo.toml | 3 + server-rs/src/metrics.rs | 99 +++++++++++ 3 files changed, 435 insertions(+), 13 deletions(-) create mode 100644 server-rs/src/metrics.rs diff --git a/server-rs/Cargo.lock b/server-rs/Cargo.lock index a5e8a5f..381d16f 100644 --- a/server-rs/Cargo.lock +++ b/server-rs/Cargo.lock @@ -207,6 +207,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" version = "0.8.8" @@ -318,6 +340,12 @@ dependencies = [ "syn", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.0" @@ -421,6 +449,15 @@ version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -502,6 +539,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -570,7 +617,7 @@ dependencies = [ "crossterm_winapi", "document-features", "parking_lot", - "rustix", + "rustix 1.1.3", "winapi", ] @@ -615,6 +662,12 @@ dependencies = [ "litrs", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" version = "1.0.20" @@ -711,6 +764,22 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28a80e3145d8ad11ba0995949bbcf48b9df2be62772b3d351ef017dff6ecb853" +[[package]] +name = "fmmap" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687c574434dc6e3cd24a363fe0944711174f947fe71696fdc9a0ae046fe6e715" +dependencies = [ + "byteorder", + "bytes", + "enum_dispatch", + "fs4", + "memmap2", + "parse-display", + "pin-project-lite", + "tokio", +] + [[package]] name = "fnv" version = "1.0.7" @@ -747,6 +816,23 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs4" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c29c30684418547d476f0b48e84f4821639119c483b1eccd566c8cd0cd05f521" +dependencies = [ + "rustix 0.38.44", + "tokio", + "windows-sys 0.52.0", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.31" @@ -950,6 +1036,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hilbert_2d" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "705f81e042b11734af35c701c7f6b65f8a968a430621fa2c95e72e27f9f8be5c" + [[package]] name = "home" version = "0.5.12" @@ -1043,6 +1135,7 @@ dependencies = [ "hyper", "hyper-util", "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", @@ -1305,6 +1398,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1393,6 +1492,53 @@ dependencies = [ "libc", ] +[[package]] +name = "metrics" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" +dependencies = [ + "ahash", + "portable-atomic", +] + +[[package]] +name = "metrics-exporter-prometheus" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034" +dependencies = [ + "base64", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "indexmap", + "ipnet", + "metrics", + "metrics-util", + "quanta", + "thiserror 1.0.69", + "tokio", + "tracing", +] + +[[package]] +name = "metrics-util" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8496cc523d1f94c1385dd8f0f0c2c480b2b8aeccb5b7e4485ad6365523ae376" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown 0.15.5", + "metrics", + "quanta", + "rand 0.9.2", + "rand_xoshiro", + "sketches-ddsketch", +] + [[package]] name = "mime" version = "0.3.17" @@ -1439,10 +1585,10 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -1537,6 +1683,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "openssl-sys" version = "0.9.111" @@ -1572,6 +1724,31 @@ dependencies = [ "windows-link", ] +[[package]] +name = "parse-display" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287d8d3ebdce117b8539f59411e4ed9ec226e0a4153c7f55495c6070d68e6f72" +dependencies = [ + "parse-display-derive", + "regex", + "regex-syntax", +] + +[[package]] +name = "parse-display-derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc048687be30d79502dea2f623d052f3a074012c6eac41726b7ab17213616b1" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "regex-syntax", + "structmeta", + "syn", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1623,6 +1800,21 @@ dependencies = [ "array-init-cursor", ] +[[package]] +name = "pmtiles" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a83db53da21fbade9a8738bccb1ef54c405ef3db02b7fa81dfbd01933bc1a838" +dependencies = [ + "async-compression", + "bytes", + "fmmap", + "hilbert_2d", + "thiserror 2.0.18", + "tokio", + "varint-rs", +] + [[package]] name = "polars" version = "0.46.0" @@ -1736,7 +1928,7 @@ dependencies = [ "rayon", "regex", "strum_macros", - "thiserror", + "thiserror 2.0.18", "version_check", "xxhash-rust", ] @@ -1750,7 +1942,7 @@ dependencies = [ "polars-arrow-format", "regex", "simdutf8", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -2116,6 +2308,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "potential_utf" version = "0.1.4" @@ -2152,6 +2350,9 @@ dependencies = [ "clap", "h3o", "lasso", + "metrics", + "metrics-exporter-prometheus", + "pmtiles", "polars", "rayon", "regex", @@ -2176,6 +2377,21 @@ dependencies = [ "cc", ] +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + [[package]] name = "quinn" version = "0.11.9" @@ -2190,7 +2406,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -2211,7 +2427,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -2315,6 +2531,15 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core 0.9.5", +] + [[package]] name = "raw-cpuid" version = "11.6.0" @@ -2466,6 +2691,19 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.3" @@ -2475,7 +2713,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.11.0", "windows-sys 0.61.2", ] @@ -2485,6 +2723,7 @@ version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ + "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", @@ -2493,6 +2732,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -2509,6 +2760,7 @@ version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -2548,7 +2800,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -2673,6 +2938,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +[[package]] +name = "sketches-ddsketch" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" + [[package]] name = "slab" version = "0.4.11" @@ -2771,6 +3042,29 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "structmeta" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329" +dependencies = [ + "proc-macro2", + "quote", + "structmeta-derive", + "syn", +] + +[[package]] +name = "structmeta-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "strum_macros" version = "0.26.4" @@ -2841,7 +3135,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -2864,8 +3158,17 @@ dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix", - "windows-sys 0.52.0", + "rustix 1.1.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", ] [[package]] @@ -2874,7 +3177,18 @@ version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -3212,6 +3526,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "varint-rs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/server-rs/Cargo.toml b/server-rs/Cargo.toml index ccd4bab..06c7223 100644 --- a/server-rs/Cargo.toml +++ b/server-rs/Cargo.toml @@ -18,9 +18,12 @@ lasso = "0.7" rustc-hash = "2" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } +metrics = "0.24" +metrics-exporter-prometheus = "0.16" reqwest = { version = "0.12", features = ["rustls-tls"] } regex = "1" urlencoding = "2" +pmtiles = { version = "0.12", features = ["mmap-async-tokio"] } [lints.clippy] min_ident_chars = "warn" diff --git a/server-rs/src/metrics.rs b/server-rs/src/metrics.rs new file mode 100644 index 0000000..46ddf04 --- /dev/null +++ b/server-rs/src/metrics.rs @@ -0,0 +1,99 @@ +use axum::body::Body; +use axum::extract::Request; +use axum::http::StatusCode; +use axum::middleware::Next; +use axum::response::{IntoResponse, Response}; +use metrics::{counter, gauge, histogram}; +use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle}; +use std::time::Instant; + +/// Initialize the Prometheus metrics exporter and return a handle for rendering metrics. +pub fn init_metrics() -> PrometheusHandle { + PrometheusBuilder::new() + .install_recorder() + .expect("Failed to install Prometheus recorder") +} + +/// Middleware to track HTTP request metrics. +pub async fn track_metrics(request: Request, next: Next) -> Response { + let path = request.uri().path().to_string(); + let method = request.method().to_string(); + + // Skip metrics endpoint itself to avoid recursion + if path == "/metrics" { + return next.run(request).await; + } + + let start = Instant::now(); + let response = next.run(request).await; + let duration = start.elapsed(); + + let status = response.status().as_u16().to_string(); + + // Normalize path for metrics (avoid high cardinality from dynamic segments) + let normalized_path = normalize_path(&path); + + // Record metrics + histogram!("http_request_duration_seconds", "method" => method.clone(), "path" => normalized_path.clone(), "status" => status.clone()) + .record(duration.as_secs_f64()); + + counter!("http_requests_total", "method" => method, "path" => normalized_path, "status" => status) + .increment(1); + + response +} + +/// Normalize paths to avoid high cardinality from dynamic segments. +fn normalize_path(path: &str) -> String { + if path.starts_with("/api/tiles/") && !path.ends_with("style.json") { + return "/api/tiles/:z/:x/:y".to_string(); + } + path.to_string() +} + +/// Handler for the /metrics endpoint. +pub async fn metrics_handler(handle: PrometheusHandle) -> impl IntoResponse { + // Update process metrics before rendering + update_process_metrics(); + + match handle.render() { + output if !output.is_empty() => (StatusCode::OK, output), + _ => (StatusCode::INTERNAL_SERVER_ERROR, "Failed to render metrics".to_string()), + } +} + +/// Update process-level metrics (memory, etc.) +fn update_process_metrics() { + #[cfg(target_os = "linux")] + { + if let Ok(status) = std::fs::read_to_string("/proc/self/status") { + for line in status.lines() { + if let Some(value) = line.strip_prefix("VmRSS:") { + if let Some(kb) = parse_kb(value) { + gauge!("process_resident_memory_bytes").set((kb * 1024) as f64); + } + } else if let Some(value) = line.strip_prefix("VmSize:") { + if let Some(kb) = parse_kb(value) { + gauge!("process_virtual_memory_bytes").set((kb * 1024) as f64); + } + } + } + } + } +} + +#[cfg(target_os = "linux")] +fn parse_kb(value: &str) -> Option { + value + .trim() + .strip_suffix(" kB") + .or_else(|| value.trim().strip_suffix("kB")) + .and_then(|num| num.trim().parse().ok()) +} + +/// Record a custom gauge for data loading (call at startup). +pub fn record_data_stats(property_count: usize, poi_count: usize, postcode_count: usize) { + gauge!("data_properties_loaded").set(property_count as f64); + gauge!("data_pois_loaded").set(poi_count as f64); + gauge!("data_postcodes_loaded").set(postcode_count as f64); +}