From 3768c4c2e56e5742fd517f8ba71bbc53087bdb18 Mon Sep 17 00:00:00 2001 From: Darksome Date: Thu, 13 Jun 2024 10:20:29 +0000 Subject: [PATCH 1/8] feat: future_metrics --- crates/future_metrics/Cargo.toml | 8 ++ crates/future_metrics/src/lib.rs | 194 +++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 crates/future_metrics/Cargo.toml create mode 100644 crates/future_metrics/src/lib.rs diff --git a/crates/future_metrics/Cargo.toml b/crates/future_metrics/Cargo.toml new file mode 100644 index 0000000..710a22b --- /dev/null +++ b/crates/future_metrics/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "future_metrics" +version = "0.1.0" +edition = "2021" + +[dependencies] +pin-project = "1" +metrics = "0.23" diff --git a/crates/future_metrics/src/lib.rs b/crates/future_metrics/src/lib.rs new file mode 100644 index 0000000..b8bfe59 --- /dev/null +++ b/crates/future_metrics/src/lib.rs @@ -0,0 +1,194 @@ +use { + metrics::{Counter, Histogram, Key, Label, Level, Metadata}, + std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, + time::{Duration, Instant}, + }, +}; + +/// Target specified in [`metrics::Metadata`] for all metrics produced by this +/// crate. +pub const METADATA_TARGET: &'static str = "future_metrics"; + +/// Metric names used by this crate. +pub mod metric_name { + pub const FUTURE_DURATION: &'static str = "future_duration"; + + pub const FUTURES_CREATED: &'static str = "futures_created"; + pub const FUTURES_STARTED: &'static str = "futures_started"; + pub const FUTURES_FINISHED: &'static str = "futures_finished"; + pub const FUTURES_CANCELLED: &'static str = "futures_cancelled"; + + pub const FUTURE_POLL_DURATION: &'static str = "future_poll_duration"; + pub const FUTURE_POLL_DURATION_TOTAL: &'static str = "future_poll_duration_total"; +} + +/// Creates a new label identifying a future by its name. +pub const fn future_name(s: &'static str) -> Label { + label("future_name", s) +} + +/// Creates a new static [`Label`]. +pub const fn label(key: &'static str, value: &'static str) -> Label { + Label::from_static_parts(key, value) +} + +pub trait FutureExt: Sized { + /// Consumes the future, returning a new future that records the executiion + /// metrics of the inner future. + /// + /// It is expected that you provide at least one label identifying the + /// future being metered. + /// Consider using [`future_name`] label, or the [`FutureExt::with_metrics`] + /// shortcut. + fn with_labeled_metrics(self, labels: &'static [Label]) -> Metered { + Metered::new(self, labels) + } + + /// A shortcut for [`FutureExt::with_labeled_metrics`] using a single label + /// only (presumably [`future_name`]). + fn with_metrics(self, label: &'static Label) -> Metered { + self.with_labeled_metrics(std::slice::from_ref(label)) + } +} + +impl FutureExt for F where F: Future {} + +#[pin_project::pin_project] +#[must_use = "futures do nothing unless you `.await` or poll them"] +pub struct Metered { + #[pin] + future: F, + state: State, +} + +struct State { + started_at: Option, + is_finished: bool, + + poll_duration_total: Duration, + + metrics: Metrics, +} + +impl Metered { + fn new(future: F, metric_labels: &'static [Label]) -> Self { + let metrics = Metrics::new(metric_labels); + + metrics.created.increment(1); + + Self { + future, + state: State { + started_at: None, + is_finished: false, + poll_duration_total: Duration::from_secs(0), + metrics, + }, + } + } +} + +impl Future for Metered { + type Output = F::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let mut this = self.project(); + let s = &mut this.state; + + if s.started_at.is_none() { + s.started_at = Some(Instant::now()); + s.metrics.started.increment(1); + } + + let poll_started_at = Instant::now(); + let result = this.future.poll(cx); + let poll_duration = poll_started_at.elapsed(); + + s.metrics.poll_duration.record(poll_duration); + s.poll_duration_total += poll_duration; + + if result.is_ready() && !s.is_finished { + s.is_finished = true; + s.metrics.finished.increment(1); + s.metrics + .duration + .record(s.started_at.unwrap_or(poll_started_at).elapsed()) + } + + result + } +} + +impl Drop for State { + fn drop(&mut self) { + if !self.is_finished { + self.metrics.cancelled.increment(1); + if let Some(started_at) = self.started_at { + self.metrics.duration.record(started_at.elapsed()) + } + } + + self.metrics + .poll_duration_total + .record(duration_as_millis_f64(self.poll_duration_total)); + } +} + +struct Metrics { + duration: Histogram, + + created: Counter, + started: Counter, + finished: Counter, + cancelled: Counter, + + poll_duration: Histogram, + poll_duration_total: Histogram, +} + +impl Metrics { + fn new(labels: &'static [Label]) -> Self { + metrics::with_recorder(|r| { + let metadata = Metadata::new(METADATA_TARGET, Level::INFO, None); + + Self { + duration: r.register_histogram( + &Key::from_static_parts(metric_name::FUTURE_DURATION, labels), + &metadata, + ), + created: r.register_counter( + &Key::from_static_parts(metric_name::FUTURES_CREATED, labels), + &metadata, + ), + started: r.register_counter( + &Key::from_static_parts(metric_name::FUTURES_STARTED, labels), + &metadata, + ), + finished: r.register_counter( + &Key::from_static_parts(metric_name::FUTURES_FINISHED, labels), + &metadata, + ), + cancelled: r.register_counter( + &Key::from_static_parts(metric_name::FUTURES_CANCELLED, labels), + &metadata, + ), + poll_duration: r.register_histogram( + &Key::from_static_parts(metric_name::FUTURE_POLL_DURATION, labels), + &metadata, + ), + poll_duration_total: r.register_histogram( + &Key::from_static_parts(metric_name::FUTURE_POLL_DURATION_TOTAL, labels), + &metadata, + ), + } + }) + } +} + +#[inline] +pub fn duration_as_millis_f64(val: Duration) -> f64 { + val.as_secs_f64() * 1000.0 +} From 99baf63b14454d6ffbf08ea5a86684e8b6d84578 Mon Sep 17 00:00:00 2001 From: Darksome Date: Thu, 13 Jun 2024 10:38:02 +0000 Subject: [PATCH 2/8] fix: ci --- .github/workflows/ci.yaml | 1 + crates/future_metrics/src/lib.rs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2333a07..2965f04 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -88,6 +88,7 @@ jobs: with: github_token: ${{ secrets.github_token }} locale: "US" + ignore: cancelled cocogitto: name: cocogitto diff --git a/crates/future_metrics/src/lib.rs b/crates/future_metrics/src/lib.rs index b8bfe59..79d310c 100644 --- a/crates/future_metrics/src/lib.rs +++ b/crates/future_metrics/src/lib.rs @@ -10,19 +10,19 @@ use { /// Target specified in [`metrics::Metadata`] for all metrics produced by this /// crate. -pub const METADATA_TARGET: &'static str = "future_metrics"; +pub const METADATA_TARGET: &str = "future_metrics"; /// Metric names used by this crate. pub mod metric_name { - pub const FUTURE_DURATION: &'static str = "future_duration"; + pub const FUTURE_DURATION: &str = "future_duration"; - pub const FUTURES_CREATED: &'static str = "futures_created"; - pub const FUTURES_STARTED: &'static str = "futures_started"; - pub const FUTURES_FINISHED: &'static str = "futures_finished"; - pub const FUTURES_CANCELLED: &'static str = "futures_cancelled"; + pub const FUTURES_CREATED: &str = "futures_created"; + pub const FUTURES_STARTED: &str = "futures_started"; + pub const FUTURES_FINISHED: &str = "futures_finished"; + pub const FUTURES_CANCELLED: &str = "futures_cancelled"; - pub const FUTURE_POLL_DURATION: &'static str = "future_poll_duration"; - pub const FUTURE_POLL_DURATION_TOTAL: &'static str = "future_poll_duration_total"; + pub const FUTURE_POLL_DURATION: &str = "future_poll_duration"; + pub const FUTURE_POLL_DURATION_TOTAL: &str = "future_poll_duration_total"; } /// Creates a new label identifying a future by its name. From 0a429e32490c9fb1ebf2597d2952198b76429a4e Mon Sep 17 00:00:00 2001 From: Darksome Date: Thu, 13 Jun 2024 10:41:34 +0000 Subject: [PATCH 3/8] fix: re-export --- Cargo.toml | 2 ++ src/lib.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 04f0b92..7a903d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ geoblock = ["geoip/middleware"] geoip = ["dep:geoip"] http = [] metrics = ["dep:metrics", "future/metrics", "alloc/metrics", "http/metrics"] +future_metrics = ["dep:future_metrics"] profiler = ["alloc/profiler"] rate_limit = ["dep:rate_limit"] @@ -47,6 +48,7 @@ future = { path = "./crates/future", optional = true } geoip = { path = "./crates/geoip", optional = true } http = { path = "./crates/http", optional = true } metrics = { path = "./crates/metrics", optional = true } +future_metrics = { path = "./crates/future_metrics", optional = true } rate_limit = { path = "./crates/rate_limit", optional = true } [dev-dependencies] diff --git a/src/lib.rs b/src/lib.rs index a16c71a..a9204a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,8 @@ pub use analytics; pub use collections; #[cfg(feature = "future")] pub use future; +#[cfg(feature = "future_metrics")] +pub use future_metrics; #[cfg(feature = "geoip")] pub use geoip; #[cfg(feature = "http")] From d99d6b24cefba0eb3aa7897d8b3a446a38087a78 Mon Sep 17 00:00:00 2001 From: Darksome Date: Thu, 13 Jun 2024 15:45:37 +0000 Subject: [PATCH 4/8] fix: remove unused fn --- crates/future_metrics/src/lib.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/future_metrics/src/lib.rs b/crates/future_metrics/src/lib.rs index 79d310c..940f460 100644 --- a/crates/future_metrics/src/lib.rs +++ b/crates/future_metrics/src/lib.rs @@ -27,12 +27,7 @@ pub mod metric_name { /// Creates a new label identifying a future by its name. pub const fn future_name(s: &'static str) -> Label { - label("future_name", s) -} - -/// Creates a new static [`Label`]. -pub const fn label(key: &'static str, value: &'static str) -> Label { - Label::from_static_parts(key, value) + Label::from_static_parts("future_name", s) } pub trait FutureExt: Sized { From 0890893b2d6d6d63cb27547ffed3c203ea0b57ed Mon Sep 17 00:00:00 2001 From: Darksome Date: Mon, 17 Jun 2024 11:20:02 +0000 Subject: [PATCH 5/8] fix: remove reviewdog locale --- .github/workflows/ci.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2965f04..5f41a4e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -87,8 +87,6 @@ jobs: uses: reviewdog/action-misspell@v1 with: github_token: ${{ secrets.github_token }} - locale: "US" - ignore: cancelled cocogitto: name: cocogitto From 7a7d414dd5098a0ab9e0732ef8bea4828bbab5e7 Mon Sep 17 00:00:00 2001 From: Darksome Date: Mon, 17 Jun 2024 12:07:42 +0000 Subject: [PATCH 6/8] fix: optimize max poll duration calculation --- crates/future_metrics/src/lib.rs | 76 +++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 26 deletions(-) diff --git a/crates/future_metrics/src/lib.rs b/crates/future_metrics/src/lib.rs index 940f460..fe4675e 100644 --- a/crates/future_metrics/src/lib.rs +++ b/crates/future_metrics/src/lib.rs @@ -1,5 +1,5 @@ use { - metrics::{Counter, Histogram, Key, Label, Level, Metadata}, + metrics::{Counter, Gauge, Histogram, Key, Label, Level, Metadata}, std::{ future::Future, pin::Pin, @@ -15,14 +15,16 @@ pub const METADATA_TARGET: &str = "future_metrics"; /// Metric names used by this crate. pub mod metric_name { pub const FUTURE_DURATION: &str = "future_duration"; + pub const FUTURE_CANCELLED_DURATION: &str = "future_cancelled_duration"; - pub const FUTURES_CREATED: &str = "futures_created"; - pub const FUTURES_STARTED: &str = "futures_started"; - pub const FUTURES_FINISHED: &str = "futures_finished"; - pub const FUTURES_CANCELLED: &str = "futures_cancelled"; + pub const FUTURES_CREATED: &str = "futures_created_count"; + pub const FUTURES_STARTED: &str = "futures_started_count"; + pub const FUTURES_FINISHED: &str = "futures_finished_count"; + pub const FUTURES_CANCELLED: &str = "futures_cancelled_count"; pub const FUTURE_POLL_DURATION: &str = "future_poll_duration"; - pub const FUTURE_POLL_DURATION_TOTAL: &str = "future_poll_duration_total"; + pub const FUTURE_POLL_DURATION_MAX: &str = "future_poll_duration_max"; + pub const FUTURE_POLLS: &str = "future_polls_count"; } /// Creates a new label identifying a future by its name. @@ -63,7 +65,9 @@ struct State { started_at: Option, is_finished: bool, - poll_duration_total: Duration, + poll_duration_sum: Duration, + poll_duration_max: Duration, + polls_count: usize, metrics: Metrics, } @@ -79,7 +83,9 @@ impl Metered { state: State { started_at: None, is_finished: false, - poll_duration_total: Duration::from_secs(0), + poll_duration_sum: Duration::from_secs(0), + poll_duration_max: Duration::from_secs(0), + polls_count: 0, metrics, }, } @@ -91,26 +97,27 @@ impl Future for Metered { fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { let mut this = self.project(); - let s = &mut this.state; + let state = &mut this.state; - if s.started_at.is_none() { - s.started_at = Some(Instant::now()); - s.metrics.started.increment(1); + if state.started_at.is_none() { + state.started_at = Some(Instant::now()); + state.metrics.started.increment(1); } let poll_started_at = Instant::now(); let result = this.future.poll(cx); let poll_duration = poll_started_at.elapsed(); - s.metrics.poll_duration.record(poll_duration); - s.poll_duration_total += poll_duration; + state.poll_duration_sum += poll_duration; + state.poll_duration_sum = state.poll_duration_max.max(poll_duration); + state.polls_count += 1; - if result.is_ready() && !s.is_finished { - s.is_finished = true; - s.metrics.finished.increment(1); - s.metrics - .duration - .record(s.started_at.unwrap_or(poll_started_at).elapsed()) + if !state.is_finished { + state.metrics.finished.increment(1); + + if let Some(started_at) = state.started_at { + state.metrics.duration.record(started_at.elapsed()) + } } result @@ -121,19 +128,27 @@ impl Drop for State { fn drop(&mut self) { if !self.is_finished { self.metrics.cancelled.increment(1); + if let Some(started_at) = self.started_at { - self.metrics.duration.record(started_at.elapsed()) + self.metrics.cancelled_duration.record(started_at.elapsed()) } } self.metrics - .poll_duration_total - .record(duration_as_millis_f64(self.poll_duration_total)); + .poll_duration + .record(duration_as_millis_f64(self.poll_duration_sum)); + + self.metrics + .poll_duration_max + .set(duration_as_millis_f64(self.poll_duration_max)); + + self.metrics.polls.increment(self.polls_count as u64); } } struct Metrics { duration: Histogram, + cancelled_duration: Histogram, created: Counter, started: Counter, @@ -141,7 +156,8 @@ struct Metrics { cancelled: Counter, poll_duration: Histogram, - poll_duration_total: Histogram, + poll_duration_max: Gauge, + polls: Counter, } impl Metrics { @@ -154,6 +170,10 @@ impl Metrics { &Key::from_static_parts(metric_name::FUTURE_DURATION, labels), &metadata, ), + cancelled_duration: r.register_histogram( + &Key::from_static_parts(metric_name::FUTURE_CANCELLED_DURATION, labels), + &metadata, + ), created: r.register_counter( &Key::from_static_parts(metric_name::FUTURES_CREATED, labels), &metadata, @@ -174,8 +194,12 @@ impl Metrics { &Key::from_static_parts(metric_name::FUTURE_POLL_DURATION, labels), &metadata, ), - poll_duration_total: r.register_histogram( - &Key::from_static_parts(metric_name::FUTURE_POLL_DURATION_TOTAL, labels), + poll_duration_max: r.register_gauge( + &Key::from_static_parts(metric_name::FUTURE_POLL_DURATION_MAX, labels), + &metadata, + ), + polls: r.register_counter( + &Key::from_static_parts(metric_name::FUTURE_POLLS, labels), &metadata, ), } From fa747a81fa2909bdd5f820e22bfd18a0b5ab8eae Mon Sep 17 00:00:00 2001 From: Darksome Date: Mon, 17 Jun 2024 12:59:42 +0000 Subject: [PATCH 7/8] fix: copy-pasta --- crates/future_metrics/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/future_metrics/src/lib.rs b/crates/future_metrics/src/lib.rs index fe4675e..66295b3 100644 --- a/crates/future_metrics/src/lib.rs +++ b/crates/future_metrics/src/lib.rs @@ -109,10 +109,10 @@ impl Future for Metered { let poll_duration = poll_started_at.elapsed(); state.poll_duration_sum += poll_duration; - state.poll_duration_sum = state.poll_duration_max.max(poll_duration); + state.poll_duration_max = state.poll_duration_max.max(poll_duration); state.polls_count += 1; - if !state.is_finished { + if result.is_ready() && !state.is_finished { state.metrics.finished.increment(1); if let Some(started_at) = state.started_at { From 9f22dc2a5f675816648dd1d0860874d2360a7ba4 Mon Sep 17 00:00:00 2001 From: Darksome Date: Mon, 17 Jun 2024 13:29:29 +0000 Subject: [PATCH 8/8] fix: set is_finished --- crates/future_metrics/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/future_metrics/src/lib.rs b/crates/future_metrics/src/lib.rs index 66295b3..a2db759 100644 --- a/crates/future_metrics/src/lib.rs +++ b/crates/future_metrics/src/lib.rs @@ -113,6 +113,8 @@ impl Future for Metered { state.polls_count += 1; if result.is_ready() && !state.is_finished { + state.is_finished = true; + state.metrics.finished.increment(1); if let Some(started_at) = state.started_at {