Skip to content

Commit a60da26

Browse files
committed
fixes #601 -- implement native histogram support for metrics-exporter-prometheus
1 parent bed3152 commit a60da26

File tree

8 files changed

+1384
-3
lines changed

8 files changed

+1384
-3
lines changed

metrics-exporter-prometheus/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ tracing-subscriber = { workspace = true, features = ["fmt"] }
6767
[build-dependencies]
6868
prost-build = { workspace = true, optional = true }
6969

70+
[[example]]
71+
name = "native_histograms"
72+
required-features = ["http-listener"]
73+
7074
[[example]]
7175
name = "prometheus_push_gateway"
7276
required-features = ["push-gateway"]
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
use metrics::histogram;
2+
use metrics_exporter_prometheus::{NativeHistogramConfig, PrometheusBuilder};
3+
use std::thread;
4+
use std::time::Duration;
5+
6+
fn main() {
7+
// Create a Prometheus builder with native histograms enabled for specific metrics
8+
let builder = PrometheusBuilder::new()
9+
.with_http_listener(([127, 0, 0, 1], 9000))
10+
// Enable native histograms for specific request_duration metrics
11+
.set_native_histogram_for_metric(
12+
metrics_exporter_prometheus::Matcher::Prefix("request_duration_api".to_string()),
13+
NativeHistogramConfig::new(1.1, 160, 1e-9).unwrap(), // Finer granularity
14+
)
15+
.set_native_histogram_for_metric(
16+
metrics_exporter_prometheus::Matcher::Prefix("response_size".to_string()),
17+
NativeHistogramConfig::new(1.1, 160, 1e-9).unwrap(), // Finer granularity
18+
);
19+
20+
// Install the recorder and get a handle
21+
builder.install().expect("failed to install recorder");
22+
23+
// Simulate some metric recording in a loop
24+
println!("Recording metrics... Check http://127.0.0.1:9000/metrics");
25+
println!("Native histograms will only be visible in protobuf format.");
26+
println!(
27+
"Try: curl -H 'Accept: application/vnd.google.protobuf' http://127.0.0.1:9000/metrics"
28+
);
29+
30+
for i in 0..1000 {
31+
// Record to native histogram (request_duration_api)
32+
let duration = (i as f64 / 10.0).sin().abs() * 5.0 + 0.1;
33+
histogram!("request_duration_api").record(duration);
34+
35+
// Record to regular histogram (response_size)
36+
let size = 1000.0 + (i as f64).cos() * 500.0;
37+
histogram!("response_size").record(size);
38+
39+
if i % 100 == 0 {
40+
println!("Recorded {} samples", i + 1);
41+
}
42+
43+
thread::sleep(Duration::from_millis(10));
44+
}
45+
46+
println!("Metrics server will continue running. Access http://127.0.0.1:9000/metrics");
47+
println!("Press Ctrl+C to exit");
48+
49+
// Keep the server running
50+
loop {
51+
thread::sleep(Duration::from_secs(1));
52+
}
53+
}

metrics-exporter-prometheus/src/distribution.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::{collections::HashMap, sync::Arc};
55
use quanta::Instant;
66

77
use crate::common::Matcher;
8+
use crate::native_histogram::{NativeHistogram, NativeHistogramConfig};
89

910
use metrics_util::{
1011
storage::{Histogram, Summary},
@@ -32,6 +33,11 @@ pub enum Distribution {
3233
/// requests were faster than 200ms, and 99% of requests were faster than
3334
/// 1000ms, etc.
3435
Summary(RollingSummary, Arc<Vec<Quantile>>, f64),
36+
/// A Prometheus native histogram.
37+
///
38+
/// Uses exponential buckets to efficiently represent histogram data without
39+
/// requiring predefined bucket boundaries.
40+
NativeHistogram(NativeHistogram),
3541
}
3642

3743
impl Distribution {
@@ -54,6 +60,12 @@ impl Distribution {
5460
Distribution::Summary(RollingSummary::new(bucket_count, bucket_duration), quantiles, 0.0)
5561
}
5662

63+
/// Creates a native histogram distribution.
64+
pub fn new_native_histogram(config: NativeHistogramConfig) -> Distribution {
65+
let hist = NativeHistogram::new(config);
66+
Distribution::NativeHistogram(hist)
67+
}
68+
5769
/// Records the given `samples` in the current distribution.
5870
pub fn record_samples(&mut self, samples: &[(f64, Instant)]) {
5971
match self {
@@ -66,6 +78,11 @@ impl Distribution {
6678
*sum += *sample;
6779
}
6880
}
81+
Distribution::NativeHistogram(hist) => {
82+
for (sample, _ts) in samples {
83+
hist.observe(*sample);
84+
}
85+
}
6986
}
7087
}
7188
}
@@ -78,6 +95,7 @@ pub struct DistributionBuilder {
7895
bucket_duration: Option<Duration>,
7996
bucket_count: Option<NonZeroU32>,
8097
bucket_overrides: Option<Vec<(Matcher, Vec<f64>)>>,
98+
native_histogram_overrides: Option<Vec<(Matcher, NativeHistogramConfig)>>,
8199
}
82100

