Skip to content

Commit 4d67922

Browse files
authored
Add support for the prometheus-client crate (#88)
* [WIP] * Update prometheus-client version * Finish support for prometheus-client * Fix doc links * Ensure that prometheus-client is actually being used * Work on fixing tests, remove const_format dep * Don't use regexes in tests (because different libraries order labels differently) * Make once_cell a required dependency * Rename concurrency tracker to gauge * Move PROMETHEUS_CLIENT_REGISTRY to integrations module * Run all the other tests first * Run the tests in the same order * The prometheus-exporter feature needs to depend on the prometheus feature (for now) * Rename export to just REGISTRY * Mention prometheus-client in readme and changelog
1 parent c5c30b6 commit 4d67922

17 files changed

Lines changed: 472 additions & 193 deletions

.github/workflows/ci.yml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,21 @@ jobs:
1818
- uses: Swatinem/rust-cache@v2
1919

2020
# Lint
21-
- name: Run Clippy
22-
# GitHub hosted runners using the latest stable version of Rust have Clippy pre-installed.
23-
run: cargo clippy --all-targets --features=prometheus-exporter,opentelemetry,metrics,prometheus
21+
# Note: GitHub hosted runners using the latest stable version of Rust have Clippy pre-installed.
22+
- run: cargo clippy --features=metrics,prometheus-exporter
23+
- run: cargo clippy --features=prometheus
24+
- run: cargo clippy --features=prometheus-client
25+
- run: cargo clippy --features=opentelemetry
2426

2527
# Build the packages
2628
- run: cargo build
27-
- run: cargo build --features=metrics
28-
- run: cargo build --features=prometheus
2929
- run: cargo build --features=custom-objective-percentile,custom-objective-latency
3030

31-
# Run the tests
31+
# Run the tests with each of the different metrics libraries
3232
- run: cargo test --features=prometheus-exporter
33+
- run: cargo test --no-default-features --features=prometheus-exporter,metrics --tests
34+
- run: cargo test --no-default-features --features=prometheus-exporter,prometheus --tests
35+
- run: cargo test --no-default-features --features=prometheus-exporter,prometheus-client --tests
3336

3437
# Compile the examples
3538
- run: cargo build --package example-axum

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313

1414
- `ResultLabels` derive macro allows to specify on an enum whether variants should
1515
always be "ok", or "error" for the success rate metrics of functions using them. (#61)
16+
- Support the official `prometheus-client` crate for producing metrics
1617

1718
### Changed
1819

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ https://github.com/autometrics-dev/autometrics-rs/assets/3262610/966ed140-1d6c-4
5454
- [🔍 Identify commits](https://docs.rs/autometrics/latest/autometrics/#identifying-commits-that-introduced-problems) that introduced errors or increased latency
5555
- [🚨 Define alerts](https://docs.rs/autometrics/latest/autometrics/objectives/index.html) using SLO best practices directly in your source code
5656
- [📊 Grafana dashboards](https://github.com/autometrics-dev#5-configuring-prometheus) work out of the box to visualize the performance of instrumented functions & SLOs
57-
- [⚙️ 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))
57+
- [⚙️ 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))
5858
- ⚡ Minimal runtime overhead
5959

6060
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
156156

157157
<br />
158158

159-
[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`.
159+
[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`.
160160

161161
```toml
162162
[dependencies]

autometrics/Cargo.toml

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ readme = "README.md"
1616
default = ["opentelemetry"]
1717
metrics = ["dep:metrics"]
1818
opentelemetry = ["opentelemetry_api"]
19-
prometheus = ["const_format", "dep:prometheus", "once_cell"]
19+
prometheus = ["dep:prometheus"]
20+
prometheus-client = ["dep:prometheus-client"]
2021
prometheus-exporter = [
2122
"metrics-exporter-prometheus",
22-
"once_cell",
2323
"opentelemetry-prometheus",
2424
"opentelemetry_sdk",
2525
"prometheus"
@@ -29,7 +29,8 @@ custom-objective-latency = []
2929

3030
[dependencies]
3131
autometrics-macros = { workspace = true }
32-
spez = { version = "0.1.2" }
32+
spez = "0.1.2"
33+
once_cell = "1.17"
3334

3435
# Used for opentelemetry feature
3536
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 }
3940

4041
# Used for prometheus-exporter feature
4142
metrics-exporter-prometheus = { version = "0.12", default-features = false, optional = true }
42-
once_cell = { version = "1.17", optional = true }
4343
opentelemetry-prometheus = { version = "0.12.0", optional = true }
4444
opentelemetry_sdk = { version = "0.19", default-features = false, features = ["metrics"], optional = true }
4545
prometheus = { version = "0.13", default-features = false, optional = true }
4646

47-
# Used for prometheus feature
48-
const_format = { version = "0.2", features = ["rust_1_51"], optional = true }
47+
# Used for prometheus-client feature
48+
prometheus-client = { version = "0.21.1", optional = true }
4949

5050
[dev-dependencies]
5151
axum = { version = "0.6", features = ["tokio"] }
52-
regex = "1.7"
5352
http = "0.2"
5453
tokio = { version = "1", features = ["full"] }
5554
trybuild = "1.0"

autometrics/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ pub async fn main() {
5353
- [🔍 Identify commits](#identifying-commits-that-introduced-problems) that introduced errors or increased latency
5454
- [🚨 Define alerts](https://docs.rs/autometrics/latest/autometrics/objectives/index.html) using SLO best practices directly in your source code
5555
- [📊 Grafana dashboards](https://github.com/autometrics-dev#5-configuring-prometheus) work out of the box to visualize the performance of instrumented functions & SLOs
56-
- [⚙️ 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))
56+
- [⚙️ 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))
5757
- ⚡ Minimal runtime overhead
5858

5959
See [Why Autometrics?](https://github.com/autometrics-dev#4-why-autometrics) for more details on the ideas behind autometrics.

autometrics/src/constants.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ pub const HISTOGRAM_NAME: &str = "function.calls.duration";
44
pub const GAUGE_NAME: &str = "function.calls.concurrent";
55
pub const BUILD_INFO_NAME: &str = "build_info";
66

7+
// Prometheus-flavored metric names
8+
pub const COUNTER_NAME_PROMETHEUS: &str = "function_calls_count";
9+
pub const HISTOGRAM_NAME_PROMETHEUS: &str = "function_calls_duration";
10+
pub const GAUGE_NAME_PROMETHEUS: &str = "function_calls_concurrent";
11+
712
// Descriptions
813
pub const COUNTER_DESCRIPTION: &str = "Autometrics counter for tracking function calls";
914
pub const HISTOGRAM_DESCRIPTION: &str = "Autometrics histogram for tracking function call duration";

autometrics/src/labels.rs

Lines changed: 112 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
use crate::{constants::*, objectives::*};
2+
#[cfg(feature = "prometheus-client")]
3+
use prometheus_client::encoding::{EncodeLabelSet, EncodeLabelValue, LabelValueEncoder};
24
use std::ops::Deref;
35

46
pub(crate) type Label = (&'static str, &'static str);
57
pub type ResultAndReturnTypeLabels = (&'static str, Option<&'static str>);
68

79
/// These are the labels used for the `build_info` metric.
10+
#[cfg_attr(
11+
feature = "prometheus-client",
12+
derive(EncodeLabelSet, Debug, Clone, PartialEq, Eq, Hash)
13+
)]
814
pub struct BuildInfoLabels {
9-
pub(crate) version: &'static str,
10-
pub(crate) commit: &'static str,
1115
pub(crate) branch: &'static str,
16+
pub(crate) commit: &'static str,
17+
pub(crate) version: &'static str,
1218
}
1319

1420
impl BuildInfoLabels {
@@ -30,12 +36,47 @@ impl BuildInfoLabels {
3036
}
3137

3238
/// These are the labels used for the `function.calls.count` metric.
39+
#[cfg_attr(
40+
feature = "prometheus-client",
41+
derive(EncodeLabelSet, Debug, Clone, PartialEq, Eq, Hash)
42+
)]
3343
pub struct CounterLabels {
3444
pub(crate) function: &'static str,
3545
pub(crate) module: &'static str,
3646
pub(crate) caller: &'static str,
37-
pub(crate) result: Option<ResultAndReturnTypeLabels>,
38-
pub(crate) objective: Option<(&'static str, ObjectivePercentile)>,
47+
pub(crate) result: Option<ResultLabel>,
48+
pub(crate) ok: Option<&'static str>,
49+
pub(crate) error: Option<&'static str>,
50+
pub(crate) objective_name: Option<&'static str>,
51+
pub(crate) objective_percentile: Option<ObjectivePercentile>,
52+
}
53+
54+
#[cfg_attr(
55+
feature = "prometheus-client",
56+
derive(Debug, Clone, PartialEq, Eq, Hash)
57+
)]
58+
pub(crate) enum ResultLabel {
59+
Ok,
60+
Error,
61+
}
62+
63+
impl ResultLabel {
64+
pub(crate) const fn as_str(&self) -> &'static str {
65+
match self {
66+
ResultLabel::Ok => OK_KEY,
67+
ResultLabel::Error => ERROR_KEY,
68+
}
69+
}
70+
}
71+
72+
#[cfg(feature = "prometheus-client")]
73+
impl EncodeLabelValue for ResultLabel {
74+
fn encode(&self, encoder: &mut LabelValueEncoder) -> Result<(), std::fmt::Error> {
75+
match self {
76+
ResultLabel::Ok => EncodeLabelValue::encode(&OK_KEY, encoder),
77+
ResultLabel::Error => EncodeLabelValue::encode(&ERROR_KEY, encoder),
78+
}
79+
}
3980
}
4081

4182
impl CounterLabels {
@@ -46,21 +87,33 @@ impl CounterLabels {
4687
result: Option<ResultAndReturnTypeLabels>,
4788
objective: Option<Objective>,
4889
) -> Self {
49-
let objective = if let Some(objective) = objective {
90+
let (objective_name, objective_percentile) = if let Some(objective) = objective {
5091
if let Some(success_rate) = objective.success_rate {
51-
Some((objective.name, success_rate))
92+
(Some(objective.name), Some(success_rate))
5293
} else {
53-
None
94+
(None, None)
95+
}
96+
} else {
97+
(None, None)
98+
};
99+
let (result, ok, error) = if let Some((result, return_value_type)) = result {
100+
match result {
101+
OK_KEY => (Some(ResultLabel::Ok), return_value_type, None),
102+
ERROR_KEY => (Some(ResultLabel::Error), None, return_value_type),
103+
_ => (None, None, None),
54104
}
55105
} else {
56-
None
106+
(None, None, None)
57107
};
58108
Self {
59109
function,
60110
module,
61111
caller,
112+
objective_name,
113+
objective_percentile,
62114
result,
63-
objective,
115+
ok,
116+
error,
64117
}
65118
}
66119

@@ -70,62 +123,86 @@ impl CounterLabels {
70123
(MODULE_KEY, self.module),
71124
(CALLER_KEY, self.caller),
72125
];
73-
if let Some((result, return_value_type)) = self.result {
74-
labels.push((RESULT_KEY, result));
75-
if let Some(return_value_type) = return_value_type {
76-
labels.push((result, return_value_type));
77-
}
126+
if let Some(result) = &self.result {
127+
labels.push((RESULT_KEY, result.as_str()));
128+
}
129+
if let Some(ok) = self.ok {
130+
labels.push((OK_KEY, ok));
131+
}
132+
if let Some(error) = self.error {
133+
labels.push((ERROR_KEY, error));
134+
}
135+
if let Some(objective_name) = self.objective_name {
136+
labels.push((OBJECTIVE_NAME, objective_name));
78137
}
79-
if let Some((name, percentile)) = &self.objective {
80-
labels.push((OBJECTIVE_NAME, name));
81-
labels.push((OBJECTIVE_PERCENTILE, percentile.as_str()));
138+
if let Some(objective_percentile) = &self.objective_percentile {
139+
labels.push((OBJECTIVE_PERCENTILE, objective_percentile.as_str()));
82140
}
83141

84142
labels
85143
}
86144
}
87145

88146
/// These are the labels used for the `function.calls.duration` metric.
147+
#[cfg_attr(
148+
feature = "prometheus-client",
149+
derive(EncodeLabelSet, Debug, Clone, PartialEq, Eq, Hash)
150+
)]
89151
pub struct HistogramLabels {
90152
pub function: &'static str,
91153
pub module: &'static str,
92-
/// The SLO name, objective percentile, and latency threshold
93-
pub objective: Option<(&'static str, ObjectivePercentile, ObjectiveLatency)>,
154+
pub objective_name: Option<&'static str>,
155+
pub objective_percentile: Option<ObjectivePercentile>,
156+
pub objective_latency_threshold: Option<ObjectiveLatency>,
94157
}
95158

96159
impl HistogramLabels {
97160
pub fn new(function: &'static str, module: &'static str, objective: Option<Objective>) -> Self {
98-
let objective = if let Some(objective) = objective {
99-
if let Some((latency, percentile)) = objective.latency {
100-
Some((objective.name, percentile, latency))
161+
let (objective_name, objective_percentile, objective_latency_threshold) =
162+
if let Some(objective) = objective {
163+
if let Some((latency, percentile)) = objective.latency {
164+
(Some(objective.name), Some(percentile), Some(latency))
165+
} else {
166+
(None, None, None)
167+
}
101168
} else {
102-
None
103-
}
104-
} else {
105-
None
106-
};
169+
(None, None, None)
170+
};
107171

108172
Self {
109173
function,
110174
module,
111-
objective,
175+
objective_name,
176+
objective_percentile,
177+
objective_latency_threshold,
112178
}
113179
}
114180

115181
pub fn to_vec(&self) -> Vec<Label> {
116182
let mut labels = vec![(FUNCTION_KEY, self.function), (MODULE_KEY, self.module)];
117183

118-
if let Some((name, percentile, latency)) = &self.objective {
119-
labels.push((OBJECTIVE_NAME, name));
120-
labels.push((OBJECTIVE_PERCENTILE, percentile.as_str()));
121-
labels.push((OBJECTIVE_LATENCY_THRESHOLD, latency.as_str()));
184+
if let Some(objective_name) = self.objective_name {
185+
labels.push((OBJECTIVE_NAME, objective_name));
186+
}
187+
if let Some(objective_percentile) = &self.objective_percentile {
188+
labels.push((OBJECTIVE_PERCENTILE, objective_percentile.as_str()));
189+
}
190+
if let Some(objective_latency_threshold) = &self.objective_latency_threshold {
191+
labels.push((
192+
OBJECTIVE_LATENCY_THRESHOLD,
193+
objective_latency_threshold.as_str(),
194+
));
122195
}
123196

124197
labels
125198
}
126199
}
127200

128201
/// These are the labels used for the `function.calls.concurrent` metric.
202+
#[cfg_attr(
203+
feature = "prometheus-client",
204+
derive(EncodeLabelSet, Debug, Clone, PartialEq, Eq, Hash)
205+
)]
129206
pub struct GaugeLabels {
130207
pub function: &'static str,
131208
pub module: &'static str,
@@ -235,11 +312,12 @@ impl_trait_for_types!(GetStaticStr);
235312
/// The macro uses the autoref specialization trick through spez to get the labels for the type in a variety of circumstances.
236313
/// Specifically, if the value is a Result, it will add the ok or error label accordingly unless one or both of the types that
237314
/// the Result<T, E> is generic over implements the GetLabels trait. The label allows to override the inferred label, and the
238-
/// [`ResultLabels`](crate::result_labels) macro implements the GetLabels trait for the user using annotations.
315+
/// [`ResultLabels`](crate::ResultLabels) macro implements the GetLabels trait for the user using annotations.
239316
///
240317
/// The macro is meant to be called with a reference as argument: `get_result_labels_for_value(&return_value)`
241318
///
242-
/// Ref: https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md
319+
/// See: <https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md>
320+
#[doc(hidden)]
243321
#[macro_export]
244322
macro_rules! get_result_labels_for_value {
245323
($e:expr) => {{

autometrics/src/lib.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,21 @@ pub use autometrics_macros::ResultLabels;
186186
#[cfg(feature = "prometheus-exporter")]
187187
pub use self::prometheus_exporter::*;
188188

189+
/// Functionality specific to the libraries used to collect metrics
190+
pub mod integrations {
191+
#[cfg(feature = "prometheus-client")]
192+
pub mod prometheus_client {
193+
pub use crate::tracker::prometheus_client::REGISTRY;
194+
}
195+
}
196+
189197
/// We use the histogram buckets recommended by the OpenTelemetry specification
190198
/// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#explicit-bucket-histogram-aggregation
191-
#[cfg(any(feature = "prometheus", feature = "prometheus-exporter"))]
199+
#[cfg(any(
200+
feature = "prometheus",
201+
feature = "prometheus-client",
202+
feature = "prometheus-exporter"
203+
))]
192204
pub(crate) const HISTOGRAM_BUCKETS: [f64; 14] = [
193205
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,
194206
];

0 commit comments

Comments
 (0)