Skip to main content

synapse_proxy/
metrics.rs

1//! OpenTelemetry metrics exported in Prometheus text format on /metrics.
2
3use axum::extract::State;
4use axum::http::{header, StatusCode};
5use axum::response::IntoResponse;
6use axum::routing::get;
7use axum::Router;
8use opentelemetry::metrics::{Counter, Histogram, MeterProvider as _};
9use opentelemetry::KeyValue;
10use opentelemetry_sdk::metrics::SdkMeterProvider;
11use prometheus::{Encoder, Registry, TextEncoder};
12
13pub struct Metrics {
14    requests: Counter<u64>,
15    duration: Histogram<f64>,
16    upstream_errors: Counter<u64>,
17    transform_errors: Counter<u64>,
18    _provider: SdkMeterProvider,
19}
20
21impl Metrics {
22    /// Build the meter provider wired to a fresh Prometheus registry; returns both.
23    pub fn new() -> anyhow::Result<(Self, Registry)> {
24        let registry = Registry::new();
25        let exporter = opentelemetry_prometheus::exporter()
26            .with_registry(registry.clone())
27            .build()?;
28        let provider = SdkMeterProvider::builder().with_reader(exporter).build();
29        let meter = provider.meter("synapse-proxy");
30        let metrics = Self {
31            requests: meter.u64_counter("synapse_proxy_requests_total").build(),
32            duration: meter
33                .f64_histogram("synapse_proxy_request_duration_seconds")
34                .build(),
35            upstream_errors: meter
36                .u64_counter("synapse_proxy_upstream_errors_total")
37                .build(),
38            transform_errors: meter
39                .u64_counter("synapse_proxy_transform_errors_total")
40                .build(),
41            _provider: provider,
42        };
43        Ok((metrics, registry))
44    }
45
46    pub fn record(&self, route: &str, method: &str, status: u16, outcome: &str, secs: f64) {
47        let labels = [
48            KeyValue::new("route", route.to_string()),
49            KeyValue::new("method", method.to_string()),
50            KeyValue::new("status", status.to_string()),
51            KeyValue::new("outcome", outcome.to_string()),
52        ];
53        self.requests.add(1, &labels);
54        self.duration.record(
55            secs,
56            &[
57                KeyValue::new("route", route.to_string()),
58                KeyValue::new("method", method.to_string()),
59            ],
60        );
61    }
62
63    pub fn upstream_error(&self, route: &str, reason: &str) {
64        self.upstream_errors.add(
65            1,
66            &[
67                KeyValue::new("route", route.to_string()),
68                KeyValue::new("reason", reason.to_string()),
69            ],
70        );
71    }
72
73    pub fn transform_error(&self, route: &str, transform: &str) {
74        self.transform_errors.add(
75            1,
76            &[
77                KeyValue::new("route", route.to_string()),
78                KeyValue::new("transform", transform.to_string()),
79            ],
80        );
81    }
82}
83
84pub fn metrics_router(registry: Registry) -> Router {
85    Router::new()
86        .route("/metrics", get(serve))
87        .with_state(registry)
88}
89
90async fn serve(State(registry): State<Registry>) -> impl IntoResponse {
91    let mut buf = Vec::new();
92    if TextEncoder::new()
93        .encode(&registry.gather(), &mut buf)
94        .is_err()
95    {
96        return (StatusCode::INTERNAL_SERVER_ERROR, "encode error").into_response();
97    }
98    ([(header::CONTENT_TYPE, "text/plain; version=0.0.4")], buf).into_response()
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn records_and_exports() {
107        let (m, registry) = Metrics::new().unwrap();
108        m.record("cortex", "POST", 200, "forwarded", 0.01);
109        let mut buf = Vec::new();
110        TextEncoder::new()
111            .encode(&registry.gather(), &mut buf)
112            .unwrap();
113        let text = String::from_utf8(buf).unwrap();
114        assert!(text.contains("synapse_proxy_requests_total"));
115        assert!(text.contains("route=\"cortex\""));
116    }
117}