From b255f8c1c82d4cfbe88f71d1e9d3e055c47bd258 Mon Sep 17 00:00:00 2001 From: Evan Schwartz <3262610+emschwartz@users.noreply.github.com> Date: Fri, 21 Apr 2023 15:58:16 +0200 Subject: [PATCH 01/16] [WIP] --- autometrics/Cargo.toml | 4 ++ autometrics/src/labels.rs | 62 +++++++++++++++++--- autometrics/src/lib.rs | 6 +- autometrics/src/objectives.rs | 25 ++++++++ autometrics/src/tracker/mod.rs | 2 + autometrics/src/tracker/prometheus_client.rs | 60 +++++++++++++++++++ 6 files changed, 150 insertions(+), 9 deletions(-) create mode 100644 autometrics/src/tracker/prometheus_client.rs diff --git a/autometrics/Cargo.toml b/autometrics/Cargo.toml index 6af6658b..4b9272f6 100644 --- a/autometrics/Cargo.toml +++ b/autometrics/Cargo.toml @@ -16,6 +16,7 @@ default = ["opentelemetry"] metrics = ["dep:metrics"] opentelemetry = ["opentelemetry_api"] prometheus = ["const_format", "dep:prometheus", "once_cell"] +prometheus-client = ["dep:prometheus-client", "once_cell"] prometheus-exporter = [ "metrics-exporter-prometheus", "once_cell", @@ -45,6 +46,9 @@ prometheus = { version = "0.13", default-features = false, optional = true } # Used for prometheus feature const_format = { version = "0.2", features = ["rust_1_51"], optional = true } +# Used for prometheus-client feature +prometheus-client = { version = "0.20", optional = true } + [dev-dependencies] regex = "1.7" http = "0.2" diff --git a/autometrics/src/labels.rs b/autometrics/src/labels.rs index 56087d9d..5bc63845 100644 --- a/autometrics/src/labels.rs +++ b/autometrics/src/labels.rs @@ -1,10 +1,19 @@ use crate::{constants::*, objectives::*}; +use prometheus_client::encoding::EncodeLabelKey; +#[cfg(feature = "prometheus-client")] +use prometheus_client::encoding::{ + EncodeLabelSet, EncodeLabelValue, LabelSetEncoder, LabelValueEncoder, +}; use std::ops::Deref; pub(crate) type Label = (&'static str, &'static str); type ResultAndReturnTypeLabels = (&'static str, Option<&'static str>); /// These are the labels used for the `build_info` metric. +#[cfg_attr( + feature = "prometheus-client", + derive(EncodeLabelSet, Debug, Clone, PartialEq, Eq, Hash) +)] pub struct BuildInfoLabels { pub(crate) version: &'static str, pub(crate) commit: &'static str, @@ -21,12 +30,38 @@ impl BuildInfoLabels { } /// These are the labels used for the `function.calls.count` metric. +#[cfg_attr( + feature = "prometheus-client", + derive(EncodeLabelSet, Debug, Clone, PartialEq, Eq, Hash) +)] pub struct CounterLabels { pub(crate) function: &'static str, pub(crate) module: &'static str, pub(crate) caller: &'static str, - pub(crate) result: Option, - pub(crate) objective: Option<(&'static str, ObjectivePercentile)>, + pub(crate) result: Option, + pub(crate) ok: Option<&'static str>, + pub(crate) error: Option<&'static str>, + pub(crate) objective_name: Option<&'static str>, + pub(crate) objective_percentile: Option, +} + +#[cfg_attr( + feature = "prometheus-client", + derive(Debug, Clone, PartialEq, Eq, Hash) +)] +pub(crate) enum ResultLabel { + Ok, + Error, +} + +#[cfg(feature = "prometheus-client")] +impl EncodeLabelValue for ResultLabel { + fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> { + match self { + ResultLabel::Ok => OK_KEY.encode(encoder), + ResultLabel::Error => ERROR_KEY.encode(encoder), + } + } } impl CounterLabels { @@ -37,21 +72,22 @@ impl CounterLabels { result: Option, objective: Option, ) -> Self { - let objective = if let Some(objective) = objective { + let (objective_name, objective_percentile) = if let Some(objective) = objective { if let Some(success_rate) = objective.success_rate { - Some((objective.name, success_rate)) + (objective.name, success_rate) } else { - None + ("", "") } } else { - None + ("", "") }; Self { function, module, caller, + objective_name, + objective_percentile, result, - objective, } } @@ -77,11 +113,17 @@ impl CounterLabels { } /// These are the labels used for the `function.calls.duration` metric. +#[cfg_attr( + feature = "prometheus-client", + derive(EncodeLabelSet, Debug, Clone, PartialEq, Eq, Hash) +)] pub struct HistogramLabels { pub function: &'static str, pub module: &'static str, /// The SLO name, objective percentile, and latency threshold - pub objective: Option<(&'static str, ObjectivePercentile, ObjectiveLatency)>, + pub objective_name: &'static str, + pub objective_percentile: ObjectivePercentile, + pub objective_latency_threshold: ObjectiveLatency, } impl HistogramLabels { @@ -117,6 +159,10 @@ impl HistogramLabels { } /// These are the labels used for the `function.calls.concurrent` metric. +#[cfg_attr( + feature = "prometheus-client", + derive(EncodeLabelSet, Debug, Clone, PartialEq, Eq, Hash) +)] pub struct GaugeLabels { pub function: &'static str, pub module: &'static str, diff --git a/autometrics/src/lib.rs b/autometrics/src/lib.rs index d786b8da..25744ce2 100644 --- a/autometrics/src/lib.rs +++ b/autometrics/src/lib.rs @@ -140,7 +140,11 @@ pub use self::prometheus_exporter::*; /// We use the histogram buckets recommended by the OpenTelemetry specification /// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#explicit-bucket-histogram-aggregation -#[cfg(any(feature = "prometheus", feature = "prometheus-exporter"))] +#[cfg(any( + feature = "prometheus", + feature = "prometheus-client", + feature = "prometheus-exporter" +))] pub(crate) const HISTOGRAM_BUCKETS: [f64; 14] = [ 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0, ]; diff --git a/autometrics/src/objectives.rs b/autometrics/src/objectives.rs index 4b45d592..53362725 100644 --- a/autometrics/src/objectives.rs +++ b/autometrics/src/objectives.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "prometheus-client")] +use prometheus_client::encoding::{EncodeLabelValue, LabelValueEncoder}; + /// This represents a Service-Level Objective (SLO) for a function or group of functions. /// The objective should be given a descriptive name and can represent /// a success rate and/or latency objective. @@ -90,6 +93,10 @@ impl Objective { } /// The percentage of requests that must meet the given criteria (success rate or latency). +#[cfg_attr( + feature = "prometheus-client", + derive(Clone, Debug, PartialEq, Eq, Hash) +)] #[non_exhaustive] pub enum ObjectivePercentile { /// 90% @@ -123,7 +130,18 @@ impl ObjectivePercentile { } } +#[cfg(feature = "prometheus-client")] +impl EncodeLabelValue for ObjectivePercentile { + fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> { + self.as_str().encode(encoder) + } +} + /// The latency threshold, in milliseoncds, for a given objective. +#[cfg_attr( + feature = "prometheus-client", + derive(Clone, Debug, PartialEq, Eq, Hash) +)] #[non_exhaustive] pub enum ObjectiveLatency { /// 5 milliseconds @@ -203,3 +221,10 @@ impl ObjectiveLatency { } } } + +#[cfg(feature = "prometheus-client")] +impl EncodeLabelValue for ObjectiveLatency { + fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> { + self.as_str().encode(encoder) + } +} diff --git a/autometrics/src/tracker/mod.rs b/autometrics/src/tracker/mod.rs index e5211e3f..a53f0764 100644 --- a/autometrics/src/tracker/mod.rs +++ b/autometrics/src/tracker/mod.rs @@ -6,6 +6,8 @@ mod metrics; mod opentelemetry; #[cfg(feature = "prometheus")] mod prometheus; +#[cfg(feature = "prometheus-client")] +mod prometheus_client; // By default, use the opentelemetry crate #[cfg(all( diff --git a/autometrics/src/tracker/prometheus_client.rs b/autometrics/src/tracker/prometheus_client.rs new file mode 100644 index 00000000..807e7896 --- /dev/null +++ b/autometrics/src/tracker/prometheus_client.rs @@ -0,0 +1,60 @@ +use super::TrackMetrics; +use crate::labels::{BuildInfoLabels, CounterLabels, GaugeLabels, HistogramLabels}; +use crate::{constants::*, HISTOGRAM_BUCKETS}; +use once_cell::sync::Lazy; +use prometheus_client::metrics::{ + counter::Counter, family::Family, gauge::Gauge, histogram::Histogram, +}; +use prometheus_client::registry::Registry; + +pub static PROMETHEUS_CLIENT_REGISTRY: Lazy = Lazy::new(|| ::default()); + +static METRICS: Lazy = Lazy::new(|| { + let counter = Family::::default(); + PROMETHEUS_CLIENT_REGISTRY.register(COUNTER_NAME, COUNTER_DESCRIPTION, counter.clone()); + + let histogram = Family::::new_with_constructor(|| { + Histogram::new(HISTOGRAM_BUCKETS.into_iter()) + }); + PROMETHEUS_CLIENT_REGISTRY.register(HISTOGRAM_NAME, HISTOGRAM_DESCRIPTION, histogram.clone()); + + let gauge = Family::::default(); + PROMETHEUS_CLIENT_REGISTRY.register(GAUGE_NAME, GAUGE_DESCRIPTION, gauge.clone()); + + let build_info = Family::::default(); + PROMETHEUS_CLIENT_REGISTRY.register( + BUILD_INFO_NAME, + BUILD_INFO_DESCRIPTION, + build_info.clone(), + ); + + Metrics { + counter, + histogram, + gauge, + build_info, + } +}); + +struct Metrics { + counter: Family, + histogram: Family, + gauge: Family, + build_info: Family, +} + +struct PrometheusClientTracker {} + +impl TrackMetrics for PrometheusClientTracker { + fn set_build_info(build_info_labels: &BuildInfoLabels) { + todo!() + } + + fn start(gauge_labels: Option<&GaugeLabels>) -> Self { + todo!() + } + + fn finish(self, counter_labels: &CounterLabels, histogram_labels: &HistogramLabels) { + todo!() + } +} From 7cbe05323782dc72f19b3bad9ea52489e069d716 Mon Sep 17 00:00:00 2001 From: Evan Schwartz <3262610+emschwartz@users.noreply.github.com> Date: Tue, 16 May 2023 15:44:12 +0200 Subject: [PATCH 02/16] Update prometheus-client version --- autometrics/Cargo.toml | 2 +- autometrics/src/labels.rs | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/autometrics/Cargo.toml b/autometrics/Cargo.toml index 4b9272f6..efd89955 100644 --- a/autometrics/Cargo.toml +++ b/autometrics/Cargo.toml @@ -47,7 +47,7 @@ prometheus = { version = "0.13", default-features = false, optional = true } const_format = { version = "0.2", features = ["rust_1_51"], optional = true } # Used for prometheus-client feature -prometheus-client = { version = "0.20", optional = true } +prometheus-client = { version = "0.21.1", optional = true } [dev-dependencies] regex = "1.7" diff --git a/autometrics/src/labels.rs b/autometrics/src/labels.rs index 5bc63845..17351d48 100644 --- a/autometrics/src/labels.rs +++ b/autometrics/src/labels.rs @@ -1,8 +1,7 @@ use crate::{constants::*, objectives::*}; -use prometheus_client::encoding::EncodeLabelKey; #[cfg(feature = "prometheus-client")] use prometheus_client::encoding::{ - EncodeLabelSet, EncodeLabelValue, LabelSetEncoder, LabelValueEncoder, + EncodeLabelKey, EncodeLabelSet, EncodeLabelValue, LabelValueEncoder, }; use std::ops::Deref; @@ -58,8 +57,8 @@ pub(crate) enum ResultLabel { impl EncodeLabelValue for ResultLabel { fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> { match self { - ResultLabel::Ok => OK_KEY.encode(encoder), - ResultLabel::Error => ERROR_KEY.encode(encoder), + ResultLabel::Ok => EncodeLabelValue::encode(&OK_KEY, encoder), + ResultLabel::Error => EncodeLabelValue::encode(&ERROR_KEY, encoder), } } } @@ -120,7 +119,6 @@ impl CounterLabels { pub struct HistogramLabels { pub function: &'static str, pub module: &'static str, - /// The SLO name, objective percentile, and latency threshold pub objective_name: &'static str, pub objective_percentile: ObjectivePercentile, pub objective_latency_threshold: ObjectiveLatency, From 9e402eb41339d7dc00d981dc809dbbd735b74507 Mon Sep 17 00:00:00 2001 From: Evan Schwartz <3262610+emschwartz@users.noreply.github.com> Date: Tue, 16 May 2023 17:10:37 +0200 Subject: [PATCH 03/16] Finish support for prometheus-client --- .github/workflows/ci.yml | 9 +- autometrics/src/labels.rs | 93 +++++++++++++------- autometrics/src/lib.rs | 3 + autometrics/src/prometheus_exporter.rs | 15 ++++ autometrics/src/tracker/metrics.rs | 2 +- autometrics/src/tracker/mod.rs | 2 +- autometrics/src/tracker/prometheus.rs | 48 ++++------ autometrics/src/tracker/prometheus_client.rs | 76 +++++++++++----- 8 files changed, 161 insertions(+), 87 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68452cf4..946345fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,14 +17,15 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - # Build the packages using the different feature flags + # Build the packages - run: cargo build - - run: cargo build --features=metrics - - run: cargo build --features=prometheus - run: cargo build --features=custom-objective-percentile,custom-objective-latency - # Run the tests + # Run the tests with each of the different metrics libraries - run: cargo test --features=prometheus-exporter + - run: cargo test --no-default-features --features=prometheus-exporter,metrics --tests + - run: cargo test --no-default-features --features=prometheus-exporter,prometheus --tests + - run: cargo test --no-default-features --features=prometheus-exporter,prometheus-client --tests # Compile the examples - run: cargo build --package example-axum diff --git a/autometrics/src/labels.rs b/autometrics/src/labels.rs index 3087d55a..8c0f05e8 100644 --- a/autometrics/src/labels.rs +++ b/autometrics/src/labels.rs @@ -1,8 +1,6 @@ use crate::{constants::*, objectives::*}; #[cfg(feature = "prometheus-client")] -use prometheus_client::encoding::{ - EncodeLabelKey, EncodeLabelSet, EncodeLabelValue, LabelValueEncoder, -}; +use prometheus_client::encoding::{EncodeLabelSet, EncodeLabelValue, LabelValueEncoder}; use std::ops::Deref; pub(crate) type Label = (&'static str, &'static str); @@ -62,6 +60,15 @@ pub(crate) enum ResultLabel { Error, } +impl ResultLabel { + pub(crate) const fn as_str(&self) -> &'static str { + match self { + ResultLabel::Ok => OK_KEY, + ResultLabel::Error => ERROR_KEY, + } + } +} + #[cfg(feature = "prometheus-client")] impl EncodeLabelValue for ResultLabel { fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> { @@ -82,12 +89,21 @@ impl CounterLabels { ) -> Self { let (objective_name, objective_percentile) = if let Some(objective) = objective { if let Some(success_rate) = objective.success_rate { - (objective.name, success_rate) + (Some(objective.name), Some(success_rate)) } else { - ("", "") + (None, None) } } else { - ("", "") + (None, None) + }; + let (result, ok, error) = if let Some((result, return_value_type)) = result { + match result { + OK_KEY => (Some(ResultLabel::Ok), return_value_type, None), + ERROR_KEY => (Some(ResultLabel::Error), None, return_value_type), + _ => (None, None, None), + } + } else { + (None, None, None) }; Self { function, @@ -96,6 +112,8 @@ impl CounterLabels { objective_name, objective_percentile, result, + ok, + error, } } @@ -105,15 +123,20 @@ impl CounterLabels { (MODULE_KEY, self.module), (CALLER_KEY, self.caller), ]; - if let Some((result, return_value_type)) = self.result { - labels.push((RESULT_KEY, result)); - if let Some(return_value_type) = return_value_type { - labels.push((result, return_value_type)); - } + if let Some(result) = &self.result { + labels.push((RESULT_KEY, result.as_str())); + } + if let Some(ok) = self.ok { + labels.push((OK_KEY, ok)); + } + if let Some(error) = self.error { + labels.push((ERROR_KEY, error)); } - if let Some((name, percentile)) = &self.objective { - labels.push((OBJECTIVE_NAME, name)); - labels.push((OBJECTIVE_PERCENTILE, percentile.as_str())); + if let Some(objective_name) = self.objective_name { + labels.push((OBJECTIVE_NAME, objective_name)); + } + if let Some(objective_percentile) = &self.objective_percentile { + labels.push((OBJECTIVE_PERCENTILE, objective_percentile.as_str())); } labels @@ -128,37 +151,47 @@ impl CounterLabels { pub struct HistogramLabels { pub function: &'static str, pub module: &'static str, - pub objective_name: &'static str, - pub objective_percentile: ObjectivePercentile, - pub objective_latency_threshold: ObjectiveLatency, + pub objective_name: Option<&'static str>, + pub objective_percentile: Option, + pub objective_latency_threshold: Option, } impl HistogramLabels { pub fn new(function: &'static str, module: &'static str, objective: Option) -> Self { - let objective = if let Some(objective) = objective { - if let Some((latency, percentile)) = objective.latency { - Some((objective.name, percentile, latency)) + let (objective_name, objective_percentile, objective_latency_threshold) = + if let Some(objective) = objective { + if let Some((latency, percentile)) = objective.latency { + (Some(objective.name), Some(percentile), Some(latency)) + } else { + (None, None, None) + } } else { - None - } - } else { - None - }; + (None, None, None) + }; Self { function, module, - objective, + objective_name, + objective_percentile, + objective_latency_threshold, } } pub fn to_vec(&self) -> Vec