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 Configured maximum concurrent buffered non-health HTTP engine calls."
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 http_execution_active Live buffered non-health HTTP engine calls."
288 );
289 let _ = writeln!(body, "# TYPE http_execution_active gauge");
290 let _ = writeln!(body, "http_execution_active {execution_active}");
291 let _ = writeln!(
292 body,
293 "# HELP http_execution_queued Buffered non-health HTTP requests waiting for an engine worker."
294 );
295 let _ = writeln!(body, "# TYPE http_execution_queued gauge");
296 let _ = writeln!(body, "http_execution_queued {execution_queued}");
297 let _ = writeln!(
298 body,
299 "# HELP http_execution_queue_capacity Maximum buffered non-health HTTP requests allowed to wait for execution."
300 );
301 let _ = writeln!(body, "# TYPE http_execution_queue_capacity gauge");
302 let _ = writeln!(
303 body,
304 "http_execution_queue_capacity {execution_queue_capacity}"
305 );
306 }
307
308 let _ = writeln!(
310 body,
311 "# HELP http_handler_rejected_total HTTP/HTTPS handler rejections by reason since process start."
312 );
313 let _ = writeln!(body, "# TYPE http_handler_rejected_total counter");
314 for transport in [HttpTransport::Http, HttpTransport::Https] {
315 for reason in HttpRejectReason::all() {
316 let _ = writeln!(
317 body,
318 "http_handler_rejected_total{{transport=\"{}\",reason=\"{}\"}} {}",
319 transport.label(),
320 reason.label(),
321 self.rejected_count(transport, reason)
322 );
323 }
324 }
325
326 let _ = writeln!(
328 body,
329 "# HELP http_handler_duration_seconds Wall-clock handler duration per transport."
330 );
331 let _ = writeln!(body, "# TYPE http_handler_duration_seconds histogram");
332 for transport in [HttpTransport::Http, HttpTransport::Https] {
333 let hist = &self.inner.duration[transport.index()];
334 for (i, le) in DURATION_BUCKETS_SECONDS.iter().enumerate() {
335 let _ = writeln!(
336 body,
337 "http_handler_duration_seconds_bucket{{transport=\"{}\",le=\"{}\"}} {}",
338 transport.label(),
339 format_bucket_le(*le),
340 hist.buckets[i].load(Ordering::Relaxed)
341 );
342 }
343 let inf = hist.inf.load(Ordering::Relaxed);
344 let _ = writeln!(
345 body,
346 "http_handler_duration_seconds_bucket{{transport=\"{}\",le=\"+Inf\"}} {}",
347 transport.label(),
348 inf
349 );
350 let sum_secs = (hist.sum_micros.load(Ordering::Relaxed) as f64) / 1_000_000.0;
351 let _ = writeln!(
352 body,
353 "http_handler_duration_seconds_sum{{transport=\"{}\"}} {}",
354 transport.label(),
355 sum_secs
356 );
357 let _ = writeln!(
358 body,
359 "http_handler_duration_seconds_count{{transport=\"{}\"}} {}",
360 transport.label(),
361 inf
362 );
363 }
364
365 let _ = writeln!(
366 body,
367 "# HELP http_execution_queue_wait_seconds Time an admitted HTTP request waited for an engine worker."
368 );
369 let _ = writeln!(body, "# TYPE http_execution_queue_wait_seconds histogram");
370 for transport in [HttpTransport::Http, HttpTransport::Https] {
371 let histogram = &self.inner.queue_wait[transport.index()];
372 for (index, upper_bound) in DURATION_BUCKETS_SECONDS.iter().enumerate() {
373 let _ = writeln!(
374 body,
375 "http_execution_queue_wait_seconds_bucket{{transport=\"{}\",le=\"{}\"}} {}",
376 transport.label(),
377 format_bucket_le(*upper_bound),
378 histogram.buckets[index].load(Ordering::Relaxed)
379 );
380 }
381 let count = histogram.inf.load(Ordering::Relaxed);
382 let _ = writeln!(
383 body,
384 "http_execution_queue_wait_seconds_bucket{{transport=\"{}\",le=\"+Inf\"}} {}",
385 transport.label(),
386 count
387 );
388 let sum_seconds = (histogram.sum_micros.load(Ordering::Relaxed) as f64) / 1_000_000.0;
389 let _ = writeln!(
390 body,
391 "http_execution_queue_wait_seconds_sum{{transport=\"{}\"}} {}",
392 transport.label(),
393 sum_seconds
394 );
395 let _ = writeln!(
396 body,
397 "http_execution_queue_wait_seconds_count{{transport=\"{}\"}} {}",
398 transport.label(),
399 count
400 );
401 }
402 }
403}
404
405impl Default for HttpHandlerMetrics {
406 fn default() -> Self {
407 Self::new()
408 }
409}
410
411fn format_bucket_le(le: f64) -> String {
412 if le == le.trunc() && le.abs() < 1e16 {
416 format!("{le:.1}")
417 } else {
418 format!("{le}")
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425
426 #[test]
427 fn rejected_counters_isolated_by_label() {
428 let m = HttpHandlerMetrics::new();
429 m.record_reject(HttpTransport::Http, HttpRejectReason::CapExhausted);
430 m.record_reject(HttpTransport::Http, HttpRejectReason::CapExhausted);
431 m.record_reject(HttpTransport::Https, HttpRejectReason::HandlerTimeout);
432 assert_eq!(
433 m.rejected_count(HttpTransport::Http, HttpRejectReason::CapExhausted),
434 2
435 );
436 assert_eq!(
437 m.rejected_count(HttpTransport::Http, HttpRejectReason::HandlerTimeout),
438 0
439 );
440 assert_eq!(
441 m.rejected_count(HttpTransport::Https, HttpRejectReason::HandlerTimeout),
442 1
443 );
444 assert_eq!(
445 m.rejected_count(HttpTransport::Https, HttpRejectReason::CapExhausted),
446 0
447 );
448 }
449
450 #[test]
451 fn duration_histogram_buckets_are_cumulative() {
452 let m = HttpHandlerMetrics::new();
453 m.record_duration(HttpTransport::Http, 0.003);
454 m.record_duration(HttpTransport::Http, 0.04);
455 m.record_duration(HttpTransport::Http, 3.0);
456 assert_eq!(m.duration_sample_count(HttpTransport::Http), 3);
457
458 let limiter = HttpConnectionLimiter::new(4);
459 let execution = ExecutionAdmission::new(2, 2);
460 let mut body = String::new();
461 m.render_with_execution(&mut body, &limiter, &execution);
462
463 assert!(body
465 .contains("http_handler_duration_seconds_bucket{transport=\"http\",le=\"0.005\"} 1"));
466 assert!(
468 body.contains("http_handler_duration_seconds_bucket{transport=\"http\",le=\"0.05\"} 2")
469 );
470 assert!(
472 body.contains("http_handler_duration_seconds_bucket{transport=\"http\",le=\"+Inf\"} 3")
473 );
474 assert!(body
476 .contains("http_handler_duration_seconds_bucket{transport=\"https\",le=\"+Inf\"} 0"));
477 }
478
479 #[test]
480 fn render_includes_cap_and_current_from_limiter() {
481 let limiter = HttpConnectionLimiter::new(7);
482 let execution = ExecutionAdmission::new(2, 5);
483 let _p = limiter.try_acquire().unwrap();
484 let m = HttpHandlerMetrics::new();
485 let mut body = String::new();
486 m.render_with_execution(&mut body, &limiter, &execution);
487 assert!(body.contains("http_handler_cap{transport=\"http\"} 7"));
488 assert!(body.contains("http_handler_cap{transport=\"https\"} 7"));
489 assert!(body.contains("http_active_handler_threads{transport=\"http\"} 1"));
490 assert!(body.contains("http_active_handler_threads{transport=\"https\"} 1"));
491 assert!(body.contains("http_execution_workers 2"));
492 assert!(body.contains("http_execution_queue_capacity 5"));
493 }
494
495 #[test]
496 fn render_emits_all_rejection_labels() {
497 let m = HttpHandlerMetrics::new();
498 let limiter = HttpConnectionLimiter::new(1);
499 let execution = ExecutionAdmission::new(1, 0);
500 let mut body = String::new();
501 m.render_with_execution(&mut body, &limiter, &execution);
502 for transport in ["http", "https"] {
503 for reason in [
504 "cap_exhausted",
505 "handler_timeout",
506 "execution_queue_full",
507 "execution_queue_timeout",
508 "principal_cap_exhausted",
509 ] {
510 let expected = format!(
511 "http_handler_rejected_total{{transport=\"{transport}\",reason=\"{reason}\"}} 0"
512 );
513 assert!(body.contains(&expected), "missing line: {expected}");
514 }
515 }
516 }
517
518 #[test]
519 fn negative_or_nan_durations_are_ignored() {
520 let m = HttpHandlerMetrics::new();
521 m.record_duration(HttpTransport::Http, -1.0);
522 m.record_duration(HttpTransport::Http, f64::NAN);
523 m.record_duration(HttpTransport::Http, f64::INFINITY);
524 assert_eq!(m.duration_sample_count(HttpTransport::Http), 0);
525 }
526
527 #[test]
528 fn queue_wait_histogram_is_distinct_from_handler_duration() {
529 let metrics = HttpHandlerMetrics::new();
530 metrics.record_queue_wait(HttpTransport::Http, 0.04);
531 assert_eq!(metrics.queue_wait_sample_count(HttpTransport::Http), 1);
532 assert_eq!(metrics.duration_sample_count(HttpTransport::Http), 0);
533
534 let limiter = HttpConnectionLimiter::new(4);
535 let execution = ExecutionAdmission::new(1, 3);
536 let mut body = String::new();
537 metrics.render_with_execution(&mut body, &limiter, &execution);
538 assert!(body.contains(
539 "http_execution_queue_wait_seconds_bucket{transport=\"http\",le=\"0.05\"} 1"
540 ));
541 assert!(body.contains("http_execution_queue_wait_seconds_count{transport=\"http\"} 1"));
542 }
543}