hotmic_stdout/
lib.rs

1//! Exports metrics by logging them in a textual format.
2//!
3//! This exporter utilizes the `log` crate to log a textual representation of metrics in a given
4//! snapshot.  Metrics that are scoped are represented using an indented tree structure.
5//!
6//! As an example, for a snapshot with two metrics -- `server.msgs_received` and `server.msgs_sent`
7//! -- we would expect to see:
8//!
9//! ```c
10//! root:
11//!   server:
12//!     msgs_received: 42
13//!     msgs_sent: 13
14//! ```
15//!
16//! If we added another metric -- `configuration_reloads` -- we would expect to see:
17//!
18//! ```c
19//! root:
20//!   configuration_reloads: 2
21//!   server:
22//!     msgs_received: 42
23//!     msgs_sent: 13
24//! ```
25//!
26//! Metrics are sorted alphabetically.
27//!
28//! ## Histograms
29//!
30//! Histograms received a little extra love and care when it comes to formatting.  This is the
31//! general format of a given histogram when rendered:
32//!
33//! ```c
34//! root:
35//!   connect_time count: 15
36//!   connect_time min: 1334ns
37//!   connect_time p50: 1934ns
38//!   connect_time p99: 5330ns
39//!   connect_time max: 139389ns
40//! ```
41//!
42//! The percentiles shown are based on the percentiles configured for hotmic itself, which are
43//! generated for us and can't be influenced at the exporter level. The `count` value represents
44//! the number of samples in the histogram at the time of measurement.
45#[macro_use]
46extern crate log;
47extern crate hotmic;
48
49use hotmic::{
50    snapshot::{Snapshot, SummarizedHistogram, TypedMeasurement},
51    Controller,
52};
53use log::Level;
54use std::collections::{HashMap, VecDeque};
55use std::fmt::Display;
56use std::thread;
57use std::time::Duration;
58
59/// Exports metrics by logging them in a textual format.
60pub struct StdoutExporter {
61    controller: Controller,
62    level: Level,
63    interval: Duration,
64}
65
66impl StdoutExporter {
67    /// Creates a new `StdoutExporter`.
68    ///
69    /// The exporter will take a snapshot based on the configured `controller`, outputting at the
70    /// configured `level`, at the configured `interval`.
71    pub fn new(controller: Controller, level: Level, interval: Duration) -> Self {
72        StdoutExporter {
73            controller,
74            level,
75            interval,
76        }
77    }
78
79    fn turn(&mut self) {
80        match self.controller.get_snapshot() {
81            Ok(snapshot) => self.process_snapshot(snapshot),
82            Err(e) => error!("caught error getting metrics snapshot: {}", e),
83        }
84    }
85
86    /// Runs the exporter synchronously, blocking the calling thread.
87    ///
88    /// You should run this in a dedicated thread:
89    ///
90    /// ```c
91    /// let mut exporter = StdoutExporter::new(controller, Level::Info, Duration::from_secs(5));
92    /// std::thread::spawn(move || exporter.run());
93    /// ```
94    pub fn run(&mut self) {
95        loop {
96            self.turn();
97            thread::sleep(self.interval);
98        }
99    }
100
101    fn process_snapshot(&self, snapshot: Snapshot) {
102        let mut nested = Nested::default();
103
104        for measurement in snapshot.into_vec() {
105            let (name_parts, mut values) = match measurement {
106                TypedMeasurement::Counter(key, value) => {
107                    let (layers, name) = name_to_parts(key);
108                    let values = single_value_to_values(name, value);
109
110                    (layers, values)
111                }
112                TypedMeasurement::Gauge(key, value) => {
113                    let (layers, name) = name_to_parts(key);
114                    let values = single_value_to_values(name, value);
115
116                    (layers, values)
117                }
118                TypedMeasurement::TimingHistogram(key, summary) => {
119                    let (layers, name) = name_to_parts(key);
120                    let values = summary_to_values(name, summary, "ns");
121
122                    (layers, values)
123                }
124                TypedMeasurement::ValueHistogram(key, summary) => {
125                    let (layers, name) = name_to_parts(key);
126                    let values = summary_to_values(name, summary, "");
127
128                    (layers, values)
129                }
130            };
131
132            nested.insert(name_parts, &mut values);
133        }
134
135        let output = nested.into_output();
136        log!(self.level, "metrics:\n{}", output);
137    }
138}
139
140fn name_to_parts(name: String) -> (VecDeque<String>, String) {
141    let mut parts = name
142        .split('.')
143        .map(ToOwned::to_owned)
144        .collect::<VecDeque<_>>();
145    let name = parts.pop_back().expect("name didn't have a single part");
146
147    (parts, name)
148}
149
150fn single_value_to_values<T>(name: String, value: T) -> Vec<String>
151where
152    T: Display,
153{
154    let fvalue = format!("{}: {}", name, value);
155    vec![fvalue]
156}
157
158fn summary_to_values(name: String, summary: SummarizedHistogram, suffix: &str) -> Vec<String> {
159    let mut values = Vec::new();
160
161    values.push(format!("{} count: {}", name, summary.count()));
162    for (percentile, value) in summary.measurements() {
163        values.push(format!(
164            "{} {}: {}{}",
165            name,
166            percentile.label(),
167            value,
168            suffix
169        ));
170    }
171
172    values
173}
174
175struct Nested {
176    level: usize,
177    current: Vec<String>,
178    next: HashMap<String, Nested>,
179}
180
181impl Nested {
182    pub fn with_level(level: usize) -> Self {
183        Nested {
184            level,
185            ..Default::default()
186        }
187    }
188
189    pub fn insert(&mut self, mut name_parts: VecDeque<String>, values: &mut Vec<String>) {
190        match name_parts.len() {
191            0 => {
192                let indent = "  ".repeat(self.level + 1);
193                let mut indented = values
194                    .iter()
195                    .map(move |x| format!("{}{}", indent, x))
196                    .collect::<Vec<_>>();
197                self.current.append(&mut indented);
198            }
199            _ => {
200                let name = name_parts
201                    .pop_front()
202                    .expect("failed to get next name component");
203                let current_level = self.level;
204                let inner = self
205                    .next
206                    .entry(name)
207                    .or_insert_with(move || Nested::with_level(current_level + 1));
208                inner.insert(name_parts, values);
209            }
210        }
211    }
212
213    pub fn into_output(self) -> String {
214        let indent = "  ".repeat(self.level + 1);
215        let mut output = String::new();
216        if self.level == 0 {
217            output.push_str("\nroot:\n");
218        }
219
220        let mut sorted = self
221            .current
222            .into_iter()
223            .map(SortEntry::Inline)
224            .chain(self.next.into_iter().map(|(k, v)| SortEntry::Nested(k, v)))
225            .collect::<Vec<_>>();
226        sorted.sort();
227
228        for entry in sorted {
229            match entry {
230                SortEntry::Inline(s) => {
231                    output.push_str(s.as_str());
232                    output.push_str("\n");
233                }
234                SortEntry::Nested(s, inner) => {
235                    output.push_str(indent.as_str());
236                    output.push_str(s.as_str());
237                    output.push_str(":\n");
238
239                    let layer_output = inner.into_output();
240                    output.push_str(layer_output.as_str());
241                }
242            }
243        }
244
245        output
246    }
247}
248
249impl Default for Nested {
250    fn default() -> Self {
251        Nested {
252            level: 0,
253            current: Vec::new(),
254            next: HashMap::new(),
255        }
256    }
257}
258
259enum SortEntry {
260    Inline(String),
261    Nested(String, Nested),
262}
263
264impl SortEntry {
265    fn name(&self) -> &String {
266        match self {
267            SortEntry::Inline(s) => s,
268            SortEntry::Nested(s, _) => s,
269        }
270    }
271}
272
273impl PartialEq for SortEntry {
274    fn eq(&self, other: &SortEntry) -> bool {
275        self.name() == other.name()
276    }
277}
278
279impl Eq for SortEntry {}
280
281impl std::cmp::PartialOrd for SortEntry {
282    fn partial_cmp(&self, other: &SortEntry) -> Option<std::cmp::Ordering> {
283        Some(self.cmp(other))
284    }
285}
286
287impl std::cmp::Ord for SortEntry {
288    fn cmp(&self, other: &SortEntry) -> std::cmp::Ordering {
289        self.name().cmp(other.name())
290    }
291}