Skip to content

Commit 71343bd

Browse files
authored
feat(route transform): Add option to enable/disable unmatched output (vectordotdev#18309)
* feat(route transform): Add option to enable/disable unmatched output This commit adds a new boolean option `reroute_unmatched` to the `route` transform. It is inspired on the `reroute_dropped` option in the `remap` transform, allowing the user to control if they want or not the `<transform_name>._unmatched` output to be created and used. For backwards compatibility, this new option defaults to `true`. Users that are not interested in processing unmatched events can use this option to avoid the following warning on Vector startup, and also to remove the unnecessary `_unmatched` output in `vector top`: ``` WARN vector::config::loading: Transform "route._unmatched" has no consumers ``` Signed-off-by: Hugo Hromic <[email protected]> * Update existing `can_serialize_remap` test * Add test for the new `reroute_unmatched` option --------- Signed-off-by: Hugo Hromic <[email protected]>
1 parent 6397edb commit 71343bd

File tree

2 files changed

+99
-23
lines changed

2 files changed

+99
-23
lines changed

src/transforms/route.rs

+68-9
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub(crate) const UNMATCHED_ROUTE: &str = "_unmatched";
1919
#[derive(Clone)]
2020
pub struct Route {
2121
conditions: Vec<(String, Condition)>,
22+
reroute_unmatched: bool,
2223
}
2324

2425
impl Route {
@@ -28,7 +29,10 @@ impl Route {
2829
let condition = condition.build(&context.enrichment_tables)?;
2930
conditions.push((output_name.clone(), condition));
3031
}
31-
Ok(Self { conditions })
32+
Ok(Self {
33+
conditions,
34+
reroute_unmatched: config.reroute_unmatched,
35+
})
3236
}
3337
}
3438

