Skip to main content

linkrs_metrics/manager/
core.rs

1//! Facade owner: state layout, construction and scoping keys.
2//!
3//! Holds every backing store but implements no recording policy itself.
4//! Counter primitives live in `counters`, query observability in `profiles`
5//! and `query_metrics`, error and pattern delegation in `errors` and
6//! `aggregated`, and per-subsystem recorders in their own files.
7
8use dashmap::DashMap;
9use parking_lot::RwLock;
10use serde::{Deserialize, Serialize};
11use std::collections::VecDeque;
12use std::sync::atomic::{AtomicUsize, Ordering};
13use std::sync::Arc;
14
15use crate::aggregated_stats::AggregatedStatsManager;
16use crate::error_stats::ErrorStatsManager;
17use crate::latency_histogram::LatencyHistogram;
18use crate::metrics::QueryMetrics;
19use crate::profile::QueryProfile;
20use crate::slow_query_logger::{SlowQueryConfig, SlowQueryLogger};
21
22use super::metric_type::MetricType;
23use super::metric_value::SpaceMetrics;
24
25/// Per-second query bucket for the fixed-length ring.
26#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
27pub struct TimeseriesBucket {
28    pub second: u64,
29    pub queries: u64,
30    pub total_latency_us: u64,
31    pub errors: u64,
32}
33
34impl TimeseriesBucket {
35    pub fn avg_latency_ms(&self) -> f64 {
36        if self.queries == 0 {
37            0.0
38        } else {
39            self.total_latency_us as f64 / self.queries as f64 / 1000.0
40        }
41    }
42}
43
44/// Resource sample point recorded by the server layer.
45#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
46pub struct ResourceSample {
47    pub second: u64,
48    pub memory_used_bytes: u64,
49    pub memory_total_bytes: u64,
50    pub cpu_usage_percent: f64,
51}
52
53fn unix_secs_now() -> u64 {
54    std::time::SystemTime::now()
55        .duration_since(std::time::UNIX_EPOCH)
56        .unwrap_or_default()
57        .as_secs()
58}
59
60/// Statistics Manager
61///
62/// Unified management of query metrics, query profiling and error statistics.
63#[derive(Debug)]
64pub struct StatsManager {
65    pub(crate) metrics: Arc<DashMap<MetricType, Arc<super::metric_value::MetricValue>>>,
66    pub(crate) space_metrics: Arc<DashMap<String, SpaceMetrics>>,
67    pub(crate) index_metrics: Arc<DashMap<String, SpaceMetrics>>,
68    pub(crate) last_query_metrics: Arc<RwLock<Option<QueryMetrics>>>,
69    pub(crate) query_metrics_history: Arc<RwLock<VecDeque<QueryMetrics>>>,
70    pub(crate) bloom_lock: Arc<parking_lot::Mutex<()>>,
71    pub(crate) query_profiles: Arc<RwLock<VecDeque<QueryProfile>>>,
72    pub(crate) query_latency_histogram: Arc<RwLock<LatencyHistogram>>,
73    pub(crate) search_latency_histogram: Arc<RwLock<LatencyHistogram>>,
74    pub(crate) monitoring_enabled: bool,
75    pub(crate) profile_cache_size: usize,
76    pub(crate) slow_query_threshold_us: u64,
77    pub(crate) error_stats: ErrorStatsManager,
78    pub(crate) slow_query_logger: Option<Arc<SlowQueryLogger>>,
79    pub(crate) aggregated_stats: AggregatedStatsManager,
80    pub(crate) timeseries: Arc<RwLock<VecDeque<TimeseriesBucket>>>,
81    pub(crate) resource_series: Arc<RwLock<VecDeque<ResourceSample>>>,
82    pub(crate) timeseries_capacity: Arc<AtomicUsize>,
83}
84
85impl StatsManager {
86    /// Single key rule: `space_{id}` for numeric spaces, `space_name:{name}`
87    /// for named collections, `target:{name}` for sync targets,
88    /// `rebuild:{target}:{phase}` for rebuild phases, and
89    /// `space_{id}.{index}` for per-space indexes. Distinct prefixes keep
90    /// same-named entities in different subsystems from polluting each other
91    /// while identical spaces share one key.
92    pub fn space_key(space_id: u64) -> String {
93        format!("space_{}", space_id)
94    }
95
96    pub fn space_key_for_name(name: &str) -> String {
97        format!("space_name:{name}")
98    }
99
100    pub fn target_key(target: &str) -> String {
101        format!("target:{target}")
102    }
103
104    pub fn rebuild_key(target: &str, phase: &str) -> String {
105        format!("rebuild:{target}:{phase}")
106    }
107
108    pub fn index_key(space_id: u64, index_name: &str) -> String {
109        format!("space_{}.{}", space_id, index_name)
110    }
111
112    pub fn unknown_index_key(index_name: &str) -> String {
113        format!("unknown.{}", index_name)
114    }
115
116    pub fn new() -> Self {
117        Self {
118            metrics: Arc::new(DashMap::new()),
119            space_metrics: Arc::new(DashMap::new()),
120            index_metrics: Arc::new(DashMap::new()),
121            last_query_metrics: Arc::new(RwLock::new(None)),
122            query_metrics_history: Arc::new(RwLock::new(VecDeque::with_capacity(1000))),
123            bloom_lock: Arc::new(parking_lot::Mutex::new(())),
124            query_profiles: Arc::new(RwLock::new(VecDeque::with_capacity(1000))),
125            query_latency_histogram: Arc::new(RwLock::new(LatencyHistogram::new(10000))),
126            search_latency_histogram: Arc::new(RwLock::new(LatencyHistogram::new(10000))),
127            monitoring_enabled: true,
128            profile_cache_size: 1000,
129            slow_query_threshold_us: 1_000_000,
130            error_stats: ErrorStatsManager::new(),
131            slow_query_logger: None,
132            aggregated_stats: AggregatedStatsManager::new(),
133            timeseries: Arc::new(RwLock::new(VecDeque::with_capacity(3600))),
134            resource_series: Arc::new(RwLock::new(VecDeque::with_capacity(3600))),
135            timeseries_capacity: Arc::new(AtomicUsize::new(3600)),
136        }
137    }
138
139    pub fn with_config(
140        monitoring_enabled: bool,
141        profile_cache_size: usize,
142        slow_query_threshold_us: u64,
143    ) -> Self {
144        Self {
145            metrics: Arc::new(DashMap::new()),
146            space_metrics: Arc::new(DashMap::new()),
147            index_metrics: Arc::new(DashMap::new()),
148            last_query_metrics: Arc::new(RwLock::new(None)),
149            query_metrics_history: Arc::new(RwLock::new(VecDeque::with_capacity(1000))),
150            bloom_lock: Arc::new(parking_lot::Mutex::new(())),
151            query_profiles: Arc::new(RwLock::new(VecDeque::with_capacity(profile_cache_size))),
152            query_latency_histogram: Arc::new(RwLock::new(LatencyHistogram::new(10000))),
153            search_latency_histogram: Arc::new(RwLock::new(LatencyHistogram::new(10000))),
154            monitoring_enabled,
155            profile_cache_size,
156            slow_query_threshold_us,
157            error_stats: ErrorStatsManager::new(),
158            slow_query_logger: None,
159            aggregated_stats: AggregatedStatsManager::new(),
160            timeseries: Arc::new(RwLock::new(VecDeque::with_capacity(3600))),
161            resource_series: Arc::new(RwLock::new(VecDeque::with_capacity(3600))),
162            timeseries_capacity: Arc::new(AtomicUsize::new(3600)),
163        }
164    }
165
166    /// Create StatsManager with slow query logger
167    pub fn with_slow_query_logger(
168        monitoring_enabled: bool,
169        profile_cache_size: usize,
170        slow_query_threshold_us: u64,
171        slow_query_config: SlowQueryConfig,
172    ) -> Result<Self, std::io::Error> {
173        let logger = Arc::new(SlowQueryLogger::new(slow_query_config)?);
174
175        Ok(Self {
176            metrics: Arc::new(DashMap::new()),
177            space_metrics: Arc::new(DashMap::new()),
178            index_metrics: Arc::new(DashMap::new()),
179            last_query_metrics: Arc::new(RwLock::new(None)),
180            query_metrics_history: Arc::new(RwLock::new(VecDeque::with_capacity(1000))),
181            bloom_lock: Arc::new(parking_lot::Mutex::new(())),
182            query_profiles: Arc::new(RwLock::new(VecDeque::with_capacity(profile_cache_size))),
183            query_latency_histogram: Arc::new(RwLock::new(LatencyHistogram::new(10000))),
184            search_latency_histogram: Arc::new(RwLock::new(LatencyHistogram::new(10000))),
185            monitoring_enabled,
186            profile_cache_size,
187            slow_query_threshold_us,
188            error_stats: ErrorStatsManager::new(),
189            slow_query_logger: Some(logger),
190            aggregated_stats: AggregatedStatsManager::new(),
191            timeseries: Arc::new(RwLock::new(VecDeque::with_capacity(3600))),
192            resource_series: Arc::new(RwLock::new(VecDeque::with_capacity(3600))),
193            timeseries_capacity: Arc::new(AtomicUsize::new(3600)),
194        })
195    }
196
197    pub fn slow_query_threshold_us(&self) -> u64 {
198        self.slow_query_threshold_us
199    }
200
201    pub fn timeseries_capacity(&self) -> usize {
202        self.timeseries_capacity.load(Ordering::Relaxed)
203    }
204
205    pub fn set_timeseries_capacity(&self, capacity: usize) {
206        let capacity = capacity.clamp(60, 86400);
207        self.timeseries_capacity.store(capacity, Ordering::Relaxed);
208        let mut series = self.timeseries.write();
209        while series.len() > capacity {
210            series.pop_front();
211        }
212        let mut resources = self.resource_series.write();
213        while resources.len() > capacity {
214            resources.pop_front();
215        }
216    }
217
218    pub fn set_histogram_max_samples(&self, max_samples: usize) {
219        let max_samples = max_samples.clamp(100, 100_000);
220        self.query_latency_histogram
221            .write()
222            .set_max_samples(max_samples);
223        self.search_latency_histogram
224            .write()
225            .set_max_samples(max_samples);
226    }
227
228    pub(crate) fn record_timeseries_query(&self, latency_us: u64, is_error: bool) {
229        let second = unix_secs_now();
230        let capacity = self.timeseries_capacity();
231        let mut series = self.timeseries.write();
232        if let Some(back) = series.back_mut() {
233            if back.second == second {
234                back.queries += 1;
235                back.total_latency_us += latency_us;
236                if is_error {
237                    back.errors += 1;
238                }
239                return;
240            }
241        }
242        series.push_back(TimeseriesBucket {
243            second,
244            queries: 1,
245            total_latency_us: latency_us,
246            errors: if is_error { 1 } else { 0 },
247        });
248        while series.len() > capacity {
249            series.pop_front();
250        }
251    }
252
253    pub fn record_resource_sample(
254        &self,
255        memory_used_bytes: u64,
256        memory_total_bytes: u64,
257        cpu_usage_percent: f64,
258    ) {
259        let second = unix_secs_now();
260        let capacity = self.timeseries_capacity();
261        let mut series = self.resource_series.write();
262        if let Some(back) = series.back_mut() {
263            if back.second == second {
264                back.memory_used_bytes = memory_used_bytes;
265                back.memory_total_bytes = memory_total_bytes;
266                back.cpu_usage_percent = cpu_usage_percent;
267                return;
268            }
269        }
270        series.push_back(ResourceSample {
271            second,
272            memory_used_bytes,
273            memory_total_bytes,
274            cpu_usage_percent,
275        });
276        while series.len() > capacity {
277            series.pop_front();
278        }
279    }
280
281    pub fn query_timeseries(&self, window_secs: u64) -> Vec<TimeseriesBucket> {
282        let cutoff = unix_secs_now().saturating_sub(window_secs);
283        self.timeseries
284            .read()
285            .iter()
286            .filter(|b| b.second >= cutoff)
287            .cloned()
288            .collect()
289    }
290
291    pub fn resource_timeseries(&self, window_secs: u64) -> Vec<ResourceSample> {
292        let cutoff = unix_secs_now().saturating_sub(window_secs);
293        self.resource_series
294            .read()
295            .iter()
296            .filter(|s| s.second >= cutoff)
297            .cloned()
298            .collect()
299    }
300}
301
302impl Default for StatsManager {
303    fn default() -> Self {
304        Self::new()
305    }
306}