diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8d578d8..07a7ef29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,18 +18,21 @@ jobs: - uses: Swatinem/rust-cache@v2 # Lint - - name: Run Clippy - # GitHub hosted runners using the latest stable version of Rust have Clippy pre-installed. - run: cargo clippy --all-targets --features=prometheus-exporter,opentelemetry,metrics,prometheus + # Note: GitHub hosted runners using the latest stable version of Rust have Clippy pre-installed. + - run: cargo clippy --features=metrics,prometheus-exporter + - run: cargo clippy --features=prometheus + - run: cargo clippy --features=prometheus-client + - run: cargo clippy --features=opentelemetry # 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/CHANGELOG.md b/CHANGELOG.md index d96d63dc..2322818a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `ResultLabels` derive macro allows to specify on an enum whether variants should always be "ok", or "error" for the success rate metrics of functions using them. (#61) +- Support the official `prometheus-client` crate for producing metrics ### Changed diff --git a/README.md b/README.md index 2136c485..0996e933 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ https://github.com/autometrics-dev/autometrics-rs/assets/3262610/966ed140-1d6c-4 - [🔍 Identify commits](https://docs.rs/autometrics/latest/autometrics/#identifying-commits-that-introduced-problems) that introduced errors or increased latency - [🚨 Define alerts](https://docs.rs/autometrics/latest/autometrics/objectives/index.html) using SLO best practices directly in your source code - [📊 Grafana dashboards](https://github.com/autometrics-dev#5-configuring-prometheus) work out of the box to visualize the performance of instrumented functions & SLOs -- [⚙️ Configurable](https://docs.rs/autometrics/latest/autometrics/#metrics-libraries) metric collection library ([`opentelemetry`](https://crates.io/crates/opentelemetry), [`prometheus`](https://crates.io/crates/prometheus), or [`metrics`](https://crates.io/crates/metrics)) +- [⚙️ Configurable](https://docs.rs/autometrics/latest/autometrics/#metrics-libraries) metric collection library ([`opentelemetry`](https://crates.io/crates/opentelemetry), [`prometheus`](https://crates.io/crates/prometheus), [`prometheus-client`](https://crates.io/crates/prometheus-client) or [`metrics`](https://crates.io/crates/metrics)) - ⚡ Minimal runtime overhead See [Why Autometrics?](https://github.com/autometrics-dev#4-why-autometrics) for more details on the ideas behind autometrics. @@ -156,7 +156,7 @@ See [Why Autometrics?](https://github.com/autometrics-dev#4-why-autometrics) for
- [Configure `autometrics`](https://docs.rs/autometrics/latest/autometrics/#metrics-libraries) to use the same underlying metrics library you use with the appropriate feature flag: `opentelemetry`, `prometheus`, or `metrics`. + [Configure `autometrics`](https://docs.rs/autometrics/latest/autometrics/#metrics-libraries) to use the same underlying metrics library you use with the appropriate feature flag: `opentelemetry`, `prometheus`, `prometheus-client`, or `metrics`. ```toml [dependencies] diff --git a/autometrics/Cargo.toml b/autometrics/Cargo.toml index 851a0c14..19090642 100644 --- a/autometrics/Cargo.toml +++ b/autometrics/Cargo.toml @@ -16,10 +16,10 @@ readme = "README.md" default = ["opentelemetry"] metrics = ["dep:metrics"] opentelemetry = ["opentelemetry_api"] -prometheus = ["const_format", "dep:prometheus", "once_cell"] +prometheus = ["dep:prometheus"] +prometheus-client = ["dep:prometheus-client"] prometheus-exporter = [ "metrics-exporter-prometheus", - "once_cell", "opentelemetry-prometheus", "opentelemetry_sdk", "prometheus" @@ -29,7 +29,8 @@ custom-objective-latency = [] [dependencies] autometrics-macros = { workspace = true } -spez = { version = "0.1.2" } +spez = "0.1.2" +once_cell = "1.17" # Used for opentelemetry feature opentelemetry_api = { version = "0.19.0", default-features = false, features = ["metrics"], optional = true } @@ -39,17 +40,15 @@ metrics = { version = "0.21", default-features = false, optional = true } # Used for prometheus-exporter feature metrics-exporter-prometheus = { version = "0.12", default-features = false, optional = true } -once_cell = { version = "1.17", optional = true } opentelemetry-prometheus = { version = "0.12.0", optional = true } opentelemetry_sdk = { version = "0.19", default-features = false, features = ["metrics"], optional = true } 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.21.1", optional = true } [dev-dependencies] axum = { version = "0.6", features = ["tokio"] } -regex = "1.7" http = "0.2" tokio = { version = "1", features = ["full"] } trybuild = "1.0" diff --git a/autometrics/README.md b/autometrics/README.md index c539606b..e56b11d7 100644 --- a/autometrics/README.md +++ b/autometrics/README.md @@ -53,7 +53,7 @@ pub async fn main() { - [🔍 Identify commits](#identifying-commits-that-introduced-problems) that introduced errors or increased latency - [🚨 Define alerts](https://docs.rs/autometrics/latest/autometrics/objectives/index.html) using SLO best practices directly in your source code - [📊 Grafana dashboards](https://github.com/autometrics-dev#5-configuring-prometheus) work out of the box to visualize the performance of instrumented functions & SLOs -- [⚙️ Configurable](#metrics-libraries) metric collection library ([`opentelemetry`](https://crates.io/crates/opentelemetry), [`prometheus`](https://crates.io/crates/prometheus), or [`metrics`](https://crates.io/crates/metrics)) +- [⚙️ Configurable](#metrics-libraries) metric collection library ([`opentelemetry`](https://crates.io/crates/opentelemetry), [`prometheus`](https://crates.io/crates/prometheus), [`prometheus-client`](https://crates.io/crates/prometheus-client) or [`metrics`](https://crates.io/crates/metrics)) - ⚡ Minimal runtime overhead See [Why Autometrics?](https://github.com/autometrics-dev#4-why-autometrics) for more details on the ideas behind autometrics. diff --git a/autometrics/src/constants.rs b/autometrics/src/constants.rs index 176fc23e..3311424d 100644 --- a/autometrics/src/constants.rs +++ b/autometrics/src/constants.rs @@ -4,6 +4,11 @@ pub const HISTOGRAM_NAME: &str = "function.calls.duration"; pub const GAUGE_NAME: &str = "function.calls.concurrent"; pub const BUILD_INFO_NAME: &str = "build_info"; +// Prometheus-flavored metric names +pub const COUNTER_NAME_PROMETHEUS: &str = "function_calls_count"; +pub const HISTOGRAM_NAME_PROMETHEUS: &str = "function_calls_duration"; +pub const GAUGE_NAME_PROMETHEUS: &str = "function_calls_concurrent"; + // Descriptions pub const COUNTER_DESCRIPTION: &str = "Autometrics counter for tracking function calls"; pub const HISTOGRAM_DESCRIPTION: &str = "Autometrics histogram for tracking function call duration"; diff --git a/autometrics/src/labels.rs b/autometrics/src/labels.rs index b27a2a3d..8462de0c 100644 --- a/autometrics/src/labels.rs +++ b/autometrics/src/labels.rs @@ -1,14 +1,20 @@ use crate::{constants::*, objectives::*}; +#[cfg(feature = "prometheus-client")] +use prometheus_client::encoding::{EncodeLabelSet, EncodeLabelValue, LabelValueEncoder}; use std::ops::Deref; pub(crate) type Label = (&'static str, &'static str); pub 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, pub(crate) branch: &'static str, + pub(crate) commit: &'static str, + pub(crate) version: &'static str, } impl BuildInfoLabels { @@ -30,12 +36,47 @@ 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, +} + +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> { + match self { + ResultLabel::Ok => EncodeLabelValue::encode(&OK_KEY, encoder), + ResultLabel::Error => EncodeLabelValue::encode(&ERROR_KEY, encoder), + } + } } impl CounterLabels { @@ -46,21 +87,33 @@ 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)) + (Some(objective.name), Some(success_rate)) } else { - None + (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, None) }; Self { function, module, caller, + objective_name, + objective_percentile, result, - objective, + ok, + error, } } @@ -70,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(objective_name) = self.objective_name { + labels.push((OBJECTIVE_NAME, objective_name)); } - if let Some((name, percentile)) = &self.objective { - labels.push((OBJECTIVE_NAME, name)); - labels.push((OBJECTIVE_PERCENTILE, percentile.as_str())); + if let Some(objective_percentile) = &self.objective_percentile { + labels.push((OBJECTIVE_PERCENTILE, objective_percentile.as_str())); } labels @@ -86,39 +144,54 @@ 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: 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