83101
impl DistributionBuilder {
@@ -88,6 +106,7 @@ impl DistributionBuilder {
88106
buckets: Option<Vec<f64>>,
89107
bucket_count: Option<NonZeroU32>,
90108
bucket_overrides: Option<HashMap<Matcher, Vec<f64>>>,
109+
native_histogram_overrides: Option<HashMap<Matcher, NativeHistogramConfig>>,
91110
) -> DistributionBuilder {
92111
DistributionBuilder {
93112
quantiles: Arc::new(quantiles),
@@ -99,11 +118,26 @@ impl DistributionBuilder {
99118
matchers.sort_by(|a, b| a.0.cmp(&b.0));
100119
matchers
101120
}),
121+
native_histogram_overrides: native_histogram_overrides.map(|entries| {
122+
let mut matchers = entries.into_iter().collect::<Vec<_>>();
123+
matchers.sort_by(|a, b| a.0.cmp(&b.0));
124+
matchers
125+
}),
102126
}
103127
}
104128

105129
/// Returns a distribution for the given metric key.
106130
pub fn get_distribution(&self, name: &str) -> Distribution {
131+
// Check for native histogram overrides first (highest priority)
132+
if let Some(ref overrides) = self.native_histogram_overrides {
133+
for (matcher, config) in overrides {
134+
if matcher.matches(name) {
135+
return Distribution::new_native_histogram(config.clone());
136+
}
137+
}
138+
}
139+
140+
// Check for histogram bucket overrides
107141
if let Some(ref overrides) = self.bucket_overrides {
108142
for (matcher, buckets) in overrides {
109143
if matcher.matches(name) {
@@ -112,10 +146,12 @@ impl DistributionBuilder {
112146
}
113147
}
114148

149+
// Check for global histogram buckets
115150
if let Some(ref buckets) = self.buckets {
116151
return Distribution::new_histogram(buckets);
117152
}
118153

154+
// Default to summary
119155
let b_duration = self.bucket_duration.map_or(DEFAULT_SUMMARY_BUCKET_DURATION, |d| d);
120156
let b_count = self.bucket_count.map_or(DEFAULT_SUMMARY_BUCKET_COUNT, |c| c);
121157

@@ -124,6 +160,16 @@ impl DistributionBuilder {
124160

125161
/// Returns the distribution type for the given metric key.
126162
pub fn get_distribution_type(&self, name: &str) -> &'static str {
163+
// Check for native histogram overrides first (highest priority)
164+
if let Some(ref overrides) = self.native_histogram_overrides {
165+
for (matcher, _) in overrides {
166+
if matcher.matches(name) {
167+
return "native_histogram";
168+
}
169+
}
170+
}
171+
172+
// Check for regular histogram buckets
127173
if self.buckets.is_some() {
128174
return "histogram";
129175
}

metrics-exporter-prometheus/src/exporter/builder.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use metrics_util::{
2424

2525
use crate::common::Matcher;
2626
use crate::distribution::DistributionBuilder;
27+
use crate::native_histogram::NativeHistogramConfig;
2728
use crate::recorder::{Inner, PrometheusRecorder};
2829
use crate::registry::AtomicStorage;
2930
use crate::{common::BuildError, PrometheusHandle};
@@ -44,6 +45,7 @@ pub struct PrometheusBuilder {
4445
bucket_count: Option<NonZeroU32>,
4546
buckets: Option<Vec<f64>>,
4647
bucket_overrides: Option<HashMap<Matcher, Vec<f64>>>,
48+
native_histogram_overrides: Option<HashMap<Matcher, NativeHistogramConfig>>,
4749
idle_timeout: Option<Duration>,
4850
upkeep_timeout: Duration,
4951
recency_mask: MetricKindMask,
@@ -79,6 +81,7 @@ impl PrometheusBuilder {
7981
bucket_count: None,
8082
buckets: None,
8183
bucket_overrides: None,
84+
native_histogram_overrides: None,
8285
idle_timeout: None,
8386
upkeep_timeout,
8487
recency_mask: MetricKindMask::NONE,
@@ -345,6 +348,26 @@ impl PrometheusBuilder {
345348
Ok(self)
346349
}
347350

351+
/// Sets native histogram configuration for a specific pattern.
352+
///
353+
/// The match pattern can be a full match (equality), prefix match, or suffix match. The matchers are applied in
354+
/// that order if two or more matchers would apply to a single metric. That is to say, if a full match and a prefix
355+
/// match applied to a metric, the full match would win, and if a prefix match and a suffix match applied to a
356+
/// metric, the prefix match would win.
357+
///
358+
/// Native histograms use exponential buckets and take precedence over regular histograms and summaries.
359+
/// They are only supported in the protobuf format.
360+
#[must_use]
361+
pub fn set_native_histogram_for_metric(
362+
mut self,
363+
matcher: Matcher,
364+
config: NativeHistogramConfig,
365+
) -> Self {
366+
let overrides = self.native_histogram_overrides.get_or_insert_with(HashMap::new);
367+
overrides.insert(matcher.sanitized(), config);
368+
self
369+
}
370+
348371
/// Sets the idle timeout for metrics.
349372
///
350373
/// If a metric hasn't been updated within this timeout, it will be removed from the registry and in turn removed
@@ -554,6 +577,7 @@ impl PrometheusBuilder {
554577
self.buckets,
555578
self.bucket_count,
556579
self.bucket_overrides,
580+
self.native_histogram_overrides,
557581
),
558582
descriptions: RwLock::new(HashMap::new()),
559583
global_labels: self.global_labels.unwrap_or_default(),

metrics-exporter-prometheus/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ pub use self::common::{BuildError, Matcher};
124124
mod distribution;
125125
pub use distribution::{Distribution, DistributionBuilder};
126126

127+
mod native_histogram;
128+
pub use native_histogram::{NativeHistogram, NativeHistogramConfig};
129+
127130
mod exporter;
128131
pub use self::exporter::builder::PrometheusBuilder;
129132
#[cfg(any(feature = "http-listener", feature = "push-gateway"))]

0 commit comments

Comments
 (0)