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}