Skip to content

Commit 4c78061

Browse files
author
Shivangi Kumar
committed
Ensure that exporting metrics doesn't interfere with logging metrics
Signed-off-by: Shivangi Kumar <[email protected]>
1 parent dabe5c7 commit 4c78061

File tree

3 files changed

+103
-57
lines changed

3 files changed

+103
-57
lines changed

mountpoint-s3-fs/src/metrics.rs

Lines changed: 13 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
//! This module hooks up the [metrics](https://docs.rs/metrics) facade to a metrics sink that
44
//! currently just emits them to a tracing log entry.
55
6-
use crate::metrics_otel::{OtlpConfig, OtlpMetricsExporter};
6+
pub use crate::metrics_otel::OtlpConfig;
7+
use crate::metrics_otel::OtlpMetricsExporter;
78
use opentelemetry::KeyValue;
89

910
use std::thread::{self, JoinHandle};
@@ -17,6 +18,7 @@ use crate::sync::Arc;
1718
use crate::sync::mpsc::{RecvTimeoutError, Sender, channel};
1819

1920
mod data;
21+
pub use data::MetricValue;
2022
use data::*;
2123

2224
mod tracing_span;
@@ -160,38 +162,22 @@ impl MetricsSink {
160162
for mut entry in self.metrics.iter_mut() {
161163
let (key, metric) = entry.pair_mut();
162164

163-
// If OTLP export is enabled, also send metrics to OpenTelemetry
165+
// Get both the value and string representation of the metric (this also resets the metric)
166+
let Some((value, metric_str)) = metric.value_and_fmt_and_reset() else {
167+
continue;
168+
};
169+
170+
// If OTLP export is enabled, send metrics to OpenTelemetry
164171
if let Some(exporter) = &self.otlp_exporter {
165172
// Convert labels to OpenTelemetry KeyValue pairs
166173
let attributes: Vec<KeyValue> = key
167174
.labels()
168175
.map(|label| KeyValue::new(label.key().to_string(), label.value().to_string()))
169176
.collect();
170177

171-
// Record the metric based on its type
172-
match metric {
173-
Metric::Counter(counter) => {
174-
if let Some((value, _)) = counter.load_and_reset() {
175-
exporter.record_counter(key, value, &attributes);
176-
}
177-
}
178-
Metric::Gauge(gauge) => {
179-
if let Some(value) = gauge.load_if_changed() {
180-
exporter.record_gauge(key, value, &attributes);
181-
}
182-
}
183-
Metric::Histogram(histogram) => {
184-
histogram.run_and_reset(|h| {
185-
let value = h.mean();
186-
exporter.record_histogram(key, value, &attributes);
187-
});
188-
}
189-
}
178+
// Record the metric using its value
179+
exporter.record_metric(key, &value, &attributes);
190180
}
191-
192-
let Some(metric) = metric.fmt_and_reset() else {
193-
continue;
194-
};
195181
let labels = if key.labels().len() == 0 {
196182
String::new()
197183
} else {
@@ -203,7 +189,7 @@ impl MetricsSink {
203189
.join(",")
204190
)
205191
};
206-
metrics.push(format!("{}{}: {}", key.name(), labels, metric));
192+
metrics.push(format!("{}{}: {}", key.name(), labels, metric_str));
207193
}
208194

209195
metrics.sort();
@@ -497,7 +483,7 @@ mod tests {
497483
let error = result.unwrap_err().to_string();
498484
assert!(
499485
error.contains("Invalid OTLP endpoint configuration"),
500-
"Error message should indicate invalid configuration: {error}"
486+
"Error message should indicate invalid configuration: {error}",
501487
);
502488

503489
// Test with no OTLP config (should succeed)

mountpoint-s3-fs/src/metrics/data.rs

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
use crate::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
22
use crate::sync::{Arc, Mutex};
33

4+
/// Represents the value of a metric
5+
#[derive(Debug, Clone)]
6+
pub enum MetricValue {
7+
Counter(u64),
8+
Gauge(f64),
9+
Histogram(f64),
10+
}
11+
412
/// A single metric
513
#[derive(Debug)]
614
pub enum Metric {
@@ -43,34 +51,43 @@ impl Metric {
4351
metrics::Histogram::from_arc(inner.clone())
4452
}
4553

46-
/// Generate a string representation of this metric, or None if the metric has had no values
54+
/// Generate both the value and string representation of this metric, or None if the metric has had no values
4755
/// emitted since the last call to this function.
48-
pub fn fmt_and_reset(&self) -> Option<String> {
56+
pub fn value_and_fmt_and_reset(&self) -> Option<(MetricValue, String)> {
4957
match self {
5058
Metric::Counter(inner) => {
5159
let (sum, n) = inner.load_and_reset()?;
52-
if n == 1 {
53-
Some(format!("{sum}"))
60+
let fmt = if n == 1 {
61+
format!("{sum}")
5462
} else {
55-
Some(format!("{sum} (n={n})"))
56-
}
63+
format!("{sum} (n={n})")
64+
};
65+
Some((MetricValue::Counter(sum), fmt))
5766
}
5867
// Gauges can't reset because they can be incremented/decremented
59-
Metric::Gauge(inner) => inner.load_if_changed().map(|value| format!("{value}")),
60-
Metric::Histogram(histogram) => histogram.run_and_reset(|histogram| {
61-
format!(
62-
"n={}: min={} p10={} p50={} avg={:.2} p90={} p99={} p99.9={} max={}",
63-
histogram.len(),
64-
histogram.min(),
65-
histogram.value_at_quantile(0.1),
66-
histogram.value_at_quantile(0.5),
67-
histogram.mean(),
68-
histogram.value_at_quantile(0.9),
69-
histogram.value_at_quantile(0.99),
70-
histogram.value_at_quantile(0.999),
71-
histogram.max(),
72-
)
73-
}),
68+
Metric::Gauge(inner) => {
69+
let value = inner.load_if_changed()?;
70+
let fmt = format!("{value}");
71+
Some((MetricValue::Gauge(value), fmt))
72+
}
73+
Metric::Histogram(histogram) => {
74+
// run_and_reset already returns an Option, so we map it to our return type
75+
histogram.run_and_reset(|histogram| {
76+
let fmt = format!(
77+
"n={}: min={} p10={} p50={} avg={:.2} p90={} p99={} p99.9={} max={}",
78+
histogram.len(),
79+
histogram.min(),
80+
histogram.value_at_quantile(0.1),
81+
histogram.value_at_quantile(0.5),
82+
histogram.mean(),
83+
histogram.value_at_quantile(0.9),
84+
histogram.value_at_quantile(0.99),
85+
histogram.value_at_quantile(0.999),
86+
histogram.max(),
87+
);
88+
(MetricValue::Histogram(histogram.max() as f64), fmt)
89+
})
90+
}
7491
}
7592
}
7693
}

mountpoint-s3-fs/src/metrics_otel.rs

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use opentelemetry::global;
33
use opentelemetry_otlp::{Protocol, WithExportConfig};
44
use std::time::Duration;
55

6+
use crate::metrics::MetricValue;
67
use metrics::Key;
8+
use std::collections::HashMap;
9+
use std::sync::Mutex;
710

811
/// Configuration for OpenTelemetry metrics export
912
#[derive(Debug, Clone)]
@@ -19,7 +22,7 @@ impl OtlpConfig {
1922
pub fn new(endpoint: &str) -> Self {
2023
Self {
2124
endpoint: endpoint.to_string(),
22-
interval_secs: 5, // Default to 5 seconds
25+
interval_secs: 60, // Default to 60 seconds to align with the meter provider default interval
2326
}
2427
}
2528

@@ -33,17 +36,31 @@ impl OtlpConfig {
3336
#[derive(Debug)]
3437
pub struct OtlpMetricsExporter {
3538
meter: opentelemetry::metrics::Meter,
39+
counters: Mutex<HashMap<String, opentelemetry::metrics::Counter<u64>>>,
40+
gauges: Mutex<HashMap<String, opentelemetry::metrics::Gauge<f64>>>,
41+
histograms: Mutex<HashMap<String, opentelemetry::metrics::Histogram<f64>>>,
3642
}
3743

3844
impl OtlpMetricsExporter {
3945
/// Create a new OtlpMetricsExporter with the specified configuration
4046
/// Returns a Result containing the new exporter or an error if initialisation failed
4147
pub fn new(config: &OtlpConfig) -> Result<Self, Box<dyn std::error::Error>> {
48+
// Ensure endpoint ends with /v1/metrics
49+
let endpoint_url = if !config.endpoint.ends_with("/v1/metrics") {
50+
if config.endpoint.ends_with('/') {
51+
format!("{}v1/metrics", config.endpoint)
52+
} else {
53+
format!("{}/v1/metrics", config.endpoint)
54+
}
55+
} else {
56+
config.endpoint.to_string()
57+
};
58+
4259
// Initialise OTLP exporter using HTTP binary protocol with the specified endpoint
4360
let exporter = opentelemetry_otlp::MetricExporter::builder()
4461
.with_http()
4562
.with_protocol(Protocol::HttpBinary)
46-
.with_endpoint(&config.endpoint)
63+
.with_endpoint(&endpoint_url)
4764
.build()?;
4865

4966
// Create a meter provider with the OTLP Metric Exporter that will collect and export metrics at regular intervals
@@ -62,29 +79,55 @@ impl OtlpMetricsExporter {
6279
// The meter will be used to create specific metric instruments (counters, gauges, histograms) and record values to them
6380
let meter = global::meter("mountpoint-s3");
6481

65-
Ok(Self { meter })
82+
Ok(Self {
83+
meter,
84+
counters: Mutex::new(HashMap::new()),
85+
gauges: Mutex::new(HashMap::new()),
86+
histograms: Mutex::new(HashMap::new()),
87+
})
6688
}
6789

6890
/// Record a counter metric in OTel format
6991
pub fn record_counter(&self, key: &Key, value: u64, attributes: &[KeyValue]) {
70-
let counter = self.meter.u64_counter(key.name().to_string()).build();
71-
92+
let name = format!("mountpoint.{}", key.name());
93+
let mut counters = self.counters.lock().unwrap();
94+
let counter = counters
95+
.entry(name.clone())
96+
.or_insert_with(|| self.meter.u64_counter(name).build());
7297
counter.add(value, attributes);
7398
}
7499

75100
/// Record a gauge metric in OTel format
76101
pub fn record_gauge(&self, key: &Key, value: f64, attributes: &[KeyValue]) {
77-
let gauge = self.meter.f64_gauge(key.name().to_string()).build();
78-
102+
let name = format!("mountpoint.{}", key.name());
103+
let mut gauges = self.gauges.lock().unwrap();
104+
let gauge = gauges
105+
.entry(name.clone())
106+
.or_insert_with(|| self.meter.f64_gauge(name).build());
79107
gauge.record(value, attributes);
80108
}
81109

82110
/// Record a histogram metric in OTel format
83111
pub fn record_histogram(&self, key: &Key, value: f64, attributes: &[KeyValue]) {
84-
let histogram = self.meter.f64_histogram(key.name().to_string()).build();
85-
112+
let name = format!("mountpoint.{}", key.name());
113+
let mut histograms = self.histograms.lock().unwrap();
114+
let histogram = histograms
115+
.entry(name.clone())
116+
.or_insert_with(|| self.meter.f64_histogram(name).build());
86117
histogram.record(value, attributes);
87118
}
119+
120+
/// Record a metric using its MetricValue
121+
pub fn record_metric(&self, key: &Key, value: &MetricValue, attributes: &[KeyValue]) {
122+
match value {
123+
MetricValue::Counter(count) => self.record_counter(key, *count, attributes),
124+
MetricValue::Gauge(val) => self.record_gauge(key, *val, attributes),
125+
MetricValue::Histogram(_mean) => {
126+
// Do nothing for histograms for now
127+
// Will be implemented later
128+
}
129+
}
130+
}
88131
}
89132

90133
#[cfg(test)]

0 commit comments

Comments
 (0)