Skip to main content

vector/transforms/throttle/
transform.rs

1use std::{hash::Hash, num::NonZeroU32, pin::Pin, time::Duration};
2
3use async_stream::stream;
4use futures::{Stream, StreamExt};
5use governor::{Quota, clock};
6use metrics::Counter;
7use snafu::Snafu;
8
9use super::{
10    config::{ThrottleConfig, ThrottleInternalMetricsConfig},
11    rate_limiter::RateLimiterRunner,
12};
13use crate::{
14    conditions::Condition,
15    config::TransformContext,
16    event::Event,
17    internal_events::{TemplateRenderingError, ThrottleEventDiscarded},
18    template::Template,
19    transforms::TaskTransform,
20};
21
22#[derive(Clone)]
23pub struct Throttle<C: clock::Clock<Instant = I>, I: clock::Reference> {
24    pub quota: Quota,
25    pub flush_keys_interval: Duration,
26    key_field: Option<Template>,
27    exclude: Option<Condition>,
28    pub clock: C,
29    internal_metrics: ThrottleInternalMetricsConfig,
30    pub cpu_ns: Option<Counter>,
31}
32
33impl<C, I> Throttle<C, I>
34where
35    C: clock::Clock<Instant = I> + Clone + Send + Sync + 'static,
36    I: clock::Reference,
37{
38    pub fn new(
39        config: &ThrottleConfig,
40        context: &TransformContext,
41        clock: C,
42    ) -> crate::Result<Self> {
43        let flush_keys_interval = config.window_secs;
44
45        let threshold = match NonZeroU32::new(config.threshold) {
46            Some(threshold) => threshold,
47            None => return Err(Box::new(ConfigError::NonZero)),
48        };
49
50        let quota = match Quota::with_period(Duration::from_secs_f64(
51            flush_keys_interval.as_secs_f64() / f64::from(threshold.get()),
52        )) {
53            Some(quota) => quota.allow_burst(threshold),
54            None => return Err(Box::new(ConfigError::NonZero)),
55        };
56        let exclude = config
57            .exclude
58            .as_ref()
59            .map(|condition| condition.build(&context.enrichment_tables, &context.metrics_storage))
60            .transpose()?;
61
62        Ok(Self {
63            quota,
64            clock,
65            flush_keys_interval,
66            key_field: config.key_field.clone(),
67            exclude,
68            internal_metrics: config.internal_metrics.clone(),
69            cpu_ns: context.cpu_ns.clone(),
70        })
71    }
72
73    #[must_use]
74    pub fn start_rate_limiter<K>(&self) -> RateLimiterRunner<K, C>
75    where
76        K: Hash + Eq + Clone + Send + Sync + 'static,
77    {
78        RateLimiterRunner::start(self)
79    }
80
81    pub fn emit_event_discarded(&self, key: String) {
82        emit!(ThrottleEventDiscarded {
83            key,
84            emit_events_discarded_per_key: self.internal_metrics.emit_events_discarded_per_key
85        });
86    }
87}
88
89impl<C, I> TaskTransform<Event> for Throttle<C, I>
90where
91    C: clock::Clock<Instant = I> + Clone + Send + Sync + 'static,
92    I: clock::Reference + Send + 'static,
93{
94    fn transform(
95        self: Box<Self>,
96        mut input_rx: Pin<Box<dyn Stream<Item = Event> + Send>>,
97    ) -> Pin<Box<dyn Stream<Item = Event> + Send>>
98    where
99        Self: 'static,
100    {
101        let limiter = self.start_rate_limiter();
102
103        Box::pin(stream! {
104            while let Some(event) = input_rx.next().await {
105                let (throttle, event) = match self.exclude.as_ref() {
106                    Some(condition) => {
107                        let (result, event) = condition.check(event);
108                        (!result, event)
109                    },
110                    _ => (true, event)
111                };
112                let output = if throttle {
113                    let key = self.key_field.as_ref().and_then(|t| {
114                        t.render_string(&event)
115                            .map_err(|error| {
116                                emit!(TemplateRenderingError {
117                                    error,
118                                    field: Some("key_field"),
119                                    drop_event: false,
120                                })
121                            })
122                            .ok()
123                    });
124
125                    if limiter.check_key(&key) {
126                        Some(event)
127                    } else {
128                        self.emit_event_discarded(key.unwrap_or_else(|| "None".to_string()));
129                        None
130                    }
131                } else {
132                    Some(event)
133                };
134                if let Some(event) = output {
135                    yield event;
136                }
137            }
138        })
139    }
140}
141
142#[derive(Debug, Snafu)]
143pub enum ConfigError {
144    #[snafu(display("`threshold`, and `window_secs` must be non-zero"))]
145    NonZero,
146}
147
148#[cfg(test)]
149mod tests {
150    use std::task::Poll;
151
152    use futures::SinkExt;
153    use indoc::indoc;
154    use tokio::sync::mpsc;
155    use tokio_stream::wrappers::ReceiverStream;
156    use vrl::event_path;
157
158    use super::*;
159    use crate::{
160        event::LogEvent,
161        test_util::components::assert_transform_compliance,
162        transforms::{Transform, test::create_topology},
163    };
164
165    #[tokio::test]
166    async fn throttle_events() {
167        let clock = clock::FakeRelativeClock::default();
168        let config = serde_yaml::from_str::<ThrottleConfig>(indoc! {"
169            threshold: 2
170            window_secs: 5
171        "})
172        .unwrap();
173
174        let throttle = Throttle::new(&config, &TransformContext::default(), clock.clone())
175            .map(Transform::event_task)
176            .unwrap();
177
178        let throttle = throttle.into_task();
179
180        let (mut tx, rx) = futures::channel::mpsc::channel(10);
181        let mut out_stream = throttle.transform_events(Box::pin(rx));
182
183        // tokio interval is always immediately ready, so we poll once to make sure
184        // we trip it/set the interval in the future
185        assert_eq!(Poll::Pending, futures::poll!(out_stream.next()));
186
187        tx.send(LogEvent::default().into()).await.unwrap();
188        tx.send(LogEvent::default().into()).await.unwrap();
189
190        let mut count = 0_u8;
191        while count < 2 {
192            match out_stream.next().await {
193                Some(_event) => {
194                    count += 1;
195                }
196                _ => {
197                    panic!("Unexpectedly received None in output stream");
198                }
199            }
200        }
201        assert_eq!(2, count);
202
203        clock.advance(Duration::from_secs(2));
204
205        tx.send(LogEvent::default().into()).await.unwrap();
206
207        // We should be back to pending, having the second event dropped
208        assert_eq!(Poll::Pending, futures::poll!(out_stream.next()));
209
210        clock.advance(Duration::from_secs(3));
211
212        tx.send(LogEvent::default().into()).await.unwrap();
213
214        // The rate limiter should now be refreshed and allow an additional event through
215        match out_stream.next().await {
216            Some(_event) => {}
217            _ => {
218                panic!("Unexpectedly received None in output stream");
219            }
220        }
221
222        // We should be back to pending, having nothing waiting for us
223        assert_eq!(Poll::Pending, futures::poll!(out_stream.next()));
224
225        tx.disconnect();
226
227        // And still nothing there
228        assert_eq!(Poll::Ready(None), futures::poll!(out_stream.next()));
229    }
230
231    #[tokio::test]
232    async fn throttle_exclude() {
233        let clock = clock::FakeRelativeClock::default();
234        let config = serde_yaml::from_str::<ThrottleConfig>(indoc! {"
235            threshold: 2
236            window_secs: 5
237            exclude: \"exists(.special)\"
238        "})
239        .unwrap();
240
241        let throttle = Throttle::new(&config, &TransformContext::default(), clock.clone())
242            .map(Transform::event_task)
243            .unwrap();
244
245        let throttle = throttle.into_task();
246
247        let (mut tx, rx) = futures::channel::mpsc::channel(10);
248        let mut out_stream = throttle.transform_events(Box::pin(rx));
249
250        // tokio interval is always immediately ready, so we poll once to make sure
251        // we trip it/set the interval in the future
252        assert_eq!(Poll::Pending, futures::poll!(out_stream.next()));
253
254        tx.send(LogEvent::default().into()).await.unwrap();
255        tx.send(LogEvent::default().into()).await.unwrap();
256
257        let mut count = 0_u8;
258        while count < 2 {
259            match out_stream.next().await {
260                Some(_event) => {
261                    count += 1;
262                }
263                _ => {
264                    panic!("Unexpectedly received None in output stream");
265                }
266            }
267        }
268        assert_eq!(2, count);
269
270        clock.advance(Duration::from_secs(2));
271
272        tx.send(LogEvent::default().into()).await.unwrap();
273
274        // We should be back to pending, having the second event dropped
275        assert_eq!(Poll::Pending, futures::poll!(out_stream.next()));
276
277        let mut special_log = LogEvent::default();
278        special_log.insert(event_path!("special"), "true");
279        tx.send(special_log.into()).await.unwrap();
280        // The rate limiter should allow this log through regardless of current limit
281        match out_stream.next().await {
282            Some(_event) => {}
283            _ => {
284                panic!("Unexpectedly received None in output stream");
285            }
286        }
287
288        clock.advance(Duration::from_secs(3));
289
290        tx.send(LogEvent::default().into()).await.unwrap();
291
292        // The rate limiter should now be refreshed and allow an additional event through
293        match out_stream.next().await {
294            Some(_event) => {}
295            _ => {
296                panic!("Unexpectedly received None in output stream");
297            }
298        }
299
300        // We should be back to pending, having nothing waiting for us
301        assert_eq!(Poll::Pending, futures::poll!(out_stream.next()));
302
303        tx.disconnect();
304
305        // And still nothing there
306        assert_eq!(Poll::Ready(None), futures::poll!(out_stream.next()));
307    }
308
309    #[tokio::test]
310    async fn throttle_buckets() {
311        let clock = clock::FakeRelativeClock::default();
312        let config = serde_yaml::from_str::<ThrottleConfig>(indoc! {r#"
313            threshold: 1
314            window_secs: 5
315            key_field: "{{ bucket }}"
316        "#})
317        .unwrap();
318
319        let throttle = Throttle::new(&config, &TransformContext::default(), clock.clone())
320            .map(Transform::event_task)
321            .unwrap();
322
323        let throttle = throttle.into_task();
324
325        let (mut tx, rx) = futures::channel::mpsc::channel(10);
326        let mut out_stream = throttle.transform_events(Box::pin(rx));
327
328        // tokio interval is always immediately ready, so we poll once to make sure
329        // we trip it/set the interval in the future
330        assert_eq!(Poll::Pending, futures::poll!(out_stream.next()));
331
332        let mut log_a = LogEvent::default();
333        log_a.insert(event_path!("bucket"), "a");
334        let mut log_b = LogEvent::default();
335        log_b.insert(event_path!("bucket"), "b");
336        tx.send(log_a.into()).await.unwrap();
337        tx.send(log_b.into()).await.unwrap();
338
339        let mut count = 0_u8;
340        while count < 2 {
341            match out_stream.next().await {
342                Some(_event) => {
343                    count += 1;
344                }
345                _ => {
346                    panic!("Unexpectedly received None in output stream");
347                }
348            }
349        }
350        assert_eq!(2, count);
351
352        // We should be back to pending, having nothing waiting for us
353        assert_eq!(Poll::Pending, futures::poll!(out_stream.next()));
354
355        tx.disconnect();
356
357        // And still nothing there
358        assert_eq!(Poll::Ready(None), futures::poll!(out_stream.next()));
359    }
360
361    #[tokio::test]
362    async fn emits_internal_events() {
363        assert_transform_compliance(async move {
364            let config = ThrottleConfig {
365                threshold: 1,
366                window_secs: Duration::from_secs_f64(1.0),
367                key_field: None,
368                exclude: None,
369                internal_metrics: Default::default(),
370            };
371            let (tx, rx) = mpsc::channel(1);
372            let (topology, mut out) = create_topology(ReceiverStream::new(rx), config).await;
373
374            let log = LogEvent::from("hello world");
375            tx.send(log.into()).await.unwrap();
376
377            _ = out.recv().await;
378
379            drop(tx);
380            topology.stop().await;
381            assert_eq!(out.recv().await, None);
382        })
383        .await
384    }
385}