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}