saluki_core/observability/metrics/
api.rs1use std::sync::Arc;
4
5use async_trait::async_trait;
6use saluki_api::{
7 extract::State,
8 response::IntoResponse,
9 routing::{get, Router},
10 APIHandler, DynamicRoute, EndpointType,
11};
12use saluki_common::sync::shutdown::ShutdownHandle;
13use saluki_error::generic_error;
14use tokio::sync::Mutex;
15
16use super::{get_shared_metrics_state, AggregatedMetricsProcessor, Reflector, RemapperRule, TelemetryProcessor};
17use crate::runtime::{state::DataspaceRegistry, InitializationError, Supervisable, SupervisorFuture};
18
19#[derive(Clone)]
21pub struct InternalTelemetryState {
22 metrics: Reflector<AggregatedMetricsProcessor>,
23 raw: Arc<Mutex<TelemetryProcessor>>,
24 compat: Arc<Mutex<TelemetryProcessor>>,
25}
26
27pub struct InternalTelemetryAPIHandler {
30 state: InternalTelemetryState,
31 compat_enabled: bool,
32}
33
34impl InternalTelemetryAPIHandler {
35 fn new(metrics: Reflector<AggregatedMetricsProcessor>, compat_remapper_rules: Option<Vec<RemapperRule>>) -> Self {
36 let compat_enabled = compat_remapper_rules.is_some();
37 let compat_remapper_rules = compat_remapper_rules.unwrap_or_default();
38
39 Self {
40 state: InternalTelemetryState {
41 metrics,
42 raw: Arc::new(Mutex::new(TelemetryProcessor::new())),
43 compat: Arc::new(Mutex::new(
44 TelemetryProcessor::new().with_remapper_rules(compat_remapper_rules),
45 )),
46 },
47 compat_enabled,
48 }
49 }
50
51 async fn metrics_handler(State(state): State<InternalTelemetryState>) -> impl IntoResponse {
52 let mut processor = state.raw.lock().await;
53 processor.process(state.metrics.state())
54 }
55
56 async fn compat_metrics_handler(State(state): State<InternalTelemetryState>) -> impl IntoResponse {
57 let mut processor = state.compat.lock().await;
58 processor.process(state.metrics.state())
59 }
60}
61
62impl APIHandler for InternalTelemetryAPIHandler {
63 type State = InternalTelemetryState;
64
65 fn generate_initial_state(&self) -> Self::State {
66 self.state.clone()
67 }
68
69 fn generate_routes(&self) -> Router<Self::State> {
70 let router = Router::new().route("/metrics", get(Self::metrics_handler));
71 if self.compat_enabled {
72 router.route("/compat/metrics", get(Self::compat_metrics_handler))
73 } else {
74 router
75 }
76 }
77}
78
79pub struct InternalTelemetryAPIWorker {
87 compat_remapper_rules: Option<Vec<RemapperRule>>,
88}
89
90impl InternalTelemetryAPIWorker {
91 pub fn new(compat_remapper_rules: Option<Vec<RemapperRule>>) -> Self {
95 Self { compat_remapper_rules }
96 }
97}
98
99#[async_trait]
100impl Supervisable for InternalTelemetryAPIWorker {
101 fn name(&self) -> &str {
102 "internal-telemetry-api"
103 }
104
105 async fn initialize(&self, process_shutdown: ShutdownHandle) -> Result<SupervisorFuture, InitializationError> {
106 let metrics = get_shared_metrics_state();
107 let handler = InternalTelemetryAPIHandler::new(metrics, self.compat_remapper_rules.clone());
108 let route = DynamicRoute::http(EndpointType::Unprivileged, &handler);
109
110 Ok(Box::pin(async move {
111 DataspaceRegistry::try_current()
112 .ok_or_else(|| generic_error!("Dataspace not available."))?
113 .assert(route, "internal-telemetry-api");
114
115 process_shutdown.await;
116 Ok(())
117 }))
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use futures::stream;
124 use http::{Request, StatusCode};
125 use http_body_util::Empty;
126 use hyper::body::Bytes;
127 use tower::ServiceExt as _;
128
129 use super::*;
130 use crate::observability::metrics::MetricsSnapshot;
131
132 fn handler(compat_remapper_rules: Option<Vec<RemapperRule>>) -> InternalTelemetryAPIHandler {
133 let (metrics, _worker) = Reflector::new(stream::empty::<Vec<MetricsSnapshot>>(), AggregatedMetricsProcessor);
134 InternalTelemetryAPIHandler::new(metrics, compat_remapper_rules)
135 }
136
137 async fn status(handler: &InternalTelemetryAPIHandler, path: &str) -> StatusCode {
138 let request = Request::get(path).body(Empty::<Bytes>::new()).unwrap();
139 handler
140 .generate_routes()
141 .with_state(handler.generate_initial_state())
142 .oneshot(request)
143 .await
144 .unwrap()
145 .status()
146 }
147
148 #[tokio::test]
149 async fn compat_route_is_only_served_with_compat_rules() {
150 let without_compat = handler(None);
151 assert_eq!(status(&without_compat, "/metrics").await, StatusCode::OK);
152 assert_eq!(status(&without_compat, "/compat/metrics").await, StatusCode::NOT_FOUND);
153
154 let with_compat = handler(Some(Vec::new()));
155 assert_eq!(status(&with_compat, "/metrics").await, StatusCode::OK);
156 assert_eq!(status(&with_compat, "/compat/metrics").await, StatusCode::OK);
157 }
158}