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#[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let transform_config = make_ignore_transform_config(1, vec![]);
340 age_out(transform_config).await;
341 }
342
343 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 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 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 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 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 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 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 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 tx.send(event1.clone()).await.unwrap();
438
439 tokio::time::sleep(Duration::from_millis(101)).await;
440
441 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 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 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 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 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 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 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 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 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 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}