slog_extlog/
stats.rs

1//! Statistics generator for [`slog`].
2//!
3//! This crate allows for statistics - counters, gauges, and bucket counters - to be automatically
4//! calculated and reported based on logged events.  The logged events MUST implement the
5//! [`ExtLoggable`] trait.
6//!
7//! To support this, the [`slog-extlog-derive`] crate can be used to link logs to a specific
8//! statistic.   This generates fast, compile-time checking for statistics updates at the point
9//! of logging.
10//!
11//! Users should use the [`define_stats`] macro to list their statistics.  They can then pass the
12//! list (along with stats from any dependencies) to a [`StatisticsLogger`] wrapping an
13//! [`slog::Logger`].  The statistics trigger function on the `ExtLoggable` objects then triggers
14//! statistic updates based on logged values.
15//!
16//! Library users should export the result of `define_stats!`, so that binary developers can
17//! track the set of stats from all dependent crates in a single tracker.
18//!
19//! Triggers should be added to [`ExtLoggable`] objects using the [`slog-extlog-derive`] crate.
20//!
21//! [`ExtLoggable`]: ../trait.ExtLoggable.html
22//! [`define_stats`]: ../macro.define_stats.html
23//! [`slog::Logger`]: ../../slog/struct.Logger.html
24//! [`slog`]: ../../slog/index.html
25//! [`slog-extlog-derive`]: ../../slog_extlog_derive/index.html
26//! [`StatisticsLogger`]: ./struct.StatisticsLogger.html
27
28use std::collections::HashMap;
29use std::fmt;
30use std::ops::Deref;
31use std::panic::RefUnwindSafe;
32use std::sync::atomic::{AtomicIsize, Ordering};
33use std::sync::Arc;
34use std::sync::RwLock;
35
36#[cfg(feature = "interval_logging")]
37use std::time::Duration;
38
39use slog::info;
40
41//////////////////////////////////////////////////////
42// Public types - stats definitions
43//////////////////////////////////////////////////////
44
45/// A configured statistic, defined in terms of the external logs that trigger it to change.
46///
47/// These definitions are provided at start of day to populate the tracker.
48///
49/// These should NOT be constructed directly but by using the
50/// [`define_stats`](./macro.define_stats.html) macro.
51///
52pub trait StatDefinition: fmt::Debug {
53    /// The name of this metric.  This name is reported in logs as the `metric_name` field.
54    fn name(&self) -> &'static str;
55    /// A human readable-description of the statistic, describing its meaning.  When logged this
56    /// is the log message.
57    fn description(&self) -> &'static str;
58    /// The type of statistic.
59    fn stype(&self) -> StatType;
60    /// An optional list of field names to group the statistic by.
61    fn group_by(&self) -> Vec<&'static str>;
62    /// An optional set of numerical buckets to group the statistic by.
63    fn buckets(&self) -> Option<Buckets>;
64}
65
66/// A macro to define the statistics that can be tracked by the logger.
67/// Use of this macro requires [`StatDefinition`](trait.StatDefinition.html) to be in scope.
68///
69/// All statistics should be defined by their library using this macro.
70///
71/// The syntax is as follows:
72///
73/// ```text
74///   define_stats!{
75///      STATS_LIST_NAME = {
76///          StatName(Type, "Description", ["tag1", "tag2", ...]),
77///          StatName2(...),
78///          ...
79///      }
80///   }
81/// ```
82///
83/// The `STATS_LIST_NAME` is then created as a vector of definitions that can be passed in as the
84/// `stats` field on a `StatsConfig` object.
85///
86/// Each definition in the list has the format above, with the fields as follows.
87///
88///   - `StatName` is the externally-facing metric name.
89///   - `Type` is the `StatType` of this statistic, for example `Counter`.
90///    Must be a valid subtype of that enum.
91///   - `Description`  is a human readable description of the statistic.  This will be logged as
92///   the log message,
93///   - The list of `tags` define field names to group the statistic by.
94///    A non-empty list indicates that this statistic should be split into groups,
95///   counting the stat separately for each different value of these fields that is seen.
96///   These might be a remote hostname, say, or a tag field.
97///     - If multiple tags are provided, the stat is counted separately for all distinct
98///       combinations of tag values.
99///     - Use of this feature should be avoided for fields that can take very many values, such as
100///   a subscriber number, or for large numbers of tags - each tag name and seen value adds a
101///   performance dip and a small memory overhead that is never freed.
102///   - If the `Type` field is set to `BucketCounter`, then a `BucketMethod`, bucket label and bucket limits must
103///     also be provided like so:
104///
105/// ```text
106///    define_stats!{
107///      STATS_LIST_NAME = {
108///          StatName(BucketCounter, "Description", ["tag1", "tag2", ...], (BucketMethod, "bucket_label", [1, 2, 3, ...])),
109///          StatName2(...),
110///          ...
111///      }
112///   }
113/// ```
114///
115///   - The `BucketMethod` determines how the stat will be sorted into numerical buckets and should
116///   - be a subtype of that enum.
117///   - The bucket limits should be a list of `i64` values, each representing the upper bound of
118///     that bucket.
119///   - The bucket label should describe what the buckets measure and should be distinct from the tags.
120///     Each stat log will be labelled with the pair `(bucket_label, bucket_value)` in addition to the tags,
121///     where `bucket_value` is the numerical value of the bucket the log falls into.
122#[macro_export]
123macro_rules! define_stats {
124
125    // Entry point - match each individual stat name and pass on the details for further parsing
126    ($name:ident = {$($stat:ident($($details:tt),*)),*}) => {
127        /// A vector of stats that can be passed in as the `stats` field on a `StatsConfig` object.
128        pub static $name: $crate::stats::StatDefinitions = &[$(&$stat),*];
129
130        mod inner_stats {
131            $(
132                #[derive(Debug, Clone)]
133                // Prometheus metrics are snake_case, so allow non-camel-case types here.
134                #[allow(non_camel_case_types)]
135                pub struct $stat;
136            )*
137        }
138
139        $(
140            $crate::define_stats!{@single $stat, $($details),*}
141        )*
142    };
143
144    // `BucketCounter`s require a `BucketMethod`, bucket label and bucket limits
145    (@single $stat:ident, BucketCounter, $desc:expr, [$($tags:tt),*], ($bmethod:ident, $blabel:expr, [$($blimits:expr),*]) ) => {
146        $crate::define_stats!{@inner $stat, BucketCounter, $desc, $bmethod, $blabel, [$($tags),*], [$($blimits),*]}
147    };
148
149    // Non `BucketCounter` stat types
150    (@single $stat:ident, $stype:ident, $desc:expr, [$($tags:tt),*] ) => {
151        $crate::define_stats!{@inner $stat, $stype, $desc, Freq, "", [$($tags),*], []}
152    };
153
154    // Retained for backwards-compatibility
155    (@single $stat:ident, $stype:ident, $id:expr, $desc:expr, [$($tags:tt),*] ) => {
156        $crate::define_stats!{@inner $stat, $stype, $desc, Freq, "", [$($tags),*], []}
157    };
158
159    // Trait impl for StatDefinition
160    (@inner $stat:ident, $stype:ident, $desc:expr, $bmethod:ident, $blabel:expr, [$($tags:tt),*], [$($blimits:expr),*]) => {
161
162        // Suppress the warning about cases - this value is never going to be seen
163        #[allow(non_upper_case_globals)]
164        static $stat : inner_stats::$stat = inner_stats::$stat;
165
166        impl $crate::stats::StatDefinition for inner_stats::$stat {
167            /// The name of this statistic.
168            fn name(&self) -> &'static str { stringify!($stat) }
169            /// A human readable-description of the statistic, describing its meaning.
170            fn description(&self) -> &'static str { $desc }
171            /// The type
172            fn stype(&self) -> $crate::stats::StatType { $crate::stats::StatType::$stype }
173            /// An optional list of field names to group the statistic by.
174            fn group_by(&self) -> Vec<&'static str> { vec![$($tags),*] }
175            /// The numerical buckets and bucketing method used to group the statistic.
176            fn buckets(&self) -> Option<$crate::stats::Buckets> {
177                match self.stype() {
178                    $crate::stats::StatType::BucketCounter => {
179                        Some($crate::stats::Buckets::new($crate::stats::BucketMethod::$bmethod,
180                            $blabel,
181                            &[$($blimits as i64),* ],
182                        ))
183                    },
184                    _ => None
185                }
186            }
187        }
188    };
189}
190
191/// A stat definition, possibly filtered with some specific tag values.
192pub struct StatDefinitionTagged {
193    /// The statistic definition
194    pub defn: &'static (dyn StatDefinition + Sync),
195    /// The fixed tag values.  The keys *must* match keys in `defn`.
196    pub fixed_tags: &'static [(&'static str, &'static str)],
197}
198
199impl StatDefinitionTagged {
200    /// Check if the passed set of fixed tags corresponds to this statistic definition.
201    pub fn has_fixed_groups(&self, tags: &[(&str, &str)]) -> bool {
202        if self.fixed_tags.len() != tags.len() {
203            return false;
204        }
205
206        self.fixed_tags.iter().all(|self_tag| tags.contains(self_tag))
207    }
208}
209
210/// A trait indicating that this log can be used to trigger a statistics change.
211pub trait StatTrigger {
212    /// The list of stats that this trigger applies to.
213    fn stat_list(&self) -> &[StatDefinitionTagged];
214    /// The condition that must be satisfied for this stat to change
215    fn condition(&self, _stat_id: &StatDefinitionTagged) -> bool {
216        false
217    }
218    /// Get the associated tag value for this log.
219    /// The value must be convertible to a string so it can be stored internally.
220    fn tag_value(&self, stat_id: &StatDefinitionTagged, _tag_name: &'static str) -> String;
221    /// The details of the change to make for this stat, if `condition` returned true.
222    fn change(&self, _stat_id: &StatDefinitionTagged) -> Option<ChangeType> {
223        None
224    }
225    /// The value to be used to sort the statistic into the correct bucket(s).
226    fn bucket_value(&self, _stat_id: &StatDefinitionTagged) -> Option<f64> {
227        None
228    }
229}
230
231/// Types of changes made to a statistic.
232// LCOV_EXCL_START not interesting to track automatic derive coverage
233#[derive(Debug, Clone, Eq, PartialEq, Hash)]
234pub enum ChangeType {
235    /// Increment by a fixed amount.
236    Incr(usize),
237    /// Decrement by a fixed amount.
238    Decr(usize),
239    /// Set to a specific value.
240    SetTo(isize),
241}
242
243/// Used to represent the upper limit of a bucket.
244#[derive(Debug, Clone, Copy, serde::Serialize, PartialEq, PartialOrd, Eq, Ord)]
245pub enum BucketLimit {
246    /// A numerical upper limit.
247    Num(i64),
248    /// Represents a bucket with no upper limit.
249    Unbounded,
250}
251
252impl slog::Value for BucketLimit {
253    fn serialize(
254        &self,
255        _record: &::slog::Record<'_>,
256        key: ::slog::Key,
257        serializer: &mut dyn (::slog::Serializer),
258    ) -> ::slog::Result {
259        match *self {
260            BucketLimit::Num(value) => serializer.emit_i64(key, value),
261            BucketLimit::Unbounded => serializer.emit_str(key, "Unbounded"),
262        }
263    } // LCOV_EXCL_LINE Kcov bug?
264}
265
266impl fmt::Display for BucketLimit {
267    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268        match self {
269            BucketLimit::Num(val) => write!(f, "{}", val),
270            BucketLimit::Unbounded => write!(f, "Unbounded"),
271        }
272    }
273}
274
275/// A set of numerical buckets together with a method for sorting values into them.
276#[derive(Debug, Clone, serde::Serialize, PartialEq)]
277pub struct Buckets {
278    /// The method to use to sort values into buckets.
279    pub method: BucketMethod,
280    /// Label name describing what the buckets measure.
281    pub label_name: &'static str,
282    /// The upper bounds of the buckets.
283    limits: Vec<BucketLimit>,
284}
285
286impl Buckets {
287    /// Create a new Buckets instance.
288    pub fn new(method: BucketMethod, label_name: &'static str, limits: &[i64]) -> Buckets {
289        let mut limits: Vec<BucketLimit> = limits.iter().map(|f| BucketLimit::Num(*f)).collect();
290        limits.push(BucketLimit::Unbounded);
291        Buckets {
292            method,
293            label_name,
294            limits,
295        }
296    }
297
298    /// return a vector containing the indices of the buckets that should be updated
299    pub fn assign_buckets(&self, value: f64) -> Vec<usize> {
300        match self.method {
301            BucketMethod::CumulFreq => self
302                .limits
303                .iter()
304                .enumerate()
305                .filter(|(_, limit)| match limit {
306                    BucketLimit::Num(b) => value <= *b as f64,
307                    BucketLimit::Unbounded => true,
308                })
309                .map(|(i, _)| i)
310                .collect(),
311            BucketMethod::Freq => {
312                let mut min_limit_index = self.limits.len() - 1;
313                for (i, limit) in self.limits.iter().enumerate() {
314                    if let BucketLimit::Num(b) = limit {
315                        if value <= *b as f64 && *limit <= self.limits[min_limit_index] {
316                            min_limit_index = i
317                        }
318                    }
319                }
320                vec![min_limit_index]
321            }
322        }
323    }
324    /// The number of buckets.
325    pub fn len(&self) -> usize {
326        self.limits.len()
327    }
328    /// Whether or not the number of buckets is greater than zero.
329    pub fn is_empty(&self) -> bool {
330        self.limits.is_empty()
331    }
332    /// Get the bound of an individual bucket by index.
333    pub fn get(&self, index: usize) -> Option<BucketLimit> {
334        self.limits.get(index).cloned()
335    }
336}
337
338/// Used to determine which buckets to update when a BucketCounter stat is updated
339#[derive(Debug, Clone, Copy, serde::Serialize, PartialEq)]
340pub enum BucketMethod {
341    /// When a value is recorded, only update the bucket it lands in
342    Freq,
343    /// When a value us recorded, update its bucket and every higher bucket
344    CumulFreq,
345}
346
347/// Types of statistics.  Automatically determined from the `StatDefinition`.
348#[derive(Debug, Clone, Copy, serde::Serialize, PartialEq)]
349pub enum StatType {
350    /// A counter - a value that only increments.
351    Counter,
352    /// A gauge - a value that represents a current value and can go up or down.
353    Gauge,
354    /// A counter that is additionally grouped into numerical buckets
355    BucketCounter,
356}
357// LCOV_EXCL_STOP
358
359impl slog::Value for StatType {
360    fn serialize(
361        &self,
362        _record: &::slog::Record<'_>,
363        key: ::slog::Key,
364        serializer: &mut dyn (::slog::Serializer),
365    ) -> ::slog::Result {
366        match *self {
367            StatType::Counter => serializer.emit_str(key, "counter"),
368            StatType::Gauge => serializer.emit_str(key, "gauge"),
369            StatType::BucketCounter => serializer.emit_str(key, "bucket counter"),
370        }
371    } // LCOV_EXCL_LINE Kcov bug?
372}
373
374/////////////////////////////////////////////////////////////////////////////////
375// The statistics tracker and related fields.
376/////////////////////////////////////////////////////////////////////////////////
377
378/// An object that tracks statistics and can be asked to log them
379// LCOV_EXCL_START not interesting to track automatic derive coverage
380#[derive(Debug, Default)]
381pub struct StatsTracker {
382    // The list of statistics, mapping from stat name to value.
383    stats: HashMap<&'static str, Stat>,
384}
385// LCOV_EXCL_STOP
386
387impl StatsTracker {
388    /// Create a new tracker.
389    pub fn new() -> Self {
390        Default::default()
391    }
392
393    /// Add a new statistic to this tracker.
394    pub fn add_statistic(&mut self, defn: &'static (dyn StatDefinition + Sync + RefUnwindSafe)) {
395        let stat = Stat {
396            defn,
397            is_grouped: !defn.group_by().is_empty(),
398            group_values: RwLock::new(HashMap::new()),
399            stat_type_data: StatTypeData::new(defn),
400            value: StatValue::new(0, 1),
401        }; // LCOV_EXCL_LINE Kcov bug?
402
403        self.stats.insert(defn.name(), stat);
404    } // LCOV_EXCL_LINE Kcov bug
405
406    /// Update the statistics for the current log.
407    ///
408    /// This checks for any configured stats that are triggered by this log, and
409    /// updates their value appropriately.
410    fn update_stats(&self, log: &dyn StatTrigger) {
411        for stat_def in log.stat_list() {
412            if log.condition(stat_def) {
413                let stat = &self.stats.get(stat_def.defn.name()).unwrap_or_else(|| {
414                    panic!(
415                        "No statistic found with name {}, did you try writing a log through a
416                         logger which wasn't initialized with your stats definitions?",
417                        stat_def.defn.name()
418                    )
419                });
420
421                stat.update(stat_def, log)
422            }
423        }
424    }
425
426    /// Log all statistics.
427    ///
428    /// This function is usually just called on a timer by the logger directly.
429    pub fn log_all<T: StatisticsLogFormatter>(&self, logger: &StatisticsLogger) {
430        for stat in self.stats.values() {
431            // Log all the grouped and bucketed values.
432            let outputs = stat.get_tagged_vals();
433
434            // The `outputs` is a vector of tuples containing the (tag value, bucket_index, stat value).
435            for (tag_values, val) in outputs {
436                // The tags require a vector of (tag name, tag value) types, so get these if present.
437                let tags = stat.get_tag_pairs(&tag_values);
438
439                T::log_stat(
440                    logger,
441                    &StatLogData {
442                        stype: stat.defn.stype(),
443                        name: stat.defn.name(),
444                        description: stat.defn.description(),
445                        value: val,
446                        tags,
447                    },
448                ); // LCOV_EXCL_LINE Kcov bug?
449            }
450        }
451    }
452
453    /// Retrieve the current values of all stats tracked by this logger.
454    pub fn get_stats(&self) -> Vec<StatSnapshot> {
455        self.stats
456            .values()
457            .map(|stat| stat.get_snapshot())
458            .collect::<Vec<_>>()
459    }
460}
461
462////////////////////////////////////
463// Types to help integrate the tracker with loggers.
464////////////////////////////////////
465
466/// The default period between logging all statistics.
467pub const DEFAULT_LOG_INTERVAL_SECS: u64 = 300;
468
469/// Type alias for the return of [`define_stats`](../macro.define_stats.html).
470pub type StatDefinitions = &'static [&'static (dyn StatDefinition + Sync + RefUnwindSafe)];
471
472/// A builder to allow customization of stats config.  This gives flexibility when the other
473/// methods are insufficient.
474///
475/// Create the builder using `new()` and chain other methods as required, ending with `fuse()` to
476/// return the `StatsConfig`.
477///
478/// # Example
479/// Creating a config with a custom stats interval and the default formatter.
480///
481/// ```
482/// # use slog_extlog::stats::*;
483/// # #[tokio::main]
484/// # async fn main() {
485///
486/// slog_extlog::define_stats! {
487///     MY_STATS = {
488///         SomeStat(Counter, "A test counter", []),
489///         SomeOtherStat(Counter, "Another test counter", [])
490///     }
491/// }
492///
493/// let full_stats = vec![MY_STATS];
494/// let logger = slog::Logger::root(slog::Discard, slog::o!());
495/// let stats = StatsLoggerBuilder::default()
496///     .with_stats(full_stats)
497///     .fuse(logger);
498///
499/// # }
500/// ```
501#[derive(Debug, Default)]
502pub struct StatsLoggerBuilder {
503    /// The list of statistics to track.  This MUST be created using the
504    /// [`define_stats`](../macro.define_stats.html) macro.
505    pub stats: Vec<StatDefinitions>,
506}
507
508impl StatsLoggerBuilder {
509    /// Set the list of statistics to track.
510    pub fn with_stats(mut self, stats: Vec<StatDefinitions>) -> Self {
511        self.stats = stats;
512        self
513    }
514
515    /// Construct the StatisticsLogger
516    pub fn fuse(self, logger: slog::Logger) -> StatisticsLogger {
517        let mut tracker = StatsTracker::new();
518        for set in self.stats {
519            for s in set {
520                tracker.add_statistic(*s)
521            }
522        }
523
524        StatisticsLogger {
525            logger,
526            tracker: Arc::new(tracker),
527        }
528    }
529
530    /// Construct the StatisticsLogger - this will start the interval logging if requested.
531    ///
532    /// interval_secs: The period, in seconds, to log the generated metrics into the log stream.
533    /// One log will be generated for each metric value.
534    #[cfg(feature = "interval_logging")]
535    pub fn fuse_with_log_interval<T: StatisticsLogFormatter>(
536        self,
537        interval_secs: u64,
538        logger: slog::Logger,
539    ) -> StatisticsLogger {
540        // Fuse the logger using the standard function
541        let stats_logger = self.fuse(logger);
542
543        // Clone the logger for using on the timer.
544        let timer_full_logger = stats_logger.clone();
545
546        tokio::spawn(async move {
547            let mut interval = tokio::time::interval(Duration::from_secs(interval_secs));
548
549            // The first tick completes immediately, so we skip it.
550            interval.tick().await;
551
552            loop {
553                interval.tick().await;
554                timer_full_logger.tracker.log_all::<T>(&timer_full_logger);
555            }
556        });
557
558        // Return the fused logger.
559        stats_logger
560    }
561}
562
563/// Data and callback type for actually generating the log.
564///
565/// This allows the user to decide what format to actually log the stats in.
566// LCOV_EXCL_START not interesting to track automatic derive coverage
567#[derive(Debug)]
568pub struct StatLogData<'a> {
569    /// The description, as provided on the definition.
570    pub description: &'static str,
571    /// The statistic type, automatically determined from the definition.
572    pub stype: StatType,
573    /// The statistic name, as provided on the definition.
574    pub name: &'static str,
575    /// The current value.
576    pub value: f64,
577    /// The groups and name.
578    pub tags: Vec<(&'static str, &'a str)>,
579}
580
581/// Structure to use for the default implementation of `StatisticsLogFormatter`.
582#[derive(Debug, Clone)]
583pub struct DefaultStatisticsLogFormatter;
584// LCOV_EXCL_STOP
585
586/// The log identifier for the default formatter.
587pub static DEFAULT_LOG_ID: &str = "STATS-1";
588
589impl StatisticsLogFormatter for DefaultStatisticsLogFormatter {
590    /// The formatting callback.  This default implementation just logs each field.
591    fn log_stat(logger: &StatisticsLogger, stat: &StatLogData<'_>)
592    where
593        Self: Sized,
594    {
595        // A realistic implementation would use `xlog`.  However, since the derivation of
596        // `ExtLoggable` depends on this crate, we can't use it here!
597        //
598        // So just log the value manually using the `slog` macros.
599        info!(logger, "New statistic value";
600               "log_id" => DEFAULT_LOG_ID,
601               "name" => stat.name,
602               "metric_type" => stat.stype,
603               "description" => stat.description,
604               "value" => stat.value,
605               "tags" => stat.tags.iter().
606                   map(|x| format!("{}={}", x.0, x.1)).collect::<Vec<_>>().join(","))
607    }
608}
609
610/// A trait object to allow users to customise the format of stats when logged.
611pub trait StatisticsLogFormatter: Sync + Send + 'static {
612    /// The formatting callback.  This should take the statistic information and log it through the
613    /// provided logger in the relevant format.
614    ///
615    /// The `DefaultStatisticsLogFormatter` provides a basic format, or users can override the
616    /// format of the generated logs by providing an object that implements this trait in the
617    /// `StatsConfig`.
618    fn log_stat(logger: &StatisticsLogger, stat: &StatLogData<'_>)
619    where
620        Self: Sized;
621}
622
623/// A logger with statistics tracking.
624///
625/// This should only be created through the `new` method.
626#[derive(Debug, Clone)]
627pub struct StatisticsLogger {
628    /// The logger that receives the logs.
629    logger: slog::Logger,
630    /// The stats tracker.
631    tracker: Arc<StatsTracker>,
632}
633
634impl Deref for StatisticsLogger {
635    type Target = slog::Logger;
636    fn deref(&self) -> &Self::Target {
637        &self.logger
638    }
639}
640
641impl StatisticsLogger {
642    /// Build a child logger with new parameters.
643    ///
644    /// This is essentially a wrapper around `slog::Logger::new()`.
645    pub fn with_params<P>(&self, params: slog::OwnedKV<P>) -> Self
646    where
647        P: slog::SendSyncRefUnwindSafeKV + 'static,
648    {
649        StatisticsLogger {
650            logger: self.logger.new(params),
651            tracker: self.tracker.clone(),
652        } // LCOV_EXCL_LINE Kcov bug
653    }
654
655    /// Update the statistics for the current log.
656    pub fn update_stats(&self, log: &dyn StatTrigger) {
657        self.tracker.update_stats(log)
658    }
659
660    /// Modify the logger field without changing the stats tracker
661    pub fn set_slog_logger(&mut self, logger: slog::Logger) {
662        self.logger = logger;
663    }
664
665    /// Retrieve the current values of all stats tracked by this logger.
666    pub fn get_stats(&self) -> Vec<StatSnapshot> {
667        self.tracker.get_stats()
668    }
669}
670
671/// The values contained in a `StatSnapshot` for each stat type.
672#[derive(Debug)]
673pub enum StatSnapshotValues {
674    /// `StatSnapshot` values for the Counter stat type.
675    Counter(Vec<StatSnapshotValue>),
676    /// `StatSnapshot` values for the Gauge stat type.
677    Gauge(Vec<StatSnapshotValue>),
678    /// Bucket description, and `StatSnapshot` values by bucket for the BucketCounter stat type.
679    BucketCounter(Buckets, Vec<(StatSnapshotValue, BucketLimit)>),
680}
681
682impl StatSnapshotValues {
683    /// Returns true if self contains no StatSnapshotValue entries.
684    pub fn is_empty(&self) -> bool {
685        match self {
686            StatSnapshotValues::Counter(ref vals) | StatSnapshotValues::Gauge(ref vals) => {
687                vals.is_empty()
688            }
689            StatSnapshotValues::BucketCounter(_, ref vals) => vals.is_empty(),
690        }
691    }
692}
693
694/// A snapshot of the current values for a particular stat.
695// LCOV_EXCL_START not interesting to track automatic derive coverage
696#[derive(Debug)]
697pub struct StatSnapshot {
698    /// A configured statistic, defined in terms of the external logs that trigger it to change.
699    pub definition: &'static dyn StatDefinition,
700    /// The values contained in a `StatSnapshot` for each stat type.
701    pub values: StatSnapshotValues,
702}
703// LCOV_EXCL_STOP
704
705impl StatSnapshot {
706    /// Create a new snapshot of a stat. The StatSnapshotValues enum variant passed
707    /// should match the stat type in the definition.
708    pub fn new(definition: &'static dyn StatDefinition, values: StatSnapshotValues) -> Self {
709        StatSnapshot { definition, values }
710    }
711}
712
713/// A snapshot of a current (possibly grouped) value for a stat.
714// LCOV_EXCL_START not interesting to track automatic derive coverage
715#[derive(Debug)]
716pub struct StatSnapshotValue {
717    /// A vec of the set of tags that this value belongs to. A group can have several tags
718    /// and the stat is counted separately for all distinct combinations of tag values.
719    /// This may be an empty vec is the stat is not grouped.
720    pub group_values: Vec<String>,
721    /// The value of the stat with the above combination of groups (note that this may be bucketed).
722    pub value: f64,
723}
724// LCOV_EXCL_STOP
725
726impl StatSnapshotValue {
727    /// Create a new snapshot value.
728    pub fn new(group_values: Vec<String>, value: f64) -> Self {
729        StatSnapshotValue {
730            group_values,
731            value,
732        }
733    }
734}
735
736///////////////////////////
737// Private types and private methods.
738///////////////////////////
739
740/// A struct used to store any data internal to a stat that is specific to the
741/// stat type.
742#[derive(Debug)]
743enum StatTypeData {
744    Counter,
745    Gauge,
746    BucketCounter(BucketCounterData),
747}
748
749impl StatTypeData {
750    /// Create a new `StatTypeData`
751    fn new(defn: &'static dyn StatDefinition) -> Self {
752        match defn.stype() {
753            StatType::Counter => StatTypeData::Counter,
754            StatType::Gauge => StatTypeData::Gauge,
755            StatType::BucketCounter => {
756                let is_grouped = !defn.group_by().is_empty();
757                StatTypeData::BucketCounter(BucketCounterData::new(
758                    defn.buckets().expect(
759                        "Stat definition with type BucketCounter did not contain bucket info",
760                    ),
761                    is_grouped,
762                ))
763            }
764        }
765    }
766
767    /// Update the stat values
768    fn update(&self, defn: &StatDefinitionTagged, trigger: &dyn StatTrigger) {
769        if let StatTypeData::BucketCounter(ref bucket_counter_data) = self {
770            bucket_counter_data.update(defn, trigger);
771        }
772    }
773
774    /// Get all the tags for this stat as a vector of `(name, value)` tuples.
775    fn get_tag_pairs<'a>(
776        &self,
777        tag_values: &'a str,
778        defn: &dyn StatDefinition,
779    ) -> Option<Vec<(&'static str, &'a str)>> {
780        if let StatTypeData::BucketCounter(ref bucket_counter_data) = self {
781            Some(bucket_counter_data.get_tag_pairs(tag_values, defn))
782        } else {
783            None
784        }
785    }
786
787    /// Get all the tagged value names currently tracked.
788    fn get_tagged_vals(&self) -> Option<Vec<(String, f64)>> {
789        if let StatTypeData::BucketCounter(ref bucket_counter_data) = self {
790            Some(bucket_counter_data.get_tagged_vals())
791        } else {
792            None
793        }
794    }
795}
796
797/// Contains data that is specific to the `BucketCounter` stat type.
798#[derive(Debug)]
799struct BucketCounterData {
800    buckets: Buckets,
801    bucket_values: Vec<StatValue>,
802    bucket_group_values: RwLock<HashMap<String, Vec<StatValue>>>,
803    is_grouped: bool,
804}
805
806impl BucketCounterData {
807    fn new(buckets: Buckets, is_grouped: bool) -> Self {
808        let buckets_len = buckets.len();
809        let mut bucket_values = Vec::new();
810        bucket_values.reserve_exact(buckets_len);
811        for _ in 0..buckets_len {
812            bucket_values.push(StatValue::new(0, 1));
813        }
814
815        BucketCounterData {
816            buckets,
817            bucket_values,
818            bucket_group_values: RwLock::new(HashMap::new()),
819            is_grouped,
820        }
821    }
822
823    /// Update the stat values.
824    fn update(&self, defn: &StatDefinitionTagged, trigger: &dyn StatTrigger) {
825        // Update the bucketed values.
826        let bucket_value = trigger.bucket_value(defn).expect("Bad log definition");
827        let buckets_to_update = self.buckets.assign_buckets(bucket_value);
828
829        for index in &buckets_to_update {
830            self.bucket_values
831                .get(*index)
832                .expect("Invalid bucket index")
833                .update(&trigger.change(defn).expect("Bad log definition"));
834        }
835
836        if self.is_grouped {
837            // Update the grouped and bucketed values.
838            self.update_grouped(defn, trigger, &buckets_to_update);
839        }
840    }
841
842    /// Update the grouped stat values.
843    fn update_grouped(
844        &self,
845        defn: &StatDefinitionTagged,
846        trigger: &dyn StatTrigger,
847        buckets_to_update: &[usize],
848    ) {
849        let change = trigger.change(defn).expect("Bad log definition");
850        let tag_values = defn
851            .defn
852            .group_by()
853            .iter()
854            .map(|n| trigger.tag_value(defn, n))
855            .collect::<Vec<String>>()
856            .join(","); // LCOV_EXCL_LINE Kcov bug?
857
858        let found_values = {
859            let inner_vals = self.bucket_group_values.read().expect("Poisoned lock");
860            if let Some(tagged_bucket_vals) = inner_vals.get(&tag_values) {
861                update_bucket_values(tagged_bucket_vals, buckets_to_update, &change);
862                true
863            } else {
864                false
865            }
866        };
867
868        if !found_values {
869            // we didn't find bucketed values for this tag combination. Create them now.
870            let mut new_bucket_vals = Vec::new();
871            let bucket_len = self.buckets.len();
872            new_bucket_vals.reserve_exact(bucket_len);
873            for _ in 0..bucket_len {
874                new_bucket_vals.push(StatValue::new(0, 1));
875            }
876
877            let mut inner_vals = self.bucket_group_values.write().expect("Poisoned lock");
878            // It's possible that while we were waiting for the write lock another thread got
879            // in and created the bucketed entries, so check again.
880            let vals = inner_vals
881                .entry(tag_values)
882                .or_insert_with(|| new_bucket_vals);
883
884            update_bucket_values(vals, buckets_to_update, &change);
885        }
886    }
887
888    /// Get all the tags for this stat as a vector of `(name, value)` tuples.
889    fn get_tag_pairs<'a>(
890        &self,
891        tag_values: &'a str,
892        defn: &dyn StatDefinition,
893    ) -> Vec<(&'static str, &'a str)> {
894        let mut tag_names = defn.group_by();
895        // Add the bucket label name as an additional tag name.
896        tag_names.push(self.buckets.label_name);
897        tag_names
898            .iter()
899            .cloned()
900            .zip(tag_values.split(','))
901            .collect::<Vec<_>>()
902    }
903
904    /// Get all the tagged values currently tracked.
905    fn get_tagged_vals(&self) -> Vec<(String, f64)> {
906        if self.is_grouped {
907            let mut tag_bucket_vals = Vec::new();
908
909            {
910                // Only hold the read lock long enough to get the keys, bucket indices, and values.
911                let inner_vals = self.bucket_group_values.read().expect("Poisoned lock");
912
913                for (group_values_str, bucket_values) in inner_vals.iter() {
914                    for (index, val) in bucket_values.iter().enumerate() {
915                        tag_bucket_vals.push((group_values_str.to_string(), index, val.as_float()));
916                    }
917                }
918            }
919
920            tag_bucket_vals
921                .into_iter()
922                .map(|(mut tag_values, index, val)| {
923                    let bucket = self.buckets.get(index).expect("Invalid bucket index");
924                    // Add the bucket label value as an additional tag value.
925                    tag_values.push_str(&format!(",{}", bucket));
926                    (tag_values, val)
927                })
928                .collect()
929        } else {
930            self.bucket_values
931                .iter()
932                .enumerate()
933                .map(|(index, val)| {
934                    let bucket = self.buckets.get(index).expect("Invalid bucket index");
935                    (bucket.to_string(), val.as_float())
936                })
937                .collect()
938        }
939    }
940
941    /// Get a snapshot of the current stat values.
942    fn get_snapshot_values(&self) -> Vec<(StatSnapshotValue, BucketLimit)> {
943        if self.is_grouped {
944            let mut tag_bucket_vals = Vec::new();
945
946            {
947                // Only hold the read lock long enough to get the keys, bucket indices, and values.
948                let inner_vals = self.bucket_group_values.read().expect("Poisoned lock");
949
950                for (group_values_str, bucket_values) in inner_vals.iter() {
951                    for (index, val) in bucket_values.iter().enumerate() {
952                        tag_bucket_vals.push((group_values_str.to_string(), index, val.as_float()));
953                    }
954                }
955            }
956
957            tag_bucket_vals
958                .into_iter()
959                .map(|(tag_values, index, val)| {
960                    let bucket = self.buckets.get(index).expect("Invalid bucket index");
961                    let group_values = tag_values
962                        .split(',')
963                        .map(|group| group.to_string())
964                        .collect::<Vec<_>>();
965
966                    (StatSnapshotValue::new(group_values, val), bucket)
967                })
968                .collect()
969        } else {
970            self.bucket_values
971                .iter()
972                .enumerate()
973                .map(|(index, val)| {
974                    let bucket = self.buckets.get(index).expect("Invalid bucket index");
975                    (StatSnapshotValue::new(vec![], val.as_float()), bucket)
976                })
977                .collect()
978        }
979    }
980}
981
982// LCOV_EXCL_START not interesting to track automatic derive coverage
983/// The internal representation of a tracked statistic.
984#[derive(Debug)]
985struct Stat {
986    // The definition fields, as a trait object.
987    defn: &'static (dyn StatDefinition + Sync + RefUnwindSafe),
988    // The value - if grouped, this is the total value across all statistics.
989    value: StatValue,
990    // Does this stat use groups.  Cached here for efficiency.
991    is_grouped: bool,
992    // The fields the stat is grouped by.  If empty, then there is no grouping.
993    group_values: RwLock<HashMap<String, StatValue>>,
994    // Data specific to the stat type.
995    stat_type_data: StatTypeData,
996}
997// LCOV_EXCL_STOP
998
999impl Stat {
1000    // Get all the tags for this stat as a vector of `(name, value)` tuples.
1001    fn get_tag_pairs<'a>(&self, tag_values: &'a str) -> Vec<(&'static str, &'a str)> {
1002        // if the stat type has its own `get_tag_pairs` method use that, otherwise
1003        // use the default.
1004        self.stat_type_data
1005            .get_tag_pairs(tag_values, self.defn)
1006            .unwrap_or_else(|| {
1007                self.defn
1008                    .group_by()
1009                    .iter()
1010                    .cloned()
1011                    .zip(tag_values.split(','))
1012                    .collect::<Vec<_>>()
1013            })
1014    }
1015
1016    /// Get all the tagged value names currently tracked.
1017    fn get_tagged_vals(&self) -> Vec<(String, f64)> {
1018        // if the stat type has its own `get_tagged_vals` method use that, otherwise
1019        // use the default.
1020        self.stat_type_data.get_tagged_vals().unwrap_or_else(|| {
1021            if self.is_grouped {
1022                // Only hold the read lock long enough to get the keys and values.
1023                let inner_vals = self.group_values.read().expect("Poisoned lock");
1024                inner_vals
1025                    .iter()
1026                    .map(|(group_values_str, value)| {
1027                        (group_values_str.to_string(), value.as_float())
1028                    })
1029                    .collect()
1030            } else {
1031                vec![("".to_string(), self.value.as_float())]
1032            }
1033        })
1034    }
1035
1036    /// Update the stat's value(s) according to the given `StatTrigger` and `StatDefinition`.
1037    fn update(&self, defn: &StatDefinitionTagged, trigger: &dyn StatTrigger) {
1038        // update the stat value
1039        self.value
1040            .update(&trigger.change(defn).expect("Bad log definition"));
1041
1042        // If the stat is grouped, update the grouped values.
1043        if self.is_grouped {
1044            self.update_grouped(defn, trigger)
1045        }
1046
1047        // Update type-specific stat data.
1048        self.stat_type_data.update(defn, trigger);
1049    }
1050
1051    fn update_grouped(&self, defn: &StatDefinitionTagged, trigger: &dyn StatTrigger) {
1052        let change = trigger.change(defn).expect("Bad log definition");
1053
1054        let tag_values = self
1055            .defn
1056            .group_by()
1057            .iter()
1058            .map(|n| trigger.tag_value(defn, n))
1059            .collect::<Vec<String>>()
1060            .join(","); // LCOV_EXCL_LINE Kcov bug?
1061
1062        let found_values = {
1063            let inner_vals = self.group_values.read().expect("Poisoned lock");
1064            if let Some(val) = inner_vals.get(&tag_values) {
1065                val.update(&change);
1066                true
1067            } else {
1068                false
1069            }
1070        };
1071
1072        if !found_values {
1073            // We didn't find a grouped value.  Get the write lock on the map so we can add it.
1074            let mut inner_vals = self.group_values.write().expect("Poisoned lock");
1075            // It's possible that while we were waiting for the write lock another thread got
1076            // in and created the stat entry, so check again.
1077            let val = inner_vals
1078                .entry(tag_values)
1079                .or_insert_with(|| StatValue::new(0, 1));
1080
1081            val.update(&change);
1082        }
1083    }
1084
1085    /// Get the current values for this stat as a StatSnapshot.
1086    fn get_snapshot(&self) -> StatSnapshot {
1087        let stat_snapshot_values = match self.stat_type_data {
1088            StatTypeData::BucketCounter(ref bucket_counter_data) => {
1089                StatSnapshotValues::BucketCounter(
1090                    bucket_counter_data.buckets.clone(),
1091                    bucket_counter_data.get_snapshot_values(),
1092                )
1093            }
1094            StatTypeData::Counter => StatSnapshotValues::Counter(self.get_snapshot_values()),
1095            StatTypeData::Gauge => StatSnapshotValues::Gauge(self.get_snapshot_values()),
1096        };
1097
1098        StatSnapshot::new(self.defn, stat_snapshot_values)
1099    }
1100
1101    /// Get a snapshot of the current stat values.
1102    fn get_snapshot_values(&self) -> Vec<StatSnapshotValue> {
1103        self.get_tagged_vals()
1104            .iter()
1105            .map(|(group_values_str, value)| {
1106                let group_values = if !group_values_str.is_empty() {
1107                    group_values_str
1108                        .split(',')
1109                        .map(|group| group.to_string())
1110                        .collect::<Vec<_>>()
1111                } else {
1112                    vec![]
1113                };
1114                StatSnapshotValue::new(group_values, *value)
1115            })
1116            .collect()
1117    }
1118}
1119
1120// LCOV_EXCL_START not interesting to track automatic derive coverage
1121/// A single statistic value.
1122#[derive(Debug)]
1123struct StatValue {
1124    // The tracked integer value.
1125    num: AtomicIsize,
1126    // A divisor for printing the stat value only - currently this is always 1 but is here
1127    // to allow in future for, say, percentages.
1128    divisor: u64,
1129}
1130// LCOV_EXCL_STOP
1131
1132impl StatValue {
1133    /// Create a new value.
1134    fn new(num: isize, divisor: u64) -> Self {
1135        StatValue {
1136            num: AtomicIsize::new(num),
1137            divisor,
1138        }
1139    }
1140
1141    /// Update the stat and return whether it has changed.
1142    fn update(&self, change: &ChangeType) -> bool {
1143        match *change {
1144            ChangeType::Incr(i) => {
1145                self.num.fetch_add(i as isize, Ordering::Relaxed);
1146                true
1147            }
1148            ChangeType::Decr(d) => {
1149                self.num.fetch_sub(d as isize, Ordering::Relaxed);
1150                true
1151            }
1152
1153            ChangeType::SetTo(v) => self.num.swap(v, Ordering::Relaxed) != v,
1154        }
1155    }
1156
1157    /// Return the statistic value as a float, for use in display.
1158    fn as_float(&self) -> f64 {
1159        (self.num.load(Ordering::Relaxed) as f64) / (self.divisor as isize as f64)
1160    }
1161}
1162
1163fn update_bucket_values(
1164    bucket_values: &[StatValue],
1165    buckets_to_update: &[usize],
1166    change: &ChangeType,
1167) {
1168    for index in buckets_to_update.iter() {
1169        bucket_values
1170            .get(*index)
1171            .expect("Invalid bucket index")
1172            .update(change);
1173    }
1174}
1175
1176#[cfg(test)]
1177mod tests {
1178    use super::*;
1179
1180    #[allow(dead_code)]
1181    struct DummyNonCloneFormatter;
1182    impl StatisticsLogFormatter for DummyNonCloneFormatter {
1183        fn log_stat(_logger: &StatisticsLogger, _stat: &StatLogData<'_>)
1184        where
1185            Self: Sized,
1186        {
1187        }
1188    }
1189
1190    #[test]
1191    // Check that loggers can be cloned even if the formatter can't.
1192    fn check_clone() {
1193        let builder = StatsLoggerBuilder::default();
1194        let logger = builder.fuse(slog::Logger::root(slog::Discard, slog::o!()));
1195        fn is_clone<T: Clone>(_: &T) {}
1196        is_clone(&logger);
1197    }
1198}