vector/internal_events/template.rs
1use vector_lib::{
2 NamedInternalEvent, counter,
3 internal_event::{
4 ComponentEventsDropped, CounterName, INTENTIONAL, InternalEvent, UNINTENTIONAL,
5 error_stage, error_type,
6 },
7};
8
9#[derive(NamedInternalEvent)]
10pub struct TemplateRenderingError<'a> {
11 pub field: Option<&'a str>,
12 pub drop_event: bool,
13 pub error: crate::template::TemplateRenderingError,
14}
15
16impl InternalEvent for TemplateRenderingError<'_> {
17 fn emit(self) {
18 let confined = matches!(
19 self.error,
20 crate::template::TemplateRenderingError::Confined { .. }
21 );
22
23 // Message wording tracks BOTH the error class (confinement vs render
24 // failure) AND whether the caller is dropping the event. A caller
25 // like the KeyPartitioner falling back to a dead-letter key emits
26 // `Confined` + `drop_event: false` — we still want to log the
27 // confinement violation, but claiming "dropping event" would be a
28 // lie because the event still routes to the dead-letter.
29 let mut msg = match (confined, self.drop_event) {
30 (true, true) => {
31 "Templated routing value was outside the configured confinement; dropping event"
32 .to_owned()
33 }
34 (true, false) => {
35 "Templated routing value was outside the configured confinement".to_owned()
36 }
37 (false, _) => "Failed to render template".to_owned(),
38 };
39 if let Some(field) = self.field {
40 use std::fmt::Write;
41 _ = write!(msg, " for \"{field}\"");
42 }
43 msg.push('.');
44
45 // A `Confined` error is always alert-worthy: an attacker attempted
46 // to steer routing via event data. Some callers legitimately don't
47 // drop the event (e.g. a partitioner falling back to a dead-letter
48 // key), but the confinement fire itself must still surface in logs
49 // and `component_errors_total` regardless of the caller's
50 // drop_event decision.
51 //
52 // `check-events` requires `error_type` counter tag values to be
53 // constants, so the two error-class branches are split
54 // explicitly. Confined renders always surface at `error!` +
55 // `component_errors_total` (they're security-relevant even when
56 // the caller doesn't drop the event); non-confined renders only
57 // surface on the drop path.
58 if confined {
59 error!(
60 message = %msg,
61 error = %self.error,
62 error_type = error_type::CONFINEMENT_FAILED,
63 stage = error_stage::PROCESSING,
64 );
65 counter!(
66 CounterName::ComponentErrorsTotal,
67 "error_type" => error_type::CONFINEMENT_FAILED,
68 "stage" => error_stage::PROCESSING,
69 )
70 .increment(1);
71 } else if self.drop_event {
72 error!(
73 message = %msg,
74 error = %self.error,
75 error_type = error_type::TEMPLATE_FAILED,
76 stage = error_stage::PROCESSING,
77 );
78 counter!(
79 CounterName::ComponentErrorsTotal,
80 "error_type" => error_type::TEMPLATE_FAILED,
81 "stage" => error_stage::PROCESSING,
82 )
83 .increment(1);
84 } else {
85 warn!(
86 message = %msg,
87 error = %self.error,
88 error_type = error_type::TEMPLATE_FAILED,
89 stage = error_stage::PROCESSING,
90 );
91 }
92
93 // Only emit `ComponentEventsDropped` when the caller actually
94 // dropped the event. `Confined` + `drop_event: false` (e.g. the
95 // dead-letter fallback) doesn't count as a drop — the event still
96 // lands somewhere, just at the operator-authored fallback key.
97 if self.drop_event {
98 if confined {
99 emit!(ComponentEventsDropped::<INTENTIONAL> {
100 count: 1,
101 reason: "Template rendered a value outside the confined base.",
102 });
103 } else {
104 emit!(ComponentEventsDropped::<UNINTENTIONAL> {
105 count: 1,
106 reason: "Failed to render template.",
107 });
108 }
109 }
110 }
111}