1use axum::extract::State;
4use axum::http::{header, StatusCode};
5use axum::response::IntoResponse;
6use axum::routing::get;
7use axum::Router;
8use opentelemetry::metrics::{Counter, Histogram, Meter, MeterProvider as _};
9use opentelemetry::KeyValue;
10use opentelemetry_otlp::WithExportConfig;
11use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider};
12use opentelemetry_sdk::Resource;
13use prometheus::{Encoder, Registry, TextEncoder};
14
15pub struct Metrics {
16 requests: Counter<u64>,
17 duration: Histogram<f64>,
18 upstream_errors: Counter<u64>,
19 transform_errors: Counter<u64>,
20 provider: SdkMeterProvider,
21}
22
23impl Metrics {
24 pub fn new() -> anyhow::Result<(Self, Registry)> {
26 Self::with_otlp(None, "synapse-proxy")
27 }
28
29 pub fn with_otlp(
39 otlp_endpoint: Option<&str>,
40 service_name: &str,
41 ) -> anyhow::Result<(Self, Registry)> {
42 let registry = Registry::new();
43 let prom = opentelemetry_prometheus::exporter()
44 .with_registry(registry.clone())
45 .build()?;
46 let resource = Resource::builder()
47 .with_service_name(service_name.to_string())
48 .build();
49 let mut builder = SdkMeterProvider::builder()
50 .with_reader(prom)
51 .with_resource(resource);
52 if let Some(endpoint) = otlp_endpoint {
53 let exporter = opentelemetry_otlp::MetricExporter::builder()
54 .with_http()
55 .with_endpoint(format!("{}/v1/metrics", endpoint.trim_end_matches('/')))
56 .build()?;
57 builder = builder.with_reader(PeriodicReader::builder(exporter).build());
58 }
59 let provider = builder.build();
60 let meter = provider.meter("synapse-proxy");
61 let metrics = Self {
62 requests: meter.u64_counter("synapse_proxy_requests_total").build(),
63 duration: meter
64 .f64_histogram("synapse_proxy_request_duration_seconds")
65 .build(),
66 upstream_errors: meter
67 .u64_counter("synapse_proxy_upstream_errors_total")
68 .build(),
69 transform_errors: meter
70 .u64_counter("synapse_proxy_transform_errors_total")
71 .build(),
72 provider,
73 };
74 Ok((metrics, registry))
75 }
76
77 pub fn meter(&self) -> Meter {
81 self.provider.meter("sandbox-broker")
82 }
83
84 pub fn record(&self, route: &str, method: &str, status: u16, outcome: &str, secs: f64) {
85 let labels = [
86 KeyValue::new("route", route.to_string()),
87 KeyValue::new("method", method.to_string()),
88 KeyValue::new("status", status.to_string()),
89 KeyValue::new("outcome", outcome.to_string()),
90 ];
91 self.requests.add(1, &labels);
92 self.duration.record(
93 secs,
94 &[
95 KeyValue::new("route", route.to_string()),
96 KeyValue::new("method", method.to_string()),
97 ],
98 );
99 }
100
101 pub fn upstream_error(&self, route: &str, reason: &str) {
102 self.upstream_errors.add(
103 1,
104 &[
105 KeyValue::new("route", route.to_string()),
106 KeyValue::new("reason", reason.to_string()),
107 ],
108 );
109 }
110
111 pub fn transform_error(&self, route: &str, transform: &str) {
112 self.transform_errors.add(
113 1,
114 &[
115 KeyValue::new("route", route.to_string()),
116 KeyValue::new("transform", transform.to_string()),
117 ],
118 );
119 }
120}
121
122pub fn metrics_router(registry: Registry) -> Router {
123 Router::new()
124 .route("/metrics", get(serve))
125 .with_state(registry)
126}
127
128async fn serve(State(registry): State<Registry>) -> impl IntoResponse {
129 let mut buf = Vec::new();
130 if TextEncoder::new()
131 .encode(®istry.gather(), &mut buf)
132 .is_err()
133 {
134 return (StatusCode::INTERNAL_SERVER_ERROR, "encode error").into_response();
135 }
136 ([(header::CONTENT_TYPE, "text/plain; version=0.0.4")], buf).into_response()
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn records_and_exports() {
145 let (m, registry) = Metrics::new().unwrap();
146 m.record("cortex", "POST", 200, "forwarded", 0.01);
147 let mut buf = Vec::new();
148 TextEncoder::new()
149 .encode(®istry.gather(), &mut buf)
150 .unwrap();
151 let text = String::from_utf8(buf).unwrap();
152 assert!(text.contains("synapse_proxy_requests_total"));
153 assert!(text.contains("route=\"cortex\""));
154 }
155
156 #[test]
157 fn otlp_and_prometheus_readers_coexist() {
158 let (m, registry) =
160 Metrics::with_otlp(Some("http://127.0.0.1:4318"), "sandbox-broker").unwrap();
161 m.record("cortex", "POST", 200, "forwarded", 0.01);
162 let mut buf = Vec::new();
163 TextEncoder::new()
164 .encode(®istry.gather(), &mut buf)
165 .unwrap();
166 let text = String::from_utf8(buf).unwrap();
167 assert!(text.contains("synapse_proxy_requests_total"));
168 let _c = m.meter().u64_counter("probe_total").build();
170 }
171}