Skip to main content

vector/transforms/dedupe/
config.rs

1use vector_lib::{config::clone_input_definitions, configurable::configurable_component};
2
3use super::{
4    common::{
5        CacheConfig, FieldMatchConfig, TimedCacheConfig, default_cache_config,
6        fill_default_fields_match,
7    },
8    timed_transform::TimedDedupe,
9    transform::Dedupe,
10};
11use crate::{
12    config::{
13        DataType, GenerateConfig, Input, OutputId, TransformConfig, TransformContext,
14        TransformOutput,
15    },
16    schema,
17    transforms::Transform,
18};
19
20/// Configuration for the `dedupe` transform.
21#[configurable_component(transform("dedupe", "Deduplicate logs passing through a topology."))]
22#[derive(Clone, Debug)]
23#[serde(deny_unknown_fields)]
24pub struct DedupeConfig {
25    #[configurable(derived)]
26    #[serde(default)]
27    pub fields: Option<FieldMatchConfig>,
28
29    #[configurable(derived)]
30    #[serde(default = "default_cache_config")]
31    pub cache: CacheConfig,
32
33    #[configurable(derived)]
34    #[serde(default)]
35    pub time_settings: Option<TimedCacheConfig>,
36}
37
38impl GenerateConfig for DedupeConfig {
39    fn generate_config() -> toml::Value {
40        toml::Value::try_from(Self {
41            fields: None,
42            cache: default_cache_config(),
43            time_settings: None,
44        })
45        .unwrap()
46    }
47}
48
49#[async_trait::async_trait]
50#[typetag::serde(name = "dedupe")]
51impl TransformConfig for DedupeConfig {
52    async fn build(&self, _context: &TransformContext) -> crate::Result<Transform> {
53        if let Some(time_config) = &self.time_settings {
54            Ok(Transform::event_task(TimedDedupe::new(
55                self.cache.num_events,
56                fill_default_fields_match(self.fields.as_ref()),
57                time_config.clone(),
58            )))
59        } else {
60            Ok(Transform::event_task(Dedupe::new(
61                self.cache.num_events,
62                fill_default_fields_match(self.fields.as_ref()),
63            )))
64        }
65    }
66
67    fn input(&self) -> Input {
68        Input::log()
69    }
70
71    fn outputs(
72        &self,
73        _: &TransformContext,
74        input_definitions: &[(OutputId, schema::Definition)],
75    ) -> Vec<TransformOutput> {
76        vec![TransformOutput::new(
77            DataType::Log,
78            clone_input_definitions(input_definitions),
79        )]
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use std::{sync::Arc, time::Duration};
86
87    use tokio::sync::mpsc;
88    use tokio_stream::wrappers::ReceiverStream;
89    use vector_lib::{
90        config::{ComponentKey, OutputId},
91        lookup::lookup_v2::ConfigTargetPath,
92    };
93
94    use crate::{
95        config::schema::Definition,
96        event::{Event, LogEvent, ObjectMap, Value},
97        test_util::components::assert_transform_compliance,
98        transforms::{
99            dedupe::{
100                common::TimedCacheConfig,
101                config::{CacheConfig, DedupeConfig, FieldMatchConfig},
102            },
103            test::create_topology,
104        },
105    };
106
107    const TEST_SOURCE_COMPONENT_ID: &str = "in";
108    const TEST_UPSTREAM_COMPONENT_ID: &str = "transform";
109    const TEST_SOURCE_TYPE: &str = "unit_test_stream";
110
111    fn set_expected_metadata(event: &mut Event) {
112        event.set_source_id(Arc::new(ComponentKey::from(TEST_SOURCE_COMPONENT_ID)));
113        event.set_upstream_id(Arc::new(OutputId::from(TEST_UPSTREAM_COMPONENT_ID)));
114        event.set_source_type(TEST_SOURCE_TYPE);
115        // The schema definition is copied from the source for dedupe.
116        event
117            .metadata_mut()
118            .set_schema_definition(&Arc::new(Definition::default_legacy_namespace()));
119    }
120
121    #[test]
122    fn generate_config() {
123        crate::test_util::test_generate_config::<DedupeConfig>();
124    }
125
126    const fn make_match_transform_config(
127        num_events: usize,
128        fields: Vec<ConfigTargetPath>,
129    ) -> DedupeConfig {
130        DedupeConfig {
131            cache: CacheConfig {
132                num_events: std::num::NonZeroUsize::new(num_events).expect("non-zero num_events"),
133            },
134            fields: Some(FieldMatchConfig::MatchFields(fields)),
135            time_settings: None,
136        }
137    }
138
139    fn make_ignore_transform_config(
140        num_events: usize,
141        given_fields: Vec<ConfigTargetPath>,
142    ) -> DedupeConfig {
143        // "message" and "timestamp" are added automatically to all Events
144        let mut fields = vec!["message".into(), "timestamp".into()];
145        fields.extend(given_fields);
146
147        DedupeConfig {
148            cache: CacheConfig {
149                num_events: std::num::NonZeroUsize::new(num_events).expect("non-zero num_events"),
150            },
151            fields: Some(FieldMatchConfig::IgnoreFields(fields)),
152            time_settings: None,
153        }
154    }
155
156    #[tokio::test]
157    async fn dedupe_match_basic() {
158        let transform_config = make_match_transform_config(5, vec!["matched".into()]);
159        basic(transform_config, "matched", "unmatched").await;
160    }
161
162    #[tokio::test]
163    async fn dedupe_ignore_basic() {
164        let transform_config = make_ignore_transform_config(5, vec!["unmatched".into()]);
165        basic(transform_config, "matched", "unmatched").await;
166    }
167
168    #[tokio::test]
169    async fn dedupe_ignore_with_metadata_field() {
170        let transform_config = make_ignore_transform_config(5, vec!["%ignored".into()]);
171        basic(transform_config, "matched", "%ignored").await;
172    }
173
174    async fn basic(transform_config: DedupeConfig, first_path: &str, second_path: &str) {
175        let first_path = vrl::path::parse_target_path(first_path).unwrap();
176        let second_path = vrl::path::parse_target_path(second_path).unwrap();
177        assert_transform_compliance(async {
178            let (tx, rx) = mpsc::channel(1);
179            let (topology, mut out) =
180                create_topology(ReceiverStream::new(rx), transform_config).await;
181
182            let mut event1 = Event::Log(LogEvent::from("message"));
183            event1.as_mut_log().insert(&first_path, "some value");
184            event1.as_mut_log().insert(&second_path, "another value");
185
186            // Test that unmatched field isn't considered
187            let mut event2 = Event::Log(LogEvent::from("message"));
188            event2.as_mut_log().insert(&first_path, "some value2");
189            event2.as_mut_log().insert(&second_path, "another value");
190
191            // Test that matched field is considered
192            let mut event3 = Event::Log(LogEvent::from("message"));
193            event3.as_mut_log().insert(&first_path, "some value");
194            event3.as_mut_log().insert(&second_path, "another value2");
195
196            // First event should always be passed through as-is.
197            tx.send(event1.clone()).await.unwrap();
198            let new_event = out.recv().await.unwrap();
199
200            set_expected_metadata(&mut event1);
201            assert_eq!(new_event, event1);
202
203            // Second event differs in matched field so should be output even though it
204            // has the same value for unmatched field.
205            tx.send(event2.clone()).await.unwrap();
206            let new_event = out.recv().await.unwrap();
207
208            set_expected_metadata(&mut event2);
209            assert_eq!(new_event, event2);
210
211            // Third event has the same value for "matched" as first event, so it should be dropped.
212            tx.send(event3.clone()).await.unwrap();
213
214            drop(tx);
215            topology.stop().await;
216            assert_eq!(out.recv().await, None);
217        })
218        .await;
219    }
220
221    #[tokio::test]
222    async fn dedupe_match_field_name_matters() {
223        let transform_config =
224            make_match_transform_config(5, vec!["matched1".into(), "matched2".into()]);
225        field_name_matters(transform_config).await;
226    }
227
228    #[tokio::test]
229    async fn dedupe_ignore_field_name_matters() {
230        let transform_config = make_ignore_transform_config(5, vec![]);
231        field_name_matters(transform_config).await;
232    }
233
234    async fn field_name_matters(transform_config: DedupeConfig) {
235        assert_transform_compliance(async {
236            let (tx, rx) = mpsc::channel(1);
237            let (topology, mut out) =
238                create_topology(ReceiverStream::new(rx), transform_config).await;
239
240            let mut event1 = Event::Log(LogEvent::from("message"));
241            event1
242                .as_mut_log()
243                .insert(vrl::event_path!("matched1"), "some value");
244
245            let mut event2 = Event::Log(LogEvent::from("message"));
246            event2
247                .as_mut_log()
248                .insert(vrl::event_path!("matched2"), "some value");
249
250            // First event should always be passed through as-is.
251            tx.send(event1.clone()).await.unwrap();
252            let new_event = out.recv().await.unwrap();
253
254            set_expected_metadata(&mut event1);
255            assert_eq!(new_event, event1);
256
257            // Second event has a different matched field name with the same value,
258            // so it should not be considered a dupe
259            tx.send(event2.clone()).await.unwrap();
260            let new_event = out.recv().await.unwrap();
261
262            set_expected_metadata(&mut event2);
263            assert_eq!(new_event, event2);
264
265            drop(tx);
266            topology.stop().await;
267            assert_eq!(out.recv().await, None);
268        })
269        .await;
270    }
271
272    #[tokio::test]
273    async fn dedupe_match_field_order_irrelevant() {
274        let transform_config =
275            make_match_transform_config(5, vec!["matched1".into(), "matched2".into()]);
276        field_order_irrelevant(transform_config).await;
277    }
278
279    #[tokio::test]
280    async fn dedupe_ignore_field_order_irrelevant() {
281        let transform_config = make_ignore_transform_config(5, vec!["randomData".into()]);
282        field_order_irrelevant(transform_config).await;
283    }
284
285    /// Test that two Events that are considered duplicates get handled that
286    /// way, even if the order of the matched fields is different between the
287    /// two.
288    async fn field_order_irrelevant(transform_config: DedupeConfig) {
289        assert_transform_compliance(async {
290            let (tx, rx) = mpsc::channel(1);
291            let (topology, mut out) =
292                create_topology(ReceiverStream::new(rx), transform_config).await;
293
294            let mut event1 = Event::Log(LogEvent::from("message"));
295            event1
296                .as_mut_log()
297                .insert(vrl::event_path!("matched1"), "value1");
298            event1
299                .as_mut_log()
300                .insert(vrl::event_path!("matched2"), "value2");
301
302            // Add fields in opposite order
303            let mut event2 = Event::Log(LogEvent::from("message"));
304            event2
305                .as_mut_log()
306                .insert(vrl::event_path!("matched2"), "value2");
307            event2
308                .as_mut_log()
309                .insert(vrl::event_path!("matched1"), "value1");
310
311            // First event should always be passed through as-is.
312            tx.send(event1.clone()).await.unwrap();
313            let new_event = out.recv().await.unwrap();
314
315            set_expected_metadata(&mut event1);
316            assert_eq!(new_event, event1);
317
318            // Second event is the same just with different field order, so it
319            // shouldn't be output.
320            tx.send(event2).await.unwrap();
321
322            drop(tx);
323            topology.stop().await;
324            assert_eq!(out.recv().await, None);
325        })
326        .await;
327    }
328
329    #[tokio::test]
330    async fn dedupe_match_age_out() {
331        // Construct transform with a cache size of only 1 entry.
332        let transform_config = make_match_transform_config(1, vec!["matched".into()]);
333        age_out(transform_config).await;
334    }
335
336    #[tokio::test]
337    async fn dedupe_ignore_age_out() {
338        // Construct transform with a cache size of only 1 entry.
339        let transform_config = make_ignore_transform_config(1, vec![]);
340        age_out(transform_config).await;
341    }
342
343    /// Test the eviction behavior of the underlying LruCache
344    async fn age_out(transform_config: DedupeConfig) {
345        assert_transform_compliance(async {
346            let (tx, rx) = mpsc::channel(1);
347            let (topology, mut out) =
348                create_topology(ReceiverStream::new(rx), transform_config).await;
349
350            let mut event1 = Event::Log(LogEvent::from("message"));
351            event1
352                .as_mut_log()
353                .insert(vrl::event_path!("matched"), "some value");
354
355            let mut event2 = Event::Log(LogEvent::from("message"));
356            event2
357                .as_mut_log()
358                .insert(vrl::event_path!("matched"), "some value2");
359
360            // First event should always be passed through as-is.
361            tx.send(event1.clone()).await.unwrap();
362            let new_event = out.recv().await.unwrap();
363
364            set_expected_metadata(&mut event1);
365            assert_eq!(new_event, event1);
366
367            // Second event gets output because it's not a dupe. This causes the first
368            // Event to be evicted from the cache.
369            tx.send(event2.clone()).await.unwrap();
370            let new_event = out.recv().await.unwrap();
371
372            set_expected_metadata(&mut event2);
373
374            assert_eq!(new_event, event2);
375
376            // Third event is a dupe but gets output anyway because the first
377            // event has aged out of the cache.
378            tx.send(event1.clone()).await.unwrap();
379            let new_event = out.recv().await.unwrap();
380
381            set_expected_metadata(&mut event1);
382            assert_eq!(new_event, event1);
383
384            drop(tx);
385            topology.stop().await;
386            assert_eq!(out.recv().await, None);
387        })
388        .await;
389    }
390
391    #[tokio::test]
392    async fn dedupe_match_timed_age_out() {
393        // Construct transform with timed cache
394        let transform_config = DedupeConfig {
395            time_settings: Some(TimedCacheConfig {
396                max_age_ms: Duration::from_millis(100),
397                refresh_on_drop: false,
398            }),
399            ..make_match_transform_config(5, vec!["matched".into()])
400        };
401        timed_age_out(transform_config).await;
402    }
403
404    #[tokio::test]
405    async fn dedupe_ignore_timed_age_out() {
406        // Construct transform with timed cache
407        let transform_config = DedupeConfig {
408            time_settings: Some(TimedCacheConfig {
409                max_age_ms: Duration::from_millis(100),
410                refresh_on_drop: false,
411            }),
412            ..make_ignore_transform_config(1, vec![])
413        };
414        timed_age_out(transform_config).await;
415    }
416
417    /// Test the eviction behavior of the underlying LruCache
418    async fn timed_age_out(transform_config: DedupeConfig) {
419        assert_transform_compliance(async {
420            let (tx, rx) = mpsc::channel(1);
421            let (topology, mut out) =
422                create_topology(ReceiverStream::new(rx), transform_config).await;
423
424            let mut event1 = Event::Log(LogEvent::from("message"));
425            event1
426                .as_mut_log()
427                .insert(vrl::event_path!("matched"), "some value");
428
429            // First event should always be passed through as-is.
430            tx.send(event1.clone()).await.unwrap();
431            let new_event = out.recv().await.unwrap();
432
433            set_expected_metadata(&mut event1);
434            assert_eq!(new_event, event1);
435
436            // Second time the event gets dropped because it's a dupe.
437            tx.send(event1.clone()).await.unwrap();
438
439            tokio::time::sleep(Duration::from_millis(101)).await;
440
441            // Third time the event is a dupe but enough time has passed to accept it.
442            tx.send(event1.clone()).await.unwrap();
443            let new_event = out.recv().await.unwrap();
444
445            set_expected_metadata(&mut event1);
446            assert_eq!(new_event, event1);
447
448            drop(tx);
449            topology.stop().await;
450            assert_eq!(out.recv().await, None);
451        })
452        .await;
453    }
454
455    #[tokio::test]
456    async fn dedupe_match_type_matching() {
457        let transform_config = make_match_transform_config(5, vec!["matched".into()]);
458        type_matching(transform_config).await;
459    }
460
461    #[tokio::test]
462    async fn dedupe_ignore_type_matching() {
463        let transform_config = make_ignore_transform_config(5, vec![]);
464        type_matching(transform_config).await;
465    }
466
467    /// Test that two events with values for the matched fields that have
468    /// different types but the same string representation aren't considered
469    /// duplicates.
470    async fn type_matching(transform_config: DedupeConfig) {
471        assert_transform_compliance(async {
472            let (tx, rx) = mpsc::channel(1);
473            let (topology, mut out) =
474                create_topology(ReceiverStream::new(rx), transform_config).await;
475
476            let mut event1 = Event::Log(LogEvent::from("message"));
477            event1
478                .as_mut_log()
479                .insert(vrl::event_path!("matched"), "123");
480
481            let mut event2 = Event::Log(LogEvent::from("message"));
482            event2.as_mut_log().insert(vrl::event_path!("matched"), 123);
483
484            // First event should always be passed through as-is.
485            tx.send(event1.clone()).await.unwrap();
486            let new_event = out.recv().await.unwrap();
487
488            set_expected_metadata(&mut event1);
489            assert_eq!(new_event, event1);
490
491            // Second event should also get passed through even though the string
492            // representations of "matched" are the same.
493            tx.send(event2.clone()).await.unwrap();
494            let new_event = out.recv().await.unwrap();
495
496            set_expected_metadata(&mut event2);
497            assert_eq!(new_event, event2);
498
499            drop(tx);
500            topology.stop().await;
501            assert_eq!(out.recv().await, None);
502        })
503        .await;
504    }
505
506    #[tokio::test]
507    async fn dedupe_match_type_matching_nested_objects() {
508        let transform_config = make_match_transform_config(5, vec!["matched".into()]);
509        type_matching_nested_objects(transform_config).await;
510    }
511
512    #[tokio::test]
513    async fn dedupe_ignore_type_matching_nested_objects() {
514        let transform_config = make_ignore_transform_config(5, vec![]);
515        type_matching_nested_objects(transform_config).await;
516    }
517
518    /// Test that two events where the matched field is a sub object and that
519    /// object contains values that have different types but the same string
520    /// representation aren't considered duplicates.
521    async fn type_matching_nested_objects(transform_config: DedupeConfig) {
522        assert_transform_compliance(async {
523            let (tx, rx) = mpsc::channel(1);
524            let (topology, mut out) =
525                create_topology(ReceiverStream::new(rx), transform_config).await;
526
527            let mut map1 = ObjectMap::new();
528            map1.insert("key".into(), "123".into());
529            let mut event1 = Event::Log(LogEvent::from("message"));
530            event1
531                .as_mut_log()
532                .insert(vrl::event_path!("matched"), map1);
533
534            let mut map2 = ObjectMap::new();
535            map2.insert("key".into(), 123.into());
536            let mut event2 = Event::Log(LogEvent::from("message"));
537            event2
538                .as_mut_log()
539                .insert(vrl::event_path!("matched"), map2);
540
541            // First event should always be passed through as-is.
542            tx.send(event1.clone()).await.unwrap();
543            let new_event = out.recv().await.unwrap();
544
545            set_expected_metadata(&mut event1);
546            assert_eq!(new_event, event1);
547
548            // Second event should also get passed through even though the string
549            // representations of "matched" are the same.
550            tx.send(event2.clone()).await.unwrap();
551            let new_event = out.recv().await.unwrap();
552
553            set_expected_metadata(&mut event2);
554            assert_eq!(new_event, event2);
555
556            drop(tx);
557            topology.stop().await;
558            assert_eq!(out.recv().await, None);
559        })
560        .await;
561    }
562
563    #[tokio::test]
564    async fn dedupe_match_null_vs_missing() {
565        let transform_config = make_match_transform_config(5, vec!["matched".into()]);
566        ignore_vs_missing(transform_config).await;
567    }
568
569    #[tokio::test]
570    async fn dedupe_ignore_null_vs_missing() {
571        let transform_config = make_ignore_transform_config(5, vec![]);
572        ignore_vs_missing(transform_config).await;
573    }
574
575    /// Test an explicit null vs a field being missing are treated as different.
576    async fn ignore_vs_missing(transform_config: DedupeConfig) {
577        assert_transform_compliance(async {
578            let (tx, rx) = mpsc::channel(1);
579            let (topology, mut out) =
580                create_topology(ReceiverStream::new(rx), transform_config).await;
581
582            let mut event1 = Event::Log(LogEvent::from("message"));
583            event1
584                .as_mut_log()
585                .insert(vrl::event_path!("matched"), Value::Null);
586
587            let mut event2 = Event::Log(LogEvent::from("message"));
588
589            // First event should always be passed through as-is.
590            tx.send(event1.clone()).await.unwrap();
591            let new_event = out.recv().await.unwrap();
592
593            set_expected_metadata(&mut event1);
594            assert_eq!(new_event, event1);
595
596            // Second event should also get passed through as null is different than
597            // missing
598            tx.send(event2.clone()).await.unwrap();
599            let new_event = out.recv().await.unwrap();
600
601            set_expected_metadata(&mut event2);
602            assert_eq!(new_event, event2);
603
604            drop(tx);
605            topology.stop().await;
606            assert_eq!(out.recv().await, None);
607        })
608        .await;
609    }
610}