@@ -15,35 +15,86 @@ pub struct TemplateRenderingError<'a> {
1515
1616impl InternalEvent for TemplateRenderingError < ' _ > {
1717 fn emit ( self ) {
18- let mut msg = "Failed to render template" . to_owned ( ) ;
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+ } ;
1939 if let Some ( field) = self . field {
2040 use std:: fmt:: Write ;
2141 _ = write ! ( msg, " for \" {field}\" " ) ;
2242 }
2343 msg. push ( '.' ) ;
2444
25- let confined = matches ! (
26- self . error,
27- crate :: template:: TemplateRenderingError :: Confined { .. }
28- ) ;
29-
30- if self . drop_event {
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 {
3172 error ! (
3273 message = %msg,
3374 error = %self . error,
3475 error_type = error_type:: TEMPLATE_FAILED ,
3576 stage = error_stage:: PROCESSING ,
3677 ) ;
37-
3878 counter ! (
3979 CounterName :: ComponentErrorsTotal ,
4080 "error_type" => error_type:: TEMPLATE_FAILED ,
4181 "stage" => error_stage:: PROCESSING ,
4282 )
4383 . 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+ }
4492
45- // Confinement violations are intentional security discards.
46- // All other template render failures are unintentional drops.
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 {
4798 if confined {
4899 emit ! ( ComponentEventsDropped :: <INTENTIONAL > {
49100 count: 1 ,
@@ -55,13 +106,6 @@ impl InternalEvent for TemplateRenderingError<'_> {
55106 reason: "Failed to render template." ,
56107 } ) ;
57108 }
58- } else {
59- warn ! (
60- message = %msg,
61- error = %self . error,
62- error_type = error_type:: TEMPLATE_FAILED ,
63- stage = error_stage:: PROCESSING ,
64- ) ;
65109 }
66110 }
67111}
0 commit comments