1use std::collections::HashMap;
34use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
35use std::sync::{Mutex, OnceLock};
36use std::time::Instant;
37
38use crate::application::Application;
39use crate::middleware::Middleware;
40use crate::request::Request;
41use crate::response::Response;
42use crate::server::ConnectionInfo;
43
44pub mod exposition;
45pub mod http;
46pub mod tls;
47
48pub static SERVER_READY: AtomicBool = AtomicBool::new(false);
55
56pub static REQUESTS_TOTAL: AtomicU64 = AtomicU64::new(0);
58
59pub static ERRORS_TOTAL: AtomicU64 = AtomicU64::new(0);
61
62pub static ACTIVE_CONNECTIONS: AtomicI64 = AtomicI64::new(0);
64
65pub static THREAD_POOL_QUEUED: AtomicI64 = AtomicI64::new(0);
67
68pub fn record_request() {
69 REQUESTS_TOTAL.fetch_add(1, Ordering::Relaxed);
70}
71
72pub fn record_error() {
73 ERRORS_TOTAL.fetch_add(1, Ordering::Relaxed);
74}
75
76pub fn connection_open() {
77 ACTIVE_CONNECTIONS.fetch_add(1, Ordering::Relaxed);
78}
79
80pub fn connection_close() {
81 ACTIVE_CONNECTIONS.fetch_sub(1, Ordering::Relaxed);
82}
83
84const BUCKET_BOUNDS: [f64; 11] = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0];
89
90struct HistogramEntry {
91 buckets: [u64; 11],
93 sum: f64,
94 count: u64,
95}
96
97impl HistogramEntry {
98 fn new() -> Self {
99 HistogramEntry { buckets: [0; 11], sum: 0.0, count: 0 }
100 }
101
102 fn observe(&mut self, secs: f64) {
103 for (i, &upper) in BUCKET_BOUNDS.iter().enumerate() {
104 if secs <= upper {
105 self.buckets[i] += 1;
106 }
107 }
108 self.sum += secs;
109 self.count += 1;
110 }
111}
112
113struct RouteEntry {
114 counts: HashMap<i16, u64>,
115 latency: HistogramEntry,
116}
117
118impl RouteEntry {
119 fn new() -> Self {
120 RouteEntry { counts: HashMap::new(), latency: HistogramEntry::new() }
121 }
122}
123
124struct RouteStore {
125 entries: HashMap<(String, String), RouteEntry>,
127}
128
129static ROUTE_STORE: OnceLock<Mutex<RouteStore>> = OnceLock::new();
130
131fn route_store() -> &'static Mutex<RouteStore> {
132 ROUTE_STORE.get_or_init(|| Mutex::new(RouteStore { entries: HashMap::new() }))
133}
134
135pub fn record_route(method: &str, path: &str, status: i16, elapsed_secs: f64) {
140 let key = (method.to_string(), path.to_string());
141 let mut guard = route_store().lock().unwrap();
142 let entry = guard.entries.entry(key).or_insert_with(RouteEntry::new);
143 *entry.counts.entry(status).or_insert(0) += 1;
144 entry.latency.observe(elapsed_secs);
145}
146
147fn strip_query(uri: &str) -> &str {
149 match uri.find('?') {
150 Some(i) => &uri[..i],
151 None => uri,
152 }
153}
154
155pub struct MetricsLayer;
170
171impl Middleware for MetricsLayer {
172 fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
174 true
175 }
176
177
178 fn handle(
179 &self,
180 request: &Request,
181 connection: &ConnectionInfo,
182 next: &dyn Application,
183 ) -> Result<Response, String> {
184 let start = Instant::now();
185 let result = next.execute(request, connection);
186 let elapsed = start.elapsed().as_secs_f64();
187
188 let path = strip_query(&request.request_uri).to_string();
189 let status = match &result {
190 Ok(r) => r.status_code,
191 Err(_) => 500,
192 };
193 record_route(&request.method, &path, status, elapsed);
194
195 result
196 }
197
198 fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
200 true
201 }
202}
203
204#[cfg(test)]
207mod tests;
208
209pub fn prometheus_text() -> String {
211 let mut out = prometheus_text_without_http();
212 let http_text = http::prometheus_text();
213 if !http_text.is_empty() {
214 out.push('\n');
215 out.push_str(&http_text);
216 }
217 out
218}
219
220fn prometheus_text_without_http() -> String {
223 let requests = REQUESTS_TOTAL.load(Ordering::Relaxed);
224 let errors = ERRORS_TOTAL.load(Ordering::Relaxed);
225 let active = ACTIVE_CONNECTIONS.load(Ordering::Relaxed);
226
227 let mut out = format!(
228 "# HELP rws_requests_total Total HTTP requests handled\n\
229 # TYPE rws_requests_total counter\n\
230 rws_requests_total {}\n\n\
231 # HELP rws_errors_total HTTP requests that returned an application error\n\
232 # TYPE rws_errors_total counter\n\
233 rws_errors_total {}\n\n\
234 # HELP rws_active_connections Currently open connections\n\
235 # TYPE rws_active_connections gauge\n\
236 rws_active_connections {}\n",
237 requests, errors, active
238 );
239
240 let route_text = route_prometheus_text();
241 if !route_text.is_empty() {
242 out.push('\n');
243 out.push_str(&route_text);
244 }
245
246 let upstream_text = crate::proxy_config::upstream::prometheus_text();
247 if !upstream_text.is_empty() {
248 out.push('\n');
249 out.push_str(&upstream_text);
250 }
251
252 let tls_text = tls::prometheus_text();
253 if !tls_text.is_empty() {
254 out.push('\n');
255 out.push_str(&tls_text);
256 }
257 let breaker_text = circuit_breaker_prometheus_text();
258 if !breaker_text.is_empty() {
259 out.push('\n');
260 out.push_str(&breaker_text);
261 }
262
263 out
264}
265
266#[cfg(target_arch = "wasm32")]
279fn circuit_breaker_prometheus_text() -> String {
280 String::new()
281}
282
283#[cfg(not(target_arch = "wasm32"))]
284fn circuit_breaker_prometheus_text() -> String {
285 let mut states = crate::circuit_breaker::global().lock().unwrap().all_states();
286 if states.is_empty() {
287 return String::new();
288 }
289 states.sort_by(|a, b| a.0.cmp(&b.0));
290
291 let mut out = String::new();
292 out.push_str("# HELP rws_circuit_breaker_state Circuit breaker state per backend (0=closed, 1=half_open, 2=open)\n");
293 out.push_str("# TYPE rws_circuit_breaker_state gauge\n");
294 for (backend, state) in states {
295 let value = match state {
296 crate::circuit_breaker::BreakerState::Closed => 0,
297 crate::circuit_breaker::BreakerState::HalfOpen => 1,
298 crate::circuit_breaker::BreakerState::Open => 2,
299 };
300 out.push_str(&format!("rws_circuit_breaker_state{{backend=\"{}\"}} {}\n", backend, value));
301 }
302 out
303}
304
305fn route_prometheus_text() -> String {
306 let guard = route_store().lock().unwrap();
307 if guard.entries.is_empty() {
308 return String::new();
309 }
310
311 let mut keys: Vec<&(String, String)> = guard.entries.keys().collect();
313 keys.sort();
314
315 let mut out = String::new();
316
317 out.push_str("# HELP rws_route_requests_total Total requests handled per route\n");
319 out.push_str("# TYPE rws_route_requests_total counter\n");
320 for key in &keys {
321 let entry = &guard.entries[key];
322 let (method, path) = key;
323 let mut statuses: Vec<i16> = entry.counts.keys().cloned().collect();
324 statuses.sort();
325 for status in statuses {
326 let count = entry.counts[&status];
327 out.push_str(&format!(
328 "rws_route_requests_total{{method=\"{}\",path=\"{}\",status=\"{}\"}} {}\n",
329 method, path, status, count,
330 ));
331 }
332 }
333
334 out.push('\n');
336 out.push_str("# HELP rws_route_duration_seconds Request duration in seconds per route\n");
337 out.push_str("# TYPE rws_route_duration_seconds histogram\n");
338 for key in &keys {
339 let entry = &guard.entries[key];
340 let (method, path) = key;
341 let lat = &entry.latency;
342
343 for (i, &upper) in BUCKET_BOUNDS.iter().enumerate() {
344 out.push_str(&format!(
345 "rws_route_duration_seconds_bucket{{method=\"{}\",path=\"{}\",le=\"{}\"}} {}\n",
346 method, path, upper, lat.buckets[i],
347 ));
348 }
349 out.push_str(&format!(
350 "rws_route_duration_seconds_bucket{{method=\"{}\",path=\"{}\",le=\"+Inf\"}} {}\n",
351 method, path, lat.count,
352 ));
353 out.push_str(&format!(
354 "rws_route_duration_seconds_sum{{method=\"{}\",path=\"{}\"}} {:.9}\n",
355 method, path, lat.sum,
356 ));
357 out.push_str(&format!(
358 "rws_route_duration_seconds_count{{method=\"{}\",path=\"{}\"}} {}\n",
359 method, path, lat.count,
360 ));
361 }
362
363 out
364}