Skip to main content

rust_webx_host/
metrics.rs

1//! HTTP request counters exposed at `GET /metrics` (Prometheus text format).
2
3use rust_webx_core::error::Result;
4use rust_webx_core::http::IHttpContext;
5use rust_webx_core::middleware::IMiddleware;
6use rust_webx_core::routing::IEndpoint;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Arc;
9
10/// Shared HTTP metrics counters.
11#[derive(Default)]
12pub struct HttpMetrics {
13    total: AtomicU64,
14    status_2xx: AtomicU64,
15    status_4xx: AtomicU64,
16    status_5xx: AtomicU64,
17}
18
19impl HttpMetrics {
20    pub fn new() -> Arc<Self> {
21        Arc::new(Self::default())
22    }
23
24    pub fn record(&self, status: u16) {
25        self.total.fetch_add(1, Ordering::Relaxed);
26        match status {
27            200..=299 => {
28                self.status_2xx.fetch_add(1, Ordering::Relaxed);
29            }
30            400..=499 => {
31                self.status_4xx.fetch_add(1, Ordering::Relaxed);
32            }
33            500..=599 => {
34                self.status_5xx.fetch_add(1, Ordering::Relaxed);
35            }
36            _ => {}
37        }
38    }
39
40    pub fn render_prometheus(&self) -> String {
41        let total = self.total.load(Ordering::Relaxed);
42        let s2 = self.status_2xx.load(Ordering::Relaxed);
43        let s4 = self.status_4xx.load(Ordering::Relaxed);
44        let s5 = self.status_5xx.load(Ordering::Relaxed);
45        format!(
46            "# HELP http_requests_total Total HTTP requests processed\n\
47             # TYPE http_requests_total counter\n\
48             http_requests_total {total}\n\
49             # HELP http_responses_2xx_total 2xx responses\n\
50             # TYPE http_responses_2xx_total counter\n\
51             http_responses_2xx_total {s2}\n\
52             # HELP http_responses_4xx_total 4xx responses\n\
53             # TYPE http_responses_4xx_total counter\n\
54             http_responses_4xx_total {s4}\n\
55             # HELP http_responses_5xx_total 5xx responses\n\
56             # TYPE http_responses_5xx_total counter\n\
57             http_responses_5xx_total {s5}\n"
58        )
59    }
60}
61
62/// Records response status codes into [`HttpMetrics`].
63pub struct MetricsMiddleware {
64    metrics: Arc<HttpMetrics>,
65}
66
67impl MetricsMiddleware {
68    pub fn new(metrics: Arc<HttpMetrics>) -> Self {
69        Self { metrics }
70    }
71}
72
73#[async_trait::async_trait]
74impl IMiddleware for MetricsMiddleware {
75    async fn invoke(
76        &self,
77        _ctx: &mut dyn IHttpContext,
78    ) -> Result<std::ops::ControlFlow<()>> {
79        Ok(std::ops::ControlFlow::Continue(()))
80    }
81
82    async fn after(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
83        self.metrics.record(ctx.response().status());
84        Ok(())
85    }
86}
87
88/// Serves Prometheus text metrics from a shared [`HttpMetrics`] instance.
89pub struct MetricsEndpoint {
90    metrics: Arc<HttpMetrics>,
91}
92
93impl MetricsEndpoint {
94    pub fn new(metrics: Arc<HttpMetrics>) -> Self {
95        Self { metrics }
96    }
97}
98
99#[async_trait::async_trait]
100impl IEndpoint for MetricsEndpoint {
101    async fn handle(&self, ctx: &mut dyn IHttpContext) -> Result<()> {
102        let body = self.metrics.render_prometheus();
103        ctx.response_mut().set_status(200);
104        ctx.response_mut()
105            .set_header("content-type", "text/plain; version=0.0.4; charset=utf-8");
106        ctx.response_mut().write_bytes(body.into_bytes()).await
107    }
108}