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 THREAD_POOL_QUEUED: AtomicI64 = AtomicI64::new(0);
70
71pub fn record_request() {
72 REQUESTS_TOTAL.fetch_add(1, Ordering::Relaxed);
73}
74
75pub fn record_error() {
76 ERRORS_TOTAL.fetch_add(1, Ordering::Relaxed);
77}
78
79pub fn connection_open() {
80 ACTIVE_CONNECTIONS.fetch_add(1, Ordering::Relaxed);
81}
82
83pub fn connection_close() {
84 ACTIVE_CONNECTIONS.fetch_sub(1, Ordering::Relaxed);
85}
86
87const 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];
92
93struct HistogramEntry {
94 buckets: [u64; 11],
96 sum: f64,
97 count: u64,
98}
99
100impl HistogramEntry {
101 fn new() -> Self {
102 HistogramEntry { buckets: [0; 11], sum: 0.0, count: 0 }
103 }
104
105 fn observe(&mut self, secs: f64) {
106 for (i, &upper) in BUCKET_BOUNDS.iter().enumerate() {
107 if secs <= upper {
108 self.buckets[i] += 1;
109 }
110 }
111 self.sum += secs;
112 self.count += 1;
113 }
114}
115
116struct RouteEntry {
117 counts: HashMap<i16, u64>,
118 latency: HistogramEntry,
119}
120
121impl RouteEntry {
122 fn new() -> Self {
123 RouteEntry { counts: HashMap::new(), latency: HistogramEntry::new() }
124 }
125}
126
127struct RouteStore {
128 entries: HashMap<(String, String), RouteEntry>,
130}
131
132static ROUTE_STORE: OnceLock<Mutex<RouteStore>> = OnceLock::new();
133
134fn route_store() -> &'static Mutex<RouteStore> {
135 ROUTE_STORE.get_or_init(|| Mutex::new(RouteStore { entries: HashMap::new() }))
136}
137
138pub fn record_route(method: &str, path: &str, status: i16, elapsed_secs: f64) {
143 let key = (method.to_string(), path.to_string());
144 let mut guard = route_store().lock().unwrap();
145 let entry = guard.entries.entry(key).or_insert_with(RouteEntry::new);
146 *entry.counts.entry(status).or_insert(0) += 1;
147 entry.latency.observe(elapsed_secs);
148}
149
150fn strip_query(uri: &str) -> &str {
152 match uri.find('?') {
153 Some(i) => &uri[..i],
154 None => uri,
155 }
156}
157
158pub struct MetricsLayer;
174
175impl Middleware for MetricsLayer {
176 fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
178 true
179 }
180
181
182 fn handle(
183 &self,
184 request: &Request,
185 connection: &ConnectionInfo,
186 next: &dyn Application,
187 ) -> Result<Response, String> {
188 let start = Instant::now();
189 let result = next.execute(request, connection);
190 let elapsed = start.elapsed().as_secs_f64();
191
192 let path = strip_query(&request.request_uri).to_string();
193 let status = match &result {
194 Ok(r) => r.status_code,
195 Err(_) => 500,
196 };
197 record_route(&request.method, &path, status, elapsed);
198
199 result
200 }
201
202 fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
204 true
205 }
206}
207
208#[cfg(test)]
211mod tests;
212
213pub fn prometheus_text() -> String {
215 let mut out = prometheus_text_without_http();
216 let http_text = http::prometheus_text();
217 if !http_text.is_empty() {
218 out.push('\n');
219 out.push_str(&http_text);
220 }
221 out
222}
223
224fn prometheus_text_without_http() -> String {
227 let requests = REQUESTS_TOTAL.load(Ordering::Relaxed);
228 let errors = ERRORS_TOTAL.load(Ordering::Relaxed);
229 let active = ACTIVE_CONNECTIONS.load(Ordering::Relaxed);
230
231 let mut out = format!(
232 "# HELP rws_requests_total Total HTTP requests handled\n\
233 # TYPE rws_requests_total counter\n\
234 rws_requests_total {}\n\n\
235 # HELP rws_errors_total HTTP requests that returned an application error\n\
236 # TYPE rws_errors_total counter\n\
237 rws_errors_total {}\n\n\
238 # HELP rws_active_connections Currently open connections\n\
239 # TYPE rws_active_connections gauge\n\
240 rws_active_connections {}\n",
241 requests, errors, active
242 );
243
244 let mut sections: Vec<(u16, fn() -> String)> = vec![
247 (section::ROUTES, route_prometheus_text),
248 (section::DISK, crate::disk_guard::prometheus_text),
249 (section::EARLY_HINTS, crate::early_hints::prometheus_text),
250 (section::TLS, tls::prometheus_text),
251 (section::CIRCUIT_BREAKER, circuit_breaker_prometheus_text),
252 ];
253 #[cfg(not(target_arch = "wasm32"))]
254 sections.push((section::LLM, llm::prometheus_text));
255 sections.extend(registered_sections());
256 sections.sort_by_key(|(order, _)| *order);
257 for (_, text) in sections {
258 let text = text();
259 if !text.is_empty() {
260 out.push('\n');
261 out.push_str(&text);
262 }
263 }
264
265 out
266}
267
268#[doc(hidden)]
271pub mod section {
272 pub const ROUTES: u16 = 10;
273 pub const UPSTREAMS: u16 = 20;
274 pub const WAF: u16 = 30;
275 pub const MIRROR: u16 = 40;
276 pub const DISK: u16 = 50;
277 pub const EARLY_HINTS: u16 = 60;
278 pub const WASM_PLUGINS: u16 = 70;
279 pub const LLM: u16 = 80;
280 pub const TLS: u16 = 90;
281 pub const CIRCUIT_BREAKER: u16 = 100;
282}
283
284static SECTIONS: std::sync::Mutex<Vec<(u16, fn() -> String)>> = std::sync::Mutex::new(Vec::new());
285
286#[doc(hidden)]
290pub fn register_section(order: u16, text: fn() -> String) {
291 let mut sections = SECTIONS.lock().unwrap_or_else(|e| e.into_inner());
292 if !sections.iter().any(|(o, _)| *o == order) {
293 sections.push((order, text));
294 }
295}
296
297fn registered_sections() -> Vec<(u16, fn() -> String)> {
298 SECTIONS.lock().unwrap_or_else(|e| e.into_inner()).clone()
299}
300
301#[cfg(target_arch = "wasm32")]
314fn circuit_breaker_prometheus_text() -> String {
315 String::new()
316}
317
318#[cfg(not(target_arch = "wasm32"))]
319fn circuit_breaker_prometheus_text() -> String {
320 let mut states = crate::circuit_breaker::global().lock().unwrap().all_states();
321 if states.is_empty() {
322 return String::new();
323 }
324 states.sort_by(|a, b| a.0.cmp(&b.0));
325
326 let mut out = String::new();
327 out.push_str("# HELP rws_circuit_breaker_state Circuit breaker state per backend (0=closed, 1=half_open, 2=open)\n");
328 out.push_str("# TYPE rws_circuit_breaker_state gauge\n");
329 for (backend, state) in states {
330 let value = match state {
331 crate::circuit_breaker::BreakerState::Closed => 0,
332 crate::circuit_breaker::BreakerState::HalfOpen => 1,
333 crate::circuit_breaker::BreakerState::Open => 2,
334 };
335 out.push_str(&format!("rws_circuit_breaker_state{{backend=\"{}\"}} {}\n", backend, value));
336 }
337 out
338}
339
340fn route_prometheus_text() -> String {
341 let guard = route_store().lock().unwrap();
342 if guard.entries.is_empty() {
343 return String::new();
344 }
345
346 let mut keys: Vec<&(String, String)> = guard.entries.keys().collect();
348 keys.sort();
349
350 let mut out = String::new();
351
352 out.push_str("# HELP rws_route_requests_total Total requests handled per route\n");
354 out.push_str("# TYPE rws_route_requests_total counter\n");
355 for key in &keys {
356 let entry = &guard.entries[*key];
357 let (method, path) = key;
358 let mut statuses: Vec<i16> = entry.counts.keys().cloned().collect();
359 statuses.sort();
360 for status in statuses {
361 let count = entry.counts[&status];
362 out.push_str(&format!(
363 "rws_route_requests_total{{method=\"{}\",path=\"{}\",status=\"{}\"}} {}\n",
364 method, path, status, count,
365 ));
366 }
367 }
368
369 out.push('\n');
371 out.push_str("# HELP rws_route_duration_seconds Request duration in seconds per route\n");
372 out.push_str("# TYPE rws_route_duration_seconds histogram\n");
373 for key in &keys {
374 let entry = &guard.entries[*key];
375 let (method, path) = key;
376 let lat = &entry.latency;
377
378 for (i, &upper) in BUCKET_BOUNDS.iter().enumerate() {
379 out.push_str(&format!(
380 "rws_route_duration_seconds_bucket{{method=\"{}\",path=\"{}\",le=\"{}\"}} {}\n",
381 method, path, upper, lat.buckets[i],
382 ));
383 }
384 out.push_str(&format!(
385 "rws_route_duration_seconds_bucket{{method=\"{}\",path=\"{}\",le=\"+Inf\"}} {}\n",
386 method, path, lat.count,
387 ));
388 out.push_str(&format!(
389 "rws_route_duration_seconds_sum{{method=\"{}\",path=\"{}\"}} {:.9}\n",
390 method, path, lat.sum,
391 ));
392 out.push_str(&format!(
393 "rws_route_duration_seconds_count{{method=\"{}\",path=\"{}\"}} {}\n",
394 method, path, lat.count,
395 ));
396 }
397
398 out
399}