Skip to main content

lfsx_server/
metrics.rs

1use std::time::Instant;
2
3use axum::extract::{MatchedPath, Request, State};
4use axum::middleware::Next;
5use axum::response::Response;
6
7use crate::state::Shared;
8use prometheus::{
9    Encoder, Histogram, HistogramVec, IntCounter, IntCounterVec, IntGauge, Registry, TextEncoder,
10    histogram_opts, opts, register_histogram_vec_with_registry, register_histogram_with_registry,
11    register_int_counter_vec_with_registry, register_int_counter_with_registry,
12    register_int_gauge_with_registry,
13};
14
15pub struct Metrics {
16    registry: Registry,
17    pub requests: IntCounterVec,
18    pub duration: HistogramVec,
19    pub rejections: IntCounterVec,
20    pub uploaded_bytes: IntCounter,
21    pub downloaded_bytes: IntCounter,
22    pub object_size: Histogram,
23    pub objects_stored: IntGauge,
24    pub store_bytes: IntGauge,
25    pub store_scans: IntGauge,
26}
27
28const SIZE_BUCKETS: &[f64] = &[
29    1_024.0,
30    65_536.0,
31    1_048_576.0,
32    16_777_216.0,
33    134_217_728.0,
34    1_073_741_824.0,
35    8_589_934_592.0,
36];
37
38const DURATION_BUCKETS: &[f64] = &[0.005, 0.05, 0.5, 2.0, 10.0, 60.0, 300.0];
39
40impl Metrics {
41    pub fn new() -> Self {
42        let registry = Registry::new();
43
44        Self {
45            registry: registry.clone(),
46            requests: register_int_counter_vec_with_registry!(
47                opts!(
48                    "lfsx_requests_total",
49                    "Requests served, by route and status"
50                ),
51                &["route", "status"],
52                registry
53            )
54            .expect("metric"),
55            duration: register_histogram_vec_with_registry!(
56                histogram_opts!(
57                    "lfsx_request_duration_seconds",
58                    "Time to serve a request",
59                    DURATION_BUCKETS.to_vec()
60                ),
61                &["route"],
62                registry
63            )
64            .expect("metric"),
65            rejections: register_int_counter_vec_with_registry!(
66                opts!("lfsx_rejections_total", "Requests refused, by cause"),
67                &["cause"],
68                registry
69            )
70            .expect("metric"),
71            uploaded_bytes: register_int_counter_with_registry!(
72                opts!("lfsx_uploaded_bytes_total", "Object bytes accepted"),
73                registry
74            )
75            .expect("metric"),
76            downloaded_bytes: register_int_counter_with_registry!(
77                opts!("lfsx_downloaded_bytes_total", "Object bytes served"),
78                registry
79            )
80            .expect("metric"),
81            object_size: register_histogram_with_registry!(
82                histogram_opts!(
83                    "lfsx_object_size_bytes",
84                    "Size of objects accepted",
85                    SIZE_BUCKETS.to_vec()
86                ),
87                registry
88            )
89            .expect("metric"),
90            objects_stored: register_int_gauge_with_registry!(
91                opts!("lfsx_objects_stored", "Objects on disk at the last scrape"),
92                registry
93            )
94            .expect("metric"),
95            store_bytes: register_int_gauge_with_registry!(
96                opts!("lfsx_store_bytes", "Bytes on disk at the last scrape"),
97                registry
98            )
99            .expect("metric"),
100            store_scans: register_int_gauge_with_registry!(
101                opts!(
102                    "lfsx_store_scans",
103                    "Full walks of the store performed to measure it"
104                ),
105                registry
106            )
107            .expect("metric"),
108        }
109    }
110
111    pub fn render(&self) -> String {
112        let mut buffer = Vec::new();
113        let encoder = TextEncoder::new();
114
115        match encoder.encode(&self.registry.gather(), &mut buffer) {
116            Ok(()) => String::from_utf8(buffer).unwrap_or_default(),
117            Err(error) => {
118                tracing::error!(%error, "could not encode metrics");
119                String::new()
120            }
121        }
122    }
123}
124
125#[derive(Debug, Clone, Copy)]
126pub struct Cause(pub &'static str);
127
128impl Default for Metrics {
129    fn default() -> Self {
130        Self::new()
131    }
132}
133
134pub async fn record(State(state): State<Shared>, request: Request, next: Next) -> Response {
135    let route = request
136        .extensions()
137        .get::<MatchedPath>()
138        .map(|matched| matched.as_str().to_owned())
139        .unwrap_or_else(|| "<unmatched>".to_owned());
140
141    let started = Instant::now();
142    let response = next.run(request).await;
143
144    let metrics = &state.metrics;
145    metrics
146        .duration
147        .with_label_values(&[route.as_str()])
148        .observe(started.elapsed().as_secs_f64());
149    metrics
150        .requests
151        .with_label_values(&[route.as_str(), response.status().as_str()])
152        .inc();
153
154    if let Some(Cause(cause)) = response.extensions().get::<Cause>() {
155        metrics.rejections.with_label_values(&[cause]).inc();
156    }
157
158    response
159}