saluki_context/
context.rs

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