1use std::fmt::Write;
29use std::sync::atomic::{AtomicU64, Ordering};
30use std::sync::Arc;
31
32use super::http_connection_limiter::HttpConnectionLimiter;
33use crate::transport::execution_admission::ExecutionAdmission;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum HttpTransport {
37 Http,
38 Https,
39}
40
41impl HttpTransport {
42 fn label(self) -> &'static str {
43 match self {
44 HttpTransport::Http => "http",
45 HttpTransport::Https => "https",
46 }
47 }
48
49 fn index(self) -> usize {
50 match self {
51 HttpTransport::Http => 0,
52 HttpTransport::Https => 1,
53 }
54 }
55}
56
57#[derive(Debug, Clone, Copy)]
58pub enum HttpRejectReason {
59 CapExhausted,
60 HandlerTimeout,
61 ExecutionQueueFull,
62 ExecutionQueueTimeout,
63 PrincipalCapExhausted,
66}
67
68const REJECT_REASONS: usize = 5;
71
72impl HttpRejectReason {
73 fn label(self) -> &'static str {
74 match self {
75 HttpRejectReason::CapExhausted => "cap_exhausted",
76 HttpRejectReason::HandlerTimeout => "handler_timeout",
77 HttpRejectReason::ExecutionQueueFull => "execution_queue_full",
78 HttpRejectReason::ExecutionQueueTimeout => "execution_queue_timeout",
79 HttpRejectReason::PrincipalCapExhausted => "principal_cap_exhausted",
80 }
81 }
82
83 fn index(self) -> usize {
84 match self {
85 HttpRejectReason::CapExhausted => 0,
86 HttpRejectReason::HandlerTimeout => 1,
87 HttpRejectReason::ExecutionQueueFull => 2,
88 HttpRejectReason::ExecutionQueueTimeout => 3,
89 HttpRejectReason::PrincipalCapExhausted => 4,
90 }
91 }
92
93 fn all() -> [HttpRejectReason; REJECT_REASONS] {
95 [
96 HttpRejectReason::CapExhausted,
97 HttpRejectReason::HandlerTimeout,
98 HttpRejectReason::ExecutionQueueFull,
99 HttpRejectReason::ExecutionQueueTimeout,
100 HttpRejectReason::PrincipalCapExhausted,
101 ]
102 }
103}
104
105const DURATION_BUCKETS_SECONDS: [f64; 11] = [
109 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
110];
111
112#[derive(Debug)]
113struct TransportHistogram {
114 buckets: [AtomicU64; 11],
117 inf: AtomicU64,
118 sum_micros: AtomicU64,
121}
122
123impl TransportHistogram {
124 fn new() -> Self {
125 Self {
126 buckets: [
127 AtomicU64::new(0),
128 AtomicU64::new(0),
129 AtomicU64::new(0),
130 AtomicU64::new(0),
131 AtomicU64::new(0),
132 AtomicU64::new(0),
133 AtomicU64::new(0),
134 AtomicU64::new(0),
135 AtomicU64::new(0),
136 AtomicU64::new(0),
137 AtomicU64::new(0),
138 ],
139 inf: AtomicU64::new(0),
140 sum_micros: AtomicU64::new(0),
141 }
142 }
143
144 fn observe_seconds(&self, value: f64) {
145 let micros = (value * 1_000_000.0).round().clamp(0.0, u64::MAX as f64) as u64;
146 self.sum_micros.fetch_add(micros, Ordering::Relaxed);
147 self.inf.fetch_add(1, Ordering::Relaxed);
148 for (i, le) in DURATION_BUCKETS_SECONDS.iter().enumerate() {
149 if value <= *le {
150 self.buckets[i].fetch_add(1, Ordering::Relaxed);
151 }
152 }
153 }
154}
155
156#[derive(Debug)]
157struct Inner {
158 rejected: [[AtomicU64; REJECT_REASONS]; 2],
159 duration: [TransportHistogram; 2],
160 queue_wait: [TransportHistogram; 2],
161}
162
163fn zeroed_reject_row() -> [AtomicU64; REJECT_REASONS] {
165 std::array::from_fn(|_| AtomicU64::new(0))
166}
167
168#[derive(Debug, Clone)]
169pub struct HttpHandlerMetrics {
170 inner: Arc<Inner>,
171}
172
173impl HttpHandlerMetrics {
174 pub fn new() -> Self {
175 Self {
176 inner: Arc::new(Inner {
177 rejected: [zeroed_reject_row(), zeroed_reject_row()],
178 duration: [TransportHistogram::new(), TransportHistogram::new()],
179 queue_wait: [TransportHistogram::new(), TransportHistogram::new()],
180 }),
181 }
182 }
183
184 pub fn record_reject(&self, transport: HttpTransport, reason: HttpRejectReason) {
185 self.inner.rejected[transport.index()][reason.index()].fetch_add(1, Ordering::Relaxed);
186 }
187
188 pub fn record_duration(&self, transport: HttpTransport, seconds: f64) {
189 if !seconds.is_finite() || seconds < 0.0 {
190 return;
191 }
192 self.inner.duration[transport.index()].observe_seconds(seconds);
193 }
194
195 pub fn record_queue_wait(&self, transport: HttpTransport, seconds: f64) {
196 if !seconds.is_finite() || seconds < 0.0 {
197 return;
198 }
199 self.inner.queue_wait[transport.index()].observe_seconds(seconds);
200 }
201
202 pub fn rejected_count(&self, transport: HttpTransport, reason: HttpRejectReason) -> u64 {
203 self.inner.rejected[transport.index()][reason.index()].load(Ordering::Relaxed)
204 }
205
206 pub fn duration_sample_count(&self, transport: HttpTransport) -> u64 {
207 self.inner.duration[transport.index()]
208 .inf
209 .load(Ordering::Relaxed)
210 }
211
212 pub fn queue_wait_sample_count(&self, transport: HttpTransport) -> u64 {
213 self.inner.queue_wait[transport.index()]
214 .inf
215 .load(Ordering::Relaxed)
216 }
217
218 pub fn render(&self, body: &mut String, limiter: &HttpConnectionLimiter) {
222 self.render_inner(body, limiter, None);
223 }
224
225 pub(crate) fn render_with_execution(
227 &self,
228 body: &mut String,
229 limiter: &HttpConnectionLimiter,
230 execution: &ExecutionAdmission,
231 ) {
232 self.render_inner(body, limiter, Some(execution));
233 }
234
235 fn render_inner(
236 &self,
237 body: &mut String,
238 limiter: &HttpConnectionLimiter,
239 execution: Option<&ExecutionAdmission>,
240 ) {
241 let cap = limiter.cap();
242 let current = limiter.current();
243
244 let _ = writeln!(
250 body,
251 "# HELP http_active_handler_threads Live admitted HTTP/HTTPS requests holding a limiter permit."
252 );
253 let _ = writeln!(body, "# TYPE http_active_handler_threads gauge");
254 let _ = writeln!(
255 body,
256 "http_active_handler_threads{{transport=\"http\"}} {}",
257 current
258 );
259 let _ = writeln!(
260 body,
261 "http_active_handler_threads{{transport=\"https\"}} {}",
262 current
263 );
264
265 let _ = writeln!(
267 body,
268 "# HELP http_handler_cap Configured maximum admitted in-flight HTTP/HTTPS requests."
269 );
270 let _ = writeln!(body, "# TYPE http_handler_cap gauge");
271 let _ = writeln!(body, "http_handler_cap{{transport=\"http\"}} {}", cap);
272 let _ = writeln!(body, "http_handler_cap{{transport=\"https\"}} {}", cap);
273
274 if let Some(execution) = execution {
275 let execution_active = execution.active();
276 let execution_queued = execution.queued();
277 let execution_workers = execution.active_max();
278 let execution_queue_capacity = execution.queued_max();
279 let _ = writeln!(
280 body,
281 "# HELP http_execution_workers Legacy alias for the runtime-wide engine-call limit."
282 );
283 let _ = writeln!(body, "# TYPE http_execution_workers gauge");
284 let _ = writeln!(body, "http_execution_workers {execution_workers}");
285 let _ = writeln!(
286 body,
287 "# HELP reddb_execution_active_limit Runtime-wide maximum concurrent engine calls across server transports."
288 );
289 let _ = writeln!(body, "# TYPE reddb_execution_active_limit gauge");
290 let _ = writeln!(body, "reddb_execution_active_limit {execution_workers}");
291 let _ = writeln!(
292 body,
293 "# HELP http_execution_active Legacy alias for runtime-wide active engine calls."
294 );
295 let _ = writeln!(body, "# TYPE http_execution_active gauge");
296 let _ = writeln!(body, "http_execution_active {execution_active}");
297 let _ = writeln!(
298 body,
299 "# HELP reddb_execution_active Runtime-wide active engine calls across server transports."
300 );
301 let _ = writeln!(body, "# TYPE reddb_execution_active gauge");
302 let _ = writeln!(body, "reddb_execution_active {execution_active}");
303 let _ = writeln!(
304 body,
305 "# HELP http_execution_queued Legacy alias for runtime-wide operations waiting for an engine slot."
306 );
307 let _ = writeln!(body, "# TYPE http_execution_queued gauge");
308 let _ = writeln!(body, "http_execution_queued {execution_queued}");
309 let _ = writeln!(
310 body,
311 "# HELP reddb_execution_queued Runtime-wide requests waiting for an engine execution slot."
312 );
313 let _ = writeln!(body, "# TYPE reddb_execution_queued gauge");
314 let _ = writeln!(body, "reddb_execution_queued {execution_queued}");
315 let _ = writeln!(
316 body,
317 "# HELP http_execution_queue_capacity Legacy alias for the runtime-wide execution wait-queue limit."
318 );
319 let _ = writeln!(body, "# TYPE http_execution_queue_capacity gauge");
320 let _ = writeln!(
321 body,
322 "http_execution_queue_capacity {execution_queue_capacity}"
323 );
324 let _ = writeln!(
325 body,
326 "# HELP reddb_execution_queue_limit Runtime-wide bounded execution wait-queue limit."
327 );
328 let _ = writeln!(body, "# TYPE reddb_execution_queue_limit gauge");
329 let _ = writeln!(
330 body,
331 "reddb_execution_queue_limit {execution_queue_capacity}"
332 );
333 }
334
335 let _ = writeln!(
337 body,
338 "# HELP http_handler_rejected_total HTTP/HTTPS handler rejections by reason since process start."
339 );
340 let _ = writeln!(body, "# TYPE http_handler_rejected_total counter");
341 for transport in [HttpTransport::Http, HttpTransport::Https] {
342 for reason in HttpRejectReason::all() {
343 let _ = writeln!(
344 body,
345 "http_handler_rejected_total{{transport=\"{}\",reason=\"{}\"}} {}",
346 transport.label(),
347 reason.label(),
348 self.rejected_count(transport, reason)
349 );
350 }
351 }
352
353 let _ = writeln!(
355 body,
356 "# HELP http_handler_duration_seconds Wall-clock handler duration per transport."
357 );
358 let _ = writeln!(body, "# TYPE http_handler_duration_seconds histogram");
359 for transport in [HttpTransport::Http, HttpTransport::Https] {
360 let hist = &self.inner.duration[transport.index()];
361 for (i, le) in DURATION_BUCKETS_SECONDS.iter().enumerate() {
362 let _ = writeln!(
363 body,
364 "http_handler_duration_seconds_bucket{{transport=\"{}\",le=\"{}\"}} {}",
365 transport.label(),
366 format_bucket_le(*le),
367 hist.buckets[i].load(Ordering::Relaxed)
368 );
369 }
370 let inf = hist.inf.load(Ordering::Relaxed);
371 let _ = writeln!(
372 body,
373 "http_handler_duration_seconds_bucket{{transport=\"{}\",le=\"+Inf\"}} {}",
374 transport.label(),
375 inf
376 );
377 let sum_secs = (hist.sum_micros.load(Ordering::Relaxed) as f64) / 1_000_000.0;
378 let _ = writeln!(
379 body,
380 "http_handler_duration_seconds_sum{{transport=\"{}\"}} {}",
381 transport.label(),
382 sum_secs
383 );
384 let _ = writeln!(
385 body,
386 "http_handler_duration_seconds_count{{transport=\"{}\"}} {}",
387 transport.label(),
388 inf
389 );
390 }
391
392 let _ = writeln!(
393 body,
394 "# HELP http_execution_queue_wait_seconds Time an admitted HTTP request waited for an engine worker."
395 );
396 let _ = writeln!(body, "# TYPE http_execution_queue_wait_seconds histogram");
397 for transport in [HttpTransport::Http, HttpTransport::Https] {
398 let histogram = &self.inner.queue_wait[transport.index()];
399 for (index, upper_bound) in DURATION_BUCKETS_SECONDS.iter().enumerate() {
400 let _ = writeln!(
401 body,
402 "http_execution_queue_wait_seconds_bucket{{transport=\"{}\",le=\"{}\"}} {}",
403 transport.label(),
404 format_bucket_le(*upper_bound),
405 histogram.buckets[index].load(Ordering::Relaxed)
406 );
407 }
408 let count = histogram.inf.load(Ordering::Relaxed);
409 let _ = writeln!(
410 body,
411 "http_execution_queue_wait_seconds_bucket{{transport=\"{}\",le=\"+Inf\"}} {}",
412 transport.label(),
413 count
414 );
415 let sum_seconds = (histogram.sum_micros.load(Ordering::Relaxed) as f64) / 1_000_000.0;
416 let _ = writeln!(
417 body,
418 "http_execution_queue_wait_seconds_sum{{transport=\"{}\"}} {}",
419 transport.label(),
420 sum_seconds
421 );
422 let _ = writeln!(
423 body,
424 "http_execution_queue_wait_seconds_count{{transport=\"{}\"}} {}",
425 transport.label(),
426 count
427 );
428 }
429 }
430}
431
432impl Default for HttpHandlerMetrics {
433 fn default() -> Self {
434 Self::new()
435 }
436}
437
438fn format_bucket_le(le: f64) -> String {
439 if le == le.trunc() && le.abs() < 1e16 {
443 format!("{le:.1}")
444 } else {
445 format!("{le}")
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452
453 #[test]
454 fn rejected_counters_isolated_by_label() {
455 let m = HttpHandlerMetrics::new();
456 m.record_reject(HttpTransport::Http, HttpRejectReason::CapExhausted);
457 m.record_reject(HttpTransport::Http, HttpRejectReason::CapExhausted);
458 m.record_reject(HttpTransport::Https, HttpRejectReason::HandlerTimeout);
459 assert_eq!(
460 m.rejected_count(HttpTransport::Http, HttpRejectReason::CapExhausted),
461 2
462 );
463 assert_eq!(
464 m.rejected_count(HttpTransport::Http, HttpRejectReason::HandlerTimeout),
465 0
466 );
467 assert_eq!(
468 m.rejected_count(HttpTransport::Https, HttpRejectReason::HandlerTimeout),
469 1
470 );
471 assert_eq!(
472 m.rejected_count(HttpTransport::Https, HttpRejectReason::CapExhausted),
473 0
474 );
475 }
476
477 #[test]
478 fn duration_histogram_buckets_are_cumulative() {
479 let m = HttpHandlerMetrics::new();
480 m.record_duration(HttpTransport::Http, 0.003);
481 m.record_duration(HttpTransport::Http, 0.04);
482 m.record_duration(HttpTransport::Http, 3.0);
483 assert_eq!(m.duration_sample_count(HttpTransport::Http), 3);
484
485 let limiter = HttpConnectionLimiter::new(4);
486 let execution = ExecutionAdmission::new(2, 2);
487 let mut body = String::new();
488 m.render_with_execution(&mut body, &limiter, &execution);
489
490 assert!(body
492 .contains("http_handler_duration_seconds_bucket{transport=\"http\",le=\"0.005\"} 1"));
493 assert!(
495 body.contains("http_handler_duration_seconds_bucket{transport=\"http\",le=\"0.05\"} 2")
496 );
497 assert!(
499 body.contains("http_handler_duration_seconds_bucket{transport=\"http\",le=\"+Inf\"} 3")
500 );
501 assert!(body
503 .contains("http_handler_duration_seconds_bucket{transport=\"https\",le=\"+Inf\"} 0"));
504 }
505
506 #[test]
507 fn render_includes_cap_and_current_from_limiter() {
508 let limiter = HttpConnectionLimiter::new(7);
509 let execution = ExecutionAdmission::new(2, 5);
510 let _p = limiter.try_acquire().unwrap();
511 let m = HttpHandlerMetrics::new();
512 let mut body = String::new();
513 m.render_with_execution(&mut body, &limiter, &execution);
514 assert!(body.contains("http_handler_cap{transport=\"http\"} 7"));
515 assert!(body.contains("http_handler_cap{transport=\"https\"} 7"));
516 assert!(body.contains("http_active_handler_threads{transport=\"http\"} 1"));
517 assert!(body.contains("http_active_handler_threads{transport=\"https\"} 1"));
518 assert!(body.contains("http_execution_workers 2"));
519 assert!(body.contains("http_execution_queue_capacity 5"));
520 assert!(body.contains("reddb_execution_active_limit 2"));
521 assert!(body.contains("reddb_execution_queue_limit 5"));
522 }
523
524 #[test]
525 fn render_emits_all_rejection_labels() {
526 let m = HttpHandlerMetrics::new();
527 let limiter = HttpConnectionLimiter::new(1);
528 let execution = ExecutionAdmission::new(1, 0);
529 let mut body = String::new();
530 m.render_with_execution(&mut body, &limiter, &execution);
531 for transport in ["http", "https"] {
532 for reason in [
533 "cap_exhausted",
534 "handler_timeout",
535 "execution_queue_full",
536 "execution_queue_timeout",
537 "principal_cap_exhausted",
538 ] {
539 let expected = format!(
540 "http_handler_rejected_total{{transport=\"{transport}\",reason=\"{reason}\"}} 0"
541 );
542 assert!(body.contains(&expected), "missing line: {expected}");
543 }
544 }
545 }
546
547 #[test]
548 fn negative_or_nan_durations_are_ignored() {
549 let m = HttpHandlerMetrics::new();
550 m.record_duration(HttpTransport::Http, -1.0);
551 m.record_duration(HttpTransport::Http, f64::NAN);
552 m.record_duration(HttpTransport::Http, f64::INFINITY);
553 assert_eq!(m.duration_sample_count(HttpTransport::Http), 0);
554 }
555
556 #[test]
557 fn queue_wait_histogram_is_distinct_from_handler_duration() {
558 let metrics = HttpHandlerMetrics::new();
559 metrics.record_queue_wait(HttpTransport::Http, 0.04);
560 assert_eq!(metrics.queue_wait_sample_count(HttpTransport::Http), 1);
561 assert_eq!(metrics.duration_sample_count(HttpTransport::Http), 0);
562
563 let limiter = HttpConnectionLimiter::new(4);
564 let execution = ExecutionAdmission::new(1, 3);
565 let mut body = String::new();
566 metrics.render_with_execution(&mut body, &limiter, &execution);
567 assert!(body.contains(
568 "http_execution_queue_wait_seconds_bucket{transport=\"http\",le=\"0.05\"} 1"
569 ));
570 assert!(body.contains("http_execution_queue_wait_seconds_count{transport=\"http\"} 1"));
571 }
572}