1#![allow(non_snake_case)]
19
20use std::collections::{VecDeque, HashMap};
84use std::sync::{Arc, RwLock};
85use std::time::{SystemTime, UNIX_EPOCH, Duration};
86use serde::{Serialize, Deserialize};
87
88#[cfg(feature = "pyo3")]
89use pyo3::prelude::*;
90
91use crate::core::RiResult;
92use crate::core::lock::RwLockExtensions;
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
97pub enum RiMetricType {
98 Counter,
99 Gauge,
100 Histogram,
101 Summary,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
107pub struct RiMetricSample {
108 pub timestamp: u64, pub value: f64,
110 pub labels: Vec<(String, String)>,
111}
112
113#[derive(Debug, Clone)]
115#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
116pub struct RiMetricConfig {
117 pub metric_type: RiMetricType,
118 pub name: String,
119 pub help: String,
120 pub buckets: Vec<f64>, pub quantiles: Vec<f64>, pub max_age: Duration, pub age_buckets: usize, }
125
126#[allow(dead_code)]
128struct RiSlidingWindow {
129 #[allow(dead_code)]
130 window_size: Duration,
131 #[allow(dead_code)]
132 bucket_size: Duration,
133 buckets: VecDeque<Vec<RiMetricSample>>,
134 current_bucket: Vec<RiMetricSample>,
135 #[allow(dead_code)]
136 last_rotation: u64,
137}
138
139impl RiSlidingWindow {
140 fn new(window_size: Duration, bucket_size: Duration) -> Self {
141 let bucket_count = window_size.as_secs().div_ceil(bucket_size.as_secs());
142
143 Self {
144 window_size,
145 bucket_size,
146 buckets: VecDeque::with_capacity(bucket_count as usize),
147 current_bucket: Vec::new(),
148 last_rotation: Self::current_timestamp(),
149 }
150 }
151
152 #[allow(dead_code)]
153 fn current_timestamp() -> u64 {
154 SystemTime::now()
155 .duration_since(UNIX_EPOCH)
156 .unwrap_or(Duration::from_secs(0))
157 .as_secs()
158 }
159
160 #[allow(dead_code)]
161 fn rotate_if_needed(&mut self) {
162 let now = Self::current_timestamp();
163 let elapsed = now.saturating_sub(self.last_rotation);
164
165 if elapsed >= self.bucket_size.as_secs() {
166 let rotations = elapsed / self.bucket_size.as_secs();
167
168 for _ in 0..rotations {
169 self.buckets.push_back(std::mem::take(&mut self.current_bucket));
170
171 let max_buckets = self.window_size.as_secs().div_ceil(self.bucket_size.as_secs());
173 while self.buckets.len() > max_buckets as usize {
174 self.buckets.pop_front();
175 }
176 }
177
178 self.last_rotation = now;
179 }
180 }
181
182 #[allow(dead_code)]
183 fn add_sample(&mut self, sample: RiMetricSample) {
184 self.rotate_if_needed();
185 self.current_bucket.push(sample);
186 }
187
188 #[allow(dead_code)]
189 fn get_samples(&self) -> Vec<RiMetricSample> {
190 let mut all_samples = Vec::with_capacity(8);
191
192 for bucket in &self.buckets {
193 all_samples.extend(bucket.iter().cloned());
194 }
195 all_samples.extend(self.current_bucket.iter().cloned());
196
197 all_samples
198 }
199
200 #[allow(dead_code)]
201 fn get_window_stats(&self) -> RiWindowStats {
202 let samples = self.get_samples();
203
204 if samples.is_empty() {
205 return RiWindowStats::default();
206 }
207
208 let mut sorted_values: Vec<f64> = samples.iter().map(|s| s.value).collect();
209 sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
210
211 let count = sorted_values.len();
212 let sum: f64 = sorted_values.iter().sum();
213 let min = sorted_values[0];
214 let max = sorted_values[count - 1];
215 let mean = sum / count as f64;
216
217 let variance: f64 = sorted_values
219 .iter()
220 .map(|x| (x - mean).powi(2))
221 .sum::<f64>() / count as f64;
222 let stddev = variance.sqrt();
223
224 let p50 = Self::quantile(&sorted_values, 0.50);
226 let p90 = Self::quantile(&sorted_values, 0.90);
227 let p95 = Self::quantile(&sorted_values, 0.95);
228 let p99 = Self::quantile(&sorted_values, 0.99);
229
230 RiWindowStats {
231 count: count as u64,
232 sum,
233 min,
234 max,
235 mean,
236 stddev,
237 p50,
238 p90,
239 p95,
240 p99,
241 }
242 }
243
244 #[allow(dead_code)]
245 fn quantile(sorted_values: &[f64], q: f64) -> f64 {
246 if sorted_values.is_empty() {
247 return 0.0;
248 }
249
250 let index = q * (sorted_values.len() - 1) as f64;
251 let lower = index.floor() as usize;
252 let upper = index.ceil() as usize;
253
254 if lower == upper {
255 sorted_values[lower]
256 } else {
257 let weight = index - lower as f64;
258 sorted_values[lower] * (1.0 - weight) + sorted_values[upper] * weight
259 }
260 }
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct RiWindowStats {
266 pub count: u64,
267 pub sum: f64,
268 pub min: f64,
269 pub max: f64,
270 pub mean: f64,
271 pub stddev: f64,
272 pub p50: f64,
273 pub p90: f64,
274 pub p95: f64,
275 pub p99: f64,
276}
277
278impl Default for RiWindowStats {
279 fn default() -> Self {
280 Self {
281 count: 0,
282 sum: 0.0,
283 min: 0.0,
284 max: 0.0,
285 mean: 0.0,
286 stddev: 0.0,
287 p50: 0.0,
288 p90: 0.0,
289 p95: 0.0,
290 p99: 0.0,
291 }
292 }
293}
294
295#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
297pub struct RiMetric {
298 config: RiMetricConfig,
299 sliding_window: RwLock<RiSlidingWindow>,
300 total_count: RwLock<u64>,
301 #[allow(dead_code)]
302 total_sum: RwLock<f64>,
303}
304
305impl RiMetric {
306 pub fn new(config: RiMetricConfig) -> Self {
307 let sliding_window = RiSlidingWindow::new(
308 Duration::from_secs(300), Duration::from_secs(10), );
311
312 Self {
313 config,
314 sliding_window: RwLock::new(sliding_window),
315 total_count: RwLock::new(0),
316 total_sum: RwLock::new(0.0),
317 }
318 }
319
320 pub fn record(&self, value: f64, labels: Vec<(String, String)>) -> RiResult<()> {
321 let sample = RiMetricSample {
322 timestamp: Self::current_timestamp(),
323 value,
324 labels,
325 };
326
327 {
328 let mut window = self.sliding_window.write_safe("sliding window")?;
329 window.add_sample(sample);
330 }
331
332 {
333 let mut count = self.total_count.write_safe("total count")?;
334 *count += 1;
335 }
336
337 {
338 let mut sum = self.total_sum.write_safe("total sum")?;
339 *sum += value;
340 }
341
342 Ok(())
343 }
344
345 #[allow(dead_code)]
346 fn get_stats(&self) -> RiWindowStats {
347 match self.sliding_window.read_safe("sliding window stats") {
348 Ok(window) => window.get_window_stats(),
349 Err(_) => RiWindowStats::default(),
350 }
351 }
352
353 pub fn get_total_count(&self) -> u64 {
354 match self.total_count.read_safe("total count") {
355 Ok(count) => *count,
356 Err(_) => 0,
357 }
358 }
359
360 #[allow(dead_code)]
361 fn get_total_sum(&self) -> f64 {
362 match self.total_sum.read_safe("total sum") {
363 Ok(sum) => *sum,
364 Err(_) => 0.0,
365 }
366 }
367
368 pub fn get_config(&self) -> &RiMetricConfig {
369 &self.config
370 }
371
372 pub fn get_value(&self) -> f64 {
373 match self.total_count.read_safe("total count value") {
374 Ok(count) => *count as f64,
375 Err(_) => 0.0,
376 }
377 }
378
379 #[allow(dead_code)]
380 fn current_timestamp() -> u64 {
381 SystemTime::now()
382 .duration_since(UNIX_EPOCH)
383 .unwrap_or_default()
384 .as_secs()
385 }
386}
387
388#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
390#[derive(Clone)]
391pub struct RiMetricsRegistry {
392 metrics: Arc<RwLock<HashMap<String, Arc<RiMetric>>>>,
393}
394
395impl RiMetricsRegistry {
396 const MAX_METRICS: usize = 10000;
398
399 const MAX_NAME_LENGTH: usize = 256;
401
402 pub fn new() -> Self {
403 Self {
404 metrics: Arc::new(RwLock::new(HashMap::new())),
405 }
406 }
407
408 fn validate_metric_name(name: &str) -> RiResult<()> {
417 if name.is_empty() || name.len() > Self::MAX_NAME_LENGTH {
418 return Err(crate::core::RiError::Other(format!(
419 "Metric name must be 1-{} characters",
420 Self::MAX_NAME_LENGTH
421 )));
422 }
423
424 let chars: Vec<char> = name.chars().collect();
425
426 if !chars[0].is_ascii_alphabetic() {
428 return Err(crate::core::RiError::Other(
429 "Metric name must start with a letter".to_string()
430 ));
431 }
432
433 for c in &chars {
434 if !c.is_ascii_alphanumeric() && *c != '_' && *c != ':' {
435 return Err(crate::core::RiError::Other(
436 "Metric name can only contain alphanumeric characters, underscores, and colons".to_string()
437 ));
438 }
439 }
440
441 Ok(())
442 }
443
444 pub fn register(&self, metric: Arc<RiMetric>) -> RiResult<()> {
445 let name = metric.get_config().name.clone();
446
447 Self::validate_metric_name(&name)?;
449
450 let mut metrics = self.metrics.write_safe("metrics registry")?;
451
452 if metrics.len() >= Self::MAX_METRICS && !metrics.contains_key(&name) {
454 return Err(crate::core::RiError::Other(format!(
455 "Maximum metrics limit reached: {} metrics",
456 Self::MAX_METRICS
457 )));
458 }
459
460 metrics.insert(name, metric);
461 Ok(())
462 }
463
464 pub fn get_metric(&self, name: &str) -> Option<Arc<RiMetric>> {
465 match self.metrics.read_safe("metrics registry") {
466 Ok(metrics) => metrics.get(name).cloned(),
467 Err(_) => None,
468 }
469 }
470
471 pub fn get_all_metrics(&self) -> HashMap<String, Arc<RiMetric>> {
472 match self.metrics.read_safe("metrics registry") {
473 Ok(metrics) => metrics.clone(),
474 Err(_) => HashMap::new(),
475 }
476 }
477
478 #[cfg(feature = "observability")]
480 pub fn export_prometheus(&self) -> String {
481 let mut output = String::new();
482 let metrics = match self.metrics.read_safe("metrics registry for export") {
483 Ok(m) => m,
484 Err(_) => return "# Error: Failed to acquire metrics registry lock".to_string(),
485 };
486
487 for (name, metric) in metrics.iter() {
488 let config = metric.get_config();
489
490 output.push_str(&format!("# HELP {} {}\n", name, config.help));
492 output.push_str(&format!("# TYPE {} {:?}\n", name, config.metric_type));
493
494 let stats = metric.get_stats();
496 match config.metric_type {
497 RiMetricType::Counter => {
498 output.push_str(&format!("{} {}\n", name, metric.get_total_count()));
499 }
500 RiMetricType::Gauge => {
501 output.push_str(&format!("{} {}\n", name, stats.mean));
502 }
503 RiMetricType::Histogram => {
504 output.push_str(&format!("{}_count {}\n", name, stats.count));
505 output.push_str(&format!("{}_sum {}\n", name, stats.sum));
506 output.push_str(&format!("{}_min {}\n", name, stats.min));
507 output.push_str(&format!("{}_max {}\n", name, stats.max));
508 output.push_str(&format!("{}_avg {}\n", name, stats.mean));
509 output.push_str(&format!("{}_p50 {}\n", name, stats.p50));
510 output.push_str(&format!("{}_p90 {}\n", name, stats.p90));
511 output.push_str(&format!("{}_p95 {}\n", name, stats.p95));
512 output.push_str(&format!("{}_p99 {}\n", name, stats.p99));
513 }
514 RiMetricType::Summary => {
515 output.push_str(&format!("{} {}\n", name, stats.mean));
516 }
517 }
518
519 output.push('\n');
520 }
521
522 output
523 }
524}
525
526#[cfg(feature = "pyo3")]
527#[pyo3::prelude::pymethods]
529impl RiMetricsRegistry {
530 #[new]
532 fn py_new() -> Self {
533 Self::new()
534 }
535
536 #[pyo3(name = "register")]
538 fn register_py(&self, metric: &RiMetric) -> PyResult<()> {
539 let name = metric.config.name.clone();
540 let mut metrics = self.metrics.write_safe("metrics registry")
541 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;
542 metrics.insert(name, Arc::new(RiMetric::new(metric.config.clone())));
543 Ok(())
544 }
545
546 #[pyo3(name = "get_metric_value")]
548 fn get_metric_value_impl(&self, name: &str) -> Option<f64> {
549 self.get_metric(name).map(|m| m.get_value())
550 }
551
552 #[pyo3(name = "get_all_metric_names")]
554 fn get_all_metric_names_impl(&self) -> Vec<String> {
555 let metrics = match self.metrics.read_safe("metrics registry for names") {
556 Ok(m) => m,
557 Err(_) => return Vec::new(),
558 };
559 metrics.keys().cloned().collect()
560 }
561
562 #[pyo3(name = "export_prometheus")]
564 fn export_prometheus_impl(&self) -> String {
565 #[cfg(feature = "observability")]
566 {
567 self.export_prometheus()
568 }
569 #[cfg(not(feature = "observability"))]
570 {
571 "# Observability feature not enabled".to_string()
572 }
573 }
574
575 #[pyo3(name = "get_metric_count")]
577 fn get_metric_count_impl(&self) -> usize {
578 let metrics = match self.metrics.read_safe("metrics registry for count") {
579 Ok(m) => m,
580 Err(_) => return 0,
581 };
582 metrics.len()
583 }
584}
585
586#[cfg(feature = "pyo3")]
587#[pyo3::prelude::pymethods]
588impl RiMetricConfig {
589 #[new]
590 #[pyo3(signature = (name, metric_type, help="", buckets=None, quantiles=None))]
591 fn py_new(
592 name: String,
593 metric_type: RiMetricType,
594 help: &str,
595 buckets: Option<Vec<f64>>,
596 quantiles: Option<Vec<f64>>,
597 ) -> Self {
598 Self {
599 name,
600 metric_type,
601 help: help.to_string(),
602 buckets: buckets.unwrap_or_else(|| vec![0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]),
603 quantiles: quantiles.unwrap_or_else(|| vec![0.5, 0.9, 0.95, 0.99]),
604 max_age: Duration::from_secs(600),
605 age_buckets: 5,
606 }
607 }
608}
609
610#[cfg(feature = "pyo3")]
611#[pyo3::prelude::pymethods]
612impl RiMetric {
613 #[new]
614 fn py_new(config: RiMetricConfig) -> Self {
615 Self::new(config)
616 }
617
618 #[pyo3(name = "record")]
619 fn record_py(&self, value: f64) -> PyResult<()> {
620 self.record(value, vec![])
621 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
622 }
623
624 #[pyo3(name = "get_value")]
625 fn get_value_py(&self) -> f64 {
626 self.get_value()
627 }
628
629 #[pyo3(name = "get_total_count")]
630 fn get_total_count_py(&self) -> u64 {
631 self.get_total_count()
632 }
633}