diff --git a/Cargo.lock b/Cargo.lock index 8230ede..a40c951 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2726,7 +2726,7 @@ dependencies = [ "indexmap 2.9.0", "ipnet", "metrics", - "metrics-util", + "metrics-util 0.19.0", "quanta", "thiserror 1.0.69", "tokio", @@ -2735,9 +2735,9 @@ dependencies = [ [[package]] name = "metrics-exporter-prometheus" -version = "0.17.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989903b4c7abfa6827a8d1128ef42faf83f8969d429797c5431f236f2cae8b8b" +checksum = "2b166dea96003ee2531cf14833efedced545751d800f03535801d833313f8c15" dependencies = [ "base64 0.22.1", "http-body-util", @@ -2747,7 +2747,7 @@ dependencies = [ "indexmap 2.9.0", "ipnet", "metrics", - "metrics-util", + "metrics-util 0.20.0", "quanta", "thiserror 2.0.12", "tokio", @@ -2782,7 +2782,23 @@ dependencies = [ "metrics", "quanta", "rand 0.8.5", - "rand_xoshiro", + "rand_xoshiro 0.6.0", + "sketches-ddsketch", +] + +[[package]] +name = "metrics-util" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe8db7a05415d0f919ffb905afa37784f71901c9a773188876984b4f769ab986" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown 0.15.2", + "metrics", + "quanta", + "rand 0.9.1", + "rand_xoshiro 0.7.0", "sketches-ddsketch", ] @@ -3398,6 +3414,15 @@ dependencies = [ "rand_core 0.6.4", ] +[[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.3", +] + [[package]] name = "ratelimit" version = "0.10.0" @@ -4162,7 +4187,7 @@ dependencies = [ "links", "log", "metrics", - "metrics-exporter-prometheus 0.17.1", + "metrics-exporter-prometheus 0.17.2", "rand 0.9.1", "schemars", "semver", @@ -4752,7 +4777,7 @@ dependencies = [ "log", "lsm-tree", "metrics", - "metrics-exporter-prometheus 0.17.1", + "metrics-exporter-prometheus 0.17.2", "schemars", "semver", "serde", @@ -5063,6 +5088,7 @@ dependencies = [ "handlebars", "hickory-resolver", "metrics", + "metrics-exporter-prometheus 0.17.2", "rand 0.9.1", "reqwest", "serde", diff --git a/who-am-i/Cargo.toml b/who-am-i/Cargo.toml index cbf66b4..bfa02d2 100644 --- a/who-am-i/Cargo.toml +++ b/who-am-i/Cargo.toml @@ -17,6 +17,7 @@ dashmap = "6.1.0" handlebars = { version = "6.3.2", features = ["dir_source"] } hickory-resolver = "0.25.2" metrics = "0.24.2" +metrics-exporter-prometheus = { version = "0.17.2", features = ["http-listener"] } rand = "0.9.1" reqwest = { version = "0.12.22", features = ["native-tls-vendored"] } serde = { version = "1.0.219", features = ["derive"] } diff --git a/who-am-i/src/expiring_task_map.rs b/who-am-i/src/expiring_task_map.rs index 9f0bb88..9a8f3ba 100644 --- a/who-am-i/src/expiring_task_map.rs +++ b/who-am-i/src/expiring_task_map.rs @@ -49,10 +49,12 @@ impl ExpiringTaskMap { .run_until_cancelled(sleep(expiration)) .await .is_some() - // is Some if the (sleep) task completed first { + // is Some if the (sleep) task completed first map.remove(&k); cancel.cancel(); + metrics::counter!("whoami_task_map_completions", "result" => "expired") + .increment(1); } }); @@ -60,6 +62,7 @@ impl ExpiringTaskMap { } pub fn take(&self, key: &str) -> Option> { + metrics::counter!("whoami_task_map_completions", "result" => "retrieved").increment(1); // when the _guard drops, the token gets cancelled for us self.0.map.remove(key).map(|(_, (_guard, handle))| handle) } diff --git a/who-am-i/src/main.rs b/who-am-i/src/main.rs index 785bf97..e4f5ed1 100644 --- a/who-am-i/src/main.rs +++ b/who-am-i/src/main.rs @@ -1,4 +1,5 @@ use clap::{ArgAction, Parser}; +use metrics_exporter_prometheus::PrometheusBuilder; use tokio_util::sync::CancellationToken; use who_am_i::serve; @@ -35,7 +36,7 @@ async fn main() { let args = Args::parse(); if args.allowed_hosts.is_empty() { - panic!("at least one --one-click host must be set"); + panic!("at least one --allowed-host host must be set"); } println!("starting with allowed_hosts hosts:"); @@ -43,5 +44,24 @@ async fn main() { println!(" - {host}"); } + if let Err(e) = install_metrics_server() { + eprintln!("failed to install metrics server: {e:?}"); + }; + serve(shutdown, args.app_secret, args.allowed_hosts, args.dev).await; } + +fn install_metrics_server() -> Result<(), metrics_exporter_prometheus::BuildError> { + println!("installing metrics server..."); + let host = [0, 0, 0, 0]; + let port = 8765; + PrometheusBuilder::new() + .set_enable_unit_suffix(false) + .with_http_listener((host, port)) + .install()?; + println!( + "metrics server installed! listening on http://{}.{}.{}.{}:{port}", + host[0], host[1], host[2], host[3] + ); + Ok(()) +} diff --git a/who-am-i/src/oauth.rs b/who-am-i/src/oauth.rs index 8f2b626..4b735f6 100644 --- a/who-am-i/src/oauth.rs +++ b/who-am-i/src/oauth.rs @@ -198,12 +198,15 @@ impl DnsTxtResolver for HickoryDnsTxtResolver { &self, query: &str, ) -> core::result::Result, Box> { - Ok(self - .0 - .txt_lookup(query) - .await? - .iter() - .map(|txt| txt.to_string()) - .collect()) + match self.0.txt_lookup(query).await { + Ok(r) => { + metrics::counter!("whoami_resolve_dns_txt", "success" => "true").increment(1); + Ok(r.iter().map(|r| r.to_string()).collect()) + } + Err(e) => { + metrics::counter!("whoami_resolve_dns_txt", "success" => "false").increment(1); + Err(e.into()) + } + } } } diff --git a/who-am-i/src/server.rs b/who-am-i/src/server.rs index b5e6d3f..9e15638 100644 --- a/who-am-i/src/server.rs +++ b/who-am-i/src/server.rs @@ -172,6 +172,7 @@ async fn prompt( headers: HeaderMap, ) -> impl IntoResponse { let err = |reason, check_frame| { + metrics::counter!("whoami_auth_prompt", "ok" => "false", "reason" => reason).increment(1); let info = json!({ "reason": reason, "check_frame": check_frame }); let html = RenderHtml("prompt-error", engine.clone(), info); (StatusCode::BAD_REQUEST, html).into_response() @@ -222,15 +223,16 @@ async fn prompt( shutdown.child_token(), ); + metrics::counter!("whoami_auth_prompt", "ok" => "true", "known" => "true").increment(1); let info = json!({ "did": did, "fetch_key": fetch_key, "parent_host": parent_host, "parent_origin": parent_origin, }); - (frame_headers, jar, RenderHtml("prompt", engine, info)).into_response() } else { + metrics::counter!("whoami_auth_prompt", "ok" => "true", "known" => "false").increment(1); let info = json!({ "parent_host": parent_host, "parent_origin": parent_origin, @@ -250,7 +252,11 @@ async fn user_info( }): State, Query(params): Query, ) -> impl IntoResponse { - let err = |status, reason| (status, Json(json!({ "reason": reason }))).into_response(); + let err = |status, reason: &str| { + metrics::counter!("whoami_user_info", "found" => "false", "reason" => reason.to_string()) + .increment(1); + (status, Json(json!({ "reason": reason }))).into_response() + }; let Some(task_handle) = resolve_handles.take(¶ms.fetch_key) else { return err(StatusCode::NOT_FOUND, "fetch key does not exist or expired"); @@ -279,7 +285,10 @@ async fn user_info( StatusCode::INTERNAL_SERVER_ERROR, &format!("handle appears invalid: {reason}"), ), - Ok(Ok(handle)) => Json(json!({ "handle": handle })).into_response(), + Ok(Ok(handle)) => { + metrics::counter!("whoami_user_info", "found" => "true").increment(1); + Json(json!({ "handle": handle })).into_response() + } } } @@ -299,7 +308,9 @@ async fn start_oauth( use atrium_identity::Error as IdError; use atrium_oauth::Error as OAuthError; - let err = |code, reason| { + let err = |code, reason: &str| { + metrics::counter!("whoami_auth_start", "ok" => "false", "reason" => reason.to_string()) + .increment(1); let info = json!({ "result": "fail", "reason": reason, @@ -308,7 +319,6 @@ async fn start_oauth( }; match oauth.begin(¶ms.handle).await { - Ok(auth_url) => (jar, Redirect::to(&auth_url)).into_response(), Err(OAuthError::Identity( IdError::NotFound | IdError::HttpStatus(StatusCode::NOT_FOUND), )) => err(StatusCode::NOT_FOUND, "handle not found"), @@ -317,6 +327,10 @@ async fn start_oauth( eprintln!("begin auth failed: {e:?}"); err(StatusCode::INTERNAL_SERVER_ERROR, "unknown") } + Ok(auth_url) => { + metrics::counter!("whoami_auth_start", "ok" => "true").increment(1); + (jar, Redirect::to(&auth_url)).into_response() + } } } @@ -331,7 +345,9 @@ async fn complete_oauth( Query(params): Query, jar: SignedCookieJar, ) -> Response { - let err = |code, result, reason| { + let err = |code, result, reason: &str| { + metrics::counter!("whoami_auth_complete", "ok" => "false", "reason" => reason.to_string()) + .increment(1); let info = json!({ "result": result, "reason": reason, @@ -378,6 +394,8 @@ async fn complete_oauth( }, shutdown.child_token(), ); + + metrics::counter!("whoami_auth_complete", "ok" => "true").increment(1); let info = json!({ "did": did, "fetch_key": fetch_key, @@ -386,6 +404,7 @@ async fn complete_oauth( } async fn disconnect(jar: SignedCookieJar) -> impl IntoResponse { + metrics::counter!("whoami_disconnect").increment(1); let jar = jar.remove(DID_COOKIE_KEY); (jar, Json(json!({ "ok": true }))) }