metrics_rs_dashboard_actix/
lib.rs

1//! # Metrics Module
2//!
3//! This module provides comprehensive Prometheus metrics integration for Actix web applications.
4//! It enables robust monitoring capabilities with automatic metric collection, exposition,
5//! and visualization through an integrated dashboard.
6//!
7//! ## Features
8//! - **Prometheus Integration**: Full support for collecting and exposing metrics in Prometheus format
9//! - **Interactive Dashboard**: Built-in web UI for visualizing metrics in real-time
10//! - **Rate Metrics**: Automatic calculation and tracking of per-second rates from counter values
11//! - **Customizable Histograms**: Fine-grained control over histogram bucket configuration
12//! - **Easy Integration**: Seamlessly integrates with Actix web applications via a simple API
13//! - **Thread-Safe**: Designed for concurrent access with proper synchronization
14//! - **Low Overhead**: Minimal performance impact on your application
15//!
16//! ## Architecture
17//! The module uses a multi-recorder approach with a fanout pattern to capture both metric values
18//! and their associated metadata (like units). This information is then made available both in
19//! Prometheus format for scraping and through a dashboard for human-readable visualization.
20//!
21//! ## Getting Started
22//! Simply add the metrics scope to your Actix application as shown in the examples below.
23
24/// Re-export of the `metrics` crate for measuring and recording application metrics
25pub use metrics;
26use metrics::{Counter, CounterFn, Gauge, GaugeFn, Histogram, HistogramFn, Key, Recorder, Unit};
27/// Re-export of the `metrics_exporter_prometheus` crate for exposing metrics in Prometheus format
28pub use metrics_exporter_prometheus;
29/// Re-export of the `metrics_util` crate for utility functions related to metrics
30pub use metrics_util;
31
32use actix_web::{HttpResponse, Responder, Scope, web};
33use anyhow::Result;
34use log::debug;
35use log_once::debug_once;
36use metrics_exporter_prometheus::{Matcher, PrometheusBuilder, PrometheusHandle};
37use metrics_util::layers::FanoutBuilder;
38use mime_guess::from_path;
39use rust_embed::Embed;
40use std::{
41    collections::HashMap,
42    sync::{
43        Arc, Mutex, OnceLock,
44        atomic::{AtomicBool, Ordering},
45    },
46    time::{Duration, Instant},
47};
48
49/// Global flag to track if metrics recorders have been configured
50static IS_CONFIGURED: AtomicBool = AtomicBool::new(false);
51
52/// Global Prometheus recorder instance
53static PROMETHEUS_HANDLE: OnceLock<PrometheusHandle> = OnceLock::new();
54
55/// Global storage for metric unit information
56///
57/// Maps metric names to their corresponding units, which is used
58/// by the dashboard to correctly display unit information in charts
59static UNITS_FOR_METRICS: OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();
60
61/// Global storage for rate trackers
62///
63/// Maps counter names to their rate tracking instances
64static RATE_TRACKERS: OnceLock<Mutex<HashMap<String, RateTracker>>> = OnceLock::new();
65
66/// Embedded assets for the metrics dashboard
67#[derive(Embed)]
68#[folder = "public/"]
69struct Asset;
70
71/// Rate tracking utility for calculating per-second rates from counter values
72///
73/// This struct tracks the last value and timestamp of a counter to calculate
74/// the rate of change over time. It's used internally by the rate metric
75/// functionality to provide per-second rate calculations.
76#[derive(Debug, Clone)]
77pub struct RateTracker {
78    samples: Vec<(f64, Instant)>,
79    window_duration: Duration,
80    max_samples: usize,
81    last_value: f64,                   // Store the last value to handle resets
82    start_time: Option<Instant>,       // Track when we first started
83    last_calculated_rate: f64,         // Store the last calculated rate
84    last_update_time: Option<Instant>, // Time of last update for better rate calculation
85}
86
87impl Default for RateTracker {
88    fn default() -> Self {
89        Self::new()
90    }
91}
92
93impl RateTracker {
94    /// Creates a new RateTracker with sliding window for high-frequency updates
95    pub fn new() -> Self {
96        Self {
97            samples: Vec::new(),
98            window_duration: Duration::from_secs(10), // 10-second sliding window for better stability
99            max_samples: 200,                         // Limit memory usage
100            last_value: 0.0,
101            start_time: None,
102            last_calculated_rate: 0.0,
103            last_update_time: None,
104        }
105    }
106
107    /// Updates the tracker with a new value and calculates the rate
108    ///
109    /// # Arguments
110    /// * `new_value` - The new counter value
111    ///
112    /// # Returns
113    /// The calculated rate per second based on sliding window analysis
114    pub fn update(&mut self, new_value: f64) -> f64 {
115        let now = Instant::now();
116
117        // Initialize start time if this is the first update
118        if self.start_time.is_none() {
119            self.start_time = Some(now);
120            self.last_update_time = Some(now);
121            self.last_value = new_value;
122            self.samples.push((new_value, now));
123            return 0.0; // First sample, can't calculate rate yet
124        }
125
126        // Calculate time since last update for short-term rate
127        let short_term_rate = if let Some(last_time) = self.last_update_time {
128            let elapsed = now.duration_since(last_time).as_secs_f64();
129            // If update happens rapidly, use elapsed time for immediate rate feedback
130            if elapsed > 0.001 && new_value > self.last_value {
131                let instant_rate = (new_value - self.last_value) / elapsed;
132                // If we have a previous rate, blend them for stability
133                if self.last_calculated_rate > 0.0 {
134                    // Weighted blend: 30% new rate, 70% old rate for stability
135                    0.3 * instant_rate + 0.7 * self.last_calculated_rate
136                } else {
137                    instant_rate
138                }
139            } else {
140                // If values aren't changing or time is too short, use last calculated rate
141                self.last_calculated_rate
142            }
143        } else {
144            0.0
145        };
146
147        // Skip full recalculation if the new value is the same as the last one
148        if new_value == self.last_value {
149            return short_term_rate;
150        }
151
152        // Detect counter resets (new value < last value)
153        if new_value < self.last_value {
154            // Counter reset detected - clear samples and start fresh
155            self.samples.clear();
156            self.start_time = Some(now);
157            self.last_value = new_value;
158            self.last_update_time = Some(now);
159            self.samples.push((new_value, now));
160            self.last_calculated_rate = 0.0;
161            return 0.0;
162        }
163
164        // Add new sample
165        self.samples.push((new_value, now));
166        self.last_value = new_value;
167        self.last_update_time = Some(now);
168
169        // Remove samples outside the window
170        let cutoff = now - self.window_duration;
171        self.samples.retain(|(_, timestamp)| *timestamp > cutoff);
172
173        // Limit samples to prevent unbounded growth
174        if self.samples.len() > self.max_samples {
175            let excess = self.samples.len() - self.max_samples;
176            self.samples.drain(0..excess);
177        }
178
179        // If we don't have at least 2 samples, use start time as fallback
180        if self.samples.len() < 2 {
181            if let Some(start) = self.start_time {
182                let elapsed = now.duration_since(start).as_secs_f64();
183                if elapsed > 0.0 {
184                    // Use first value in samples and the elapsed time since start
185                    let first_value = self.samples[0].0;
186                    let rate = (new_value - first_value) / elapsed;
187                    self.last_calculated_rate = rate.max(0.0);
188                    return self.last_calculated_rate;
189                }
190            }
191            return short_term_rate;
192        }
193
194        // Calculate rate using oldest and newest samples in window
195        let (first_value, first_time) = self.samples[0];
196        let (last_value, last_time) = self.samples[self.samples.len() - 1];
197
198        let time_diff = last_time.duration_since(first_time).as_secs_f64();
199
200        if time_diff <= 0.0 {
201            return short_term_rate;
202        }
203
204        let value_diff = last_value - first_value;
205
206        // Ensure we don't return negative rates for counters
207        let long_term_rate = (value_diff / time_diff).max(0.0);
208
209        // Blend short and long term rates for stability
210        // If they're very different, prefer the long-term rate
211        let rate = if (long_term_rate - short_term_rate).abs() > long_term_rate * 0.5 {
212            long_term_rate
213        } else {
214            // Otherwise use weighted average
215            0.7 * long_term_rate + 0.3 * short_term_rate
216        };
217
218        self.last_calculated_rate = rate;
219        rate
220    }
221}
222
223/// Configuration options for the metrics dashboard
224#[derive(Debug, Clone, Default)]
225pub struct DashboardInput<'a> {
226    /// Custom set of buckets for histogram metrics.
227    ///
228    /// Each tuple contains:
229    /// - A `Matcher` to identify which metrics should use these buckets
230    /// - A slice of f64 values representing the bucket boundaries
231    ///
232    /// This allows fine-tuning the histogram resolution for specific metrics.
233    /// For example, setting different bucket ranges for latency metrics vs.
234    /// memory usage metrics.
235    ///
236    /// # Example
237    /// ```
238    /// use metrics_exporter_prometheus::Matcher;
239    /// let latency_buckets = &[0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0];
240    /// let buckets = vec![(Matcher::Full("http_request_duration".to_string()), latency_buckets)];
241    /// ```
242    pub buckets_for_metrics: Vec<(Matcher, &'a [f64])>,
243}
244
245/// The UnitRecorder captures unit metadata from metrics registrations
246///
247/// This recorder doesn't actually record metric values - it only stores the
248/// unit information associated with each metric in a global map. This information
249/// is later used by the dashboard to correctly label and scale visualizations.
250///
251/// The unit information is sent to the client via a custom HTTP header when
252/// metrics are requested from the dashboard.
253///
254/// Format of header: x-dashboard-metrics-unit: {"request_latency":"count","request_latency_gauge":"count","async_counter":"count","async_gauge":"milliseconds"}
255
256#[derive(Debug)]
257struct UnitRecorder;
258
259/// Handle for the UnitRecorder
260///
261/// This is a no-op implementation that just stores the metric key
262/// but doesn't actually record any values.
263#[derive(Clone, Debug)]
264#[allow(dead_code)]
265struct UnitRecorderHandle(Key);
266
267impl CounterFn for UnitRecorderHandle {
268    fn increment(&self, _value: u64) {
269        // No-op
270    }
271
272    fn absolute(&self, _value: u64) {
273        // No-op
274    }
275}
276
277impl GaugeFn for UnitRecorderHandle {
278    fn increment(&self, _value: f64) {
279        // No-op
280    }
281
282    fn decrement(&self, _value: f64) {
283        // No-op
284    }
285
286    fn set(&self, _value: f64) {
287        // No-op
288    }
289}
290
291impl HistogramFn for UnitRecorderHandle {
292    fn record(&self, _value: f64) {
293        // No-op
294    }
295}
296
297impl Recorder for UnitRecorder {
298    fn describe_counter(
299        &self,
300        key: metrics::KeyName,
301        unit: Option<metrics::Unit>,
302        _description: metrics::SharedString,
303    ) {
304        self.register_unit(key, unit);
305    }
306
307    fn describe_gauge(
308        &self,
309        key: metrics::KeyName,
310        unit: Option<metrics::Unit>,
311        _description: metrics::SharedString,
312    ) {
313        self.register_unit(key, unit);
314    }
315
316    fn describe_histogram(
317        &self,
318        key: metrics::KeyName,
319        unit: Option<metrics::Unit>,
320        _description: metrics::SharedString,
321    ) {
322        self.register_unit(key, unit);
323    }
324
325    fn register_counter(
326        &self,
327        key: &metrics::Key,
328        _metadata: &metrics::Metadata<'_>,
329    ) -> metrics::Counter {
330        Counter::from_arc(Arc::new(UnitRecorderHandle(key.clone())))
331    }
332
333    fn register_gauge(
334        &self,
335        key: &metrics::Key,
336        _metadata: &metrics::Metadata<'_>,
337    ) -> metrics::Gauge {
338        Gauge::from_arc(Arc::new(UnitRecorderHandle(key.clone())))
339    }
340
341    fn register_histogram(
342        &self,
343        key: &metrics::Key,
344        _metadata: &metrics::Metadata<'_>,
345    ) -> metrics::Histogram {
346        Histogram::from_arc(Arc::new(UnitRecorderHandle(key.clone())))
347    }
348}
349
350impl UnitRecorder {
351    /// Registers a metric's unit in the global units map
352    ///
353    /// This method extracts the unit information from a metric registration
354    /// and stores it in the global UNITS_FOR_METRICS map for later use.
355    ///
356    /// # Arguments
357    ///
358    /// * `key` - The name of the metric
359    /// * `unit` - Optional unit of the metric (defaults to Count if None)
360    fn register_unit(&self, key: metrics::KeyName, unit: Option<metrics::Unit>) {
361        let key = key.as_str().to_owned();
362        let unit = unit.unwrap_or(Unit::Count);
363        let unit = unit.as_str().to_owned();
364        let g_unit = UNITS_FOR_METRICS.get_or_init(|| Mutex::new(HashMap::new()));
365        if let Ok(mut locked) = g_unit.lock() {
366            locked.insert(key, unit);
367        }
368    }
369}
370
371/// Serves embedded files from the Asset struct
372///
373/// This helper function handles serving static files that are embedded
374/// in the binary using rust-embed. It automatically sets the proper
375/// content type based on file extension.
376///
377/// # Arguments
378///
379/// * `path` - Path to the file within the embedded assets
380///
381/// # Returns
382///
383/// HttpResponse containing the file content with appropriate MIME type,
384/// or a 404 Not Found response if the asset doesn't exist
385fn handle_embedded_file(path: &str) -> HttpResponse {
386    match Asset::get(path) {
387        Some(content) => HttpResponse::Ok()
388            .content_type(from_path(path).first_or_octet_stream().as_ref())
389            .body(content.data.into_owned()),
390        None => HttpResponse::NotFound().body("404 Not Found"),
391    }
392}
393
394/// Handler for the metrics dashboard index page
395///
396/// Serves the main HTML interface for the metrics dashboard.
397/// This interactive dashboard provides visualizations of all
398/// application metrics with auto-refreshing charts.
399///
400/// # Returns
401///
402/// The main dashboard HTML page
403#[actix_web::get("/dashboard")]
404async fn get_dashboard() -> impl Responder {
405    handle_embedded_file("index.html")
406}
407
408/// Handler for serving dashboard assets (JS, CSS, etc.)
409///
410/// Handles requests for static assets needed by the dashboard UI.
411/// This includes JavaScript files, stylesheets, images, and any
412/// other resources required by the dashboard interface.
413///
414/// # Arguments
415///
416/// * `path` - Path to the requested asset, extracted from the URL
417///
418/// # Returns
419///
420/// The requested asset file with appropriate content type
421#[actix_web::get("/dashboard/{_:.*}")]
422async fn get_dashboard_assets(path: web::Path<String>) -> impl Responder {
423    handle_embedded_file(path.as_str())
424}
425
426/// Endpoint for exposing Prometheus metrics
427///
428/// This endpoint is where Prometheus should scrape to collect metrics.
429/// It returns all application metrics in the standard Prometheus text format.
430/// Additionally, it includes unit information in a custom HTTP header for
431/// use by the dashboard.
432///
433/// # Returns
434///
435/// Prometheus metrics in the standard text-based exposition format
436/// with an additional "x-dashboard-metrics-unit" header containing
437/// unit information for metrics
438#[actix_web::get("/prometheus")]
439async fn get_prometheus_metrics() -> impl Responder {
440    debug!("Gathering prometheus metrics...");
441    let prometheus_handle = PROMETHEUS_HANDLE.get();
442    let metrics_units = UNITS_FOR_METRICS.get();
443    let mut response = HttpResponse::Ok();
444
445    if let Some(metrics_units) = metrics_units {
446        let header = serde_json::to_string(metrics_units).unwrap_or_default();
447        response.append_header(("x-dashboard-metrics-unit", header));
448    }
449
450    if let Some(handle) = prometheus_handle {
451        let metrics = handle.render();
452        return response.body(metrics);
453    }
454
455    HttpResponse::Ok().body(String::from(""))
456}
457
458/// Configures metrics recorders if they haven't been configured yet
459///
460/// This function is idempotent and safe to call multiple times.
461/// Only the first call will actually configure the recorders, subsequent
462/// calls will return early with success. This is achieved through thread-safe
463/// synchronization using atomic operations.
464///
465/// The function sets up:
466/// 1. A Prometheus recorder for actual metric values
467/// 2. A UnitRecorder to capture unit metadata
468/// 3. A FanoutBuilder to dispatch metrics to both recorders
469///
470/// # Arguments
471///
472/// * `input` - Configuration options for the metrics system, including custom histogram buckets
473///
474/// # Returns
475///
476/// Result indicating success or failure of configuration
477///
478/// # Errors
479///
480/// Returns an error if:
481/// - Cannot acquire the configuration lock
482/// - Failed to set custom histogram buckets
483/// - Unable to set the Prometheus handle
484/// - Unable to register the global recorder
485fn configure_metrics_recorders_once(input: &DashboardInput) -> Result<()> {
486    // Return early if already configured, using "Acquire" ordering to ensure
487    // visibility of all operations performed before setting to true
488    if IS_CONFIGURED.load(Ordering::Acquire) {
489        debug_once!("Metrics recorder already configured. Skipping duplicate configuration.");
490        return Ok(());
491    }
492
493    // Try to be the first thread to configure
494    if IS_CONFIGURED
495        .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
496        .is_err()
497    {
498        // Another thread configured the metrics in the meantime
499        debug_once!("Another thread configured metrics. Skipping duplicate configuration.");
500        return Ok(());
501    }
502
503    let mut prometheus_recorder = PrometheusBuilder::new();
504
505    if !input.buckets_for_metrics.is_empty() {
506        for (matcher, buckets) in input.buckets_for_metrics.iter() {
507            prometheus_recorder = prometheus_recorder
508                .set_buckets_for_metric(matcher.to_owned(), buckets)
509                .map_err(|e| anyhow::anyhow!("Failed to set buckets for metric: {}", e))?;
510        }
511    }
512
513    let prometheus_recorder = prometheus_recorder
514        .set_enable_unit_suffix(false)
515        .build_recorder();
516
517    PROMETHEUS_HANDLE
518        .set(prometheus_recorder.handle())
519        .map_err(|e| anyhow::anyhow!("Unable to set Prometheus handle: {}", e.render()))?;
520
521    let fanout = FanoutBuilder::default()
522        .add_recorder(UnitRecorder)
523        .add_recorder(prometheus_recorder)
524        .build();
525
526    tokio::spawn(async move {
527        let handle = PROMETHEUS_HANDLE.get();
528
529        if let Some(handle) = handle {
530            loop {
531                tokio::time::sleep(std::time::Duration::from_secs(30)).await;
532                handle.run_upkeep();
533            }
534        } else {
535            debug!("Prometheus handle not set. Skipping recorder cleanup.");
536        }
537    });
538
539    metrics::set_global_recorder(fanout).map_err(|e| {
540        anyhow::anyhow!(
541            "Unable to register a recorder: {}. Did you call this function multiple times?",
542            e
543        )
544    })?;
545
546    Ok(())
547}
548
549/// Updates a rate tracker and returns the calculated rate
550///
551/// This function is used internally by the rate macros to calculate
552/// and track per-second rates from counter values.
553pub fn update_rate_tracker(_counter_name: &str, value: f64, tracker_key: String) -> f64 {
554    let rate_trackers = RATE_TRACKERS.get_or_init(|| Mutex::new(HashMap::new()));
555    if let Ok(mut trackers) = rate_trackers.lock() {
556        let tracker = trackers
557            .entry(tracker_key.clone())
558            .or_insert_with(RateTracker::new);
559
560        // Always calculate a rate, even with the same value
561        // The RateTracker will handle the logic to determine the actual rate
562        tracker.update(value)
563    } else {
564        // If we can't get the lock, attempt a minimal calculation
565        // This is better than returning 0.0 which would indicate no activity
566        static LAST_VALUES: OnceLock<Mutex<HashMap<String, (f64, Instant)>>> = OnceLock::new();
567        let last_values = LAST_VALUES.get_or_init(|| Mutex::new(HashMap::new()));
568
569        if let Ok(mut values) = last_values.lock() {
570            let now = Instant::now();
571            let entry = values.entry(tracker_key).or_insert((0.0, now));
572
573            let (last_value, last_time) = *entry;
574            let elapsed = now.duration_since(last_time).as_secs_f64();
575
576            if elapsed > 0.0 && value > last_value {
577                let rate = (value - last_value) / elapsed;
578                *entry = (value, now);
579                return rate;
580            }
581
582            // Update even if we can't calculate a rate
583            *entry = (value, now);
584        }
585
586        // Fallback if everything fails
587        0.001 // Return tiny non-zero value to show some activity
588    }
589}
590
591/// Macro for recording a counter with automatic rate tracking
592///
593/// This macro records both a counter value and its per-second rate.
594///
595/// # Example
596///
597/// ```rust
598/// use metrics_rs_dashboard_actix::counter_with_rate;
599///
600/// // Simple counter with rate
601/// counter_with_rate!("requests_total", 1.0);
602///
603/// // Counter with labels and rate
604/// counter_with_rate!("requests_total", 1.0, "endpoint", "/api/users");
605/// ```
606#[macro_export]
607macro_rules! counter_with_rate {
608    ($name:expr, $value:expr) => {{
609        use $crate::update_rate_tracker;
610
611        // Record the counter
612        let counter = metrics::counter!($name);
613        counter.increment($value as u64);
614
615        // Get the current absolute value of the counter for rate calculation
616        // We track the cumulative value separately for rate calculations
617        use std::sync::OnceLock;
618        use std::sync::Mutex;
619        use std::collections::HashMap;
620
621        static COUNTER_VALUES: OnceLock<Mutex<HashMap<String, f64>>> = OnceLock::new();
622        let counter_values = COUNTER_VALUES.get_or_init(|| Mutex::new(HashMap::new()));
623
624        let absolute_value = if let Ok(mut values) = counter_values.lock() {
625            let key = format!("{}_default", $name);
626            let current = values.entry(key).or_insert(0.0);
627            *current += $value;
628            *current
629        } else {
630            // If lock fails, still try to update with just the increment value
631            $value
632        };
633
634        // Calculate and record the rate using absolute counter value
635        let rate_name = format!("{}_rate_per_sec", $name);
636        let tracker_key = format!("{}_default", $name);
637        let rate = update_rate_tracker($name, absolute_value, tracker_key);
638
639        // Ensure we always set a rate value, even if it's very small
640        let display_rate = if rate < 0.001 && $value > 0.0 { 0.001 } else { rate };
641        metrics::gauge!(rate_name).set(display_rate);
642    }};
643    ($name:expr, $value:expr, $label_key:expr, $label_value:expr) => {{
644        use $crate::update_rate_tracker;
645
646        // Record the counter with labels
647        let counter = metrics::counter!($name, $label_key => $label_value);
648        counter.increment($value as u64);
649
650        // Get the current absolute value of the counter for rate calculation
651        use std::sync::OnceLock;
652        use std::sync::Mutex;
653        use std::collections::HashMap;
654
655        static COUNTER_VALUES: OnceLock<Mutex<HashMap<String, f64>>> = OnceLock::new();
656        let counter_values = COUNTER_VALUES.get_or_init(|| Mutex::new(HashMap::new()));
657
658        let absolute_value = if let Ok(mut values) = counter_values.lock() {
659            let key = format!("{}_{}_{}", $name, $label_key, $label_value);
660            let current = values.entry(key).or_insert(0.0);
661            *current += $value;
662            *current
663        } else {
664            // If lock fails, still try to update with just the increment value
665            $value
666        };
667
668        // Calculate and record the rate using absolute counter value
669        let rate_name = format!("{}_rate_per_sec", $name);
670        let tracker_key = format!("{}_{}_{}", $name, $label_key, $label_value);
671        let rate = update_rate_tracker($name, absolute_value, tracker_key);
672
673        // Ensure we always set a rate value, even if it's very small
674        let display_rate = if rate < 0.001 && $value > 0.0 { 0.001 } else { rate };
675        metrics::gauge!(rate_name, $label_key => $label_value).set(display_rate);
676    }};
677}
678
679/// Macro for recording an absolute counter value with automatic rate tracking
680///
681/// This macro is similar to `counter_with_rate!` but sets the counter to an absolute value.
682///
683/// # Example
684///
685/// ```rust
686/// use metrics_rs_dashboard_actix::absolute_counter_with_rate;
687///
688/// // Simple absolute counter with rate
689/// absolute_counter_with_rate!("bytes_processed_total", 1024.0);
690///
691/// // Absolute counter with labels and rate
692/// absolute_counter_with_rate!("db_queries_total", 42.0, "type", "SELECT");
693/// ```
694#[macro_export]
695macro_rules! absolute_counter_with_rate {
696    ($name:expr, $value:expr) => {{
697        use $crate::update_rate_tracker;
698
699        // Record the absolute counter
700        metrics::counter!($name).absolute($value as u64);
701
702        // Calculate and record the rate directly using the absolute value
703        let rate_name = format!("{}_rate_per_sec", $name);
704        let tracker_key = format!("{}_default", $name);
705        let rate = update_rate_tracker($name, $value, tracker_key);
706
707        // Ensure we always set a rate value, even if it's very small
708        let display_rate = if rate < 0.001 && $value > 0.0 { 0.001 } else { rate };
709        metrics::gauge!(rate_name).set(display_rate);
710    }};
711    ($name:expr, $value:expr, $label_key:expr, $label_value:expr) => {{
712        use $crate::update_rate_tracker;
713
714        // Record the absolute counter with labels
715        metrics::counter!($name, $label_key => $label_value).absolute($value as u64);
716
717        // Calculate and record the rate directly using the absolute value
718        let rate_name = format!("{}_rate_per_sec", $name);
719        let tracker_key = format!("{}_{}_{}", $name, $label_key, $label_value);
720        let rate = update_rate_tracker($name, $value, tracker_key);
721
722        // Ensure we always set a rate value, even if it's very small
723        let display_rate = if rate < 0.001 && $value > 0.0 { 0.001 } else { rate };
724        metrics::gauge!(rate_name, $label_key => $label_value).set(display_rate);
725    }};
726}
727
728/// Creates an Actix web scope for metrics endpoints
729///
730/// This function configures metrics recorders and creates a scope with
731/// all necessary routes for the metrics dashboard and Prometheus endpoint.
732/// It's the main entry point for integrating metrics into your Actix application.
733///
734/// The function:
735/// 1. Initializes the metrics system (if not already done)
736/// 2. Creates an Actix web scope with path "/metrics"
737/// 3. Registers all necessary endpoints (/prometheus, /dashboard, etc.)
738///
739/// # Arguments
740///
741/// * `input` - Configuration options for the metrics system
742///
743/// # Returns
744///
745/// Result containing the configured Actix web Scope that can be integrated
746/// into an Actix web application
747///
748/// # Example
749///
750/// ```rust,no_run
751/// use actix_web::{App, HttpServer};
752/// use metrics_rs_dashboard_actix::{create_metrics_actx_scope, DashboardInput};
753///
754/// #[actix_web::main]
755/// async fn main() -> std::io::Result<()> {
756///     HttpServer::new(|| {
757///         App::new()
758///             .service(create_metrics_actx_scope(&DashboardInput::default()).unwrap())
759///             // Your other services...
760///     })
761///     .bind(("127.0.0.1", 8080))?
762///     .run()
763///     .await
764/// }
765/// ```
766pub fn create_metrics_actx_scope(input: &DashboardInput) -> Result<Scope> {
767    configure_metrics_recorders_once(input)?;
768    let scope = web::scope("/metrics")
769        .service(get_prometheus_metrics)
770        .service(get_dashboard)
771        .service(get_dashboard_assets);
772    Ok(scope)
773}
774
775#[cfg(test)]
776mod tests {
777    use super::*;
778    use std::thread;
779    use std::time::Duration;
780
781    #[test]
782    fn test_rate_tracker_new() {
783        let tracker = RateTracker::new();
784        assert!(tracker.samples.is_empty());
785        assert_eq!(tracker.window_duration, Duration::from_secs(2));
786        assert_eq!(tracker.max_samples, 200);
787    }
788
789    #[test]
790    fn test_rate_tracker_default() {
791        let tracker = RateTracker::default();
792        assert!(tracker.samples.is_empty());
793        assert_eq!(tracker.window_duration, Duration::from_secs(2));
794        assert_eq!(tracker.max_samples, 200);
795    }
796
797    #[test]
798    fn test_rate_tracker_first_update() {
799        let mut tracker = RateTracker::new();
800
801        let rate = tracker.update(10.0);
802
803        // First update should return 0.0 (no previous sample)
804        assert_eq!(rate, 0.0);
805        assert_eq!(tracker.samples.len(), 1);
806        assert_eq!(tracker.samples[0].0, 10.0);
807    }
808
809    #[test]
810    fn test_rate_tracker_subsequent_updates() {
811        let mut tracker = RateTracker::new();
812
813        // First update
814        tracker.update(10.0);
815
816        // Wait a bit to ensure time difference
817        thread::sleep(Duration::from_millis(20));
818
819        // Second update
820        let rate = tracker.update(20.0);
821
822        // Rate should be positive (10 units over ~0.02 seconds = ~500 units/sec)
823        assert!(rate > 0.0);
824        assert!(rate > 100.0); // Should be high due to short time interval
825        assert_eq!(tracker.samples.len(), 2);
826    }
827
828    #[test]
829    fn test_rate_tracker_negative_rate_clamping() {
830        let mut tracker = RateTracker::new();
831
832        // First update with higher value
833        tracker.update(20.0);
834
835        thread::sleep(Duration::from_millis(20));
836
837        // Second update with lower value (would normally give negative rate)
838        let rate = tracker.update(10.0);
839
840        // Rate should be clamped to 0.0 for counters (negative rates become 0.0)
841        assert_eq!(rate, 0.0);
842        assert_eq!(tracker.samples.len(), 2);
843        assert_eq!(tracker.samples[1].0, 10.0);
844    }
845
846    #[test]
847    fn test_rate_tracker_high_frequency_updates() {
848        let mut tracker = RateTracker::new();
849
850        // First update
851        tracker.update(10.0);
852
853        // Immediate second update (now handles high frequency)
854        let rate = tracker.update(20.0);
855
856        // Should calculate rate even for very fast updates
857        assert!(rate >= 0.0);
858        assert_eq!(tracker.samples.len(), 2);
859        assert_eq!(tracker.samples[1].0, 20.0);
860    }
861
862    #[test]
863    fn test_update_rate_tracker_function() {
864        let tracker_key = "test_metric_default".to_string();
865
866        // First call
867        let rate1 = update_rate_tracker("test_metric", 10.0, tracker_key.clone());
868        assert_eq!(rate1, 0.0); // First call should return 0
869
870        thread::sleep(Duration::from_millis(200));
871
872        // Second call
873        let rate2 = update_rate_tracker("test_metric", 20.0, tracker_key);
874        assert!(rate2 >= 0.0); // Should return a valid rate
875    }
876
877    #[test]
878    fn test_counter_with_rate_macro_simple() {
879        // This test verifies the macro compiles and doesn't panic
880        // We can't easily test the actual metric recording without setting up the full recorder
881        let result = std::panic::catch_unwind(|| {
882            counter_with_rate!("test_counter", 1.0);
883        });
884
885        // The macro should complete without panicking
886        // Note: In a real test environment, you'd verify the metrics were actually recorded
887        assert!(result.is_ok());
888    }
889
890    #[test]
891    fn test_counter_with_rate_macro_with_labels() {
892        // This test verifies the macro with labels compiles and doesn't panic
893        let result = std::panic::catch_unwind(|| {
894            counter_with_rate!("test_counter_labeled", 2.0, "service", "api");
895        });
896
897        assert!(result.is_ok());
898    }
899
900    #[test]
901    fn test_absolute_counter_with_rate_macro_simple() {
902        // This test verifies the macro compiles and doesn't panic
903        let result = std::panic::catch_unwind(|| {
904            absolute_counter_with_rate!("test_absolute_counter", 42.0);
905        });
906
907        assert!(result.is_ok());
908    }
909
910    #[test]
911    fn test_absolute_counter_with_rate_macro_with_labels() {
912        // This test verifies the macro with labels compiles and doesn't panic
913        let result = std::panic::catch_unwind(|| {
914            absolute_counter_with_rate!("test_absolute_counter_labeled", 100.0, "type", "batch");
915        });
916
917        assert!(result.is_ok());
918    }
919
920    #[test]
921    fn test_rate_calculation_accuracy() {
922        let mut tracker = RateTracker::new();
923
924        // Set initial value
925        tracker.update(0.0);
926
927        // Wait exactly 1 second
928        thread::sleep(Duration::from_secs(1));
929
930        // Add 10 units after 1 second
931        let rate = tracker.update(10.0);
932
933        // Rate should be approximately 10 units/second
934        assert!(
935            (rate - 10.0).abs() < 1.0,
936            "Rate {} should be close to 10.0",
937            rate
938        );
939    }
940
941    #[test]
942    fn test_multiple_rate_tracker_instances() {
943        let key1 = "metric1_default".to_string();
944        let key2 = "metric2_default".to_string();
945
946        // Test that different tracker keys maintain separate state
947        update_rate_tracker("metric1", 10.0, key1.clone());
948        update_rate_tracker("metric2", 20.0, key2.clone());
949
950        thread::sleep(Duration::from_millis(200));
951
952        let rate1 = update_rate_tracker("metric1", 15.0, key1);
953        let rate2 = update_rate_tracker("metric2", 30.0, key2);
954
955        // Both should return valid rates
956        assert!(rate1 >= 0.0);
957        assert!(rate2 >= 0.0);
958
959        // Rates should be different since the value changes are different
960        // (5 units vs 10 units over the same time period)
961        if rate1 > 0.0 && rate2 > 0.0 {
962            assert!(
963                (rate2 / rate1 - 2.0).abs() < 0.5,
964                "Rate2 ({}) should be approximately twice rate1 ({})",
965                rate2,
966                rate1
967            );
968        }
969    }
970
971    #[test]
972    fn test_dashboard_input_default() {
973        let input = DashboardInput::default();
974        assert!(input.buckets_for_metrics.is_empty());
975    }
976
977    #[test]
978    fn test_dashboard_input_with_buckets() {
979        let buckets = &[1.0, 5.0, 10.0];
980        let input = DashboardInput {
981            buckets_for_metrics: vec![(
982                metrics_exporter_prometheus::Matcher::Full("test_metric".to_string()),
983                buckets,
984            )],
985        };
986
987        assert_eq!(input.buckets_for_metrics.len(), 1);
988        assert_eq!(input.buckets_for_metrics[0].1, buckets);
989    }
990
991    #[test]
992    fn test_rate_tracker_zero_value_update() {
993        let mut tracker = RateTracker::new();
994
995        thread::sleep(Duration::from_millis(150));
996
997        // Update with 0.0 value
998        let rate = tracker.update(0.0);
999
1000        // Should return 0.0 rate (first update)
1001        assert_eq!(rate, 0.0);
1002        assert_eq!(tracker.samples.len(), 1);
1003        assert_eq!(tracker.samples[0].0, 0.0);
1004    }
1005
1006    #[test]
1007    fn test_rate_tracker_large_values() {
1008        let mut tracker = RateTracker::new();
1009
1010        // First update
1011        tracker.update(500_000.0);
1012
1013        thread::sleep(Duration::from_millis(20));
1014
1015        // Test with large values
1016        let large_value = 1_000_000.0;
1017        let rate = tracker.update(large_value);
1018
1019        assert!(rate > 0.0);
1020        assert_eq!(tracker.samples.len(), 2);
1021        assert_eq!(tracker.samples[1].0, large_value);
1022    }
1023
1024    #[test]
1025    fn test_rate_tracker_fractional_values() {
1026        let mut tracker = RateTracker::new();
1027
1028        // First update with fractional value
1029        tracker.update(1.5);
1030
1031        thread::sleep(Duration::from_millis(20));
1032
1033        // Second update with another fractional value
1034        let rate = tracker.update(3.7);
1035
1036        // Should handle fractional values correctly
1037        assert!(rate > 0.0);
1038        assert_eq!(tracker.samples.len(), 2);
1039        assert_eq!(tracker.samples[1].0, 3.7);
1040    }
1041
1042    #[test]
1043    fn test_update_rate_tracker_concurrent_access() {
1044        use std::thread;
1045
1046        let handles: Vec<_> = (0..5)
1047            .map(|i| {
1048                thread::spawn(move || {
1049                    let tracker_key = format!("concurrent_test_{}", i);
1050
1051                    // Each thread updates its own tracker
1052                    update_rate_tracker("concurrent_metric", 10.0, tracker_key.clone());
1053
1054                    thread::sleep(Duration::from_millis(200));
1055
1056                    update_rate_tracker("concurrent_metric", 20.0, tracker_key)
1057                })
1058            })
1059            .collect();
1060
1061        // Wait for all threads to complete
1062        for handle in handles {
1063            let rate = handle.join().expect("Thread should complete successfully");
1064            assert!(rate >= 0.0);
1065        }
1066    }
1067
1068    #[test]
1069    fn test_rate_tracker_consistent_timestamps() {
1070        let mut tracker = RateTracker::new();
1071
1072        let start_time = std::time::Instant::now();
1073
1074        thread::sleep(Duration::from_millis(20));
1075
1076        tracker.update(5.0);
1077
1078        // Check that the sample was recorded with a reasonable timestamp
1079        assert_eq!(tracker.samples.len(), 1);
1080        assert!(tracker.samples[0].1 > start_time);
1081    }
1082}