Skip to main content

enya_client/otlp/
metrics_client.rs

1//! OTLP metrics client that reads from an in-memory [`TelemetryStore`].
2
3use std::sync::Arc;
4
5use super::store::TelemetryStore;
6use crate::prometheus::response::MetricLabels;
7use crate::{
8    BackendInfo, HealthCheckResult, LabelsResult, MetricLabelsResult, MetricsClient, QueryRequest,
9    QueryResult,
10};
11use poll_promise::Promise;
12
13/// Metrics client backed by the in-memory OTLP telemetry store.
14///
15/// Unlike [`PrometheusClient`](crate::prometheus::PrometheusClient) which makes
16/// HTTP requests to Prometheus, this reads directly from shared memory.
17/// Promises resolve immediately.
18pub struct OtlpMetricsClient {
19    store: Arc<TelemetryStore>,
20}
21
22impl OtlpMetricsClient {
23    /// Create a new OTLP metrics client reading from the given store.
24    pub fn new(store: Arc<TelemetryStore>) -> Self {
25        Self { store }
26    }
27}
28
29impl MetricsClient for OtlpMetricsClient {
30    fn query(&self, request: QueryRequest, _ctx: &egui::Context) -> Promise<QueryResult> {
31        // Use the query string as the metric name (request.metric is the pane
32        // display name, not the actual metric). Fall back to request.metric if
33        // query is empty or a wildcard.
34        let metric = if request.query.is_empty() || request.query == "*" {
35            &request.metric
36        } else {
37            &request.query
38        };
39
40        // Parse time range from nanoseconds
41        let now_ns = crate::now_unix_secs() * 1_000_000_000;
42        let start_ns = request
43            .start
44            .unwrap_or((now_ns - 3_600_000_000_000) as u128) as u64;
45        let end_ns = request.end.unwrap_or(now_ns as u128) as u64;
46        let step_ns = request.step_secs * 1_000_000_000;
47
48        let response = self.store.query_metric(
49            metric,
50            &rustc_hash::FxHashMap::default(),
51            start_ns,
52            end_ns,
53            step_ns,
54        );
55
56        Promise::from_ready(Ok(response))
57    }
58
59    fn fetch_label_names(&self, _ctx: &egui::Context) -> Promise<LabelsResult> {
60        // Return all unique label names across all metrics
61        let names = self.store.metric_names();
62        let mut all_labels: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
63        for name in &names {
64            for label in self.store.metric_label_names(name) {
65                if !label.starts_with("__") {
66                    all_labels.insert(label);
67                }
68            }
69        }
70        let mut sorted: Vec<String> = all_labels.into_iter().collect();
71        sorted.sort();
72        Promise::from_ready(Ok(sorted))
73    }
74
75    fn fetch_label_values(&self, label: &str, _ctx: &egui::Context) -> Promise<LabelsResult> {
76        let names = self.store.metric_names();
77        let mut all_values: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
78        for name in &names {
79            for value in self.store.metric_label_values(name, label) {
80                all_values.insert(value);
81            }
82        }
83        let mut sorted: Vec<String> = all_values.into_iter().collect();
84        sorted.sort();
85        Promise::from_ready(Ok(sorted))
86    }
87
88    fn fetch_metric_names(&self, _ctx: &egui::Context) -> Promise<LabelsResult> {
89        let names = self.store.metric_names();
90        Promise::from_ready(Ok(names))
91    }
92
93    #[allow(clippy::disallowed_types)]
94    fn fetch_metric_labels(
95        &self,
96        metric: &str,
97        _ctx: &egui::Context,
98    ) -> Promise<MetricLabelsResult> {
99        let label_names = self.store.metric_label_names(metric);
100        let mut label_values = std::collections::HashMap::new();
101        for name in &label_names {
102            if name.starts_with("__") {
103                continue;
104            }
105            let values = self.store.metric_label_values(metric, name);
106            label_values.insert(name.clone(), values);
107        }
108        Promise::from_ready(Ok(MetricLabels {
109            labels: label_values,
110        }))
111    }
112
113    fn backend_type(&self) -> &'static str {
114        "otlp"
115    }
116
117    fn health_check(&self, _ctx: &egui::Context) -> Promise<HealthCheckResult> {
118        Promise::from_ready(Ok(BackendInfo {
119            backend_type: "otlp".to_string(),
120            version: format!(
121                "in-memory ({} series, {} points)",
122                self.store.metric_series_count(),
123                self.store.metric_point_count()
124            ),
125        }))
126    }
127}