-
Notifications
You must be signed in to change notification settings - Fork 10
enhancement(context): allow mutating tagsets on metric contexts #1270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -115,6 +115,39 @@ impl Context { | |
| &self.inner.origin_tags | ||
| } | ||
|
|
||
| /// Mutates the instrumented tags of this context via a closure. | ||
| /// | ||
| /// Uses copy-on-write semantics: if this context shares its inner data with other clones, the | ||
| /// inner data is cloned first so that mutations do not affect other holders. If this context is | ||
| /// the sole owner, the mutation happens in place. | ||
| /// | ||
| /// The context key is automatically recomputed after the closure returns. | ||
| pub fn mutate_tags(&mut self, f: impl FnOnce(&mut TagSet)) { | ||
| self.mutate_inner(|inner| f(&mut inner.tags)); | ||
| } | ||
|
|
||
| /// Mutates the origin tags of this context via a closure. | ||
| /// | ||
| /// Uses copy-on-write semantics: if this context shares its inner data with other clones, the | ||
| /// inner data is cloned first so that mutations do not affect other holders. If this context is | ||
| /// the sole owner, the mutation happens in place. | ||
| /// | ||
| /// The context key is automatically recomputed after the closure returns. | ||
| pub fn mutate_origin_tags(&mut self, f: impl FnOnce(&mut TagSet)) { | ||
| self.mutate_inner(|inner| f(&mut inner.origin_tags)); | ||
| } | ||
|
Comment on lines
+118
to
+138
|
||
|
|
||
| /// Runs the given closure on the inner context data, recomputing the context key afterwards. | ||
| /// | ||
| /// When the inner context state is shared (we aren't the only ones with a strong reference), we clone the inner | ||
| /// data first to have our own copy. Otherwise, we modify the inner data in place. | ||
| fn mutate_inner(&mut self, f: impl FnOnce(&mut ContextInner)) { | ||
|
tobz marked this conversation as resolved.
|
||
| let inner = Arc::make_mut(&mut self.inner); | ||
| f(inner); | ||
| let (key, _) = hash_context(&inner.name, &inner.tags, &inner.origin_tags); | ||
| inner.key = key; | ||
| } | ||
|
Comment on lines
+144
to
+149
|
||
|
|
||
| /// Returns the size of this context in bytes. | ||
| /// | ||
| /// A context's size is the sum of the sizes of its fields and the size of the `Context` struct itself, and | ||
|
|
@@ -325,4 +358,79 @@ mod tests { | |
| BASE_CONTEXT_SIZE + SIZE_OF_CONTEXT_NAME.len() + tags.size_of() + origin_tags.size_of() | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn with_tags_mut_clones_shared_context() { | ||
| let original = Context::from_static_parts("metric", &["env:prod"]); | ||
| let mut mutated = original.clone(); | ||
|
|
||
| // They share the same Arc before mutation. | ||
| assert!(original.ptr_eq(&mutated)); | ||
|
|
||
| mutated.mutate_tags(|tags| { | ||
| tags.insert_tag(Tag::from("service:web")); | ||
| }); | ||
|
|
||
| // After mutation, they no longer share the same inner. | ||
| assert!(!original.ptr_eq(&mutated)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn with_tags_mut_does_not_affect_original() { | ||
| let original = Context::from_static_parts("metric", &["env:prod"]); | ||
| let mut mutated = original.clone(); | ||
|
|
||
| mutated.mutate_tags(|tags| { | ||
| tags.insert_tag(Tag::from("service:web")); | ||
| }); | ||
|
|
||
| // Original is unchanged. | ||
| assert_eq!(original.tags().len(), 1); | ||
| assert!(original.tags().has_tag("env:prod")); | ||
| assert!(!original.tags().has_tag("service:web")); | ||
|
|
||
| // Mutated has both tags. | ||
| assert_eq!(mutated.tags().len(), 2); | ||
| assert!(mutated.tags().has_tag("env:prod")); | ||
| assert!(mutated.tags().has_tag("service:web")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn with_tags_mut_rehashes() { | ||
| // Build a context and mutate it to add a tag. | ||
| let mut mutated = Context::from_static_parts("metric", &["env:prod"]); | ||
| mutated.mutate_tags(|tags| { | ||
| tags.insert_tag(Tag::from("service:web")); | ||
| }); | ||
|
|
||
| // Build an equivalent context from scratch with both tags. | ||
| let expected = Context::from_static_parts("metric", &["env:prod", "service:web"]); | ||
|
|
||
| // The recomputed key should match a freshly-constructed context with the same state. | ||
| assert_eq!(mutated, expected); | ||
|
tobz marked this conversation as resolved.
|
||
|
|
||
| // Modify a tag on the mutated context that _isn't_ shared with `expected` to ensure that there's no asymmetric | ||
| // equality logic. | ||
| mutated.mutate_tags(|tags| { | ||
| tags.insert_tag(Tag::from("cluster:foo")); | ||
| }); | ||
| assert_ne!(mutated, expected); | ||
| } | ||
|
|
||
| #[test] | ||
| fn with_origin_tags_mut_clones_shared_context() { | ||
| let original = Context::from_static_name("metric"); | ||
| let mut mutated = original.clone(); | ||
|
|
||
| assert!(original.ptr_eq(&mutated)); | ||
|
|
||
| mutated.mutate_origin_tags(|tags| { | ||
| tags.insert_tag(Tag::from("origin:tag")); | ||
| }); | ||
|
|
||
| assert!(!original.ptr_eq(&mutated)); | ||
| assert!(original.origin_tags().is_empty()); | ||
| assert_eq!(mutated.origin_tags().len(), 1); | ||
| assert!(mutated.origin_tags().has_tag("origin:tag")); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -537,6 +537,10 @@ impl<'a> Iterator for BaseIndexIter<'a> { | |
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use std::collections::{BTreeSet, HashSet}; | ||
|
|
||
| use proptest::{collection::vec as arb_vec, prelude::*, prop_oneof}; | ||
|
|
||
| use super::*; | ||
|
|
||
| /// Helper: create a SharedTagSet from static tag strings. | ||
|
|
@@ -878,15 +882,6 @@ mod tests { | |
| ts.insert_tag(Tag::from("c:3")); | ||
| assert_eq!(ts.len(), 3); | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod proptests { | ||
| use std::collections::BTreeSet; | ||
|
|
||
| use proptest::prelude::*; | ||
|
|
||
| use super::*; | ||
|
|
||
| /// Operations we can apply to a TagSet. | ||
| #[derive(Clone, Debug)] | ||
|
|
@@ -907,23 +902,23 @@ mod proptests { | |
|
|
||
| /// Strategy for generating a random operation. | ||
| fn arb_op() -> impl Strategy<Value = Op> { | ||
| prop_oneof![arb_tag().prop_map(Op::Insert), arb_key().prop_map(Op::RemoveByName),] | ||
| prop_oneof![arb_tag().prop_map(Op::Insert), arb_key().prop_map(Op::RemoveByName)] | ||
| } | ||
|
|
||
| /// Strategy for generating a group of tags (for one FrozenTagSet in the chain). | ||
| fn arb_tag_group() -> impl Strategy<Value = Vec<String>> { | ||
| prop::collection::vec(arb_tag(), 0..10) | ||
| arb_vec(arb_tag(), 0..10) | ||
| } | ||
|
|
||
| /// Strategy for generating a base with 1-3 chained tag groups. | ||
| fn arb_base_groups() -> impl Strategy<Value = Vec<Vec<String>>> { | ||
| prop::collection::vec(arb_tag_group(), 1..4) | ||
| arb_vec(arb_tag_group(), 1..4) | ||
| } | ||
|
|
||
| /// Build a SharedTagSet from multiple groups (each becomes a chained FrozenTagSet). | ||
| /// Deduplicates exact tags across groups to avoid cross-chain duplicates. | ||
| fn build_chained_base(groups: &[Vec<String>]) -> SharedTagSet { | ||
| let mut seen_tags = std::collections::HashSet::new(); | ||
| let mut seen_tags = HashSet::new(); | ||
|
|
||
| let mut shared = { | ||
| let ts: TagSet = groups[0] | ||
|
|
@@ -979,9 +974,9 @@ mod proptests { | |
|
|
||
| #[test] | ||
| #[cfg_attr(miri, ignore)] | ||
| fn overlay_matches_reference( | ||
| fn property_test_overlay_matches_reference( | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Renamed this one so it would be properly filtered when running unit tests vs when specifically trying to run property tests. |
||
| base_groups in arb_base_groups(), | ||
| ops in prop::collection::vec(arb_op(), 0..20), | ||
| ops in arb_vec(arb_op(), 0..20), | ||
| ) { | ||
| let base = build_chained_base(&base_groups); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This transform used to rebuild contexts via a
ContextResolver, which provides interning + caching of resolved contexts. With the new in-placewith_tags_mutapproach, any metric contexts that are shared (common withContextResolver-produced contexts) will be detached viaArc::make_mut, potentially producing one enrichedContextInnerper metric and losing cross-metric sharing. If host-tag enrichment is expected to touch many metrics, consider retaining a resolver/cache for enriched contexts or another strategy that preserves sharing.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Host Tags transform generally only operates for a few minutes after startup, so the reduction in sharing due to creating an off-shoot
Context, and not caching that via something likeContextResolver, is pretty small and not sustained over time.However, it could be useful to add something in
ContextResolverwhere we support modifying how the input pieces are used to build the resulting context. In order to actually make use ofContextResolverin this scenario, we'd want to feed it to the incoming metric context untouched, and then generate a new context (which is the one we'd cache) with the additional host tags bolted on... such that we didn't have to do the chaining/additional host tags for subsequent lookups.We could do a cache, too, but
ContextResolveralready wraps up most of the things we want here -- cache config, expiration, telemetry, etc -- in a nice package that's specific to contexts and so it'd be nicer to somehow add it there than to just use a cache naïvely.