Skip to main content

solti_prometheus/
api.rs

1//! API-layer metrics: Prometheus implementation of [`solti_api::ApiMetricsBackend`].
2
3use std::sync::Arc;
4
5use prometheus::{CounterVec, GaugeVec, HistogramVec, Registry};
6use solti_api::{ApiMetricsBackend, Transport};
7
8use crate::register::{Sub, ms_to_secs};
9
10/// Prometheus implementation of [`ApiMetricsBackend`].
11///
12/// ## Metrics (`solti_api_*`)
13///
14/// | Metric                               | Type      | Labels                                   | Description              |
15/// |--------------------------------------|-----------|------------------------------------------|--------------------------|
16/// | `solti_api_requests_total`           | Counter   | `transport`, `method`, `path`, `status`  | Completed requests       |
17/// | `solti_api_request_duration_seconds` | Histogram | `transport`, `method`, `path`            | Request duration         |
18/// | `solti_api_in_flight_requests`       | Gauge     | `transport`                              | In-flight request count  |
19///
20/// ## Cardinality
21///
22/// `path` is a **templated** route (e.g. `/api/v1/tasks/{id}`) for HTTP thanks to`axum::extract::MatchedPath`, and a full method path (`/solti.v1.SoltiApi/SubmitTask`) for gRPC.
23/// In both cases the set is bounded by the proto/api definition.
24pub struct PrometheusApiMetrics {
25    requests_total: CounterVec,
26    duration_seconds: HistogramVec,
27    in_flight: GaugeVec,
28}
29
30impl PrometheusApiMetrics {
31    /// Register all API metrics into `registry`.
32    pub fn new(registry: Arc<Registry>) -> Result<Self, prometheus::Error> {
33        let r = Sub::new(&registry, "api");
34
35        let requests_total = r.counter_vec(
36            "requests_total",
37            "Total completed API requests",
38            &["transport", "method", "path", "status"],
39        )?;
40        let duration_seconds = r.histogram_vec(
41            "request_duration_seconds",
42            "API request duration",
43            vec![
44                0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
45            ],
46            &["transport", "method", "path"],
47        )?;
48        let in_flight = r.gauge_vec(
49            "in_flight_requests",
50            "Current in-flight API requests",
51            &["transport"],
52        )?;
53
54        Ok(Self {
55            requests_total,
56            duration_seconds,
57            in_flight,
58        })
59    }
60}
61
62impl std::fmt::Debug for PrometheusApiMetrics {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        f.debug_struct("PrometheusApiMetrics").finish()
65    }
66}
67
68impl ApiMetricsBackend for PrometheusApiMetrics {
69    fn record_request(
70        &self,
71        transport: Transport,
72        method: &str,
73        path: &str,
74        status: u16,
75        duration_ms: u64,
76    ) {
77        let t = transport.as_label();
78        // Zero-alloc stringify: `itoa::Buffer` lives on the stack.
79        let mut buf = itoa::Buffer::new();
80        let s = buf.format(status);
81        self.requests_total
82            .with_label_values(&[t, method, path, s])
83            .inc();
84        self.duration_seconds
85            .with_label_values(&[t, method, path])
86            .observe(ms_to_secs(duration_ms));
87    }
88
89    fn record_in_flight_delta(&self, transport: Transport, delta: i64) {
90        self.in_flight
91            .with_label_values(&[transport.as_label()])
92            .add(delta as f64);
93    }
94}