vector/transforms/throttle/
transform.rs1use 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 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 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 match out_stream.next().await {
216 Some(_event) => {}
217 _ => {
218 panic!("Unexpectedly received None in output stream");
219 }
220 }
221
222 assert_eq!(Poll::Pending, futures::poll!(out_stream.next()));
224
225 tx.disconnect();
226
227 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 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 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 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 match out_stream.next().await {
294 Some(_event) => {}
295 _ => {
296 panic!("Unexpectedly received None in output stream");
297 }
298 }
299
300 assert_eq!(Poll::Pending, futures::poll!(out_stream.next()));
302
303 tx.disconnect();
304
305 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 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 assert_eq!(Poll::Pending, futures::poll!(out_stream.next()));
354
355 tx.disconnect();
356
357 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}