Skip to main content

redis_objects/
counters.rs

1//! Objects and helpers for publishing metrics in an efficent manner.
2
3use std::{borrow::BorrowMut, sync::Arc};
4use std::marker::PhantomData;
5use std::time::Duration;
6
7use log::{error, info};
8use redis::AsyncTypedCommands;
9use serde::Serialize;
10use parking_lot::Mutex;
11use serde_json::json;
12
13use crate::{retry_call, ErrorTypes, RedisObjects};
14
15/// Trait for metric messages being exported
16pub trait MetricMessage: Serialize + Default + Send + Sync + 'static {}
17impl<T: Serialize + Default + Send + Sync + 'static> MetricMessage for T {}
18
19/// A builder to help configure a metrics counter that exports regularly to redis
20///
21/// This struct also acts as the internal config object for the counter once built.
22pub struct AutoExportingMetricsBuilder<Message: MetricMessage> {
23    channel_name: String,
24    counter_name: Option<String>,
25    counter_type: String,
26    host: String,
27    store: Arc<RedisObjects>,
28    data_type: PhantomData<Message>,
29    export_zero: bool,
30    export_interval: Duration,
31
32    /// Notification that wakes up the background task causing it to export early
33    export_notify: tokio::sync::Notify,
34}
35
36impl<Message: MetricMessage> AutoExportingMetricsBuilder<Message> {
37
38    pub (crate) fn new(store: Arc<RedisObjects>, channel_name: String, counter_type: String) -> Self {
39        Self {
40            channel_name,
41            counter_name: None,
42            counter_type,
43            host: format!("{:x}", rand::random::<u128>()),
44            store,
45            export_zero: true,
46            export_interval: Duration::from_secs(5),
47            data_type: Default::default(),
48            export_notify: tokio::sync::Notify::new(),
49        }
50    }
51
52    /// Set the name field for this counter
53    pub fn counter_name(mut self, value: String) -> Self {
54        self.counter_name = Some(value); self
55    }
56
57    /// Set the hostname, otherwise a random id is used.
58    pub fn host(mut self, value: String) -> Self {
59        self.host = value; self
60    }
61
62    /// Set the export interval
63    pub fn export_interval(mut self, value: Duration) -> Self {
64        self.export_interval = value; self
65    }
66
67    /// Configure if messages should be sent when no content has been added
68    pub fn export_zero(mut self, value: bool) -> Self {
69        self.export_zero = value; self
70    }
71
72    /// Launch the auto exporting process and return a handle for incrementing metrics
73    pub fn start(self) -> AutoExportingMetrics<Message> {
74        let current = Arc::new(Mutex::new(Message::default()));
75        let metrics = AutoExportingMetrics{
76            config: Arc::new(self),
77            current,
78        };
79
80        // start the background exporter
81        metrics.clone().exporter();
82
83        // return the original as metric interface
84        metrics
85    }
86
87    // /// build an empty message with current exporter settings
88    // fn empty_message(&self) -> Message {
89    //     let counter_name = match &self.counter_name {
90    //         Some(name) => name,
91    //         None => &self.counter_type,
92    //     };
93
94    //     Message::new(&self.counter_type, counter_name, &self.host)
95    // }
96}
97
98/// Increase the field given, by default incrementing by 1
99#[macro_export]
100macro_rules! increment {
101    ($counter:expr, $field:ident) => {
102        increment!($counter, $field, 1)
103    };
104    ($counter:expr, $field:ident, $value:expr) => {
105        $counter.lock().$field += $value
106    };
107    (timer, $counter:expr, $field:ident) => {
108        increment!(timer, $counter, $field, 0.0)
109    };
110    (timer, $counter:expr, $field:ident, $value:expr) => {
111        $counter.lock().$field.increment($value)
112    };
113}
114pub use increment;
115
116
117/// A wrapper around a Message class that adds periodic backup.
118///
119/// At the specified interval and (best efforts) program exit, the current message will be
120/// exported to the given channel and reset with the message Default.
121pub struct AutoExportingMetrics<Message: MetricMessage> {
122    config: Arc<AutoExportingMetricsBuilder<Message>>,
123    current: Arc<Mutex<Message>>
124}
125
126impl<Message: MetricMessage> Clone for AutoExportingMetrics<Message> {
127    fn clone(&self) -> Self {
128        Self { config: self.config.clone(), current: self.current.clone() }
129    }
130}
131
132impl<Message: MetricMessage> AutoExportingMetrics<Message> {
133    /// Launch the background export worker
134    fn exporter(mut self) {
135        tokio::spawn(async move {
136            while let Err(err) = self.export_loop().await {
137                error!("Error in metrics exporter {}: {}", self.config.counter_type, err);
138                tokio::time::sleep(Duration::from_secs(5)).await;
139            }
140        });
141    }
142
143    fn is_zero(&self, obj: &serde_json::Value) -> bool {
144        if let Some(number) = obj.as_i64() {
145            if number == 0 {
146                return true
147            }
148        }
149        if let Some(number) = obj.as_u64() {
150            if number == 0 {
151                return true
152            }
153        }
154        if let Some(number) = obj.as_f64() {
155            if number == 0.0 {
156                return true
157            }
158        }
159        false
160    }
161
162    fn is_all_zero(&self, obj: &serde_json::Value) -> bool {
163        if let Some(obj) = obj.as_object() {
164            for value in obj.values() {
165                if !self.is_zero(value) {
166                    return false
167                }
168            }
169            true
170        } else {
171            false
172        }
173    }
174
175    async fn export_once(&mut self) -> Result<(), ErrorTypes> {
176        // Fetch the message that needs to be sent
177        let outgoing = self.reset();
178
179        // create mapping
180        let mut outgoing = serde_json::to_value(&outgoing)?;
181
182        // check if we will export this message
183        if self.config.export_zero || !self.is_all_zero(&outgoing) {
184            // add extra fields
185            if let Some(obj) = outgoing.as_object_mut() {
186                obj.insert("type".to_owned(), json!(self.config.counter_type));
187                obj.insert("name".to_owned(), json!(self.config.counter_name));
188                obj.insert("host".to_owned(), json!(self.config.host));
189            }
190
191            // send the message
192            let data = serde_json::to_string(&outgoing)?;
193            let _recievers: usize = retry_call!(self.config.store.pool, publish, self.config.store.pubsub_prefix.clone() + self.config.channel_name.as_str(), data.as_str())?;
194        }
195        Ok(())
196    }
197
198    async fn export_loop(&mut self) -> Result<(), ErrorTypes> {
199        loop {
200            // wait for the configured duration (or we get notified to do it now)
201            let _ = tokio::time::timeout(self.config.export_interval, self.config.export_notify.notified()).await;
202            self.export_once().await?;
203
204            // check if the public object has been dropped
205            if Arc::strong_count(&self.current) == 1 {
206                info!("Stopping metrics exporter: {}", self.config.channel_name);
207                self.export_once().await?; // make sure we report any last minute messages that happened during/after the last export
208                return Ok(())
209            }
210        }
211    }
212
213    /// Get a writeable guard holding the message that will next be exported
214    /// Rather than using this directly the increment macro can be used
215    pub fn lock(&'_ self) -> parking_lot::MutexGuard<'_, Message> {
216        self.current.lock()
217    }
218
219    /// Replace the current outgoing message with an empty one
220    /// returns the replaced message
221    pub fn reset(&self) -> Message {
222        let mut message: Message = Default::default();
223        std::mem::swap(&mut message, self.current.lock().borrow_mut());
224        message
225    }
226
227    /// Trigger the background task to export immediately
228    pub fn export(&self) {
229        self.config.export_notify.notify_one()
230    }
231
232//     def set(self, name, value):
233//         try:
234//             if name not in self.counter_schema:
235//                 raise ValueError(f"{name} is not an accepted counter for this module: f{self.counter_schema}")
236//             with self.lock:
237//                 self.values[name] = value
238//                 return value
239//         except Exception:  # Don't let increment fail anything.
240//             log.exception("Setting Metric")
241//             return 0
242
243//     def increment_execution_time(self, name, execution_time):
244//         try:
245//             if name not in self.timer_schema:
246//                 raise ValueError(f"{name} is not an accepted counter for this module: f{self.timer_schema}")
247//             with self.lock:
248//                 self.counts[name + ".c"] += 1
249//                 self.counts[name + ".t"] += execution_time
250//                 return execution_time
251//         except Exception:  # Don't let increment fail anything.
252//             log.exception("Incrementing counter")
253//             return 0
254
255
256
257}
258
259impl<M: MetricMessage> Drop for AutoExportingMetrics<M> {
260    fn drop(&mut self) {
261        if Arc::strong_count(&self.current) <= 2 {
262            self.export()
263        }
264    }
265}
266
267
268#[cfg(test)]
269fn init() {
270    let _ = env_logger::builder().filter_level(log::LevelFilter::Debug).is_test(true).try_init();
271}
272
273#[tokio::test]
274async fn auto_exporting_counter() {
275    use log::info;
276    init();
277
278    use serde::Deserialize;
279    use crate::test::redis_connection;
280    let connection = redis_connection().await;
281    info!("redis connected");
282
283    #[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
284    struct MetricKind {
285        started: u64,
286        finished: u64,
287    }
288
289    // Subscribe on the pubsub being used
290    let mut subscribe = connection.subscribe_json::<MetricKind>("test_metrics_channel".to_owned()).await;
291
292    {
293        info!("Fast export");
294        // setup an exporter that sends metrics automatically very fast
295        let counter = connection.auto_exporting_metrics::<MetricKind>("test_metrics_channel".to_owned(), "component-x".to_owned())
296            .export_interval(Duration::from_micros(10))
297            .export_zero(false)
298            .start();
299
300        // Send a non default quantity via timer
301        increment!(counter, started, 5);
302        info!("Waiting for export");
303        assert_eq!(subscribe.recv().await.unwrap().unwrap(), MetricKind{started: 5, finished: 0});
304    }
305
306    {
307        info!("slow export");
308        // setup a slow export
309        let counter = connection.auto_exporting_metrics::<MetricKind>("test_metrics_channel".to_owned(), "component-x".to_owned())
310            .export_interval(Duration::from_secs(1000))
311            .export_zero(false)
312            .start();
313
314        // set some quantities then erase them
315        increment!(counter, started);
316        increment!(counter, finished);
317        counter.reset();
318
319        // Send a default quantities explicity
320        increment!(counter, started);
321        increment!(counter, started);
322        increment!(counter, finished);
323        counter.export();
324        assert_eq!(subscribe.recv().await.unwrap().unwrap(), MetricKind{started: 2, finished: 1});
325
326        // send a message and let the drop signal an export
327        increment!(counter, finished, 5);
328        increment!(counter, finished);
329    }
330
331    let result = tokio::time::timeout(Duration::from_secs(10), subscribe.recv()).await.unwrap();
332    assert_eq!(result.unwrap().unwrap(), MetricKind{started: 0, finished: 6});
333}
334
335
336    // # noinspection PyShadowingNames
337    // def test_basic_counters(redis_connection):
338    //     if redis_connection:
339    //         from assemblyline.remote.datatypes.counters import Counters
340    //         with Counters('test-counter') as ct:
341    //             ct.delete()
342
343    //             for x in range(10):
344    //                 ct.inc('t1')
345    //             for x in range(20):
346    //                 ct.inc('t2', value=2)
347    //             ct.dec('t1')
348    //             ct.dec('t2')
349    //             assert sorted(ct.get_queues()) == ['test-counter-t1',
350    //                                                'test-counter-t2']
351    //             assert ct.get_queues_sizes() == {'test-counter-t1': 9,
352    //                                              'test-counter-t2': 39}
353    //             ct.reset_queues()
354    //             assert ct.get_queues_sizes() == {'test-counter-t1': 0,
355    //                                              'test-counter-t2': 0}
356
357
358    // # noinspection PyShadowingNames
359    // def test_tracked_counters(redis_connection):
360    //     if redis_connection:
361    //         from assemblyline.remote.datatypes.counters import Counters
362    //         with Counters('tracked-test-counter', track_counters=True) as ct:
363    //             ct.delete()
364
365    //             for x in range(10):
366    //                 ct.inc('t1')
367    //             for x in range(20):
368    //                 ct.inc('t2', value=2)
369    //             assert ct.tracker.keys() == ['t1', 't2']
370    //             ct.dec('t1')
371    //             ct.dec('t2')
372    //             assert ct.tracker.keys() == []
373    //             assert sorted(ct.get_queues()) == ['tracked-test-counter-t1',
374    //                                                'tracked-test-counter-t2']
375    //             assert ct.get_queues_sizes() == {'tracked-test-counter-t1': 9,
376    //                                              'tracked-test-counter-t2': 39}
377    //             ct.reset_queues()
378    //             assert ct.get_queues_sizes() == {'tracked-test-counter-t1': 0,
379    //                                              'tracked-test-counter-t2': 0}