Skip to content

Commit 261b894

Browse files
committed
Implement per tag control in tag cardinality processor
1 parent 9a136f2 commit 261b894

5 files changed

Lines changed: 648 additions & 51 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
The `tag_cardinality_limit` transform gained two new configuration capabilities:
2+
3+
- **Per-tag overrides**: each entry in `per_metric_limits` now supports a `per_tag_limits` map
4+
whose entries can override `value_limit` and `mode` for a specific tag key. When a per-tag
5+
entry omits `value_limit`, it inherits the enclosing per-metric (or global) `value_limit`
6+
rather than falling back to the default. `limit_exceeded_action` and `internal_metrics`
7+
are always inherited from the enclosing per-metric (or global) configuration and are not
8+
per-tag-overridable. Resolution order is per-tag → per-metric → global.
9+
- **Exclusion**: `mode: excluded` is now available as a third mode option in `per_metric_limits`
10+
and `per_tag_limits` entries (not at the global level). When set, the metric or tag is opted
11+
out of cardinality control — all tag values pass through and nothing is tracked. Other
12+
tracking fields on the entry (`value_limit`) are ignored when `mode: excluded` is selected.
13+
Per-metric exclusion is blanket: when a metric's `mode` is `excluded`, every tag on that
14+
metric is excluded and any `per_tag_limits` overrides on it are ignored.
15+
16+
authors: ArunPiduguDD

src/transforms/tag_cardinality_limit/config.rs

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ pub enum TrackingScope {
6666
PerMetric,
6767
}
6868

69-
/// Configuration for the `tag_cardinality_limit` transform for a specific group of metrics.
69+
/// Configuration block used at the global level.
7070
#[configurable_component]
7171
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7272
pub struct Inner {
@@ -86,7 +86,45 @@ pub struct Inner {
8686
pub internal_metrics: InternalMetricsConfig,
8787
}
8888

89-
/// Controls the approach taken for tracking tag cardinality.
89+
/// Configuration block used at per-metric level. Same shape as the global configuration but
90+
/// with `OverrideMode`, which adds `excluded` for opting that metric out of cardinality
91+
/// control entirely.
92+
#[configurable_component]
93+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94+
pub struct OverrideInner {
95+
/// How many distinct values to accept for any given key. Ignored when `mode: excluded`.
96+
#[serde(default = "default_value_limit")]
97+
pub value_limit: usize,
98+
99+
#[configurable(derived)]
100+
#[serde(default = "default_limit_exceeded_action")]
101+
pub limit_exceeded_action: LimitExceededAction,
102+
103+
#[serde(flatten)]
104+
pub mode: OverrideMode,
105+
106+
#[configurable(derived)]
107+
#[serde(default)]
108+
pub internal_metrics: InternalMetricsConfig,
109+
}
110+
111+
/// Configuration block used at the per-tag level. Same as `OverrideInner` minus
112+
/// `limit_exceeded_action` (inherited from the enclosing per-metric config) and
113+
/// `internal_metrics` (inherited from the enclosing per-metric or global config).
114+
#[configurable_component]
115+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
116+
pub struct PerTagInner {
117+
/// How many distinct values to accept for this tag key. If unset, inherits
118+
/// the `value_limit` from the enclosing per-metric (or global) configuration.
119+
/// Ignored when `mode: excluded`.
120+
#[serde(default)]
121+
pub value_limit: Option<usize>,
122+
123+
#[serde(flatten)]
124+
pub mode: OverrideMode,
125+
}
126+
127+
/// Controls the approach taken for tracking tag cardinality at the global level.
90128
#[configurable_component]
91129
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92130
#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
@@ -109,6 +147,41 @@ pub enum Mode {
109147
Probabilistic(BloomFilterConfig),
110148
}
111149

150+
/// Controls the approach taken for tracking tag cardinality at the per-metric or per-tag level.
151+
/// Adds `excluded` to the global `Mode` variants.
152+
#[configurable_component]
153+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
154+
#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
155+
#[configurable(metadata(
156+
docs::enum_tag_description = "Controls the approach taken for tracking tag cardinality."
157+
))]
158+
pub enum OverrideMode {
159+
/// Tracks cardinality exactly. See `Mode::Exact` for details.
160+
Exact,
161+
162+
/// Tracks cardinality probabilistically. See `Mode::Probabilistic` for details.
163+
Probabilistic(BloomFilterConfig),
164+
165+
/// Skip cardinality tracking for this scope. All tag values pass through and nothing is
166+
/// recorded. Other tracking fields on the entry (`value_limit`, `limit_exceeded_action`,
167+
/// `internal_metrics`) are ignored when this is selected.
168+
///
169+
/// Only valid in `per_metric_limits` and `per_tag_limits` entries; using it as the global
170+
/// `mode` is a configuration error.
171+
Excluded,
172+
}
173+
174+
impl OverrideMode {
175+
/// Returns the equivalent global `Mode` if this scope is tracked, or `None` if excluded.
176+
pub const fn as_mode(&self) -> Option<Mode> {
177+
match self {
178+
OverrideMode::Exact => Some(Mode::Exact),
179+
OverrideMode::Probabilistic(b) => Some(Mode::Probabilistic(*b)),
180+
OverrideMode::Excluded => None,
181+
}
182+
}
183+
}
184+
112185
/// Bloom filter configuration in probabilistic mode.
113186
#[configurable_component]
114187
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -143,8 +216,29 @@ pub struct PerMetricConfig {
143216
#[serde(default)]
144217
pub namespace: Option<String>,
145218

219+
/// Per-tag-key overrides scoped to this metric.
220+
///
221+
/// Each entry may override `value_limit` and `mode` for a specific tag key.
222+
/// `limit_exceeded_action` and `internal_metrics` are always inherited from the enclosing
223+
/// per-metric (or global) configuration and cannot be set per-tag.
224+
/// Tags not listed here use the per-metric configuration.
225+
#[configurable(
226+
derived,
227+
metadata(docs::additional_props_description = "An individual tag configuration.")
228+
)]
229+
#[serde(default)]
230+
pub per_tag_limits: HashMap<String, PerTagConfig>,
231+
232+
#[serde(flatten)]
233+
pub config: OverrideInner,
234+
}
235+
236+
/// Tag cardinality limit configuration for a specific tag key, scoped under a per-metric override.
237+
#[configurable_component]
238+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
239+
pub struct PerTagConfig {
146240
#[serde(flatten)]
147-
pub config: Inner,
241+
pub config: PerTagInner,
148242
}
149243

