From 555b5b537c520251c26ebbdcd07ac18c089cb552 Mon Sep 17 00:00:00 2001 From: Maple Xu Date: Sat, 4 Apr 2026 10:24:33 -0400 Subject: [PATCH 1/5] Add Prometheus metrics endpoint for Fly.io Grafana Adds per-endpoint request count and latency metrics via the metrics and metrics-exporter-prometheus crates. GET /metrics serves Prometheus exposition format. fly.toml configured to point Fly.io scraper at it. Closes #14. Co-Authored-By: Claude Opus 4.6 (1M context) --- passwords/api/Cargo.lock | 204 +++++++++++++++++++++++++++ passwords/api/Cargo.toml | 2 + passwords/api/fly.toml | 4 + passwords/api/src/lib.rs | 85 +++++++++-- passwords/api/tests/metrics_tests.rs | 49 +++++++ 5 files changed, 334 insertions(+), 10 deletions(-) create mode 100644 passwords/api/tests/metrics_tests.rs diff --git a/passwords/api/Cargo.lock b/passwords/api/Cargo.lock index 8a21c72..3dc90e4 100644 --- a/passwords/api/Cargo.lock +++ b/passwords/api/Cargo.lock @@ -53,6 +53,28 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "aws-lc-rs" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" version = "0.8.8" @@ -192,6 +214,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -201,6 +225,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "const-random" version = "0.1.18" @@ -230,6 +263,22 @@ dependencies = [ "unicode-segmentation", ] +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -422,6 +471,12 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "enum-as-inner" version = "0.6.1" @@ -493,6 +548,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "2.0.0" @@ -818,6 +879,23 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-timeout" version = "0.5.2" @@ -1001,6 +1079,16 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.91" @@ -1129,6 +1217,53 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[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", + "rand_xoshiro", + "sketches-ddsketch", +] + [[package]] name = "mime" version = "0.3.17" @@ -1276,6 +1411,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1309,6 +1450,8 @@ dependencies = [ "data-encoding", "dotenv", "http", + "metrics", + "metrics-exporter-prometheus", "mongodb", "passwords 3.1.16", "ring 0.16.20", @@ -1493,6 +1636,15 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core", +] + [[package]] name = "random-pick" version = "1.2.17" @@ -1597,6 +1749,7 @@ version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ + "aws-lc-rs", "log", "once_cell", "ring 0.17.14", @@ -1606,6 +1759,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", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -1621,6 +1786,7 @@ version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ + "aws-lc-rs", "ring 0.17.14", "rustls-pki-types", "untrusted 0.9.0", @@ -1638,12 +1804,44 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.27" @@ -1796,6 +1994,12 @@ dependencies = [ "libc", ] +[[package]] +name = "sketches-ddsketch" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b" + [[package]] name = "slab" version = "0.4.12" diff --git a/passwords/api/Cargo.toml b/passwords/api/Cargo.toml index 79e18a4..0180b80 100644 --- a/passwords/api/Cargo.toml +++ b/passwords/api/Cargo.toml @@ -27,3 +27,5 @@ serde_json = "1.0" http = "1.0" tower_governor = "0.8.0" dashmap = "6" +metrics = "0.24" +metrics-exporter-prometheus = "0.16" diff --git a/passwords/api/fly.toml b/passwords/api/fly.toml index 9152c21..e8bba61 100644 --- a/passwords/api/fly.toml +++ b/passwords/api/fly.toml @@ -23,6 +23,10 @@ primary_region = 'ord' method = 'GET' path = '/api/v2/generate' +[metrics] + port = 8000 + path = "/metrics" + [[vm]] memory = '256mb' cpus = 1 diff --git a/passwords/api/src/lib.rs b/passwords/api/src/lib.rs index b31de07..1ff7278 100644 --- a/passwords/api/src/lib.rs +++ b/passwords/api/src/lib.rs @@ -3,8 +3,9 @@ pub mod encrypt; pub mod env; use axum::{ - extract::{rejection::PathRejection, FromRequestParts, Path}, + extract::{rejection::PathRejection, FromRequestParts, MatchedPath, Path}, http::{header::HeaderName, request::Parts, HeaderValue, Method, StatusCode}, + middleware::Next, response::IntoResponse, routing::{get, post}, Json, Router, @@ -13,6 +14,7 @@ use db::DbError; use encrypt::{generate_password, Credentials, CryptoError}; use env::EnvVars; use serde::Deserialize; +use metrics_exporter_prometheus::PrometheusHandle; use tower_governor::governor::GovernorConfigBuilder; use tower_governor::GovernorLayer; use tower_http::cors::CorsLayer; @@ -251,15 +253,68 @@ async fn delete_user(creds: Credentials) -> Result { Ok(StatusCode::OK) } +// --------------------------------------------------------------------------- +// Prometheus metrics +// --------------------------------------------------------------------------- + +/// Middleware that records per-request metrics: a counter (`http_requests_total`) +/// and a histogram (`http_request_duration_seconds`), both labeled by method, +/// path template, and status code. +async fn metrics_middleware( + method: Method, + matched_path: Option, + request: axum::extract::Request, + next: Next, +) -> axum::response::Response { + let path = matched_path + .map(|p| p.as_str().to_owned()) + .unwrap_or_else(|| request.uri().path().to_owned()); + let method_str = method.to_string(); + + let start = std::time::Instant::now(); + let response = next.run(request).await; + let duration = start.elapsed().as_secs_f64(); + let status = response.status().as_u16().to_string(); + + let labels = [ + ("method", method_str), + ("path", path), + ("status", status), + ]; + metrics::counter!("http_requests_total", &labels).increment(1); + metrics::histogram!("http_request_duration_seconds", &labels).record(duration); + + response +} + +/// Handler for `GET /metrics` — renders the Prometheus exposition format. +async fn metrics_endpoint( + axum::extract::State(handle): axum::extract::State, +) -> String { + handle.render() +} + +/// Install the `metrics-exporter-prometheus` recorder and return its handle. +/// Panics if a global recorder has already been installed. +pub fn install_prometheus_recorder() -> PrometheusHandle { + let builder = metrics_exporter_prometheus::PrometheusBuilder::new(); + builder + .install_recorder() + .expect("failed to install Prometheus recorder") +} + // --------------------------------------------------------------------------- // Application builder // --------------------------------------------------------------------------- /// Build the application router, using the supplied burst size for the -/// rate limiter. The normal entry point `build_router()` calls this with -/// [`RATE_LIMIT_BURST_SIZE`]. Tests may pass a much larger value to avoid -/// accidental 429s during their busy request sequences. -pub fn build_router_with_burst(burst_size: u32) -> Router { +/// rate limiter and an existing Prometheus handle for the `/metrics` endpoint. +/// +/// The normal entry point `build_router()` calls this with +/// [`RATE_LIMIT_BURST_SIZE`] and a freshly installed recorder. +/// Tests may pass a much larger burst value to avoid accidental 429s +/// during their busy request sequences. +pub fn build_router_with_burst(burst_size: u32, prometheus_handle: PrometheusHandle) -> Router { let app = Router::new() .route("/api/v2/generate", get(generate)) .route("/api/v2/user", post(create_user).put(update_user)) @@ -277,6 +332,13 @@ pub fn build_router_with_burst(burst_size: u32) -> Router { #[cfg(any(test, debug_assertions, feature = "test-helpers"))] let app = app.route("/api/v2/user", axum::routing::delete(delete_user)); + // The /metrics endpoint is outside the /api/v2 prefix and takes the + // PrometheusHandle as state via a nested Router merged into the main app. + let metrics_router = Router::new() + .route("/metrics", get(metrics_endpoint)) + .with_state(prometheus_handle); + let app = app.merge(metrics_router); + // Build the rate limiter configuration. let mut rate_limit_builder = GovernorConfigBuilder::default() .const_per_millisecond(RATE_LIMIT_REPLENISH_PERIOD_MS) @@ -286,17 +348,20 @@ pub fn build_router_with_burst(burst_size: u32) -> Router { .expect("invalid rate-limit configuration"); // Layers wrap routes that were registered *before* the .layer() call. - // Order (outermost → innermost): CORS → rate-limit → tracing → handler. + // Order (outermost → innermost): CORS → rate-limit → metrics → tracing → handler. // CORS must be outermost so preflight OPTIONS responses are never blocked - // by the rate limiter. - app.layer(TraceLayer::new_for_http()) + // by the rate limiter. The metrics middleware is inside rate-limiting so + // that only non-throttled requests are measured. + app.layer(axum::middleware::from_fn(metrics_middleware)) + .layer(TraceLayer::new_for_http()) .layer(GovernorLayer::new(rate_limit_config)) .layer(cors_layer()) } -/// Convenience wrapper used throughout the production binary. +/// Convenience wrapper used by the production binary. pub fn build_router() -> Router { - build_router_with_burst(RATE_LIMIT_BURST_SIZE) + let handle = install_prometheus_recorder(); + build_router_with_burst(RATE_LIMIT_BURST_SIZE, handle) } #[cfg(test)] diff --git a/passwords/api/tests/metrics_tests.rs b/passwords/api/tests/metrics_tests.rs new file mode 100644 index 0000000..cbc0519 --- /dev/null +++ b/passwords/api/tests/metrics_tests.rs @@ -0,0 +1,49 @@ +//! Metrics endpoint tests for the MapoPass API. +//! +//! Verifies that the Prometheus metrics endpoint is working correctly. +//! +//! ## Running +//! +//! ```sh +//! # From passwords/api/: +//! cargo test --test metrics_tests --features test-helpers +//! ``` + +mod common; + +use axum::body::Body; +use common::{app, body_string, run}; +use http::{Request, StatusCode}; +use tower::ServiceExt; + +#[test] +fn test_metrics_endpoint() { + run(async { + // Make a request first so that metrics are recorded. + let warmup = Request::builder() + .method("GET") + .uri("/api/v2/generate") + .body(Body::empty()) + .unwrap(); + let warmup_res = app().oneshot(warmup).await.unwrap(); + assert_eq!(warmup_res.status(), StatusCode::OK); + + // Now fetch /metrics and verify the Prometheus exposition format. + let req = Request::builder() + .method("GET") + .uri("/metrics") + .body(Body::empty()) + .unwrap(); + let res = app().oneshot(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + let body = body_string(res).await; + assert!( + body.contains("axum_http_requests_total"), + "expected axum_http_requests_total counter in /metrics response", + ); + assert!( + body.contains("axum_http_requests_duration_seconds"), + "expected axum_http_requests_duration_seconds histogram in /metrics response", + ); + }); +} From 998cf9ad8aaf4b346157742d9f41db3bdde339aa Mon Sep 17 00:00:00 2001 From: Maple Xu Date: Sat, 4 Apr 2026 13:53:55 -0400 Subject: [PATCH 2/5] Replace hand-rolled metrics middleware with axum-prometheus Swap custom middleware for the axum-prometheus crate which provides the same per-endpoint counters and histograms out of the box. Co-Authored-By: Claude Opus 4.6 (1M context) --- passwords/api/Cargo.lock | 641 +++++++---------------- passwords/api/Cargo.toml | 3 +- passwords/api/src/lib.rs | 120 ++--- passwords/api/tests/integration_tests.rs | 4 +- 4 files changed, 245 insertions(+), 523 deletions(-) diff --git a/passwords/api/Cargo.lock b/passwords/api/Cargo.lock index 3dc90e4..446a47f 100644 --- a/passwords/api/Cargo.lock +++ b/passwords/api/Cargo.lock @@ -53,28 +53,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "aws-lc-rs" -version = "1.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.39.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "axum" version = "0.8.8" @@ -139,6 +117,26 @@ dependencies = [ "syn", ] +[[package]] +name = "axum-prometheus" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd44522c72edf6a45dfdd73ea201937bdaa8610cdf473151f694900837ab3879" +dependencies = [ + "axum", + "bytes", + "futures-core", + "http", + "http-body", + "matchit", + "metrics", + "metrics-exporter-prometheus", + "pin-project-lite", + "tokio", + "tower", + "tower-http", +] + [[package]] name = "base64" version = "0.22.1" @@ -209,13 +207,11 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -225,15 +221,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - [[package]] name = "const-random" version = "0.1.18" @@ -263,22 +250,6 @@ dependencies = [ "unicode-segmentation", ] -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "cpufeatures" version = "0.2.17" @@ -336,9 +307,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -346,11 +317,10 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", @@ -360,9 +330,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", @@ -411,9 +381,9 @@ dependencies = [ [[package]] name = "derive-where" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" +checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" dependencies = [ "proc-macro2", "quote", @@ -471,12 +441,6 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "enum-as-inner" version = "0.6.1" @@ -548,12 +512,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "funty" version = "2.0.0" @@ -658,20 +616,20 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -744,6 +702,12 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + [[package]] name = "heck" version = "0.5.0" @@ -858,9 +822,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -873,29 +837,11 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", ] -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-native-certs", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", -] - [[package]] name = "hyper-timeout" version = "0.5.2" @@ -923,7 +869,7 @@ dependencies = [ "hyper", "libc", "pin-project-lite", - "socket2 0.6.2", + "socket2", "tokio", "tower-service", "tracing", @@ -931,12 +877,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -944,9 +891,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -957,9 +904,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -971,15 +918,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -991,15 +938,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1045,55 +992,46 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] [[package]] name = "ipconfig" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" dependencies = [ - "socket2 0.5.10", + "socket2", "widestring", - "windows-sys 0.48.0", - "winreg", + "windows-registry", + "windows-result", + "windows-sys 0.61.2", ] [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "jobserver" -version = "0.1.34" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "once_cell", "wasm-bindgen", @@ -1113,15 +1051,15 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -1229,34 +1167,27 @@ dependencies = [ [[package]] name = "metrics-exporter-prometheus" -version = "0.16.2" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034" +checksum = "3589659543c04c7dc5526ec858591015b87cd8746583b51b48ef4353f99dbcda" dependencies = [ "base64", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", "indexmap", - "ipnet", "metrics", "metrics-util", "quanta", - "thiserror 1.0.69", - "tokio", - "tracing", + "thiserror 2.0.18", ] [[package]] name = "metrics-util" -version = "0.19.1" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8496cc523d1f94c1385dd8f0f0c2c480b2b8aeccb5b7e4485ad6365523ae376" +checksum = "cdfb1365fea27e6dd9dc1dbc19f570198bc86914533ad639dae939635f096be4" dependencies = [ "crossbeam-epoch", "crossbeam-utils", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "metrics", "quanta", "rand", @@ -1272,9 +1203,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -1283,9 +1214,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.13" +version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" dependencies = [ "crossbeam-channel", "crossbeam-epoch", @@ -1318,9 +1249,9 @@ checksum = "224484c5d09285a7b8cb0a0c117e847ebd14cb6e4470ecf68cdb89c503b0edb9" [[package]] name = "mongodb" -version = "3.5.1" +version = "3.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803dd859e8afa084c255a8effd8000ff86f7c8076a50cd6d8c99e8f3496f75c2" +checksum = "2c5941683db2ab2697f71e58dc0319024e808d3b28e7cf20f4bfb445fe54a30b" dependencies = [ "base64", "bitflags", @@ -1349,7 +1280,7 @@ dependencies = [ "serde_with", "sha1", "sha2", - "socket2 0.6.2", + "socket2", "stringprep", "strsim", "take_mut", @@ -1364,9 +1295,9 @@ dependencies = [ [[package]] name = "mongodb-internal-macros" -version = "3.5.1" +version = "3.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973ef3dd3dbc6f6e65bbdecfd9ec5e781b9e7493b0f369a7c62e35d8e5ae2c8" +checksum = "47021a12bbf0dffde9c890fa2d36ff6ae342c532016226b04a42301b2b912660" dependencies = [ "macro_magic", "proc-macro2", @@ -1397,26 +1328,20 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" dependencies = [ "critical-section", "portable-atomic", ] -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - [[package]] name = "parking_lot" version = "0.12.5" @@ -1446,12 +1371,11 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", + "axum-prometheus", "dashmap", "data-encoding", "dotenv", "http", - "metrics", - "metrics-exporter-prometheus", "mongodb", "passwords 3.1.16", "ring 0.16.20", @@ -1516,12 +1440,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "portable-atomic" version = "1.13.1" @@ -1530,9 +1448,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -1588,9 +1506,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1601,6 +1519,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -1749,7 +1673,6 @@ version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ - "aws-lc-rs", "log", "once_cell", "ring 0.17.14", @@ -1759,18 +1682,6 @@ 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", - "rustls-pki-types", - "schannel", - "security-framework", -] - [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -1782,11 +1693,10 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "20a6af516fea4b20eccceaf166e8aa666ac996208e8a644ce3ef5aa783bc7cd4" dependencies = [ - "aws-lc-rs", "ring 0.17.14", "rustls-pki-types", "untrusted 0.9.0", @@ -1804,49 +1714,17 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "schannel" -version = "0.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -1927,9 +1805,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.17.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "serde_core", "serde_with_macros", @@ -1937,9 +1815,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.17.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ "darling", "proc-macro2", @@ -2014,22 +1892,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2213,9 +2081,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -2223,9 +2091,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -2238,25 +2106,25 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" dependencies = [ "bytes", "libc", "mio", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -2317,7 +2185,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "socket2 0.6.2", + "socket2", "sync_wrapper", "tokio", "tokio-stream", @@ -2437,9 +2305,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -2514,9 +2382,9 @@ checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-xid" @@ -2556,11 +2424,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.21.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -2613,9 +2481,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -2626,9 +2494,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2636,9 +2504,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -2649,9 +2517,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -2692,9 +2560,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -2754,54 +2622,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-registry" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-targets 0.48.5", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] -name = "windows-sys" -version = "0.52.0" +name = "windows-result" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-targets 0.52.6", + "windows-link", ] [[package]] -name = "windows-sys" -version = "0.60.2" +name = "windows-strings" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-targets 0.53.5", + "windows-link", ] [[package]] name = "windows-sys" -version = "0.61.2" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-link", + "windows-targets", ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-link", ] [[package]] @@ -2810,181 +2674,64 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "wit-bindgen" version = "0.51.0" @@ -3075,9 +2822,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wyz" @@ -3090,9 +2837,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -3101,9 +2848,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -3113,18 +2860,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.40" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.40" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -3133,18 +2880,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -3160,9 +2907,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -3171,9 +2918,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -3182,9 +2929,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", diff --git a/passwords/api/Cargo.toml b/passwords/api/Cargo.toml index 0180b80..58cd151 100644 --- a/passwords/api/Cargo.toml +++ b/passwords/api/Cargo.toml @@ -27,5 +27,4 @@ serde_json = "1.0" http = "1.0" tower_governor = "0.8.0" dashmap = "6" -metrics = "0.24" -metrics-exporter-prometheus = "0.16" +axum-prometheus = "0.10" diff --git a/passwords/api/src/lib.rs b/passwords/api/src/lib.rs index 1ff7278..8d3e4bd 100644 --- a/passwords/api/src/lib.rs +++ b/passwords/api/src/lib.rs @@ -3,18 +3,17 @@ pub mod encrypt; pub mod env; use axum::{ - extract::{rejection::PathRejection, FromRequestParts, MatchedPath, Path}, + extract::{rejection::PathRejection, FromRequestParts, Path}, http::{header::HeaderName, request::Parts, HeaderValue, Method, StatusCode}, - middleware::Next, response::IntoResponse, routing::{get, post}, Json, Router, }; +use axum_prometheus::PrometheusMetricLayer; use db::DbError; use encrypt::{generate_password, Credentials, CryptoError}; use env::EnvVars; use serde::Deserialize; -use metrics_exporter_prometheus::PrometheusHandle; use tower_governor::governor::GovernorConfigBuilder; use tower_governor::GovernorLayer; use tower_http::cors::CorsLayer; @@ -253,68 +252,12 @@ async fn delete_user(creds: Credentials) -> Result { Ok(StatusCode::OK) } -// --------------------------------------------------------------------------- -// Prometheus metrics -// --------------------------------------------------------------------------- - -/// Middleware that records per-request metrics: a counter (`http_requests_total`) -/// and a histogram (`http_request_duration_seconds`), both labeled by method, -/// path template, and status code. -async fn metrics_middleware( - method: Method, - matched_path: Option, - request: axum::extract::Request, - next: Next, -) -> axum::response::Response { - let path = matched_path - .map(|p| p.as_str().to_owned()) - .unwrap_or_else(|| request.uri().path().to_owned()); - let method_str = method.to_string(); - - let start = std::time::Instant::now(); - let response = next.run(request).await; - let duration = start.elapsed().as_secs_f64(); - let status = response.status().as_u16().to_string(); - - let labels = [ - ("method", method_str), - ("path", path), - ("status", status), - ]; - metrics::counter!("http_requests_total", &labels).increment(1); - metrics::histogram!("http_request_duration_seconds", &labels).record(duration); - - response -} - -/// Handler for `GET /metrics` — renders the Prometheus exposition format. -async fn metrics_endpoint( - axum::extract::State(handle): axum::extract::State, -) -> String { - handle.render() -} - -/// Install the `metrics-exporter-prometheus` recorder and return its handle. -/// Panics if a global recorder has already been installed. -pub fn install_prometheus_recorder() -> PrometheusHandle { - let builder = metrics_exporter_prometheus::PrometheusBuilder::new(); - builder - .install_recorder() - .expect("failed to install Prometheus recorder") -} - // --------------------------------------------------------------------------- // Application builder // --------------------------------------------------------------------------- -/// Build the application router, using the supplied burst size for the -/// rate limiter and an existing Prometheus handle for the `/metrics` endpoint. -/// -/// The normal entry point `build_router()` calls this with -/// [`RATE_LIMIT_BURST_SIZE`] and a freshly installed recorder. -/// Tests may pass a much larger burst value to avoid accidental 429s -/// during their busy request sequences. -pub fn build_router_with_burst(burst_size: u32, prometheus_handle: PrometheusHandle) -> Router { +/// Register all application routes (including conditional test-only routes). +fn app_routes() -> Router { let app = Router::new() .route("/api/v2/generate", get(generate)) .route("/api/v2/user", post(create_user).put(update_user)) @@ -332,12 +275,26 @@ pub fn build_router_with_burst(burst_size: u32, prometheus_handle: PrometheusHan #[cfg(any(test, debug_assertions, feature = "test-helpers"))] let app = app.route("/api/v2/user", axum::routing::delete(delete_user)); - // The /metrics endpoint is outside the /api/v2 prefix and takes the - // PrometheusHandle as state via a nested Router merged into the main app. - let metrics_router = Router::new() - .route("/metrics", get(metrics_endpoint)) - .with_state(prometheus_handle); - let app = app.merge(metrics_router); + app +} + +/// Build the application router, using the supplied burst size for the +/// rate limiter. +/// +/// The Prometheus metric layer and `/metrics` endpoint are created +/// internally via [`PrometheusMetricLayer::pair()`]. Because the +/// underlying `metrics` crate only allows a single global recorder, +/// this function must be called **at most once per process**. +/// +/// The normal entry point `build_router()` calls this with +/// [`RATE_LIMIT_BURST_SIZE`]. +/// Tests may pass a much larger burst value to avoid accidental 429s +/// during their busy request sequences. +pub fn build_router_with_burst(burst_size: u32) -> Router { + let (prometheus_layer, metric_handle) = PrometheusMetricLayer::pair(); + + let app = app_routes() + .route("/metrics", get(|| async move { metric_handle.render() })); // Build the rate limiter configuration. let mut rate_limit_builder = GovernorConfigBuilder::default() @@ -348,20 +305,39 @@ pub fn build_router_with_burst(burst_size: u32, prometheus_handle: PrometheusHan .expect("invalid rate-limit configuration"); // Layers wrap routes that were registered *before* the .layer() call. - // Order (outermost → innermost): CORS → rate-limit → metrics → tracing → handler. + // Order (outermost → innermost): CORS → rate-limit → prometheus → tracing → handler. // CORS must be outermost so preflight OPTIONS responses are never blocked - // by the rate limiter. The metrics middleware is inside rate-limiting so + // by the rate limiter. The prometheus middleware is inside rate-limiting so // that only non-throttled requests are measured. - app.layer(axum::middleware::from_fn(metrics_middleware)) + app.layer(prometheus_layer) .layer(TraceLayer::new_for_http()) .layer(GovernorLayer::new(rate_limit_config)) .layer(cors_layer()) } +/// Build a router with the given burst size but **without** installing +/// a Prometheus recorder. Useful in tests that need a separate router +/// (e.g. to test rate-limiting with a small burst) when the global +/// recorder has already been installed by another router in the same +/// process. +pub fn build_router_with_burst_no_metrics(burst_size: u32) -> Router { + let app = app_routes(); + + let mut rate_limit_builder = GovernorConfigBuilder::default() + .const_per_millisecond(RATE_LIMIT_REPLENISH_PERIOD_MS) + .const_burst_size(burst_size); + let rate_limit_config = rate_limit_builder + .finish() + .expect("invalid rate-limit configuration"); + + app.layer(TraceLayer::new_for_http()) + .layer(GovernorLayer::new(rate_limit_config)) + .layer(cors_layer()) +} + /// Convenience wrapper used by the production binary. pub fn build_router() -> Router { - let handle = install_prometheus_recorder(); - build_router_with_burst(RATE_LIMIT_BURST_SIZE, handle) + build_router_with_burst(RATE_LIMIT_BURST_SIZE) } #[cfg(test)] diff --git a/passwords/api/tests/integration_tests.rs b/passwords/api/tests/integration_tests.rs index 02a880f..fa6766b 100644 --- a/passwords/api/tests/integration_tests.rs +++ b/passwords/api/tests/integration_tests.rs @@ -17,7 +17,7 @@ use axum::body::Body; use axum::{middleware::from_fn, extract::ConnectInfo}; use common::{app, body_string, parse_json, run, TestUser, WithAuth}; use http::{Request, StatusCode}; -use passwords::build_router; +use passwords::build_router_with_burst_no_metrics; use std::net::SocketAddr; use std::time::Duration; use tower::ServiceExt; @@ -69,7 +69,7 @@ fn test_rate_limiting() { // observe throttling. We still need the connect-info middleware that // `app()` adds, so copy that behaviour. let addr = SocketAddr::from(([127, 0, 0, 1], 0)); - let limited = build_router() + let limited = build_router_with_burst_no_metrics(10) .layer(from_fn(move |mut req: Request, next: axum::middleware::Next| async move { req.extensions_mut().insert(ConnectInfo(addr)); next.run(req).await From 1b5a9fb22205164e594cce325096d55712307040 Mon Sep 17 00:00:00 2001 From: Maple Xu Date: Fri, 10 Apr 2026 20:12:29 -0400 Subject: [PATCH 3/5] Use OnceLock for prometheus recorder so all routers match prod Remove build_router_with_burst_no_metrics. Cache the prometheus layer/handle pair in a OnceLock so build_router_with_burst is safe to call multiple times and always returns the full prod middleware stack. Co-Authored-By: Claude Opus 4.6 (1M context) --- passwords/api/src/lib.rs | 52 ++++++++++++------------ passwords/api/tests/integration_tests.rs | 4 +- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/passwords/api/src/lib.rs b/passwords/api/src/lib.rs index 8d3e4bd..dcbab5c 100644 --- a/passwords/api/src/lib.rs +++ b/passwords/api/src/lib.rs @@ -9,11 +9,13 @@ use axum::{ routing::{get, post}, Json, Router, }; +use axum_prometheus::metrics_exporter_prometheus::PrometheusHandle; use axum_prometheus::PrometheusMetricLayer; use db::DbError; use encrypt::{generate_password, Credentials, CryptoError}; use env::EnvVars; use serde::Deserialize; +use std::sync::OnceLock; use tower_governor::governor::GovernorConfigBuilder; use tower_governor::GovernorLayer; use tower_http::cors::CorsLayer; @@ -252,6 +254,24 @@ async fn delete_user(creds: Credentials) -> Result { Ok(StatusCode::OK) } +// --------------------------------------------------------------------------- +// Prometheus metrics (initialized at most once per process) +// --------------------------------------------------------------------------- + +/// Stores the Prometheus metric layer and handle so that +/// `PrometheusMetricLayer::pair()` (which installs a global recorder) is +/// called at most once. Subsequent calls to `prometheus_pair()` clone the +/// stored values. +static PROMETHEUS: OnceLock<(PrometheusMetricLayer<'static>, PrometheusHandle)> = OnceLock::new(); + +/// Return a `(layer, handle)` pair, creating it on the first call and +/// cloning the cached values on every subsequent call. +fn prometheus_pair() -> (PrometheusMetricLayer<'static>, PrometheusHandle) { + PROMETHEUS + .get_or_init(PrometheusMetricLayer::pair) + .clone() +} + // --------------------------------------------------------------------------- // Application builder // --------------------------------------------------------------------------- @@ -281,17 +301,19 @@ fn app_routes() -> Router { /// Build the application router, using the supplied burst size for the /// rate limiter. /// -/// The Prometheus metric layer and `/metrics` endpoint are created -/// internally via [`PrometheusMetricLayer::pair()`]. Because the -/// underlying `metrics` crate only allows a single global recorder, -/// this function must be called **at most once per process**. +/// The Prometheus metric layer and `/metrics` endpoint are initialised +/// lazily via [`prometheus_pair()`], which calls +/// [`PrometheusMetricLayer::pair()`] at most once per process (subsequent +/// calls clone the cached layer and handle). This makes the function safe +/// to call multiple times — important for tests that create separate +/// routers with different burst sizes. /// /// The normal entry point `build_router()` calls this with /// [`RATE_LIMIT_BURST_SIZE`]. /// Tests may pass a much larger burst value to avoid accidental 429s /// during their busy request sequences. pub fn build_router_with_burst(burst_size: u32) -> Router { - let (prometheus_layer, metric_handle) = PrometheusMetricLayer::pair(); + let (prometheus_layer, metric_handle) = prometheus_pair(); let app = app_routes() .route("/metrics", get(|| async move { metric_handle.render() })); @@ -315,26 +337,6 @@ pub fn build_router_with_burst(burst_size: u32) -> Router { .layer(cors_layer()) } -/// Build a router with the given burst size but **without** installing -/// a Prometheus recorder. Useful in tests that need a separate router -/// (e.g. to test rate-limiting with a small burst) when the global -/// recorder has already been installed by another router in the same -/// process. -pub fn build_router_with_burst_no_metrics(burst_size: u32) -> Router { - let app = app_routes(); - - let mut rate_limit_builder = GovernorConfigBuilder::default() - .const_per_millisecond(RATE_LIMIT_REPLENISH_PERIOD_MS) - .const_burst_size(burst_size); - let rate_limit_config = rate_limit_builder - .finish() - .expect("invalid rate-limit configuration"); - - app.layer(TraceLayer::new_for_http()) - .layer(GovernorLayer::new(rate_limit_config)) - .layer(cors_layer()) -} - /// Convenience wrapper used by the production binary. pub fn build_router() -> Router { build_router_with_burst(RATE_LIMIT_BURST_SIZE) diff --git a/passwords/api/tests/integration_tests.rs b/passwords/api/tests/integration_tests.rs index fa6766b..883589f 100644 --- a/passwords/api/tests/integration_tests.rs +++ b/passwords/api/tests/integration_tests.rs @@ -17,7 +17,7 @@ use axum::body::Body; use axum::{middleware::from_fn, extract::ConnectInfo}; use common::{app, body_string, parse_json, run, TestUser, WithAuth}; use http::{Request, StatusCode}; -use passwords::build_router_with_burst_no_metrics; +use passwords::build_router_with_burst; use std::net::SocketAddr; use std::time::Duration; use tower::ServiceExt; @@ -69,7 +69,7 @@ fn test_rate_limiting() { // observe throttling. We still need the connect-info middleware that // `app()` adds, so copy that behaviour. let addr = SocketAddr::from(([127, 0, 0, 1], 0)); - let limited = build_router_with_burst_no_metrics(10) + let limited = build_router_with_burst(10) .layer(from_fn(move |mut req: Request, next: axum::middleware::Next| async move { req.extensions_mut().insert(ConnectInfo(addr)); next.run(req).await From 6fd2fe62fab95e3fb76acc66b938658638449720 Mon Sep 17 00:00:00 2001 From: Maple Xu Date: Fri, 10 Apr 2026 23:50:41 -0400 Subject: [PATCH 4/5] Replace build_router_with_burst with RouterConfig struct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename to build_router(RouterConfig) — takes a config struct instead of a bare u32. Remove the convenience wrapper. Improve doc comments to focus on what the function does rather than implementation details. Co-Authored-By: Claude Opus 4.6 (1M context) --- passwords/api/src/lib.rs | 51 +++++++++++++++--------- passwords/api/src/main.rs | 4 +- passwords/api/tests/common.rs | 4 +- passwords/api/tests/integration_tests.rs | 4 +- 4 files changed, 38 insertions(+), 25 deletions(-) diff --git a/passwords/api/src/lib.rs b/passwords/api/src/lib.rs index dcbab5c..998e789 100644 --- a/passwords/api/src/lib.rs +++ b/passwords/api/src/lib.rs @@ -33,7 +33,28 @@ const RATE_LIMIT_REPLENISH_PERIOD_MS: u64 = 100; /// Maximum burst size — the number of requests a client can make /// before being throttled. -const RATE_LIMIT_BURST_SIZE: u32 = 10; +pub const RATE_LIMIT_BURST_SIZE: u32 = 10; + +// --------------------------------------------------------------------------- +// Router configuration +// --------------------------------------------------------------------------- + +/// Configuration for building the application router. +/// +/// Use [`RouterConfig::default()`] for production settings, or construct +/// manually to override values (e.g. in tests). +pub struct RouterConfig { + /// Maximum number of requests a client can make before being throttled. + pub burst_size: u32, +} + +impl Default for RouterConfig { + fn default() -> Self { + Self { + burst_size: RATE_LIMIT_BURST_SIZE, + } + } +} fn is_valid_key_length(key: &str) -> bool { key.len() <= MAX_KEY_LENGTH @@ -298,21 +319,18 @@ fn app_routes() -> Router { app } -/// Build the application router, using the supplied burst size for the -/// rate limiter. +/// Build the full application [`Router`] with all middleware layers. /// -/// The Prometheus metric layer and `/metrics` endpoint are initialised -/// lazily via [`prometheus_pair()`], which calls -/// [`PrometheusMetricLayer::pair()`] at most once per process (subsequent -/// calls clone the cached layer and handle). This makes the function safe -/// to call multiple times — important for tests that create separate -/// routers with different burst sizes. +/// Includes CORS, per-IP rate limiting, Prometheus metrics collection +/// (exposed at `/metrics`), and HTTP tracing. Safe to call multiple +/// times in the same process — the Prometheus recorder is initialised +/// at most once and reused on subsequent calls. /// -/// The normal entry point `build_router()` calls this with -/// [`RATE_LIMIT_BURST_SIZE`]. -/// Tests may pass a much larger burst value to avoid accidental 429s -/// during their busy request sequences. -pub fn build_router_with_burst(burst_size: u32) -> Router { +/// Use [`RouterConfig::default()`] for production settings. Tests can +/// override individual fields (e.g. a large `burst_size` to avoid +/// accidental 429s, or a small one to exercise throttling). +pub fn build_router(config: RouterConfig) -> Router { + let burst_size = config.burst_size; let (prometheus_layer, metric_handle) = prometheus_pair(); let app = app_routes() @@ -337,11 +355,6 @@ pub fn build_router_with_burst(burst_size: u32) -> Router { .layer(cors_layer()) } -/// Convenience wrapper used by the production binary. -pub fn build_router() -> Router { - build_router_with_burst(RATE_LIMIT_BURST_SIZE) -} - #[cfg(test)] mod tests { use super::*; diff --git a/passwords/api/src/main.rs b/passwords/api/src/main.rs index dd24085..6e948cc 100644 --- a/passwords/api/src/main.rs +++ b/passwords/api/src/main.rs @@ -1,4 +1,4 @@ -use passwords::{build_router, db}; +use passwords::{build_router, db, RouterConfig}; use std::net::SocketAddr; use tokio::net::TcpListener; use tracing_subscriber::EnvFilter; @@ -17,7 +17,7 @@ async fn main() -> Result<(), anyhow::Error> { } db::connect().await?; - let app = build_router().into_make_service_with_connect_info::(); + let app = build_router(RouterConfig::default()).into_make_service_with_connect_info::(); let listener = TcpListener::bind("0.0.0.0:8000").await?; tracing::info!(addr = %listener.local_addr()?, "listening"); axum::serve(listener, app).await?; diff --git a/passwords/api/tests/common.rs b/passwords/api/tests/common.rs index 7c8c45e..1ab975e 100644 --- a/passwords/api/tests/common.rs +++ b/passwords/api/tests/common.rs @@ -7,7 +7,7 @@ use axum::body::Body; use axum::{Router, middleware::from_fn, extract::ConnectInfo}; use http::Request; use mongodb::bson::oid::ObjectId; -use passwords::build_router_with_burst; +use passwords::{build_router, RouterConfig}; use passwords::db; use std::net::SocketAddr; use std::sync::LazyLock; @@ -32,7 +32,7 @@ static APP: LazyLock = LazyLock::new(|| { db::connect().await.expect("Failed to connect to test DB"); // use a very large burst so ordinary tests aren't disrupted by our // rate limiter; stress test will create its own router below. - build_router_with_burst(1_000_000) + build_router(RouterConfig { burst_size: 1_000_000 }) }) }); diff --git a/passwords/api/tests/integration_tests.rs b/passwords/api/tests/integration_tests.rs index 883589f..a90d6a1 100644 --- a/passwords/api/tests/integration_tests.rs +++ b/passwords/api/tests/integration_tests.rs @@ -17,7 +17,7 @@ use axum::body::Body; use axum::{middleware::from_fn, extract::ConnectInfo}; use common::{app, body_string, parse_json, run, TestUser, WithAuth}; use http::{Request, StatusCode}; -use passwords::build_router_with_burst; +use passwords::{build_router, RouterConfig}; use std::net::SocketAddr; use std::time::Duration; use tower::ServiceExt; @@ -69,7 +69,7 @@ fn test_rate_limiting() { // observe throttling. We still need the connect-info middleware that // `app()` adds, so copy that behaviour. let addr = SocketAddr::from(([127, 0, 0, 1], 0)); - let limited = build_router_with_burst(10) + let limited = build_router(RouterConfig { burst_size: 10 }) .layer(from_fn(move |mut req: Request, next: axum::middleware::Next| async move { req.extensions_mut().insert(ConnectInfo(addr)); next.run(req).await From 762a20f429f1c219ccba624fbfde4befddf6eb4d Mon Sep 17 00:00:00 2001 From: Maple Xu Date: Sat, 11 Apr 2026 00:02:45 -0400 Subject: [PATCH 5/5] Clarify layer ordering comment, trim doc comment Co-Authored-By: Claude Opus 4.6 (1M context) --- passwords/api/src/lib.rs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/passwords/api/src/lib.rs b/passwords/api/src/lib.rs index 998e789..41e5d53 100644 --- a/passwords/api/src/lib.rs +++ b/passwords/api/src/lib.rs @@ -319,16 +319,10 @@ fn app_routes() -> Router { app } -/// Build the full application [`Router`] with all middleware layers. +/// Build the application [`Router`] with middleware configured via [`RouterConfig`]. /// -/// Includes CORS, per-IP rate limiting, Prometheus metrics collection -/// (exposed at `/metrics`), and HTTP tracing. Safe to call multiple -/// times in the same process — the Prometheus recorder is initialised -/// at most once and reused on subsequent calls. -/// -/// Use [`RouterConfig::default()`] for production settings. Tests can -/// override individual fields (e.g. a large `burst_size` to avoid -/// accidental 429s, or a small one to exercise throttling). +/// Safe to call multiple times — the Prometheus recorder is initialised once +/// and reused. pub fn build_router(config: RouterConfig) -> Router { let burst_size = config.burst_size; let (prometheus_layer, metric_handle) = prometheus_pair(); @@ -344,11 +338,7 @@ pub fn build_router(config: RouterConfig) -> Router { .finish() .expect("invalid rate-limit configuration"); - // Layers wrap routes that were registered *before* the .layer() call. - // Order (outermost → innermost): CORS → rate-limit → prometheus → tracing → handler. - // CORS must be outermost so preflight OPTIONS responses are never blocked - // by the rate limiter. The prometheus middleware is inside rate-limiting so - // that only non-throttled requests are measured. + // .layer() is last-added = outermost; read bottom-to-top for execution order. app.layer(prometheus_layer) .layer(TraceLayer::new_for_http()) .layer(GovernorLayer::new(rate_limit_config))