diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07a7ef2..962257e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,15 +24,18 @@ jobs: - run: cargo clippy --features=prometheus-client - run: cargo clippy --features=opentelemetry - # Build the packages - - run: cargo build - - run: cargo build --features=custom-objective-percentile,custom-objective-latency - # 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 + - run: cargo test --features=prometheus-exporter,metrics + - run: cargo test --features=prometheus-exporter,prometheus --tests + - run: cargo test --features=prometheus-exporter,prometheus-client --tests + # The tests using opentelemetry are currently failing because of this issue: + # https://github.com/open-telemetry/opentelemetry-rust/issues/1070 + # We should remove the continue-on-error once that is fixed + - run: cargo test --features=prometheus-exporter,opentelemetry + continue-on-error: true + + # Build the crate using the other optional features + - run: cargo build --features=metrics,custom-objective-percentile,custom-objective-latency # Compile the examples - run: cargo build --package example-axum diff --git a/CHANGELOG.md b/CHANGELOG.md index 2322818..7c8fab8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,9 +17,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Users must configure the metrics library they want to use autometrics with +- Metrics library feature flags are now mutually exclusive (previously, `autometrics` would only + produce metrics using a single metrics library if multiple feature flags were enabled, using + a prioritization order defined internally) - `GetLabels` trait (publicly exported but meant for internal use) changed the signature of its function to accomodate the new `ResultLabels` macro. This change is not significant if you never imported `autometrics::__private` manually (#61) +- When using the `opentelemetry` together with the `prometheus-exporter`, it will no longer + use the default registry provided by the `prometheus` crate. It will instead use a new registry +- The `prometheus-exporter`'s `encode_global_metrics` feature now returns an error enum + defined by `autometrics` as opposed to directly returning the `prometheus::Error` type - Updated `opentelemetry` dependencies to v0.19. This means that users using autometrics with `opentelemetry` but not using the `prometheus-exporter` must update the `opentelemetry` to use v0.19. @@ -29,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +- `opentelemetry` is no longer used by default - `GetLabelsForResult` trait (publicly exported but meant for internal use) was removed to accomodate the new `ResultLabels` macro. This change is not significant if you never imported `autometrics::__private` manually (#61) diff --git a/README.md b/README.md index 0996e93..b4c865c 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ See [Why Autometrics?](https://github.com/autometrics-dev#4-why-autometrics) for 1. Add `autometrics` to your project: ```sh - cargo add autometrics --features=prometheus-exporter + cargo add autometrics --features=prometheus,prometheus-exporter ``` 2. Instrument your functions with the [`#[autometrics]`](https://docs.rs/autometrics/latest/autometrics/attr.autometrics.html) macro diff --git a/autometrics/Cargo.toml b/autometrics/Cargo.toml index 1909064..ea4aaca 100644 --- a/autometrics/Cargo.toml +++ b/autometrics/Cargo.toml @@ -13,7 +13,6 @@ categories = { workspace = true } readme = "README.md" [features] -default = ["opentelemetry"] metrics = ["dep:metrics"] opentelemetry = ["opentelemetry_api"] prometheus = ["dep:prometheus"] @@ -22,7 +21,8 @@ prometheus-exporter = [ "metrics-exporter-prometheus", "opentelemetry-prometheus", "opentelemetry_sdk", - "prometheus" + "dep:prometheus", + "thiserror" ] custom-objective-percentile = [] custom-objective-latency = [] @@ -43,6 +43,7 @@ metrics-exporter-prometheus = { version = "0.12", default-features = false, opti 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 } +thiserror = { version = "1", optional = true } # Used for prometheus-client feature prometheus-client = { version = "0.21.1", optional = true } diff --git a/autometrics/README.md b/autometrics/README.md index e56b11d..6857492 100644 --- a/autometrics/README.md +++ b/autometrics/README.md @@ -115,12 +115,13 @@ fn main() { #### Metrics Libraries -Configure the crate that autometrics will use to produce metrics by using one of the following feature flags: +**Required:** Configure the crate that autometrics will use to produce metrics by using one of the following feature flags: > **Note** > > If you are **not** using the `prometheus-exporter`, you must ensure that you are using the exact same version of the metrics library as `autometrics` (and it must come from `crates.io` rather than git or another source). If not, the autometrics metrics will not appear in your exported metrics. -- `opentelemetry` (enabled by default) - use the [opentelemetry](https://crates.io/crates/opentelemetry) crate for producing metrics. +- `opentelemetry` - use the [opentelemetry](https://crates.io/crates/opentelemetry) crate for producing metrics. - `metrics` - use the [metrics](https://crates.io/crates/metrics) crate for producing metrics - `prometheus` - use the [prometheus](https://crates.io/crates/prometheus) crate for producing metrics +- `prometheus-client` - use the official [prometheus-client](https://crates.io/crates/prometheus-client) crate for producing metrics diff --git a/autometrics/src/prometheus_exporter.rs b/autometrics/src/prometheus_exporter.rs index d2ec76f..ab7a573 100644 --- a/autometrics/src/prometheus_exporter.rs +++ b/autometrics/src/prometheus_exporter.rs @@ -1,48 +1,94 @@ -use crate::HISTOGRAM_BUCKETS; #[cfg(feature = "metrics")] use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle}; use once_cell::sync::Lazy; +#[cfg(feature = "opentelemetry")] use opentelemetry_prometheus::{exporter, PrometheusExporter}; +#[cfg(feature = "opentelemetry")] use opentelemetry_sdk::export::metrics::aggregation; +#[cfg(feature = "opentelemetry")] use opentelemetry_sdk::metrics::{controllers, processors, selectors}; -use prometheus::{default_registry, Error, TextEncoder}; +#[cfg(any(feature = "opentelemetry", feature = "prometheus"))] +use prometheus::TextEncoder; +use thiserror::Error; -static GLOBAL_EXPORTER: Lazy = Lazy::new(initialize_metrics_exporter); +#[cfg(not(any( + feature = "metrics", + feature = "opentelemetry", + feature = "prometheus", + feature = "prometheus-client" +)))] +compile_error!("At least one of the metrics, opentelemetry, prometheus, or prometheus-client features must be enabled in order to use the prometheus-exporter"); + +#[derive(Debug, Error)] +pub enum EncodingError { + #[cfg(any(feature = "prometheus", feature = "opentelemetry"))] + #[error(transparent)] + Prometheus(#[from] prometheus::Error), + #[cfg(feature = "prometheus-client")] + #[error(transparent)] + Format(#[from] std::fmt::Error), +} + +static GLOBAL_EXPORTER: Lazy = Lazy::new(|| GlobalPrometheus { + #[cfg(feature = "metrics")] + metrics_exporter: PrometheusBuilder::new() + .set_buckets(&crate::HISTOGRAM_BUCKETS) + .expect("Failed to set histogram buckets") + .install_recorder() + .expect("Failed to install recorder"), + + #[cfg(feature = "opentelemetry")] + opentelemetry_exporter: exporter( + controllers::basic(processors::factory( + selectors::simple::histogram(crate::HISTOGRAM_BUCKETS), + aggregation::cumulative_temporality_selector(), + )) + .build(), + ) + .init(), +}); #[derive(Clone)] #[doc(hidden)] pub struct GlobalPrometheus { - exporter: PrometheusExporter, + #[cfg(feature = "opentelemetry")] + opentelemetry_exporter: PrometheusExporter, #[cfg(feature = "metrics")] - handle: PrometheusHandle, + metrics_exporter: PrometheusHandle, } impl GlobalPrometheus { - fn encode_metrics(&self) -> Result { - let metric_families = self.exporter.registry().gather(); - let encoder = TextEncoder::new(); - #[allow(unused_mut)] - let mut output = encoder.encode_to_string(&metric_families)?; + fn encode_metrics(&self) -> Result { + let mut output = String::new(); #[cfg(feature = "metrics")] { + output.push_str(&self.metrics_exporter.render()); output.push('\n'); - output.push_str(&self.handle.render()); } - #[cfg(feature = "prometheus-client")] + #[cfg(feature = "opentelemetry")] { + let metric_families = self.opentelemetry_exporter.registry().gather(); + let encoder = TextEncoder::new(); + encoder.encode_utf8(&metric_families, &mut output)?; output.push('\n'); + } + + #[cfg(feature = "prometheus")] + { + let metric_families = prometheus::default_registry().gather(); + let encoder = TextEncoder::new(); + encoder.encode_utf8(&metric_families, &mut output)?; + output.push('\n'); + } + + #[cfg(feature = "prometheus-client")] + { prometheus_client::encoding::text::encode( &mut output, &crate::tracker::prometheus_client::REGISTRY, - ) - .map_err(|err| { - Error::Msg(format!( - "Failed to encode prometheus-client metrics: {}", - err - )) - })?; + )?; } Ok(output) @@ -84,30 +130,6 @@ pub fn global_metrics_exporter() -> GlobalPrometheus { /// } /// } /// ``` -pub fn encode_global_metrics() -> Result { +pub fn encode_global_metrics() -> Result { GLOBAL_EXPORTER.encode_metrics() } - -fn initialize_metrics_exporter() -> GlobalPrometheus { - let controller = controllers::basic(processors::factory( - selectors::simple::histogram(HISTOGRAM_BUCKETS), - aggregation::cumulative_temporality_selector(), - )) - .build(); - - // Use the prometheus crate's default registry so it still works with custom - // metrics defined through the prometheus crate - let registry = default_registry().clone(); - let prometheus_exporter = exporter(controller).with_registry(registry).init(); - - GlobalPrometheus { - exporter: prometheus_exporter, - - #[cfg(feature = "metrics")] - handle: PrometheusBuilder::new() - .set_buckets(&HISTOGRAM_BUCKETS) - .expect("Failed to set histogram buckets") - .install_recorder() - .expect("Failed to install recorder"), - } -} diff --git a/autometrics/src/tracker/mod.rs b/autometrics/src/tracker/mod.rs index dd7c9ce..5afc70d 100644 --- a/autometrics/src/tracker/mod.rs +++ b/autometrics/src/tracker/mod.rs @@ -9,36 +9,89 @@ mod prometheus; #[cfg(feature = "prometheus-client")] pub(crate) mod prometheus_client; -// Priority if multiple features are enabled: -// 1. prometheus -// 2. prometheus-client -// 3. metrics -// 4. opentelemetry (default) - -// By default, use the opentelemetry crate -#[cfg(all( - feature = "opentelemetry", - not(any( - feature = "metrics", - feature = "prometheus", - feature = "prometheus-client" - )) -))] -pub use self::opentelemetry::OpenTelemetryTracker as AutometricsTracker; +#[cfg(feature = "metrics")] +pub use self::metrics::MetricsTracker; +#[cfg(feature = "opentelemetry")] +pub use self::opentelemetry::OpenTelemetryTracker; +#[cfg(feature = "prometheus")] +pub use self::prometheus::PrometheusTracker; +#[cfg(feature = "prometheus-client")] +pub use self::prometheus_client::PrometheusClientTracker; -// But use one of the other crates if any of those features are enabled -#[cfg(all( - feature = "metrics", - not(any(feature = "prometheus", feature = "prometheus-client")) +#[cfg(any( + all( + feature = "metrics", + any( + feature = "opentelemetry", + feature = "prometheus", + feature = "prometheus-client" + ) + ), + all( + feature = "opentelemetry", + any(feature = "prometheus", feature = "prometheus-client") + ), + all(feature = "prometheus", feature = "prometheus-client") ))] -pub use self::metrics::MetricsTracker as AutometricsTracker; -#[cfg(feature = "prometheus")] -pub use self::prometheus::PrometheusTracker as AutometricsTracker; -#[cfg(all(feature = "prometheus-client", not(feature = "prometheus")))] -pub use self::prometheus_client::PrometheusClientTracker as AutometricsTracker; +compile_error!("Only one of the metrics, opentelemetry, prometheus, or prometheus-client features can be enabled at a time"); pub trait TrackMetrics { fn set_build_info(build_info_labels: &BuildInfoLabels); fn start(gauge_labels: Option<&GaugeLabels>) -> Self; fn finish(self, counter_labels: &CounterLabels, histogram_labels: &HistogramLabels); } + +pub struct AutometricsTracker { + #[cfg(feature = "metrics")] + metrics_tracker: MetricsTracker, + #[cfg(feature = "opentelemetry")] + opentelemetry_tracker: OpenTelemetryTracker, + #[cfg(feature = "prometheus")] + prometheus_tracker: PrometheusTracker, + #[cfg(feature = "prometheus-client")] + prometheus_client_tracker: PrometheusClientTracker, +} + +impl TrackMetrics for AutometricsTracker { + #[allow(unused_variables)] + fn set_build_info(build_info_labels: &BuildInfoLabels) { + #[cfg(feature = "metrics")] + MetricsTracker::set_build_info(build_info_labels); + #[cfg(feature = "opentelemetry")] + OpenTelemetryTracker::set_build_info(build_info_labels); + #[cfg(feature = "prometheus")] + PrometheusTracker::set_build_info(build_info_labels); + #[cfg(feature = "prometheus-client")] + PrometheusClientTracker::set_build_info(build_info_labels); + } + + #[allow(unused_variables)] + fn start(gauge_labels: Option<&GaugeLabels>) -> Self { + Self { + #[cfg(feature = "metrics")] + metrics_tracker: MetricsTracker::start(gauge_labels), + #[cfg(feature = "opentelemetry")] + opentelemetry_tracker: OpenTelemetryTracker::start(gauge_labels), + #[cfg(feature = "prometheus")] + prometheus_tracker: PrometheusTracker::start(gauge_labels), + #[cfg(feature = "prometheus-client")] + prometheus_client_tracker: PrometheusClientTracker::start(gauge_labels), + } + } + + #[allow(unused_variables)] + fn finish(self, counter_labels: &CounterLabels, histogram_labels: &HistogramLabels) { + #[cfg(feature = "metrics")] + self.metrics_tracker + .finish(counter_labels, histogram_labels); + #[cfg(feature = "opentelemetry")] + self.opentelemetry_tracker + .finish(counter_labels, histogram_labels); + #[cfg(feature = "prometheus")] + self.prometheus_tracker + .finish(counter_labels, histogram_labels); + #[cfg(feature = "prometheus-client")] + self.prometheus_client_tracker + .finish(counter_labels, histogram_labels); + } +} diff --git a/examples/axum/Cargo.toml b/examples/axum/Cargo.toml index e3e0435..e04278a 100644 --- a/examples/axum/Cargo.toml +++ b/examples/axum/Cargo.toml @@ -5,7 +5,7 @@ publish = false edition = "2021" [dependencies] -autometrics = { path = "../../autometrics", features = ["prometheus-exporter"] } +autometrics = { path = "../../autometrics", features = ["prometheus", "prometheus-exporter"] } autometrics-example-util = { path = "../util" } axum = { version = "0.6", features = ["json"] } rand = "0.8" diff --git a/examples/full-api/Cargo.toml b/examples/full-api/Cargo.toml index b343f28..85e0fe8 100644 --- a/examples/full-api/Cargo.toml +++ b/examples/full-api/Cargo.toml @@ -5,7 +5,7 @@ publish = false edition = "2021" [dependencies] -autometrics = { path = "../../autometrics", features = ["prometheus-exporter"] } +autometrics = { path = "../../autometrics", features = ["prometheus", "prometheus-exporter"] } autometrics-example-util = { path = "../util" } axum = { version = "0.6", features = ["json"] } rand = "0.8"