150244
const fn default_limit_exceeded_action() -> LimitExceededAction {

src/transforms/tag_cardinality_limit/mod.rs

Lines changed: 109 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,23 @@ mod tag_value_set;
1616
mod tests;
1717

1818
pub use config::{
19-
BloomFilterConfig, Config, Inner, LimitExceededAction, Mode, PerMetricConfig, TrackingScope,
19+
BloomFilterConfig, Config, Inner, LimitExceededAction, Mode, OverrideInner, OverrideMode,
20+
PerMetricConfig, PerTagConfig, PerTagInner, TrackingScope,
2021
};
2122
use tag_value_set::AcceptedTagValueSet;
2223

2324
use crate::event::metric::TagValueSet;
2425

2526
type MetricId = (Option<String>, String);
2627

28+
/// Tag tracking settings for a single (metric, tag) pair.
29+
enum TagSettings {
30+
/// The tag is excluded from cardinality control; pass values through unchanged.
31+
Excluded,
32+
/// The tag is tracked using these settings.
33+
Tracked(Inner),
34+
}
35+
2736
#[derive(Debug)]
2837
pub struct TagCardinalityLimit {
2938
config: Config,
@@ -38,19 +47,73 @@ impl TagCardinalityLimit {
3847
}
3948
}
4049

41-
fn get_config_for_metric(&self, metric_key: Option<&MetricId>) -> &Inner {
42-
match metric_key {
43-
Some(id) => self
44-
.config
45-
.per_metric_limits
46-
.iter()
47-
.find(|(name, config)| {
48-
**name == id.1 && (config.namespace.is_none() || config.namespace == id.0)
50+
/// Resolve the configuration that applies to a specific (metric, tag) pair.
51+
///
52+
/// Lookup chain (per field):
53+
/// - `value_limit`, `mode`, `internal_metrics`: per-tag override → per-metric override → global.
54+
/// - `limit_exceeded_action`: per-metric override → global. Per-tag entries always inherit
55+
/// the action from the enclosing per-metric (or global) config.
56+
///
57+
/// Per-metric exclusion is blanket: if the matching per-metric entry has `mode: excluded`,
58+
/// every tag on the metric is excluded and `per_tag_limits` is ignored.
59+
fn get_config_for_metric_tag(
60+
&self,
61+
metric_key: Option<&MetricId>,
62+
tag_key: &str,
63+
) -> TagSettings {
64+
// No matching per-metric override → use the global config as-is.
65+
let Some((metric_namespace, metric_name)) = metric_key else {
66+
return TagSettings::Tracked(self.config.global);
67+
};
68+
let Some((_, per_metric)) = self.config.per_metric_limits.iter().find(|(name, cfg)| {
69+
*name == metric_name && (cfg.namespace.is_none() || cfg.namespace == *metric_namespace)
70+
}) else {
71+
return TagSettings::Tracked(self.config.global);
72+
};
73+
74+
// Per-metric exclusion is blanket — per-tag overrides do not apply.
75+
let Some(metric_mode) = per_metric.config.mode.as_mode() else {
76+
return TagSettings::Excluded;
77+
};
78+
let limit_exceeded_action = per_metric.config.limit_exceeded_action;
79+
80+
// Per-tag override may further exclude a specific tag, replace `mode`,
81+
// or replace `value_limit` (unset `value_limit` inherits from the enclosing
82+
// per-metric config).
83+
if let Some(per_tag) = per_metric.per_tag_limits.get(tag_key) {
84+
let Some(mode) = per_tag.config.mode.as_mode() else {
85+
return TagSettings::Excluded;
86+
};
87+
return TagSettings::Tracked(Inner {
88+
value_limit: per_tag
89+
.config
90+
.value_limit
91+
.unwrap_or(per_metric.config.value_limit),
92+
limit_exceeded_action,
93+
mode,
94+
internal_metrics: per_metric.config.internal_metrics,
95+
});
96+
}
97+
TagSettings::Tracked(Inner {
98+
value_limit: per_metric.config.value_limit,
99+
limit_exceeded_action,
100+
mode: metric_mode,
101+
internal_metrics: per_metric.config.internal_metrics,
102+
})
103+
}
104+
105+
/// Returns the `limit_exceeded_action` that applies to this metric. Decided once per event:
106+
/// per-metric override if any, else global.
107+
fn metric_action(&self, metric_key: Option<&MetricId>) -> LimitExceededAction {
108+
if let Some(id) = metric_key
109+
&& let Some((_, pmc)) =
110+
self.config.per_metric_limits.iter().find(|(name, c)| {
111+
**name == id.1 && (c.namespace.is_none() || c.namespace == id.0)
49112
})
50-
.map(|(_, c)| &c.config)
51-
.unwrap_or(&self.config.global),
52-
None => &self.config.global,
113+
{
114+
return pmc.config.limit_exceeded_action;
53115
}
116+
self.config.global.limit_exceeded_action
54117
}
55118

56119
/// Takes in key and a value corresponding to a tag on an incoming Metric
@@ -67,7 +130,10 @@ impl TagCardinalityLimit {
67130
key: &str,
68131
value: &TagValueSet,
69132
) -> bool {
70-
let config = *self.get_config_for_metric(metric_key);
133+
let config = match self.get_config_for_metric_tag(metric_key, key) {
134+
TagSettings::Excluded => return true,
135+
TagSettings::Tracked(inner) => inner,
136+
};
71137
let metric_accepted_tags = self.accepted_tags.entry(metric_key.cloned()).or_default();
72138
let tag_value_set = metric_accepted_tags
73139
.entry_ref(key)
@@ -102,20 +168,28 @@ impl TagCardinalityLimit {
102168
key: &str,
103169
value: &TagValueSet,
104170
) -> bool {
171+
let resolved = match self.get_config_for_metric_tag(metric_key, key) {
172+
TagSettings::Excluded => return false,
173+
TagSettings::Tracked(inner) => inner,
174+
};
105175
self.accepted_tags
106176
.get(&metric_key.cloned())
107177
.and_then(|metric_accepted_tags| {
108178
metric_accepted_tags.get(key).map(|value_set| {
109-
!value_set.contains(value)
110-
&& value_set.len() >= self.get_config_for_metric(metric_key).value_limit
179+
!value_set.contains(value) && value_set.len() >= resolved.value_limit
111180
})
112181
})
113182
.unwrap_or(false)
114183
}
115184

116-
/// Record a key and value corresponding to a tag on an incoming Metric.
185+
/// Record an accepted tag value (mutation-only, no limit check). Used by the `DropEvent`
186+
/// path's record pass after a mutation-free pre-check has confirmed every tag has room.
187+
/// Excluded tags are skipped — no storage allocated.
117188
fn record_tag_value(&mut self, metric_key: Option<&MetricId>, key: &str, value: &TagValueSet) {
118-
let config = *self.get_config_for_metric(metric_key);
189+
let config = match self.get_config_for_metric_tag(metric_key, key) {
190+
TagSettings::Excluded => return,
191+
TagSettings::Tracked(inner) => inner,
192+
};
119193
let metric_accepted_tags = self.accepted_tags.entry(metric_key.cloned()).or_default();
120194
metric_accepted_tags
121195
.entry_ref(key)
@@ -143,24 +217,24 @@ impl TagCardinalityLimit {
143217
}
144218
};
145219
if let Some(tags_map) = metric.tags_mut() {
146-
match self
147-
.get_config_for_metric(metric_key.as_ref())
148-
.limit_exceeded_action
149-
{
220+
match self.metric_action(metric_key.as_ref()) {
150221
LimitExceededAction::DropEvent => {
151-
// This needs to check all the tags, to ensure that the ordering of tag names
152-
// doesn't change the behavior of the check.
153-
222+
// This needs to check all the tags, to ensure that the ordering of tag
223+
// names doesn't change the behavior of the check.
154224
for (key, value) in tags_map.iter_sets() {
225+
let TagSettings::Tracked(resolved) =
226+
self.get_config_for_metric_tag(metric_key.as_ref(), key)
227+
else {
228+
continue; // excluded tags can never trigger DropEvent
229+
};
155230
if self.tag_limit_exceeded(metric_key.as_ref(), key, value) {
156-
let config = self.get_config_for_metric(metric_key.as_ref());
231+
let include_extended_tags =
232+
resolved.internal_metrics.include_extended_tags;
157233
emit!(TagCardinalityLimitRejectingEvent {
158234
metric_name: &metric_name,
159235
tag_key: key,
160236
tag_value: &value.to_string(),
161-
include_extended_tags: config
162-
.internal_metrics
163-
.include_extended_tags,
237+
include_extended_tags,
164238
});
165239
return None;
166240
}
@@ -170,12 +244,17 @@ impl TagCardinalityLimit {
170244
}
171245
}
172246
LimitExceededAction::DropTag => {
173-
let config = self.get_config_for_metric(metric_key.as_ref());
174-
let include_extended_tags = config.internal_metrics.include_extended_tags;
175247
tags_map.retain(|key, value| {
176248
if self.try_accept_tag(metric_key.as_ref(), key, value) {
177249
true
178250
} else {
251+
let include_extended_tags =
252+
match self.get_config_for_metric_tag(metric_key.as_ref(), key) {
253+
TagSettings::Tracked(inner) => {
254+
inner.internal_metrics.include_extended_tags
255+
}
256+
TagSettings::Excluded => false, // unreachable: excluded tags accept
257+
};
179258
emit!(TagCardinalityLimitRejectingTag {
180259
metric_name: &metric_name,
181260
tag_key: key,

0 commit comments

Comments
 (0)