1use std::collections::HashMap;
29use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
30use std::sync::{Mutex, OnceLock};
31use std::time::Instant;
32
33use crate::application::Application;
34use crate::middleware::Middleware;
35use crate::request::Request;
36use crate::response::Response;
37use crate::server::ConnectionInfo;
38
39pub static SERVER_READY: AtomicBool = AtomicBool::new(false);
46
47pub static REQUESTS_TOTAL: AtomicU64 = AtomicU64::new(0);
49
50pub static ERRORS_TOTAL: AtomicU64 = AtomicU64::new(0);
52
53pub static ACTIVE_CONNECTIONS: AtomicI64 = AtomicI64::new(0);
55
56pub static THREAD_POOL_QUEUED: AtomicI64 = AtomicI64::new(0);
58
59pub fn record_request() {
60 REQUESTS_TOTAL.fetch_add(1, Ordering::Relaxed);
61}
62
63pub fn record_error() {
64 ERRORS_TOTAL.fetch_add(1, Ordering::Relaxed);
65}
66
67pub fn connection_open() {
68 ACTIVE_CONNECTIONS.fetch_add(1, Ordering::Relaxed);
69}
70
71pub fn connection_close() {
72 ACTIVE_CONNECTIONS.fetch_sub(1, Ordering::Relaxed);
73}
74
75const 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];
80
81struct HistogramEntry {
82 buckets: [u64; 11],
84 sum: f64,
85 count: u64,
86}
87
88impl HistogramEntry {
89 fn new() -> Self {
90 HistogramEntry { buckets: [0; 11], sum: 0.0, count: 0 }
91 }
92
93 fn observe(&mut self, secs: f64) {
94 for (i, &upper) in BUCKET_BOUNDS.iter().enumerate() {
95 if secs <= upper {
96 self.buckets[i] += 1;
97 }
98 }
99 self.sum += secs;
100 self.count += 1;
101 }
102}
103
104struct RouteEntry {
105 counts: HashMap<i16, u64>,
106 latency: HistogramEntry,
107}
108
109impl RouteEntry {
110 fn new() -> Self {
111 RouteEntry { counts: HashMap::new(), latency: HistogramEntry::new() }
112 }
113}
114
115struct RouteStore {
116 entries: HashMap<(String, String), RouteEntry>,
118}
119
120static ROUTE_STORE: OnceLock<Mutex<RouteStore>> = OnceLock::new();
121
122fn route_store() -> &'static Mutex<RouteStore> {
123 ROUTE_STORE.get_or_init(|| Mutex::new(RouteStore { entries: HashMap::new() }))
124}
125
126pub fn record_route(method: &str, path: &str, status: i16, elapsed_secs: f64) {
131 let key = (method.to_string(), path.to_string());
132 let mut guard = route_store().lock().unwrap();
133 let entry = guard.entries.entry(key).or_insert_with(RouteEntry::new);
134 *entry.counts.entry(status).or_insert(0) += 1;
135 entry.latency.observe(elapsed_secs);
136}
137
138fn strip_query(uri: &str) -> &str {
140 match uri.find('?') {
141 Some(i) => &uri[..i],
142 None => uri,
143 }
144}
145
146pub struct MetricsLayer;
161
162impl Middleware for MetricsLayer {
163 fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
165 true
166 }
167
168
169 fn handle(
170 &self,
171 request: &Request,
172 connection: &ConnectionInfo,
173 next: &dyn Application,
174 ) -> Result<Response, String> {
175 let start = Instant::now();
176 let result = next.execute(request, connection);
177 let elapsed = start.elapsed().as_secs_f64();
178
179 let path = strip_query(&request.request_uri).to_string();
180 let status = match &result {
181 Ok(r) => r.status_code,
182 Err(_) => 500,
183 };
184 record_route(&request.method, &path, status, elapsed);
185
186 result
187 }
188
189 fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
191 true
192 }
193}
194
195#[cfg(test)]
198mod tests;
199
200pub fn prometheus_text() -> String {
202 let requests = REQUESTS_TOTAL.load(Ordering::Relaxed);
203 let errors = ERRORS_TOTAL.load(Ordering::Relaxed);
204 let active = ACTIVE_CONNECTIONS.load(Ordering::Relaxed);
205
206 let mut out = format!(
207 "# HELP rws_requests_total Total HTTP requests handled\n\
208 # TYPE rws_requests_total counter\n\
209 rws_requests_total {}\n\n\
210 # HELP rws_errors_total HTTP requests that returned an application error\n\
211 # TYPE rws_errors_total counter\n\
212 rws_errors_total {}\n\n\
213 # HELP rws_active_connections Currently open connections\n\
214 # TYPE rws_active_connections gauge\n\
215 rws_active_connections {}\n",
216 requests, errors, active
217 );
218
219 let route_text = route_prometheus_text();
220 if !route_text.is_empty() {
221 out.push('\n');
222 out.push_str(&route_text);
223 }
224
225 let breaker_text = circuit_breaker_prometheus_text();
226 if !breaker_text.is_empty() {
227 out.push('\n');
228 out.push_str(&breaker_text);
229 }
230
231 out
232}
233
234#[cfg(target_arch = "wasm32")]
247fn circuit_breaker_prometheus_text() -> String {
248 String::new()
249}
250
251#[cfg(not(target_arch = "wasm32"))]
252fn circuit_breaker_prometheus_text() -> String {
253 let mut states = crate::circuit_breaker::global().lock().unwrap().all_states();
254 if states.is_empty() {
255 return String::new();
256 }
257 states.sort_by(|a, b| a.0.cmp(&b.0));
258
259 let mut out = String::new();
260 out.push_str("# HELP rws_circuit_breaker_state Circuit breaker state per backend (0=closed, 1=half_open, 2=open)\n");
261 out.push_str("# TYPE rws_circuit_breaker_state gauge\n");
262 for (backend, state) in states {
263 let value = match state {
264 crate::circuit_breaker::BreakerState::Closed => 0,
265 crate::circuit_breaker::BreakerState::HalfOpen => 1,
266 crate::circuit_breaker::BreakerState::Open => 2,
267 };
268 out.push_str(&format!("rws_circuit_breaker_state{{backend=\"{}\"}} {}\n", backend, value));
269 }
270 out
271}
272
273fn route_prometheus_text() -> String {
274 let guard = route_store().lock().unwrap();
275 if guard.entries.is_empty() {
276 return String::new();
277 }
278
279 let mut keys: Vec<&(String, String)> = guard.entries.keys().collect();
281 keys.sort();
282
283 let mut out = String::new();
284
285 out.push_str("# HELP rws_route_requests_total Total requests handled per route\n");
287 out.push_str("# TYPE rws_route_requests_total counter\n");
288 for key in &keys {
289 let entry = &guard.entries[key];
290 let (method, path) = key;
291 let mut statuses: Vec<i16> = entry.counts.keys().cloned().collect();
292 statuses.sort();
293 for status in statuses {
294 let count = entry.counts[&status];
295 out.push_str(&format!(
296 "rws_route_requests_total{{method=\"{}\",path=\"{}\",status=\"{}\"}} {}\n",
297 method, path, status, count,
298 ));
299 }
300 }
301
302 out.push('\n');
304 out.push_str("# HELP rws_route_duration_seconds Request duration in seconds per route\n");
305 out.push_str("# TYPE rws_route_duration_seconds histogram\n");
306 for key in &keys {
307 let entry = &guard.entries[key];
308 let (method, path) = key;
309 let lat = &entry.latency;
310
311 for (i, &upper) in BUCKET_BOUNDS.iter().enumerate() {
312 out.push_str(&format!(
313 "rws_route_duration_seconds_bucket{{method=\"{}\",path=\"{}\",le=\"{}\"}} {}\n",
314 method, path, upper, lat.buckets[i],
315 ));
316 }
317 out.push_str(&format!(
318 "rws_route_duration_seconds_bucket{{method=\"{}\",path=\"{}\",le=\"+Inf\"}} {}\n",
319 method, path, lat.count,
320 ));
321 out.push_str(&format!(
322 "rws_route_duration_seconds_sum{{method=\"{}\",path=\"{}\"}} {:.9}\n",
323 method, path, lat.sum,
324 ));
325 out.push_str(&format!(
326 "rws_route_duration_seconds_count{{method=\"{}\",path=\"{}\"}} {}\n",
327 method, path, lat.count,
328 ));
329 }
330
331 out
332}