1use std::collections::HashMap;
35use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
36use std::sync::{Mutex, OnceLock};
37use std::time::Instant;
38
39use crate::application::Application;
40use crate::middleware::Middleware;
41use crate::request::Request;
42use crate::response::Response;
43use crate::server::ConnectionInfo;
44
45pub mod exposition;
46pub mod http;
47#[cfg(not(target_arch = "wasm32"))]
48pub mod llm;
49pub mod tls;
50
51pub static SERVER_READY: AtomicBool = AtomicBool::new(false);
58
59pub static REQUESTS_TOTAL: AtomicU64 = AtomicU64::new(0);
61
62pub static ERRORS_TOTAL: AtomicU64 = AtomicU64::new(0);
64
65pub static ACTIVE_CONNECTIONS: AtomicI64 = AtomicI64::new(0);
67
68pub static REQUESTS_IN_FLIGHT: AtomicI64 = AtomicI64::new(0);
71
72pub struct InFlight(());
74
75impl InFlight {
76 pub fn start() -> Self {
77 REQUESTS_IN_FLIGHT.fetch_add(1, Ordering::SeqCst);
78 InFlight(())
79 }
80}
81
82impl Drop for InFlight {
83 fn drop(&mut self) {
84 REQUESTS_IN_FLIGHT.fetch_sub(1, Ordering::SeqCst);
85 }
86}
87
88pub static THREAD_POOL_QUEUED: AtomicI64 = AtomicI64::new(0);
90
91pub fn record_request() {
92 REQUESTS_TOTAL.fetch_add(1, Ordering::Relaxed);
93}
94
95pub fn record_error() {
96 ERRORS_TOTAL.fetch_add(1, Ordering::Relaxed);
97}
98
99pub fn connection_open() {
100 ACTIVE_CONNECTIONS.fetch_add(1, Ordering::Relaxed);
101}
102
103pub fn connection_close() {
104 ACTIVE_CONNECTIONS.fetch_sub(1, Ordering::Relaxed);
105}
106
107const 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];
112
113struct HistogramEntry {
114 buckets: [u64; 11],
116 sum: f64,
117 count: u64,
118}
119
120impl HistogramEntry {
121 fn new() -> Self {
122 HistogramEntry { buckets: [0; 11], sum: 0.0, count: 0 }
123 }
124
125 fn observe(&mut self, secs: f64) {
126 for (i, &upper) in BUCKET_BOUNDS.iter().enumerate() {
127 if secs <= upper {
128 self.buckets[i] += 1;
129 }
130 }
131 self.sum += secs;
132 self.count += 1;
133 }
134}
135
136struct RouteEntry {
137 counts: HashMap<i16, u64>,
138 latency: HistogramEntry,
139}
140
141impl RouteEntry {
142 fn new() -> Self {
143 RouteEntry { counts: HashMap::new(), latency: HistogramEntry::new() }
144 }
145}
146
147struct RouteStore {
148 entries: HashMap<(String, String), RouteEntry>,
150}
151
152static ROUTE_STORE: OnceLock<Mutex<RouteStore>> = OnceLock::new();
153
154fn route_store() -> &'static Mutex<RouteStore> {
155 ROUTE_STORE.get_or_init(|| Mutex::new(RouteStore { entries: HashMap::new() }))
156}
157
158pub fn record_route(method: &str, path: &str, status: i16, elapsed_secs: f64) {
163 let key = (method.to_string(), path.to_string());
164 let mut guard = route_store().lock().unwrap();
165 let entry = guard.entries.entry(key).or_insert_with(RouteEntry::new);
166 *entry.counts.entry(status).or_insert(0) += 1;
167 entry.latency.observe(elapsed_secs);
168}
169
170fn strip_query(uri: &str) -> &str {
172 match uri.find('?') {
173 Some(i) => &uri[..i],
174 None => uri,
175 }
176}
177
178pub struct MetricsLayer;
194
195impl Middleware for MetricsLayer {
196 fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
198 true
199 }
200
201
202 fn handle(
203 &self,
204 request: &Request,
205 connection: &ConnectionInfo,
206 next: &dyn Application,
207 ) -> Result<Response, String> {
208 let start = Instant::now();
209 let result = next.execute(request, connection);
210 let elapsed = start.elapsed().as_secs_f64();
211
212 let path = strip_query(&request.request_uri).to_string();
213 let status = match &result {
214 Ok(r) => r.status_code,
215 Err(_) => 500,
216 };
217 record_route(&request.method, &path, status, elapsed);
218
219 result
220 }
221
222 fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
224 true
225 }
226}
227
228#[cfg(test)]
231mod tests;
232
233pub fn prometheus_text() -> String {
235 let mut out = prometheus_text_without_http();
236 let http_text = http::prometheus_text();
237 if !http_text.is_empty() {
238 out.push('\n');
239 out.push_str(&http_text);
240 }
241 out
242}
243
244fn prometheus_text_without_http() -> String {
247 let requests = REQUESTS_TOTAL.load(Ordering::Relaxed);
248 let errors = ERRORS_TOTAL.load(Ordering::Relaxed);
249 let active = ACTIVE_CONNECTIONS.load(Ordering::Relaxed);
250
251 let mut out = format!(
252 "# HELP rws_requests_total Total HTTP requests handled\n\
253 # TYPE rws_requests_total counter\n\
254 rws_requests_total {}\n\n\
255 # HELP rws_errors_total HTTP requests that returned an application error\n\
256 # TYPE rws_errors_total counter\n\
257 rws_errors_total {}\n\n\
258 # HELP rws_active_connections Currently open connections\n\
259 # TYPE rws_active_connections gauge\n\
260 rws_active_connections {}\n",
261 requests, errors, active
262 );
263
264 let mut sections: Vec<(u16, fn() -> String)> = vec![
267 (section::ROUTES, route_prometheus_text),
268 (section::DISK, crate::disk_guard::prometheus_text),
269 (section::EARLY_HINTS, crate::early_hints::prometheus_text),
270 (section::TLS, tls::prometheus_text),
271 (section::CIRCUIT_BREAKER, circuit_breaker_prometheus_text),
272 ];
273 #[cfg(not(target_arch = "wasm32"))]
274 sections.push((section::LLM, llm::prometheus_text));
275 sections.extend(registered_sections());
276 sections.sort_by_key(|(order, _)| *order);
277 for (_, text) in sections {
278 let text = text();
279 if !text.is_empty() {
280 out.push('\n');
281 out.push_str(&text);
282 }
283 }
284
285 out
286}
287
288#[doc(hidden)]
291pub mod section {
292 pub const ROUTES: u16 = 10;
293 pub const UPSTREAMS: u16 = 20;
294 pub const WAF: u16 = 30;
295 pub const MIRROR: u16 = 40;
296 pub const DISK: u16 = 50;
297 pub const EARLY_HINTS: u16 = 60;
298 pub const WASM_PLUGINS: u16 = 70;
299 pub const LLM: u16 = 80;
300 pub const TLS: u16 = 90;
301 pub const CIRCUIT_BREAKER: u16 = 100;
302 pub const CONTROLLERS: u16 = 110;
304}
305
306static SECTIONS: std::sync::Mutex<Vec<(u16, fn() -> String)>> = std::sync::Mutex::new(Vec::new());
307
308#[doc(hidden)]
312pub fn register_section(order: u16, text: fn() -> String) {
313 let mut sections = SECTIONS.lock().unwrap_or_else(|e| e.into_inner());
314 if !sections.iter().any(|(o, _)| *o == order) {
315 sections.push((order, text));
316 }
317}
318
319fn registered_sections() -> Vec<(u16, fn() -> String)> {
320 SECTIONS.lock().unwrap_or_else(|e| e.into_inner()).clone()
321}
322
323#[cfg(target_arch = "wasm32")]
336fn circuit_breaker_prometheus_text() -> String {
337 String::new()
338}
339
340#[cfg(not(target_arch = "wasm32"))]
341fn circuit_breaker_prometheus_text() -> String {
342 let mut states = crate::circuit_breaker::global().lock().unwrap().all_states();
343 if states.is_empty() {
344 return String::new();
345 }
346 states.sort_by(|a, b| a.0.cmp(&b.0));
347
348 let mut out = String::new();
349 out.push_str("# HELP rws_circuit_breaker_state Circuit breaker state per backend (0=closed, 1=half_open, 2=open)\n");
350 out.push_str("# TYPE rws_circuit_breaker_state gauge\n");
351 for (backend, state) in states {
352 let value = match state {
353 crate::circuit_breaker::BreakerState::Closed => 0,
354 crate::circuit_breaker::BreakerState::HalfOpen => 1,
355 crate::circuit_breaker::BreakerState::Open => 2,
356 };
357 out.push_str(&format!("rws_circuit_breaker_state{{backend=\"{}\"}} {}\n", backend, value));
358 }
359 out
360}
361
362fn route_prometheus_text() -> String {
363 let guard = route_store().lock().unwrap();
364 if guard.entries.is_empty() {
365 return String::new();
366 }
367
368 let mut keys: Vec<&(String, String)> = guard.entries.keys().collect();
370 keys.sort();
371
372 let mut out = String::new();
373
374 out.push_str("# HELP rws_route_requests_total Total requests handled per route\n");
376 out.push_str("# TYPE rws_route_requests_total counter\n");
377 for key in &keys {
378 let entry = &guard.entries[*key];
379 let (method, path) = key;
380 let mut statuses: Vec<i16> = entry.counts.keys().cloned().collect();
381 statuses.sort();
382 for status in statuses {
383 let count = entry.counts[&status];
384 out.push_str(&format!(
385 "rws_route_requests_total{{method=\"{}\",path=\"{}\",status=\"{}\"}} {}\n",
386 method, path, status, count,
387 ));
388 }
389 }
390
391 out.push('\n');
393 out.push_str("# HELP rws_route_duration_seconds Request duration in seconds per route\n");
394 out.push_str("# TYPE rws_route_duration_seconds histogram\n");
395 for key in &keys {
396 let entry = &guard.entries[*key];
397 let (method, path) = key;
398 let lat = &entry.latency;
399
400 for (i, &upper) in BUCKET_BOUNDS.iter().enumerate() {
401 out.push_str(&format!(
402 "rws_route_duration_seconds_bucket{{method=\"{}\",path=\"{}\",le=\"{}\"}} {}\n",
403 method, path, upper, lat.buckets[i],
404 ));
405 }
406 out.push_str(&format!(
407 "rws_route_duration_seconds_bucket{{method=\"{}\",path=\"{}\",le=\"+Inf\"}} {}\n",
408 method, path, lat.count,
409 ));
410 out.push_str(&format!(
411 "rws_route_duration_seconds_sum{{method=\"{}\",path=\"{}\"}} {:.9}\n",
412 method, path, lat.sum,
413 ));
414 out.push_str(&format!(
415 "rws_route_duration_seconds_count{{method=\"{}\",path=\"{}\"}} {}\n",
416 method, path, lat.count,
417 ));
418 }
419
420 out
421}