Skip to main content

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}