@@ -47,7 +51,7 @@ impl SyncTransform for Route {
4751
check_failed += 1;
4852
}
4953
}
50-
if check_failed == self.conditions.len() {
54+
if self.reroute_unmatched && check_failed == self.conditions.len() {
5155
output.push(Some(UNMATCHED_ROUTE), event);
5256
}
5357
}
@@ -61,11 +65,24 @@ impl SyncTransform for Route {
6165
#[derive(Clone, Debug)]
6266
#[serde(deny_unknown_fields)]
6367
pub struct RouteConfig {
68+
/// Reroutes unmatched events to a named output instead of silently discarding them.
69+
///
70+
/// Normally, if an event doesn't match any defined route, it is sent to the `<transform_name>._unmatched`
71+
/// output for further processing. In some cases, you may want to simply discard unmatched events and not
72+
/// process them any further.
73+
///
74+
/// In these cases, `reroute_unmatched` can be set to `false` to disable the `<transform_name>._unmatched`
75+
/// output and instead silently discard any unmatched events.
76+
#[serde(default = "crate::serde::default_true")]
77+
#[configurable(metadata(docs::human_name = "Reroute Unmatched Events"))]
78+
reroute_unmatched: bool,
79+
6480
/// A table of route identifiers to logical conditions representing the filter of the route.
6581
///
6682
/// Each route can then be referenced as an input by other components with the name
67-
/// `<transform_name>.<route_id>`. If an event doesn’t match any route, it is sent to the
68-
/// `<transform_name>._unmatched` output.
83+
/// `<transform_name>.<route_id>`. If an event doesn’t match any route, and if `reroute_unmatched`
84+
/// is set to `true` (the default), it is sent to the `<transform_name>._unmatched` output.
85+
/// Otherwise, the unmatched event is instead silently discarded.
6986
///
7087
/// Both `_unmatched`, as well as `_default`, are reserved output names and thus cannot be used
7188
/// as a route name.
@@ -76,6 +93,7 @@ pub struct RouteConfig {
7693
impl GenerateConfig for RouteConfig {
7794
fn generate_config() -> toml::Value {
7895
toml::Value::try_from(Self {
96+
reroute_unmatched: true,
7997
route: IndexMap::new(),
8098
})
8199
.unwrap()
@@ -118,10 +136,12 @@ impl TransformConfig for RouteConfig {
118136
.with_port(output_name)
119137
})
120138
.collect();
121-
result.push(
122-
TransformOutput::new(DataType::all(), clone_input_definitions(input_definitions))
123-
.with_port(UNMATCHED_ROUTE),
124-
);
139+
if self.reroute_unmatched {
140+
result.push(
141+
TransformOutput::new(DataType::all(), clone_input_definitions(input_definitions))
142+
.with_port(UNMATCHED_ROUTE),
143+
);
144+
}
125145
result
126146
}
127147

@@ -162,7 +182,7 @@ mod test {
162182

163183
assert_eq!(
164184
serde_json::to_string(&config).unwrap(),
165-
r#"{"route":{"first":{"type":"vrl","source":".message == \"hello world\"","runtime":"ast"}}}"#
185+
r#"{"reroute_unmatched":true,"route":{"first":{"type":"vrl","source":".message == \"hello world\"","runtime":"ast"}}}"#
166186
);
167187
}
168188

@@ -293,6 +313,45 @@ mod test {
293313
}
294314
}
295315

316+
#[test]
317+
fn route_no_unmatched_output() {
318+
let output_names = vec!["first", "second", "third", UNMATCHED_ROUTE];
319+
let event = Event::try_from(serde_json::json!({"message": "NOPE"})).unwrap();
320+
let config = toml::from_str::<RouteConfig>(
321+
r#"
322+
reroute_unmatched = false
323+
324+
route.first.type = "vrl"
325+
route.first.source = '.message == "hello world"'
326+
327+
route.second.type = "vrl"
328+
route.second.source = '.second == "second"'
329+
330+
route.third.type = "vrl"
331+
route.third.source = '.third == "third"'
332+
"#,
333+
)
334+
.unwrap();
335+
336+
let mut transform = Route::new(&config, &Default::default()).unwrap();
337+
let mut outputs = TransformOutputsBuf::new_with_capacity(
338+
output_names
339+
.iter()
340+
.map(|output_name| {
341+
TransformOutput::new(DataType::all(), HashMap::new())
342+
.with_port(output_name.to_owned())
343+
})
344+
.collect(),
345+
1,
346+
);
347+
348+
transform.transform(event.clone(), &mut outputs);
349+
for output_name in output_names {
350+
let events: Vec<_> = outputs.drain_named(output_name).collect();
351+
assert_eq!(events.len(), 0);
352+
}
353+
}
354+
296355
#[tokio::test]
297356
async fn route_metrics_with_output_tag() {
298357
init_test();
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,37 @@
11
package metadata
22

3-
base: components: transforms: route: configuration: route: {
4-
description: """
5-
A table of route identifiers to logical conditions representing the filter of the route.
3+
base: components: transforms: route: configuration: {
4+
reroute_unmatched: {
5+
description: """
6+
Reroutes unmatched events to a named output instead of silently discarding them.
67
7-
Each route can then be referenced as an input by other components with the name
8-
`<transform_name>.<route_id>`. If an event doesn’t match any route, it is sent to the
9-
`<transform_name>._unmatched` output.
8+
Normally, if an event doesn't match any defined route, it is sent to the `<transform_name>._unmatched`
9+
output for further processing. In some cases, you may want to simply discard unmatched events and not
10+
process them any further.
1011
11-
Both `_unmatched`, as well as `_default`, are reserved output names and thus cannot be used
12-
as a route name.
13-
"""
14-
required: false
15-
type: object: options: "*": {
16-
description: "An individual route."
17-
required: true
18-
type: condition: {}
12+
In these cases, `reroute_unmatched` can be set to `false` to disable the `<transform_name>._unmatched`
13+
output and instead silently discard any unmatched events.
14+
"""
15+
required: false
16+
type: bool: default: true
17+
}
18+
route: {
19+
description: """
20+
A table of route identifiers to logical conditions representing the filter of the route.
21+
22+
Each route can then be referenced as an input by other components with the name
23+
`<transform_name>.<route_id>`. If an event doesn’t match any route, and if `reroute_unmatched`
24+
is set to `true` (the default), it is sent to the `<transform_name>._unmatched` output.
25+
Otherwise, the unmatched event is instead silently discarded.
26+
27+
Both `_unmatched`, as well as `_default`, are reserved output names and thus cannot be used
28+
as a route name.
29+
"""
30+
required: false
31+
type: object: options: "*": {
32+
description: "An individual route."
33+
required: true
34+
type: condition: {}
35+
}
1936
}
2037
}

0 commit comments

Comments
 (0)