1use 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#[derive(Clone, Debug, Eq, Hash, PartialEq)]
25pub struct Context {
26 inner: Arc<ContextInner>,
27}
28
29impl Context {
30 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 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 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 pub fn with_name<S: Into<MetaString>>(&self, name: S) -> Self {
90 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 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 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 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 pub fn name(&self) -> &MetaString {
187 &self.inner.name
188 }
189
190 pub fn host(&self) -> Option<&str> {
192 self.inner.host.as_deref()
193 }
194
195 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 pub fn tags(&self) -> &TagSet {
217 &self.inner.tags
218 }
219
220 pub fn origin_tags(&self) -> &TagSet {
222 &self.inner.origin_tags
223 }
224
225 pub fn mutate_tags(&mut self, f: impl FnOnce(&mut TagSet)) {
233 self.mutate_inner(|inner| f(&mut inner.tags));
234 }
235
236 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 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 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 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 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#[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 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
368pub struct TagSetMutView<'a, 'b> {
374 context: &'a mut Context,
375 state: &'b mut TagSetMutViewState,
376}
377
378impl<'a, 'b> TagSetMutView<'a, 'b> {
379 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 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 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 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 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 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 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 assert!(original.ptr_eq(&mutated));
625
626 mutated.mutate_tags(|tags| {
627 tags.insert_tag(Tag::from("service:web"));
628 });
629
630 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 assert_eq!(original.tags().len(), 1);
645 assert!(original.tags().has_tag("env:prod"));
646 assert!(!original.tags().has_tag("service:web"));
647
648 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 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 let expected = Context::from_static_parts("metric", &["env:prod", "service:web"]);
664
665 assert_eq!(mutated, expected);
667
668 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 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 #[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 view.retain_tags(|tag| tag.name() == "a");
894 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 let mut direct = base.clone();
910 direct.mutate_tags(|tags| tags.retain(predicate));
911
912 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 let mut direct = base.clone();
932 direct.mutate_origin_tags(|tags| tags.retain(predicate));
933
934 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 assert_eq!(original.tags().len(), 2);
957 assert!(original.tags().has_tag("env:prod"));
958 assert!(original.tags().has_tag("service:web"));
959
960 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); }
976
977 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 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 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 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 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 view.retain_tags(|tag| tag.as_str() != "added:a");
1076 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 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 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 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 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 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 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 let expected = context_with_origin("metric", &["env:prod", "service:web"], &["origin:a", "origin:b"]);
1150 assert_eq!(combined, expected);
1151
1152 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 assert_eq!(Context::from_static_name("metric").to_string(), "metric");
1163
1164 let context = Context::from_static_parts("metric", &["env:prod", "service:web"]);
1167 assert_eq!(context.to_string(), "metric{env:prod, service:web}");
1168 }
1169}