fluvio_system_util/counters/
counter_tbl.rs

1//!
2//! # Counter Table
3//!
4//! Stores counters in table costruct. Each table has a header and rows with counters for each column.
5//!
6use std::cmp;
7use std::collections::BTreeMap;
8use std::sync::RwLock;
9
10#[derive(Debug)]
11pub struct CounterTable<T, C> {
12    pub columns: BTreeMap<C, Column>,
13    pub rows: RwLock<BTreeMap<T, BTreeMap<C, u32>>>,
14}
15
16#[derive(Debug, PartialEq)]
17pub struct Column {
18    label: &'static str,
19    internal: bool,
20}
21
22impl<T, C> ::std::default::Default for CounterTable<T, C>
23where
24    C: Ord,
25    T: Ord,
26{
27    fn default() -> Self {
28        Self {
29            columns: BTreeMap::new(),
30            rows: RwLock::new(BTreeMap::new()),
31        }
32    }
33}
34
35impl<T, C> ::std::cmp::PartialEq for CounterTable<T, C>
36where
37    C: PartialEq,
38    T: PartialEq,
39{
40    fn eq(&self, other: &CounterTable<T, C>) -> bool {
41        // compare columns
42        if self.columns != other.columns {
43            return false;
44        }
45
46        // compare counters
47        let local_rows = self.rows.read().unwrap();
48        let other_rows = other.rows.read().unwrap();
49        if *local_rows != *other_rows {
50            return false;
51        }
52        true
53    }
54}
55
56impl<T, C> CounterTable<T, C>
57where
58    C: Ord + Clone,
59    T: Ord,
60{
61    /// builder pattern to add columns
62    pub fn with_columns(mut self, columns: Vec<(C, &'static str, bool)>) -> Self {
63        for (column_id, label, internal) in columns {
64            self.columns.insert(column_id, Column { label, internal });
65        }
66        self
67    }
68
69    /// create a row and add counter columns
70    pub fn add_row(&self, row_id: T) {
71        // add one counter per column
72        let mut column_counters = BTreeMap::new();
73        for column_id in self.columns.keys() {
74            column_counters.insert(column_id.clone(), 0);
75        }
76
77        // add counters to row
78        self.rows.write().unwrap().insert(row_id, column_counters);
79    }
80
81    /// remove counter row
82    pub fn remove_row(&self, row_id: &T) {
83        self.rows.write().unwrap().remove(row_id);
84    }
85
86    /// number of rows
87    pub fn row_count(&self) -> usize {
88        self.rows.read().unwrap().len()
89    }
90
91    /// increment counter
92    pub fn inc_counter(&self, row_id: &T, column_id: C) {
93        if let Some(row) = self.rows.write().unwrap().get_mut(row_id) {
94            if let Some(counter) = row.get_mut(&column_id) {
95                *counter += 1;
96            }
97        }
98    }
99
100    /// set counter
101    pub fn set_counter(&self, row_id: &T, column_id: C, val: u32) {
102        if let Some(row) = self.rows.write().unwrap().get_mut(row_id) {
103            if let Some(counter) = row.get_mut(&column_id) {
104                *counter = val;
105            }
106        }
107    }
108
109    /// reset all counters
110    pub fn reset_counters(&self) {
111        for row in self.rows.write().unwrap().values_mut() {
112            for counter in row.values_mut() {
113                *counter = 0;
114            }
115        }
116    }
117
118    /// column headers in string format (center justified, min 10 spaces)
119    pub fn header_fmt(&self) -> String {
120        let mut res = String::new();
121
122        // accumulate labels
123        for column in self.columns.values() {
124            res.push_str(&format!("{:^10}", column.label));
125            res.push_str("  ");
126        }
127
128        // remove last 2 spaces
129        if res.len() > 2 {
130            res.truncate(res.len() - 2);
131        }
132
133        res
134    }
135
136    /// format values in string format (center justified to column header - min 10 spaces)
137    pub fn values_fmt(&self, row_id: T) -> String {
138        let mut res = String::new();
139        if let Some(row) = self.rows.write().unwrap().get_mut(&row_id) {
140            // accumulate labels
141            for (column_id, counter) in row {
142                let value_str = counter.to_string();
143                let column_label = if let Some(column) = self.columns.get(column_id) {
144                    &(*column.label)
145                } else {
146                    ""
147                };
148                let space_width = cmp::max(column_label.len(), 10);
149                let value_width = value_str.len();
150
151                // center justify... need to compute our own adding (as Rust formatter requires literal)
152                let (pad_left, pad_right) = if value_width > space_width {
153                    (0, 0)
154                } else {
155                    let pad_left = (space_width - value_width) / 2;
156                    let pad_right = space_width - pad_left - value_width;
157                    (pad_left, pad_right)
158                };
159
160                res.push_str(&(0..pad_left).map(|_| " ").collect::<String>());
161                res.push_str(&value_str);
162                res.push_str(&(0..pad_right).map(|_| " ").collect::<String>());
163                res.push_str("  ");
164            }
165
166            // remove last 2 spaces
167            if res.len() > 2 {
168                res.truncate(res.len() - 2);
169            }
170        }
171
172        res
173    }
174}
175
176// -----------------------------------
177// Unit Tests
178// -----------------------------------
179
180#[cfg(test)]
181pub mod test {
182    use super::*;
183
184    #[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone)]
185    enum TestCntr {
186        Ok = 0,
187        Failed = 1,
188        Retry = 2,
189        Shutdown = 3,
190        InternalErr = 4,
191    }
192
193    fn generate_counters() -> Vec<(TestCntr, &'static str, bool)> {
194        vec![
195            (TestCntr::Ok, "CONN-OK", false),
196            (TestCntr::Failed, "CONN-FAILED", false),
197            (TestCntr::Retry, "CONN-RETRY", false),
198            (TestCntr::Shutdown, "CONN-SHUTDOWN", false),
199            (TestCntr::InternalErr, "INTERNAL-ERR", true),
200        ]
201    }
202
203    #[test]
204    fn test_counter_table_all() {
205        let counter_tbl = CounterTable::default().with_columns(generate_counters());
206
207        // test generation
208        assert_eq!(counter_tbl.columns.len(), 5);
209
210        // test add rows
211        let (row0, row1): (i32, i32) = (0, 1);
212        counter_tbl.add_row(row0);
213        counter_tbl.add_row(row1);
214        assert_eq!(counter_tbl.row_count(), 2);
215
216        // test header formatter
217        let header = counter_tbl.header_fmt();
218        let expected_header =
219            " CONN-OK    CONN-FAILED  CONN-RETRY  CONN-SHUTDOWN  INTERNAL-ERR".to_owned();
220        assert_eq!(header, expected_header);
221
222        // test increment & value formatter
223        counter_tbl.set_counter(&row0, TestCntr::Ok, 4294967290);
224        counter_tbl.inc_counter(&row0, TestCntr::Failed);
225        counter_tbl.set_counter(&row0, TestCntr::Retry, 4199999999);
226        let values_r1 = counter_tbl.values_fmt(row0);
227        let expected_values_r1 =
228            "4294967290       1       4199999999        0             0      ".to_owned();
229        assert_eq!(values_r1, expected_values_r1);
230
231        counter_tbl.inc_counter(&row1, TestCntr::Shutdown);
232        counter_tbl.inc_counter(&row1, TestCntr::Shutdown);
233        let values_r2 = counter_tbl.values_fmt(row1);
234        let expected_values_r2 =
235            "    0            0           0             2             0      ".to_owned();
236        assert_eq!(values_r2, expected_values_r2);
237
238        // test equality
239        let expected_counter_tbl = CounterTable::default().with_columns(generate_counters());
240        expected_counter_tbl.add_row(row0);
241        expected_counter_tbl.add_row(row1);
242        expected_counter_tbl.set_counter(&row0, TestCntr::Ok, 4294967290);
243        expected_counter_tbl.set_counter(&row0, TestCntr::Failed, 1);
244        expected_counter_tbl.set_counter(&row0, TestCntr::Retry, 4199999999);
245        expected_counter_tbl.set_counter(&row1, TestCntr::Shutdown, 2);
246        assert_eq!(counter_tbl, expected_counter_tbl);
247
248        // test reset
249        counter_tbl.reset_counters();
250        let expected_reset_tbl = CounterTable::default().with_columns(generate_counters());
251        expected_reset_tbl.add_row(row0);
252        expected_reset_tbl.add_row(row1);
253        assert_eq!(counter_tbl, expected_reset_tbl);
254        assert_eq!(counter_tbl.row_count(), 2);
255
256        // teest remoev row
257        counter_tbl.remove_row(&row0);
258        let expected_one_row_tbl = CounterTable::default().with_columns(generate_counters());
259        expected_one_row_tbl.add_row(row1);
260        assert_eq!(counter_tbl, expected_one_row_tbl);
261        assert_eq!(counter_tbl.row_count(), 1);
262    }
263}