Skip to main content

proxima_primitives/pipe/
telemetry_surface.rs

1//! Telemetry trait surface destined for `proxima-pipe`.
2//!
3//! Lifted from `telemetry/legacy.rs` during Phase 1.5 of the
4//! decomposition (see `docs/decomposition/discipline.md`). The trait +
5//! `Labels` value type + `NoopTelemetry` default impl + `TelemetryHandle`
6//! alias live here so `request.rs` can reference them without pulling
7//! the full metrics impl (dashmap, hdrhistogram, thread_local).
8//!
9//! On Phase 2 (proxima-pipe extraction) this file's contents move to the
10//! new `proxima-pipe` crate. The full `Metrics` impl in `legacy.rs` moves
11//! to `proxima-telemetry` and depends on this trait surface.
12
13#![cfg(feature = "alloc")]
14
15use alloc::string::String;
16use alloc::string::ToString;
17use alloc::sync::Arc;
18use alloc::vec::Vec;
19
20use serde::{Deserialize, Serialize};
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub struct Labels {
24    entries: Vec<(String, String)>,
25}
26
27impl Labels {
28    #[must_use]
29    pub fn empty() -> Self {
30        Self {
31            entries: Vec::new(),
32        }
33    }
34
35    #[must_use]
36    pub fn from_pairs(pairs: &[(&str, &str)]) -> Self {
37        let mut entries: Vec<(String, String)> = pairs
38            .iter()
39            .map(|(name, value)| ((*name).to_string(), (*value).to_string()))
40            .collect();
41        entries.sort_by(|left, right| left.0.cmp(&right.0));
42        Self { entries }
43    }
44
45    #[must_use]
46    pub fn entries(&self) -> &[(String, String)] {
47        &self.entries
48    }
49
50    #[must_use]
51    pub fn is_empty(&self) -> bool {
52        self.entries.is_empty()
53    }
54}
55
56impl<const SIZE: usize> From<&[(&str, &str); SIZE]> for Labels {
57    fn from(pairs: &[(&str, &str); SIZE]) -> Self {
58        Self::from_pairs(pairs)
59    }
60}
61
62pub trait Telemetry: Send + Sync + 'static {
63    fn counter_inc(&self, metric: &str, labels: &Labels, by: u64);
64    fn gauge_set(&self, metric: &str, labels: &Labels, value: i64);
65    fn histogram_record(&self, metric: &str, labels: &Labels, value: f64);
66
67    /// Hot-path gate: when this returns `false`, a caller on a tight loop may
68    /// skip building `Labels` and computing values entirely — the records
69    /// would be dropped anyway. Defaults to `true`; the no-op handle returns
70    /// `false` so a telemetry-less client pays nothing per call.
71    fn is_active(&self) -> bool {
72        true
73    }
74}
75
76pub type TelemetryHandle = Arc<dyn Telemetry>;
77
78pub struct NoopTelemetry;
79
80impl NoopTelemetry {
81    #[must_use]
82    pub fn handle() -> TelemetryHandle {
83        Arc::new(NoopTelemetry)
84    }
85}
86
87impl Telemetry for NoopTelemetry {
88    fn counter_inc(&self, _metric: &str, _labels: &Labels, _by: u64) {}
89    fn gauge_set(&self, _metric: &str, _labels: &Labels, _value: i64) {}
90    fn histogram_record(&self, _metric: &str, _labels: &Labels, _value: f64) {}
91    fn is_active(&self) -> bool {
92        false
93    }
94}
95
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
97pub struct HistogramSummary {
98    pub count: u64,
99    pub min: f64,
100    pub max: f64,
101    pub mean: f64,
102    pub p50: f64,
103    pub p90: f64,
104    pub p95: f64,
105    pub p99: f64,
106    pub p99_9: f64,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct MetricsSnapshot {
111    pub counters: Vec<(String, Labels, u64)>,
112    pub gauges: Vec<(String, Labels, i64)>,
113    pub histograms: Vec<(String, Labels, HistogramSummary)>,
114}