Skip to content

Add support for the prometheus-client crate #88

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
May 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -156,7 +156,7 @@ See [Why Autometrics?](https://github.com/autometrics-dev#4-why-autometrics) for

<br />

[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]
Expand Down
13 changes: 6 additions & 7 deletions autometrics/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 }
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion autometrics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions autometrics/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
146 changes: 112 additions & 34 deletions autometrics/src/labels.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<ResultAndReturnTypeLabels>,
pub(crate) objective: Option<(&'static str, ObjectivePercentile)>,
pub(crate) result: Option<ResultLabel>,
pub(crate) ok: Option<&'static str>,
pub(crate) error: Option<&'static str>,
pub(crate) objective_name: Option<&'static str>,
pub(crate) objective_percentile: Option<ObjectivePercentile>,
}

#[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 {
Expand All @@ -46,21 +87,33 @@ impl CounterLabels {
result: Option<ResultAndReturnTypeLabels>,
objective: Option<Objective>,
) -> 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,
}
}

Expand All @@ -70,62 +123,86 @@ 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
}
}

/// 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<ObjectivePercentile>,
pub objective_latency_threshold: Option<ObjectiveLatency>,
}

impl HistogramLabels {
pub fn new(function: &'static str, module: &'static str, objective: Option<Objective>) -> 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<Label> {
let mut labels = vec![(FUNCTION_KEY, self.function), (MODULE_KEY, self.module)];

if let Some((name, percentile, latency)) = &self.objective {
labels.push((OBJECTIVE_NAME, name));
labels.push((OBJECTIVE_PERCENTILE, percentile.as_str()));
labels.push((OBJECTIVE_LATENCY_THRESHOLD, latency.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()));
}
if let Some(objective_latency_threshold) = &self.objective_latency_threshold {
labels.push((
OBJECTIVE_LATENCY_THRESHOLD,
objective_latency_threshold.as_str(),
));
}

labels
}
}

/// 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,
Expand Down Expand Up @@ -235,11 +312,12 @@ impl_trait_for_types!(GetStaticStr);
/// The macro uses the autoref specialization trick through spez to get the labels for the type in a variety of circumstances.
/// Specifically, if the value is a Result, it will add the ok or error label accordingly unless one or both of the types that
/// the Result<T, E> is generic over implements the GetLabels trait. The label allows to override the inferred label, and the
/// [`ResultLabels`](crate::result_labels) macro implements the GetLabels trait for the user using annotations.
/// [`ResultLabels`](crate::ResultLabels) macro implements the GetLabels trait for the user using annotations.
///
/// The macro is meant to be called with a reference as argument: `get_result_labels_for_value(&return_value)`
///
/// Ref: https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md
/// See: <https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md>
#[doc(hidden)]
#[macro_export]
macro_rules! get_result_labels_for_value {
($e:expr) => {{
Expand Down
14 changes: 13 additions & 1 deletion autometrics/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,21 @@ pub use autometrics_macros::ResultLabels;
#[cfg(feature = "prometheus-exporter")]
pub use self::prometheus_exporter::*;

/// Functionality specific to the libraries used to collect metrics
pub mod integrations {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not sure I like this name, but I can't think of anything better. metrics_libraries, producers, ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

couldn't we just put the integrations as the top level name, getting rid of integrations? so the resolved path would end up being smth like autometrics::prometheus_client instead of autometrics::integrations::prometheus_client

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm that would also be an option. The main downside with that is we may want some other submodules exported from the crate root that are different, and they could get a bit lost. For example #90. We'd have 4-5 modules that are all related to the metrics libraries and 1-2 that aren't.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems fine imo

#[cfg(feature = "prometheus-client")]
pub mod prometheus_client {
pub use crate::tracker::prometheus_client::REGISTRY;
}
}

/// 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,
];
Expand Down
Loading