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;
46#[cfg(not(target_arch = "wasm32"))]
47pub mod llm;
48pub mod tls;
49
50pub static SERVER_READY: AtomicBool = AtomicBool::new(false);
57
58pub static REQUESTS_TOTAL: AtomicU64 = AtomicU64::new(0);
60
61pub static ERRORS_TOTAL: AtomicU64 = AtomicU64::new(0);
63
64pub static ACTIVE_CONNECTIONS: AtomicI64 = AtomicI64::new(0);
66
67pub static THREAD_POOL_QUEUED: AtomicI64 = AtomicI64::new(0);
69
70pub fn record_request() {
71 REQUESTS_TOTAL.fetch_add(1, Ordering::Relaxed);
72}
73
74pub fn record_error() {
75 ERRORS_TOTAL.fetch_add(1, Ordering::Relaxed);
76}
77
78pub fn connection_open() {
79 ACTIVE_CONNECTIONS.fetch_add(1, Ordering::Relaxed);
80}
81
82pub fn connection_close() {
83 ACTIVE_CONNECTIONS.fetch_sub(1, Ordering::Relaxed);
84}
85
86const 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];
91
92struct HistogramEntry {
93 buckets: [u64; 11],
95 sum: f64,
96 count: u64,
97}
98
99impl HistogramEntry {
100 fn new() -> Self {
101 HistogramEntry { buckets: [0; 11], sum: 0.0, count: 0 }
102 }
103
104 fn observe(&mut self, secs: f64) {
105 for (i, &upper) in BUCKET_BOUNDS.iter().enumerate() {
106 if secs <= upper {
107 self.buckets[i] += 1;
108 }
109 }
110 self.sum += secs;
111 self.count += 1;
112 }
113}
114
115struct RouteEntry {
116 counts: HashMap<i16, u64>,
117 latency: HistogramEntry,
118}
119
120impl RouteEntry {
121 fn new() -> Self {
122 RouteEntry { counts: HashMap::new(), latency: HistogramEntry::new() }
123 }
124}
125
126struct RouteStore {
127 entries: HashMap<(String, String), RouteEntry>,
129}
130
131static ROUTE_STORE: OnceLock<Mutex<RouteStore>> = OnceLock::new();
132
133fn route_store() -> &'static Mutex<RouteStore> {
134 ROUTE_STORE.get_or_init(|| Mutex::new(RouteStore { entries: HashMap::new() }))
135}
136
137pub fn record_route(method: &str, path: &str, status: i16, elapsed_secs: f64) {
142 let key = (method.to_string(), path.to_string());
143 let mut guard = route_store().lock().unwrap();
144 let entry = guard.entries.entry(key).or_insert_with(RouteEntry::new);
145 *entry.counts.entry(status).or_insert(0) += 1;
146 entry.latency.observe(elapsed_secs);
147}
148
149fn strip_query(uri: &str) -> &str {
151 match uri.find('?') {
152 Some(i) => &uri[..i],
153 None => uri,
154 }
155}
156
157pub struct MetricsLayer;
172
173impl Middleware for MetricsLayer {
174 fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
176 true
177 }
178
179
180 fn handle(
181 &self,
182 request: &Request,
183 connection: &ConnectionInfo,
184 next: &dyn Application,
185 ) -> Result<Response, String> {
186 let start = Instant::now();
187 let result = next.execute(request, connection);
188 let elapsed = start.elapsed().as_secs_f64();
189
190 let path = strip_query(&request.request_uri).to_string();
191 let status = match &result {
192 Ok(r) => r.status_code,
193 Err(_) => 500,
194 };
195 record_route(&request.method, &path, status, elapsed);
196
197 result
198 }
199
200 fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
202 true
203 }
204}
205
206#[cfg(test)]
209mod tests;
210
211pub fn prometheus_text() -> String {
213 let mut out = prometheus_text_without_http();
214 let http_text = http::prometheus_text();
215 if !http_text.is_empty() {
216 out.push('\n');
217 out.push_str(&http_text);
218 }
219 out
220}
221
222fn prometheus_text_without_http() -> String {
225 let requests = REQUESTS_TOTAL.load(Ordering::Relaxed);
226 let errors = ERRORS_TOTAL.load(Ordering::Relaxed);
227 let active = ACTIVE_CONNECTIONS.load(Ordering::Relaxed);
228
229 let mut out = format!(
230 "# HELP rws_requests_total Total HTTP requests handled\n\
231 # TYPE rws_requests_total counter\n\
232 rws_requests_total {}\n\n\
233 # HELP rws_errors_total HTTP requests that returned an application error\n\
234 # TYPE rws_errors_total counter\n\
235 rws_errors_total {}\n\n\
236 # HELP rws_active_connections Currently open connections\n\
237 # TYPE rws_active_connections gauge\n\
238 rws_active_connections {}\n",
239 requests, errors, active
240 );
241
242 let route_text = route_prometheus_text();
243 if !route_text.is_empty() {
244 out.push('\n');
245 out.push_str(&route_text);
246 }
247
248 #[cfg(not(target_arch = "wasm32"))]
249 let upstream_text = crate::proxy_config::upstream::prometheus_text();
250 #[cfg(target_arch = "wasm32")]
251 let upstream_text = String::new();
252 if !upstream_text.is_empty() {
253 out.push('\n');
254 out.push_str(&upstream_text);
255 }
256 let waf_text = crate::waf::prometheus_text();
257 if !waf_text.is_empty() {
258 out.push('\n');
259 out.push_str(&waf_text);
260 }
261
262 #[cfg(not(target_arch = "wasm32"))]
263 {
264 let mirror_text = crate::proxy::mirror::prometheus_text();
265 if !mirror_text.is_empty() {
266 out.push('\n');
267 out.push_str(&mirror_text);
268 }
269 }
270
271 let disk_text = crate::disk_guard::prometheus_text();
272 if !disk_text.is_empty() {
273 out.push('\n');
274 out.push_str(&disk_text);
275 }
276
277 let hints_text = crate::early_hints::prometheus_text();
278 if !hints_text.is_empty() {
279 out.push('\n');
280 out.push_str(&hints_text);
281 }
282
283 #[cfg(feature = "wasm-plugins")]
284 {
285 let wasm_text = crate::wasm_plugin::metrics::prometheus_text();
286 if !wasm_text.is_empty() {
287 out.push('\n');
288 out.push_str(&wasm_text);
289 }
290 }
291
292 #[cfg(not(target_arch = "wasm32"))]
293 {
294 let llm_text = llm::prometheus_text();
295 if !llm_text.is_empty() {
296 out.push('\n');
297 out.push_str(&llm_text);
298 }
299 }
300
301 let tls_text = tls::prometheus_text();
302 if !tls_text.is_empty() {
303 out.push('\n');
304 out.push_str(&tls_text);
305 }
306 let breaker_text = circuit_breaker_prometheus_text();
307 if !breaker_text.is_empty() {
308 out.push('\n');
309 out.push_str(&breaker_text);
310 }
311
312 out
313}
314
315#[cfg(target_arch = "wasm32")]
328fn circuit_breaker_prometheus_text() -> String {
329 String::new()
330}
331
332#[cfg(not(target_arch = "wasm32"))]
333fn circuit_breaker_prometheus_text() -> String {
334 let mut states = crate::circuit_breaker::global().lock().unwrap().all_states();
335 if states.is_empty() {
336 return String::new();
337 }
338 states.sort_by(|a, b| a.0.cmp(&b.0));
339
340 let mut out = String::new();
341 out.push_str("# HELP rws_circuit_breaker_state Circuit breaker state per backend (0=closed, 1=half_open, 2=open)\n");
342 out.push_str("# TYPE rws_circuit_breaker_state gauge\n");
343 for (backend, state) in states {
344 let value = match state {
345 crate::circuit_breaker::BreakerState::Closed => 0,
346 crate::circuit_breaker::BreakerState::HalfOpen => 1,
347 crate::circuit_breaker::BreakerState::Open => 2,
348 };
349 out.push_str(&format!("rws_circuit_breaker_state{{backend=\"{}\"}} {}\n", backend, value));
350 }
351 out
352}
353
354fn route_prometheus_text() -> String {
355 let guard = route_store().lock().unwrap();
356 if guard.entries.is_empty() {
357 return String::new();
358 }
359
360 let mut keys: Vec<&(String, String)> = guard.entries.keys().collect();
362 keys.sort();
363
364 let mut out = String::new();
365
366 out.push_str("# HELP rws_route_requests_total Total requests handled per route\n");
368 out.push_str("# TYPE rws_route_requests_total counter\n");
369 for key in &keys {
370 let entry = &guard.entries[*key];
371 let (method, path) = key;
372 let mut statuses: Vec<i16> = entry.counts.keys().cloned().collect();
373 statuses.sort();
374 for status in statuses {
375 let count = entry.counts[&status];
376 out.push_str(&format!(
377 "rws_route_requests_total{{method=\"{}\",path=\"{}\",status=\"{}\"}} {}\n",
378 method, path, status, count,
379 ));
380 }
381 }
382
383 out.push('\n');
385 out.push_str("# HELP rws_route_duration_seconds Request duration in seconds per route\n");
386 out.push_str("# TYPE rws_route_duration_seconds histogram\n");
387 for key in &keys {
388 let entry = &guard.entries[*key];
389 let (method, path) = key;
390 let lat = &entry.latency;
391
392 for (i, &upper) in BUCKET_BOUNDS.iter().enumerate() {
393 out.push_str(&format!(
394 "rws_route_duration_seconds_bucket{{method=\"{}\",path=\"{}\",le=\"{}\"}} {}\n",
395 method, path, upper, lat.buckets[i],
396 ));
397 }
398 out.push_str(&format!(
399 "rws_route_duration_seconds_bucket{{method=\"{}\",path=\"{}\",le=\"+Inf\"}} {}\n",
400 method, path, lat.count,
401 ));
402 out.push_str(&format!(
403 "rws_route_duration_seconds_sum{{method=\"{}\",path=\"{}\"}} {:.9}\n",
404 method, path, lat.sum,
405 ));
406 out.push_str(&format!(
407 "rws_route_duration_seconds_count{{method=\"{}\",path=\"{}\"}} {}\n",
408 method, path, lat.count,
409 ));
410 }
411
412 out
413}