saluki_core/data_model/event/metric/context/
mod.rs

1//! Metric context and context resolving.
2
3use std::{
4    fmt,
5    hash::{Hash, Hasher},
6    sync::Arc,
7};
8
9use metrics::Gauge;
10use saluki_common::collections::{ContiguousBitSet, PrehashedHashSet};
11use stringtheory::MetaString;
12
13mod hash;
14use self::hash::{hash_context, hash_context_with_host_and_seen};
15pub use self::hash::{hash_context_with_host, ContextKey};
16
17mod resolver;
18pub use self::resolver::{ContextResolver, ContextResolverBuilder, TagsResolver, TagsResolverBuilder};
19use crate::data_model::tags::{Tag, TagSet};
20
21const BASE_CONTEXT_SIZE: usize = std::mem::size_of::<Context>() + std::mem::size_of::<ContextInner>();
22
23/// A metric context.
24#[derive(Clone, Debug, Eq, Hash, PartialEq)]
25pub struct Context {
26    inner: Arc<ContextInner>,
27}
28
29impl Context {
30    /// Creates a new `Context` from the given static name.
31    pub fn from_static_name(name: &'static str) -> Self {
32        let tags = TagSet::default();
33        let origin_tags = TagSet::default();
34
35        let (key, _) = hash_context(name, &tags, &origin_tags);
36        Self {
37            inner: Arc::new(ContextInner {
38                name: MetaString::from_static(name),
39                host: None,
40                tags,
41                origin_tags,
42                key,
43                active_count: Gauge::noop(),
44            }),
45        }
46    }
47
48    /// Creates a new `Context` from the given static name and given static tags.
49    pub fn from_static_parts(name: &'static str, tags: &[&'static str]) -> Self {
50        let mut tag_set = TagSet::with_capacity(tags.len());
51        for tag in tags {
52            tag_set.insert_tag(MetaString::from_static(tag));
53        }
54
55        let origin_tags = TagSet::default();
56
57        let (key, _) = hash_context(name, &tag_set, &origin_tags);
58        Self {
59            inner: Arc::new(ContextInner {
60                name: MetaString::from_static(name),
61                host: None,
62                tags: tag_set,
63                origin_tags,
64                key,
65                active_count: Gauge::noop(),
66            }),
67        }
68    }
69
70    /// Creates a new `Context` from the given name and given tags.
71    pub fn from_parts<S: Into<MetaString>>(name: S, tags: impl Into<TagSet>) -> Self {
72        let name = name.into();
73        let tags = tags.into();
74        let origin_tags = TagSet::default();
75        let (key, _) = hash_context(&name, &tags, &origin_tags);
76        Self {
77            inner: Arc::new(ContextInner {
78                name,
79                host: None,
80                tags,
81                origin_tags,
82                key,
83                active_count: Gauge::noop(),
84            }),
85        }
86    }
87
88    /// Clones this context, and uses the given name for the cloned context.
89    pub fn with_name<S: Into<MetaString>>(&self, name: S) -> Self {
90        // Regenerate the context key to account for the new name.
91        let name = name.into();
92        let host = self.inner.host.clone();
93        let tags = self.inner.tags.clone();
94        let origin_tags = self.inner.origin_tags.clone();
95        let key = ContextInner::calculate_key(&name, host.as_deref(), &tags, &origin_tags);
96
97        Self {
98            inner: Arc::new(ContextInner {
99                name,
100                host,
101                tags,
102                origin_tags,
103                key,
104                active_count: Gauge::noop(),
105            }),
106        }
107    }
108
109    /// Clones this context, and uses the given tags for the cloned context.
110    ///
111    /// The name and origin tags of this context are preserved.
112    pub fn with_tags(&self, tags: impl Into<TagSet>) -> Self {
113        let name = self.inner.name.clone();
114        let host = self.inner.host.clone();
115        let tags = tags.into();
116        let origin_tags = self.inner.origin_tags.clone();
117        let key = ContextInner::calculate_key(&name, host.as_deref(), &tags, &origin_tags);
118
119        Self {
120            inner: Arc::new(ContextInner {
121                name,
122                host,
123                tags,
124                origin_tags,
125                key,
126                active_count: Gauge::noop(),
127            }),
128        }
129    }
130
131    /// Clones this context, and uses the given origin tags for the cloned context.
132    ///
133    /// The name and instrumented tags of this context are preserved.
134    pub fn with_origin_tags(&self, origin_tags: impl Into<TagSet>) -> Self {
135        let name = self.inner.name.clone();
136        let host = self.inner.host.clone();
137        let tags = self.inner.tags.clone();
138        let origin_tags = origin_tags.into();
139        let key = ContextInner::calculate_key(&name, host.as_deref(), &tags, &origin_tags);
140
141        Self {
142            inner: Arc::new(ContextInner {
143                name,
144                host,
145                tags,
146                origin_tags,
147                key,
148                active_count: Gauge::noop(),
149            }),
150        }
151    }
152
153    /// Clones this context, replacing both instrumented tags and origin tags in a single allocation.
154    ///
155    /// Preferred over two separate `with_tags` / `with_origin_tags` calls when both sets need to
156    /// be replaced, as it halves the number of `Arc` allocations.
157    pub fn with_tags_and_origin_tags(&self, tags: impl Into<TagSet>, origin_tags: impl Into<TagSet>) -> Self {
158        let name = self.inner.name.clone();
159        let host = self.inner.host.clone();
160        let tags = tags.into();
161        let origin_tags = origin_tags.into();
162        let key = ContextInner::calculate_key(&name, host.as_deref(), &tags, &origin_tags);
163
164        Self {
165            inner: Arc::new(ContextInner {
166                name,
167                host,
168                tags,
169                origin_tags,
170                key,
171                active_count: Gauge::noop(),
172            }),
173        }
174    }
175
176    pub(super) fn from_inner(inner: ContextInner) -> Self {
177        Self { inner: Arc::new(inner) }
178    }
179
180    #[cfg(test)]
181    pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
182        Arc::ptr_eq(&self.inner, &other.inner)
183    }
184
185    /// Returns the name of this context.
186    pub fn name(&self) -> &MetaString {
187        &self.inner.name
188    }
189
190    /// Returns the host of this context, if one has been set.
191    pub fn host(&self) -> Option<&str> {
192        self.inner.host.as_deref()
193    }
194
195    /// Clones this context, and uses the given host for the cloned context.
196    pub fn with_host<S: Into<Option<MetaString>>>(&self, host: S) -> Self {
197        let name = self.inner.name.clone();
198        let host = host.into();
199        let tags = self.inner.tags.clone();
200        let origin_tags = self.inner.origin_tags.clone();
201        let key = ContextInner::calculate_key(&name, host.as_deref(), &tags, &origin_tags);
202
203        Self {
204            inner: Arc::new(ContextInner {
205                name,
206                host,
207                tags,
208                origin_tags,
209                key,
210                active_count: Gauge::noop(),
211            }),
212        }
213    }
214
215    /// Returns the instrumented tags of this context.
216    pub fn tags(&self) -> &TagSet {
217        &self.inner.tags
218    }
219
220    /// Returns the origin tags of this context.
221    pub fn origin_tags(&self) -> &TagSet {
222        &self.inner.origin_tags
223    }
224
225    /// Mutates the instrumented tags of this context via a closure.
226    ///
227    /// Uses copy-on-write semantics: if this context shares its inner data with other clones, the
228    /// inner data is cloned first so that mutations don't affect other holders. If this context is
229    /// the sole owner, the mutation happens in place.
230    ///
231    /// The context key is automatically recomputed after the closure returns.
232    pub fn mutate_tags(&mut self, f: impl FnOnce(&mut TagSet)) {
233        self.mutate_inner(|inner| f(&mut inner.tags));
234    }
235
236    /// Mutates the origin tags of this context via a closure.
237    ///
238    /// Uses copy-on-write semantics: if this context shares its inner data with other clones, the
239    /// inner data is cloned first so that mutations don't affect other holders. If this context is
240    /// the sole owner, the mutation happens in place.
241    ///
242    /// The context key is automatically recomputed after the closure returns.
243    pub fn mutate_origin_tags(&mut self, f: impl FnOnce(&mut TagSet)) {
244        self.mutate_inner(|inner| f(&mut inner.origin_tags));
245    }
246
247    /// Mutates both instrumented tags and origin tags via a single closure.
248    ///
249    /// Uses copy-on-write semantics: if this context shares its inner data with other clones, the
250    /// inner data is cloned first so that mutations don't affect other holders. If this context is
251    /// the sole owner, the mutation happens in place.
252    ///
253    /// The context key is recomputed once after the closure returns.
254    pub fn with_tag_sets_mut(&mut self, f: impl FnOnce(&mut TagSet, &mut TagSet)) {
255        self.mutate_inner(|inner| f(&mut inner.tags, &mut inner.origin_tags));
256    }
257
258    /// Runs the given closure on the inner context data, recomputing the context key afterwards.
259    ///
260    /// When the inner context state is shared (we aren't the only ones with a strong reference), we clone the inner
261    /// data first to have our own copy. Otherwise, we modify the inner data in place.
262    fn mutate_inner(&mut self, f: impl FnOnce(&mut ContextInner)) {
263        let inner = Arc::make_mut(&mut self.inner);
264        f(inner);
265        inner.recalculate_key();
266    }
267
268    /// Creates a lazy copy-on-write mutable view over this context's tag sets.
269    ///
270    /// The returned view supports mutations (for example, [`retain_tags`][TagSetMutView::retain_tags])
271    /// without immediately triggering an `Arc` clone. The actual clone, mutation, and context key
272    /// recomputation only happen when [`TagSetMutView::finish`] is called, and only if changes
273    /// were actually recorded.
274    ///
275    /// `state` provides reusable scratch space for tracking pending changes. Holding a
276    /// long-lived [`TagSetMutViewState`] across calls amortizes any vector allocations.
277    pub fn tags_mut_view<'a, 'b>(&'a mut self, state: &'b mut TagSetMutViewState) -> TagSetMutView<'a, 'b> {
278        TagSetMutView { context: self, state }
279    }
280
281    /// Returns the size of this context in bytes.
282    ///
283    /// A context's size is the sum of the sizes of its fields and the size of the `Context` struct itself, and
284    /// includes:
285    /// - the context name
286    /// - the context host and tags (both instrumented and origin)
287    ///
288    /// Since origin tags can potentially be expensive to calculate, this method will cache the size of the origin tags
289    /// when this method is first called.
290    ///
291    /// Additionally, the value returned by this method doesn't compensate for externalities such as origin tags
292    /// potentially being shared by multiple contexts, or whether or not tags are inlined, interned, or heap
293    /// allocated. This means that the value returned is essentially the worst-case usage, and should be used as a rough
294    /// estimate.
295    pub fn size_of(&self) -> usize {
296        let name_size = self.inner.name.len();
297        let host_size = self.inner.host.as_ref().map_or(0, |host| host.len());
298        let tags_size = self.inner.tags.size_of();
299        let origin_tags_size = self.inner.origin_tags.size_of();
300
301        BASE_CONTEXT_SIZE + name_size + host_size + tags_size + origin_tags_size
302    }
303}
304
305impl From<&'static str> for Context {
306    fn from(name: &'static str) -> Self {
307        Self::from_static_name(name)
308    }
309}
310
311impl<'a> From<(&'static str, &'a [&'static str])> for Context {
312    fn from((name, tags): (&'static str, &'a [&'static str])) -> Self {
313        Self::from_static_parts(name, tags)
314    }
315}
316
317impl fmt::Display for Context {
318    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319        write!(f, "{}", self.inner.name)?;
320        if !self.inner.tags.is_empty() {
321            write!(f, "{{")?;
322
323            let mut needs_separator = false;
324            for tag in &self.inner.tags {
325                if needs_separator {
326                    write!(f, ", ")?;
327                } else {
328                    needs_separator = true;
329                }
330
331                write!(f, "{}", tag)?;
332            }
333
334            write!(f, "}}")?;
335        }
336
337        Ok(())
338    }
339}
340
341/// Reusable scratch space for [`TagSetMutView`] operations.
342///
343/// Holding a long-lived instance across calls amortizes bitset allocations. The bitsets are
344/// cleared automatically when the associated [`TagSetMutView`] is dropped.
345#[derive(Debug, Default)]
346pub struct TagSetMutViewState {
347    tag_base_removals: ContiguousBitSet,
348    tag_addition_removals: ContiguousBitSet,
349    origin_base_removals: ContiguousBitSet,
350    origin_addition_removals: ContiguousBitSet,
351    hash_seen: PrehashedHashSet<u64>,
352}
353
354impl TagSetMutViewState {
355    /// Creates a new, empty state.
356    pub fn new() -> Self {
357        Self::default()
358    }
359
360    fn clear(&mut self) {
361        self.tag_base_removals.clear_all();
362        self.tag_addition_removals.clear_all();
363        self.origin_base_removals.clear_all();
364        self.origin_addition_removals.clear_all();
365    }
366}
367
368/// A lazy copy-on-write mutable view over a [`Context`]'s tag sets.
369///
370/// Operations on this view (for example, [`retain_tags`][Self::retain_tags]) are recorded but not
371/// applied immediately. The actual `Arc` clone, mutation, and context key recomputation only
372/// occur when [`finish`][Self::finish] is called, and only if changes were recorded.
373pub struct TagSetMutView<'a, 'b> {
374    context: &'a mut Context,
375    state: &'b mut TagSetMutViewState,
376}
377
378impl<'a, 'b> TagSetMutView<'a, 'b> {
379    /// Scan instrumented tags with the given predicate.
380    ///
381    /// Tags for which `f` returns `false` are flagged for removal. This is a read-only scan;
382    /// no mutation occurs until [`finish`][Self::finish] is called.
383    pub fn retain_tags(&mut self, f: impl FnMut(&Tag) -> bool) {
384        self.context.inner.tags.collect_removals(
385            f,
386            &mut self.state.tag_base_removals,
387            &mut self.state.tag_addition_removals,
388        );
389    }
390
391    /// Scan origin tags with the given predicate.
392    ///
393    /// Tags for which `f` returns `false` are flagged for removal. This is a read-only scan;
394    /// no mutation occurs until [`finish`][Self::finish] is called.
395    pub fn retain_origin_tags(&mut self, f: impl FnMut(&Tag) -> bool) {
396        self.context.inner.origin_tags.collect_removals(
397            f,
398            &mut self.state.origin_base_removals,
399            &mut self.state.origin_addition_removals,
400        );
401    }
402
403    /// Apply all recorded changes and return the total number of tags affected.
404    ///
405    /// If no changes were recorded, this is a no-op: no `Arc` clone, no rehash, returns 0.
406    /// Otherwise, triggers `Arc::make_mut` on the context, applies the changes to both tag sets,
407    /// and recomputes the context key.
408    ///
409    /// Returns the number of tags removed.
410    pub fn finish(self) -> usize {
411        let total_tags = self.state.tag_base_removals.len() + self.state.tag_addition_removals.len();
412        let total_origin = self.state.origin_base_removals.len() + self.state.origin_addition_removals.len();
413        let total = total_tags + total_origin;
414
415        if total == 0 {
416            return 0;
417        }
418
419        let inner = Arc::make_mut(&mut self.context.inner);
420
421        if total_tags > 0 {
422            inner
423                .tags
424                .apply_removals(&self.state.tag_base_removals, &self.state.tag_addition_removals);
425        }
426        if total_origin > 0 {
427            inner
428                .origin_tags
429                .apply_removals(&self.state.origin_base_removals, &self.state.origin_addition_removals);
430        }
431
432        inner.recalculate_key_with_seen(&mut self.state.hash_seen);
433
434        total
435    }
436}
437
438impl Drop for TagSetMutView<'_, '_> {
439    fn drop(&mut self) {
440        self.state.clear();
441    }
442}
443
444pub(super) struct ContextInner {
445    key: ContextKey,
446    name: MetaString,
447    host: Option<MetaString>,
448    tags: TagSet,
449    origin_tags: TagSet,
450    active_count: Gauge,
451}
452
453impl ContextInner {
454    pub fn from_parts(
455        key: ContextKey, name: MetaString, host: Option<MetaString>, tags: TagSet, origin_tags: TagSet,
456        active_count: Gauge,
457    ) -> Self {
458        Self {
459            key,
460            name,
461            host,
462            tags,
463            origin_tags,
464            active_count,
465        }
466    }
467
468    fn calculate_key(name: &str, host: Option<&str>, tags: &TagSet, origin_tags: &TagSet) -> ContextKey {
469        let mut seen = PrehashedHashSet::default();
470        let (key, _) = hash_context_with_host_and_seen(name, host, tags, origin_tags, &mut seen);
471        key
472    }
473
474    fn recalculate_key(&mut self) {
475        self.key = Self::calculate_key(&self.name, self.host.as_deref(), &self.tags, &self.origin_tags);
476    }
477
478    fn recalculate_key_with_seen(&mut self, seen: &mut PrehashedHashSet<u64>) {
479        let (key, _) =
480            hash_context_with_host_and_seen(&self.name, self.host.as_deref(), &self.tags, &self.origin_tags, seen);
481        self.key = key;
482    }
483}
484
485impl Clone for ContextInner {
486    fn clone(&self) -> Self {
487        Self {
488            key: self.key,
489            name: self.name.clone(),
490            host: self.host.clone(),
491            tags: self.tags.clone(),
492            origin_tags: self.origin_tags.clone(),
493
494            // We're specifically detaching this context from the statistics of the resolver from which `self`
495            // originated, as we only want to track the statistics of the contexts created _directly_ through the
496            // resolver.
497            active_count: Gauge::noop(),
498        }
499    }
500}
501
502impl Drop for ContextInner {
503    fn drop(&mut self) {
504        self.active_count.decrement(1);
505    }
506}
507
508impl PartialEq<ContextInner> for ContextInner {
509    fn eq(&self, other: &ContextInner) -> bool {
510        // TODO: Note about why we consider the hash good enough for equality.
511        self.key == other.key
512    }
513}
514
515impl Eq for ContextInner {}
516
517impl Hash for ContextInner {
518    fn hash<H: Hasher>(&self, state: &mut H) {
519        self.key.hash(state);
520    }
521}
522
523impl fmt::Debug for ContextInner {
524    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
525        f.debug_struct("ContextInner")
526            .field("name", &self.name)
527            .field("host", &self.host)
528            .field("tags", &self.tags)
529            .field("key", &self.key)
530            .finish()
531    }
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537    use crate::data_model::tags::Tag;
538
539    const SIZE_OF_CONTEXT_NAME: &str = "size_of_test_metric";
540    const SIZE_OF_CONTEXT_CHANGED_NAME: &str = "size_of_test_metric_changed";
541    const SIZE_OF_CONTEXT_TAGS: &[&str] = &["size_of_test_tag1", "size_of_test_tag2"];
542    const SIZE_OF_CONTEXT_ORIGIN_TAGS: &[&str] = &["size_of_test_origin_tag1", "size_of_test_origin_tag2"];
543
544    fn tag_set(tags: &[&str]) -> TagSet {
545        tags.iter().map(|s| Tag::from(*s)).collect::<TagSet>()
546    }
547
548    #[test]
549    fn size_of_context_from_static_name() {
550        let context = Context::from_static_name(SIZE_OF_CONTEXT_NAME);
551        assert_eq!(context.size_of(), BASE_CONTEXT_SIZE + SIZE_OF_CONTEXT_NAME.len());
552    }
553
554    #[test]
555    fn size_of_context_from_static_parts() {
556        let tags = tag_set(SIZE_OF_CONTEXT_TAGS);
557
558        let context = Context::from_static_parts(SIZE_OF_CONTEXT_NAME, SIZE_OF_CONTEXT_TAGS);
559        assert_eq!(
560            context.size_of(),
561            BASE_CONTEXT_SIZE + SIZE_OF_CONTEXT_NAME.len() + tags.size_of()
562        );
563    }
564
565    #[test]
566    fn size_of_context_from_parts() {
567        let tags = tag_set(SIZE_OF_CONTEXT_TAGS);
568
569        let context = Context::from_parts(SIZE_OF_CONTEXT_NAME, tags.clone());
570        assert_eq!(
571            context.size_of(),
572            BASE_CONTEXT_SIZE + SIZE_OF_CONTEXT_NAME.len() + tags.size_of()
573        );
574    }
575
576    #[test]
577    fn size_of_context_with_name() {
578        // Check the check after `with_name` when there's both tags and no tags.
579        let context = Context::from_static_name(SIZE_OF_CONTEXT_NAME).with_name(SIZE_OF_CONTEXT_CHANGED_NAME);
580        assert_eq!(
581            context.size_of(),
582            BASE_CONTEXT_SIZE + SIZE_OF_CONTEXT_CHANGED_NAME.len()
583        );
584
585        let tags = tag_set(SIZE_OF_CONTEXT_TAGS);
586
587        let context = Context::from_static_parts(SIZE_OF_CONTEXT_NAME, SIZE_OF_CONTEXT_TAGS)
588            .with_name(SIZE_OF_CONTEXT_CHANGED_NAME);
589        assert_eq!(
590            context.size_of(),
591            BASE_CONTEXT_SIZE + SIZE_OF_CONTEXT_CHANGED_NAME.len() + tags.size_of()
592        );
593    }
594
595    #[test]
596    fn size_of_context_origin_tags() {
597        let tags = tag_set(SIZE_OF_CONTEXT_TAGS);
598        let origin_tags = tag_set(SIZE_OF_CONTEXT_ORIGIN_TAGS);
599
600        let (key, _) = hash_context(SIZE_OF_CONTEXT_NAME, SIZE_OF_CONTEXT_TAGS, SIZE_OF_CONTEXT_ORIGIN_TAGS);
601
602        let context = Context::from_inner(ContextInner {
603            key,
604            name: MetaString::from_static(SIZE_OF_CONTEXT_NAME),
605            host: None,
606            tags: tags.clone(),
607            origin_tags: origin_tags.clone(),
608            active_count: Gauge::noop(),
609        });
610
611        // Make sure the size of the context is correct with origin tags.
612        assert_eq!(
613            context.size_of(),
614            BASE_CONTEXT_SIZE + SIZE_OF_CONTEXT_NAME.len() + tags.size_of() + origin_tags.size_of()
615        );
616    }
617
618    #[test]
619    fn with_tags_mut_clones_shared_context() {
620        let original = Context::from_static_parts("metric", &["env:prod"]);
621        let mut mutated = original.clone();
622
623        // They share the same Arc before mutation.
624        assert!(original.ptr_eq(&mutated));
625
626        mutated.mutate_tags(|tags| {
627            tags.insert_tag(Tag::from("service:web"));
628        });
629
630        // After mutation, they no longer share the same inner.
631        assert!(!original.ptr_eq(&mutated));
632    }
633
634    #[test]
635    fn with_tags_mut_does_not_affect_original() {
636        let original = Context::from_static_parts("metric", &["env:prod"]);
637        let mut mutated = original.clone();
638
639        mutated.mutate_tags(|tags| {
640            tags.insert_tag(Tag::from("service:web"));
641        });
642
643        // Original is unchanged.
644        assert_eq!(original.tags().len(), 1);
645        assert!(original.tags().has_tag("env:prod"));
646        assert!(!original.tags().has_tag("service:web"));
647
648        // Mutated has both tags.
649        assert_eq!(mutated.tags().len(), 2);
650        assert!(mutated.tags().has_tag("env:prod"));
651        assert!(mutated.tags().has_tag("service:web"));
652    }
653
654    #[test]
655    fn with_tags_mut_rehashes() {
656        // Build a context and mutate it to add a tag.
657        let mut mutated = Context::from_static_parts("metric", &["env:prod"]);
658        mutated.mutate_tags(|tags| {
659            tags.insert_tag(Tag::from("service:web"));
660        });
661
662        // Build an equivalent context from scratch with both tags.
663        let expected = Context::from_static_parts("metric", &["env:prod", "service:web"]);
664
665        // The recomputed key should match a freshly-constructed context with the same state.
666        assert_eq!(mutated, expected);
667
668        // Modify a tag on the mutated context that _isn't_ shared with `expected` to ensure that there's no asymmetric
669        // equality logic.
670        mutated.mutate_tags(|tags| {
671            tags.insert_tag(Tag::from("cluster:foo"));
672        });
673        assert_ne!(mutated, expected);
674    }
675
676    #[test]
677    fn with_origin_tags_mut_clones_shared_context() {
678        let original = Context::from_static_name("metric");
679        let mut mutated = original.clone();
680
681        assert!(original.ptr_eq(&mutated));
682
683        mutated.mutate_origin_tags(|tags| {
684            tags.insert_tag(Tag::from("origin:tag"));
685        });
686
687        assert!(!original.ptr_eq(&mutated));
688        assert!(original.origin_tags().is_empty());
689        assert_eq!(mutated.origin_tags().len(), 1);
690        assert!(mutated.origin_tags().has_tag("origin:tag"));
691    }
692
693    // --- Helper for contexts with origin tags ---
694
695    fn context_with_origin(name: &'static str, tags: &[&'static str], origin_tags: &[&'static str]) -> Context {
696        let (key, _) = hash_context(name, tags, origin_tags);
697        Context::from_inner(ContextInner {
698            key,
699            name: MetaString::from_static(name),
700            host: None,
701            tags: tag_set(tags),
702            origin_tags: tag_set(origin_tags),
703            active_count: Gauge::noop(),
704        })
705    }
706
707    fn context_with_host(
708        name: &'static str, host: &'static str, tags: &[&'static str], origin_tags: &[&'static str],
709    ) -> Context {
710        let tags = tag_set(tags);
711        let origin_tags = tag_set(origin_tags);
712        let key = ContextInner::calculate_key(name, Some(host), &tags, &origin_tags);
713        Context::from_inner(ContextInner {
714            key,
715            name: MetaString::from_static(name),
716            host: Some(MetaString::from_static(host)),
717            tags,
718            origin_tags,
719            active_count: Gauge::noop(),
720        })
721    }
722
723    #[test]
724    fn host_participates_in_context_identity() {
725        let host_a = context_with_host("metric", "host-a", &["env:prod"], &["origin:a"]);
726        let host_b = context_with_host("metric", "host-b", &["env:prod"], &["origin:a"]);
727        let host_a_again = context_with_host("metric", "host-a", &["env:prod"], &["origin:a"]);
728        let no_host = context_with_origin("metric", &["env:prod"], &["origin:a"]);
729
730        assert_ne!(host_a, host_b);
731        assert_ne!(host_a, no_host);
732        assert_eq!(host_a, host_a_again);
733        assert_eq!(host_a.host(), Some("host-a"));
734        assert_eq!(no_host.host(), None);
735    }
736
737    #[test]
738    fn host_is_preserved_when_context_is_copied_with_new_parts() {
739        let base = context_with_host("metric", "host-a", &["env:prod"], &["origin:a"]);
740
741        let renamed = base.with_name("renamed");
742        assert_eq!(
743            renamed,
744            context_with_host("renamed", "host-a", &["env:prod"], &["origin:a"])
745        );
746        assert_ne!(renamed, context_with_origin("renamed", &["env:prod"], &["origin:a"]));
747
748        let retagged = base.with_tags(tag_set(&["service:web"]));
749        assert_eq!(
750            retagged,
751            context_with_host("metric", "host-a", &["service:web"], &["origin:a"])
752        );
753        assert_ne!(retagged, context_with_origin("metric", &["service:web"], &["origin:a"]));
754
755        let reorigined = base.with_origin_tags(tag_set(&["origin:b"]));
756        assert_eq!(
757            reorigined,
758            context_with_host("metric", "host-a", &["env:prod"], &["origin:b"])
759        );
760        assert_ne!(reorigined, context_with_origin("metric", &["env:prod"], &["origin:b"]));
761
762        let replaced = base.with_tags_and_origin_tags(tag_set(&["service:web"]), tag_set(&["origin:b"]));
763        assert_eq!(
764            replaced,
765            context_with_host("metric", "host-a", &["service:web"], &["origin:b"])
766        );
767        assert_ne!(replaced, context_with_origin("metric", &["service:web"], &["origin:b"]));
768    }
769
770    #[test]
771    fn host_is_preserved_when_context_tags_are_mutated() {
772        let expected_with_tag = context_with_host("metric", "host-a", &["env:prod", "service:web"], &["origin:a"]);
773        let expected_with_origin = context_with_host("metric", "host-a", &["env:prod"], &["origin:a", "origin:b"]);
774
775        let mut tag_mutated = context_with_host("metric", "host-a", &["env:prod"], &["origin:a"]);
776        tag_mutated.mutate_tags(|tags| tags.insert_tag(Tag::from("service:web")));
777        assert_eq!(tag_mutated, expected_with_tag);
778        assert_eq!(tag_mutated.host(), Some("host-a"));
779
780        let mut origin_mutated = context_with_host("metric", "host-a", &["env:prod"], &["origin:a"]);
781        origin_mutated.mutate_origin_tags(|tags| tags.insert_tag(Tag::from("origin:b")));
782        assert_eq!(origin_mutated, expected_with_origin);
783        assert_eq!(origin_mutated.host(), Some("host-a"));
784    }
785
786    #[test]
787    fn host_is_preserved_when_tag_mut_view_rekeys_context() {
788        let mut ctx = context_with_host(
789            "metric",
790            "host-a",
791            &["env:prod", "service:web"],
792            &["origin:a", "origin:b"],
793        );
794        let mut state = TagSetMutViewState::new();
795
796        let mut view = ctx.tags_mut_view(&mut state);
797        view.retain_tags(|tag| tag.name() == "env");
798        view.retain_origin_tags(|tag| tag.as_str() == "origin:a");
799        assert_eq!(view.finish(), 2);
800
801        assert_eq!(ctx, context_with_host("metric", "host-a", &["env:prod"], &["origin:a"]));
802        assert_ne!(ctx, context_with_origin("metric", &["env:prod"], &["origin:a"]));
803        assert_eq!(ctx.host(), Some("host-a"));
804    }
805
806    // --- TagSetMutView ---
807
808    #[test]
809    fn mut_view_retain_tags_removes_matching() {
810        let mut ctx = Context::from_static_parts("metric", &["env:prod", "service:web", "region:us"]);
811        let mut state = TagSetMutViewState::new();
812
813        let mut view = ctx.tags_mut_view(&mut state);
814        view.retain_tags(|tag| tag.name() == "env");
815        let removed = view.finish();
816
817        assert_eq!(removed, 2);
818        assert_eq!(ctx.tags().len(), 1);
819        assert!(ctx.tags().has_tag("env:prod"));
820        assert!(!ctx.tags().has_tag("service:web"));
821        assert!(!ctx.tags().has_tag("region:us"));
822    }
823
824    #[test]
825    fn mut_view_retain_origin_tags_removes_matching() {
826        let mut ctx = context_with_origin("metric", &[], &["origin:a", "origin:b", "origin:c"]);
827        let mut state = TagSetMutViewState::new();
828
829        let mut view = ctx.tags_mut_view(&mut state);
830        view.retain_origin_tags(|tag| tag.as_str() == "origin:a");
831        let removed = view.finish();
832
833        assert_eq!(removed, 2);
834        assert_eq!(ctx.origin_tags().len(), 1);
835        assert!(ctx.origin_tags().has_tag("origin:a"));
836        assert!(!ctx.origin_tags().has_tag("origin:b"));
837        assert!(!ctx.origin_tags().has_tag("origin:c"));
838    }
839
840    #[test]
841    fn mut_view_retain_both_tag_sets() {
842        let mut ctx = context_with_origin("metric", &["env:prod", "service:web"], &["origin:a", "origin:b"]);
843        let mut state = TagSetMutViewState::new();
844
845        let mut view = ctx.tags_mut_view(&mut state);
846        view.retain_tags(|tag| tag.name() == "env");
847        view.retain_origin_tags(|tag| tag.as_str() == "origin:a");
848        let removed = view.finish();
849
850        assert_eq!(removed, 2);
851        assert_eq!(ctx.tags().len(), 1);
852        assert!(ctx.tags().has_tag("env:prod"));
853        assert!(!ctx.tags().has_tag("service:web"));
854        assert_eq!(ctx.origin_tags().len(), 1);
855        assert!(ctx.origin_tags().has_tag("origin:a"));
856        assert!(!ctx.origin_tags().has_tag("origin:b"));
857    }
858
859    #[test]
860    fn mut_view_retain_all_is_noop() {
861        let original = Context::from_static_parts("metric", &["env:prod", "service:web"]);
862        let mut ctx = original.clone();
863        let mut state = TagSetMutViewState::new();
864
865        let mut view = ctx.tags_mut_view(&mut state);
866        view.retain_tags(|_| true);
867        let removed = view.finish();
868
869        assert_eq!(removed, 0);
870        assert!(ctx.ptr_eq(&original));
871    }
872
873    #[test]
874    fn mut_view_retain_none_removes_all() {
875        let mut ctx = Context::from_static_parts("metric", &["env:prod", "service:web", "region:us"]);
876        let mut state = TagSetMutViewState::new();
877
878        let mut view = ctx.tags_mut_view(&mut state);
879        view.retain_tags(|_| false);
880        let removed = view.finish();
881
882        assert_eq!(removed, 3);
883        assert!(ctx.tags().is_empty());
884    }
885
886    #[test]
887    fn mut_view_finish_returns_correct_count() {
888        let mut ctx = context_with_origin("metric", &["a:1", "b:2", "c:3"], &["origin:x", "origin:y"]);
889        let mut state = TagSetMutViewState::new();
890
891        let mut view = ctx.tags_mut_view(&mut state);
892        // Remove b:2 and c:3 (keep a:1).
893        view.retain_tags(|tag| tag.name() == "a");
894        // Remove origin:y (keep origin:x).
895        view.retain_origin_tags(|tag| tag.as_str() == "origin:x");
896        let removed = view.finish();
897
898        assert_eq!(removed, 3);
899        assert_eq!(ctx.tags().len(), 1);
900        assert_eq!(ctx.origin_tags().len(), 1);
901    }
902
903    #[test]
904    fn mut_view_equivalent_to_direct_mutate_tags() {
905        let base = Context::from_static_parts("metric", &["env:prod", "service:web", "region:us"]);
906        let predicate = |tag: &Tag| tag.name() == "env";
907
908        // Path A: direct mutation.
909        let mut direct = base.clone();
910        direct.mutate_tags(|tags| tags.retain(predicate));
911
912        // Path B: mut view.
913        let mut via_view = base.clone();
914        let mut state = TagSetMutViewState::new();
915        let mut view = via_view.tags_mut_view(&mut state);
916        view.retain_tags(predicate);
917        view.finish();
918
919        assert_eq!(direct, via_view);
920        assert_eq!(direct.tags().len(), via_view.tags().len());
921        assert!(via_view.tags().has_tag("env:prod"));
922        assert!(!via_view.tags().has_tag("service:web"));
923    }
924
925    #[test]
926    fn mut_view_equivalent_to_direct_mutate_origin_tags() {
927        let base = context_with_origin("metric", &["env:prod"], &["origin:a", "origin:b", "origin:c"]);
928        let predicate = |tag: &Tag| tag.as_str() == "origin:a";
929
930        // Path A: direct mutation.
931        let mut direct = base.clone();
932        direct.mutate_origin_tags(|tags| tags.retain(predicate));
933
934        // Path B: mut view.
935        let mut via_view = base.clone();
936        let mut state = TagSetMutViewState::new();
937        let mut view = via_view.tags_mut_view(&mut state);
938        view.retain_origin_tags(predicate);
939        view.finish();
940
941        assert_eq!(direct, via_view);
942        assert_eq!(direct.origin_tags().len(), via_view.origin_tags().len());
943    }
944
945    #[test]
946    fn mut_view_does_not_affect_cloned_context() {
947        let original = Context::from_static_parts("metric", &["env:prod", "service:web"]);
948        let mut mutated = original.clone();
949        let mut state = TagSetMutViewState::new();
950
951        let mut view = mutated.tags_mut_view(&mut state);
952        view.retain_tags(|tag| tag.name() == "env");
953        view.finish();
954
955        // Original is unchanged.
956        assert_eq!(original.tags().len(), 2);
957        assert!(original.tags().has_tag("env:prod"));
958        assert!(original.tags().has_tag("service:web"));
959
960        // Mutated has only the retained tag.
961        assert_eq!(mutated.tags().len(), 1);
962        assert!(!original.ptr_eq(&mutated));
963    }
964
965    #[test]
966    fn mut_view_drop_without_finish_discards_changes() {
967        let original = Context::from_static_parts("metric", &["env:prod", "service:web"]);
968        let mut ctx = original.clone();
969        let mut state = TagSetMutViewState::new();
970
971        {
972            let mut view = ctx.tags_mut_view(&mut state);
973            view.retain_tags(|_| false); // Flag all for removal.
974                                         // Drop without calling finish().
975        }
976
977        // Nothing changed.
978        assert_eq!(ctx.tags().len(), 2);
979        assert!(ctx.ptr_eq(&original));
980    }
981
982    #[test]
983    fn mut_view_state_reuse_across_operations() {
984        let mut state = TagSetMutViewState::new();
985
986        // First operation.
987        let mut ctx1 = Context::from_static_parts("metric1", &["a:1", "b:2"]);
988        let mut view1 = ctx1.tags_mut_view(&mut state);
989        view1.retain_tags(|tag| tag.name() == "a");
990        let removed1 = view1.finish();
991
992        assert_eq!(removed1, 1);
993        assert_eq!(ctx1.tags().len(), 1);
994        assert!(ctx1.tags().has_tag("a:1"));
995
996        // Second operation reusing the same state.
997        let mut ctx2 = Context::from_static_parts("metric2", &["x:1", "y:2", "z:3"]);
998        let mut view2 = ctx2.tags_mut_view(&mut state);
999        view2.retain_tags(|tag| tag.name() == "z");
1000        let removed2 = view2.finish();
1001
1002        assert_eq!(removed2, 2);
1003        assert_eq!(ctx2.tags().len(), 1);
1004        assert!(ctx2.tags().has_tag("z:3"));
1005    }
1006
1007    #[test]
1008    fn mut_view_retain_tags_with_additions() {
1009        // Start with a base tag, then add one via mutation to create an overlay.
1010        let mut ctx = Context::from_static_parts("metric", &["base:tag"]);
1011        ctx.mutate_tags(|tags| {
1012            tags.insert_tag(Tag::from("added:tag"));
1013        });
1014        assert_eq!(ctx.tags().len(), 2);
1015
1016        let mut state = TagSetMutViewState::new();
1017        let mut view = ctx.tags_mut_view(&mut state);
1018        view.retain_tags(|tag| tag.name() == "added");
1019        let removed = view.finish();
1020
1021        assert_eq!(removed, 1);
1022        assert_eq!(ctx.tags().len(), 1);
1023        assert!(ctx.tags().has_tag("added:tag"));
1024        assert!(!ctx.tags().has_tag("base:tag"));
1025    }
1026
1027    #[test]
1028    fn mut_view_retain_tags_removes_only_additions() {
1029        let mut ctx = Context::from_static_parts("metric", &["base:tag"]);
1030        ctx.mutate_tags(|tags| {
1031            tags.insert_tag(Tag::from("added:tag"));
1032        });
1033
1034        let mut state = TagSetMutViewState::new();
1035        let mut view = ctx.tags_mut_view(&mut state);
1036        view.retain_tags(|tag| tag.name() == "base");
1037        let removed = view.finish();
1038
1039        assert_eq!(removed, 1);
1040        assert_eq!(ctx.tags().len(), 1);
1041        assert!(ctx.tags().has_tag("base:tag"));
1042        assert!(!ctx.tags().has_tag("added:tag"));
1043    }
1044
1045    #[test]
1046    fn mut_view_retain_tags_removes_base_and_additions() {
1047        let mut ctx = Context::from_static_parts("metric", &["base:tag"]);
1048        ctx.mutate_tags(|tags| {
1049            tags.insert_tag(Tag::from("added:tag"));
1050        });
1051
1052        let mut state = TagSetMutViewState::new();
1053        let mut view = ctx.tags_mut_view(&mut state);
1054        view.retain_tags(|_| false);
1055        let removed = view.finish();
1056
1057        assert_eq!(removed, 2);
1058        assert!(ctx.tags().is_empty());
1059    }
1060
1061    #[test]
1062    fn mut_view_multiple_retain_calls_deduplicates() {
1063        // Two retain calls that both reject the same addition tag must not panic.
1064        // Semantics: a tag survives only if ALL predicates accept it.
1065        let mut ctx = Context::from_static_parts("metric", &["base:tag"]);
1066        ctx.mutate_tags(|tags| {
1067            tags.insert_tag(Tag::from("added:a"));
1068            tags.insert_tag(Tag::from("added:b"));
1069        });
1070        assert_eq!(ctx.tags().len(), 3);
1071
1072        let mut state = TagSetMutViewState::new();
1073        let mut view = ctx.tags_mut_view(&mut state);
1074        // First predicate removes "added:a" (keeps base:tag and added:b).
1075        view.retain_tags(|tag| tag.as_str() != "added:a");
1076        // Second predicate removes "base:tag" (keeps added:a and added:b).
1077        // Combined effect: only "added:b" survives both predicates.
1078        // "added:a" is flagged by both calls -- its duplicate index must be deduplicated.
1079        view.retain_tags(|tag| tag.name() != "base");
1080        let removed = view.finish();
1081
1082        assert_eq!(removed, 2);
1083        assert_eq!(ctx.tags().len(), 1);
1084        assert!(ctx.tags().has_tag("added:b"));
1085    }
1086
1087    #[test]
1088    fn mut_view_multiple_retain_origin_calls_deduplicates() {
1089        let mut ctx = context_with_origin("metric", &[], &["origin:a", "origin:b", "origin:c"]);
1090        let mut state = TagSetMutViewState::new();
1091
1092        let mut view = ctx.tags_mut_view(&mut state);
1093        // Both predicates reject "origin:c".
1094        view.retain_origin_tags(|tag| tag.as_str() != "origin:c");
1095        view.retain_origin_tags(|tag| tag.as_str() == "origin:a");
1096        let removed = view.finish();
1097
1098        assert_eq!(removed, 2);
1099        assert_eq!(ctx.origin_tags().len(), 1);
1100        assert!(ctx.origin_tags().has_tag("origin:a"));
1101    }
1102
1103    #[test]
1104    fn mut_view_multiple_retain_equivalent_to_combined_predicate() {
1105        let base = Context::from_static_parts("metric", &["env:prod", "service:web", "region:us", "cluster:main"]);
1106
1107        // Path A: two separate retain calls.
1108        let mut via_two = base.clone();
1109        let mut state = TagSetMutViewState::new();
1110        let mut view = via_two.tags_mut_view(&mut state);
1111        view.retain_tags(|tag| tag.name() != "region");
1112        view.retain_tags(|tag| tag.name() != "cluster");
1113        view.finish();
1114
1115        // Path B: single combined predicate.
1116        let mut via_one = base.clone();
1117        let mut state2 = TagSetMutViewState::new();
1118        let mut view2 = via_one.tags_mut_view(&mut state2);
1119        view2.retain_tags(|tag| tag.name() != "region" && tag.name() != "cluster");
1120        view2.finish();
1121
1122        // Path C: direct mutation.
1123        let mut via_direct = base.clone();
1124        via_direct.mutate_tags(|tags| tags.retain(|tag| tag.name() != "region" && tag.name() != "cluster"));
1125
1126        assert_eq!(via_two, via_one);
1127        assert_eq!(via_two, via_direct);
1128    }
1129
1130    #[test]
1131    fn with_tag_sets_mut_mutates_both_and_recomputes_key() {
1132        // `with_tag_sets_mut` mutates both tag sets and recomputes the context key a single time
1133        // for the combined change. The observable guarantee is that the result matches a context
1134        // built from scratch with the combined tags, and matches applying the same two mutations
1135        // via separate `with_tags`/`with_origin_tags` calls (which would each rehash).
1136        let mut combined = context_with_origin("metric", &["env:prod"], &["origin:a"]);
1137        combined.with_tag_sets_mut(|tags, origin_tags| {
1138            tags.insert_tag(Tag::from("service:web"));
1139            origin_tags.insert_tag(Tag::from("origin:b"));
1140        });
1141
1142        // Both tag sets were updated in the single call.
1143        assert!(combined.tags().has_tag("env:prod"));
1144        assert!(combined.tags().has_tag("service:web"));
1145        assert!(combined.origin_tags().has_tag("origin:a"));
1146        assert!(combined.origin_tags().has_tag("origin:b"));
1147
1148        // The recomputed key matches a freshly-built context with the same final state.
1149        let expected = context_with_origin("metric", &["env:prod", "service:web"], &["origin:a", "origin:b"]);
1150        assert_eq!(combined, expected);
1151
1152        // ...and matches applying the two mutations separately (i.e. via two rehashes).
1153        let separate = context_with_origin("metric", &["env:prod"], &["origin:a"])
1154            .with_tags(tag_set(&["env:prod", "service:web"]))
1155            .with_origin_tags(tag_set(&["origin:a", "origin:b"]));
1156        assert_eq!(combined, separate);
1157    }
1158
1159    #[test]
1160    fn display_renders_name_and_instrumented_tags() {
1161        // With no tags, only the metric name is rendered.
1162        assert_eq!(Context::from_static_name("metric").to_string(), "metric");
1163
1164        // With tags, they are rendered in a brace-delimited, comma-space-separated list in
1165        // insertion order. Origin tags are intentionally not part of the `Display` output.
1166        let context = Context::from_static_parts("metric", &["env:prod", "service:web"]);
1167        assert_eq!(context.to_string(), "metric{env:prod, service:web}");
1168    }
1169}