@@ -15,7 +15,9 @@ mod tag_value_set;
1515#[ cfg( test) ]
1616mod tests;
1717
18- pub use config:: { BloomFilterConfig , Config , Inner , LimitExceededAction , Mode , PerMetricConfig } ;
18+ pub use config:: {
19+ BloomFilterConfig , Config , Inner , LimitExceededAction , Mode , PerMetricConfig , PerTagConfig ,
20+ } ;
1921use tag_value_set:: AcceptedTagValueSet ;
2022
2123use crate :: event:: metric:: TagValueSet ;
@@ -36,19 +38,22 @@ impl TagCardinalityLimit {
3638 }
3739 }
3840
39- fn get_config_for_metric ( & self , metric_key : Option < & MetricId > ) -> & Inner {
40- match metric_key {
41- Some ( id ) => self
42- . config
43- . per_metric_limits
44- . iter ( )
45- . find ( |( name, config ) | {
46- * * name == id. 1 && ( config . namespace . is_none ( ) || config . namespace == id. 0 )
41+ /// Resolve the configuration that applies to a specific (metric, tag) pair.
42+ ///
43+ /// Lookup chain: per-tag override → per-metric override → global.
44+ fn resolve_tag ( & self , metric_key : Option < & MetricId > , tag_key : & str ) -> & Inner {
45+ if let Some ( id ) = metric_key
46+ && let Some ( ( _ , pmc ) ) =
47+ self . config . per_metric_limits . iter ( ) . find ( |( name, c ) | {
48+ * * name == id. 1 && ( c . namespace . is_none ( ) || c . namespace == id. 0 )
4749 } )
48- . map ( |( _, c) | & c. config )
49- . unwrap_or ( & self . config . global ) ,
50- None => & self . config . global ,
50+ {
51+ if let Some ( t) = pmc. per_tag_limits . get ( tag_key) {
52+ return & t. config ;
53+ }
54+ return & pmc. config ;
5155 }
56+ & self . config . global
5257 }
5358
5459 /// Takes in key and a value corresponding to a tag on an incoming Metric
@@ -65,11 +70,15 @@ impl TagCardinalityLimit {
6570 key : & str ,
6671 value : & TagValueSet ,
6772 ) -> bool {
68- let config = * self . get_config_for_metric ( metric_key) ;
73+ let config = * self . resolve_tag ( metric_key, key) ;
74+ if config. exclude {
75+ return true ;
76+ }
77+ let mode = config. mode . expect ( "validated at build time" ) ;
6978 let metric_accepted_tags = self . accepted_tags . entry ( metric_key. cloned ( ) ) . or_default ( ) ;
7079 let tag_value_set = metric_accepted_tags
7180 . entry_ref ( key)
72- . or_insert_with ( || AcceptedTagValueSet :: new ( config. value_limit , & config . mode ) ) ;
81+ . or_insert_with ( || AcceptedTagValueSet :: new ( config. value_limit , & mode) ) ;
7382
7483 if tag_value_set. contains ( value) {
7584 // Tag value has already been accepted, nothing more to do.
@@ -100,27 +109,20 @@ impl TagCardinalityLimit {
100109 key : & str ,
101110 value : & TagValueSet ,
102111 ) -> bool {
112+ let resolved = self . resolve_tag ( metric_key, key) ;
113+ if resolved. exclude {
114+ return false ;
115+ }
103116 self . accepted_tags
104117 . get ( & metric_key. cloned ( ) )
105118 . and_then ( |metric_accepted_tags| {
106119 metric_accepted_tags. get ( key) . map ( |value_set| {
107- !value_set. contains ( value)
108- && value_set. len ( ) >= self . get_config_for_metric ( metric_key) . value_limit
120+ !value_set. contains ( value) && value_set. len ( ) >= resolved. value_limit
109121 } )
110122 } )
111123 . unwrap_or ( false )
112124 }
113125
114- /// Record a key and value corresponding to a tag on an incoming Metric.
115- fn record_tag_value ( & mut self , metric_key : Option < & MetricId > , key : & str , value : & TagValueSet ) {
116- let config = * self . get_config_for_metric ( metric_key) ;
117- let metric_accepted_tags = self . accepted_tags . entry ( metric_key. cloned ( ) ) . or_default ( ) ;
118- metric_accepted_tags
119- . entry_ref ( key)
120- . or_insert_with ( || AcceptedTagValueSet :: new ( config. value_limit , & config. mode ) )
121- . insert ( value. clone ( ) ) ;
122- }
123-
124126 pub fn transform_one ( & mut self , mut event : Event ) -> Option < Event > {
125127 let metric = event. as_mut_metric ( ) ;
126128 let metric_name = metric. name ( ) . to_string ( ) ;
@@ -135,50 +137,44 @@ impl TagCardinalityLimit {
135137 None
136138 } ;
137139 if let Some ( tags_map) = metric. tags_mut ( ) {
138- match self
139- . get_config_for_metric ( metric_key. as_ref ( ) )
140- . limit_exceeded_action
141- {
142- LimitExceededAction :: DropEvent => {
143- // This needs to check all the tags, to ensure that the ordering of tag names
144- // doesn't change the behavior of the check.
145-
146- for ( key, value) in tags_map. iter_sets ( ) {
147- if self . tag_limit_exceeded ( metric_key. as_ref ( ) , key, value) {
148- let config = self . get_config_for_metric ( metric_key. as_ref ( ) ) ;
149- emit ! ( TagCardinalityLimitRejectingEvent {
150- metric_name: & metric_name,
151- tag_key: key,
152- tag_value: & value. to_string( ) ,
153- include_extended_tags: config
154- . internal_metrics
155- . include_extended_tags,
156- } ) ;
157- return None ;
158- }
159- }
160- for ( key, value) in tags_map. iter_sets ( ) {
161- self . record_tag_value ( metric_key. as_ref ( ) , key, value) ;
162- }
163- }
164- LimitExceededAction :: DropTag => {
165- let config = self . get_config_for_metric ( metric_key. as_ref ( ) ) ;
166- let include_extended_tags = config. internal_metrics . include_extended_tags ;
167- tags_map. retain ( |key, value| {
168- if self . try_accept_tag ( metric_key. as_ref ( ) , key, value) {
169- true
170- } else {
171- emit ! ( TagCardinalityLimitRejectingTag {
172- metric_name: & metric_name,
173- tag_key: key,
174- tag_value: & value. to_string( ) ,
175- include_extended_tags,
176- } ) ;
177- false
178- }
140+ // Pre-check pass: tags whose resolved action is `DropEvent` must be checked before
141+ // any mutation, so the drop decision is order-independent across tag iteration.
142+ for ( key, value) in tags_map. iter_sets ( ) {
143+ let resolved = self . resolve_tag ( metric_key. as_ref ( ) , key) ;
144+ if resolved. limit_exceeded_action == LimitExceededAction :: DropEvent
145+ && self . tag_limit_exceeded ( metric_key. as_ref ( ) , key, value)
146+ {
147+ let include_extended_tags = resolved. internal_metrics . include_extended_tags ;
148+ emit ! ( TagCardinalityLimitRejectingEvent {
149+ metric_name: & metric_name,
150+ tag_key: key,
151+ tag_value: & value. to_string( ) ,
152+ include_extended_tags,
179153 } ) ;
154+ return None ;
180155 }
181156 }
157+
158+ // Apply pass: accept each tag. Tags whose resolved action is `DropEvent` are
159+ // guaranteed to pass after the pre-check; a rejection here means the resolved
160+ // action is `DropTag`.
161+ tags_map. retain ( |key, value| {
162+ if self . try_accept_tag ( metric_key. as_ref ( ) , key, value) {
163+ true
164+ } else {
165+ let include_extended_tags = self
166+ . resolve_tag ( metric_key. as_ref ( ) , key)
167+ . internal_metrics
168+ . include_extended_tags ;
169+ emit ! ( TagCardinalityLimitRejectingTag {
170+ metric_name: & metric_name,
171+ tag_key: key,
172+ tag_value: & value. to_string( ) ,
173+ include_extended_tags,
174+ } ) ;
175+ false
176+ }
177+ } ) ;
182178 }
183179 Some ( event)
184180 }
0 commit comments