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}