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#[derive(Clone, Debug, Eq, Hash, PartialEq)]
16pub struct Context {
17 inner: Arc<ContextInner>,
18}
19
20impl Context {
21 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 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 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 pub fn with_name<S: Into<MetaString>>(&self, name: S) -> Self {
81 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 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 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 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 pub fn name(&self) -> &MetaString {
178 &self.inner.name
179 }
180
181 pub fn host(&self) -> Option<&str> {
183 self.inner.host.as_deref()
184 }
185
186 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 pub fn tags(&self) -> &TagSet {
208 &self.inner.tags
209 }
210
211 pub fn origin_tags(&self) -> &TagSet {
213 &self.inner.origin_tags
214 }
215
216 pub fn mutate_tags(&mut self, f: impl FnOnce(&mut TagSet)) {
224 self.mutate_inner(|inner| f(&mut inner.tags));
225 }
226
227 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 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 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 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 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#[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 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
359pub struct TagSetMutView<'a, 'b> {
365 context: &'a mut Context,
366 state: &'b mut TagSetMutViewState,
367}
368
369impl<'a, 'b> TagSetMutView<'a, 'b> {
370 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 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 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 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 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 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 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 assert!(original.ptr_eq(&mutated));
616
617 mutated.mutate_tags(|tags| {
618 tags.insert_tag(Tag::from("service:web"));
619 });
620
621 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 assert_eq!(original.tags().len(), 1);
636 assert!(original.tags().has_tag("env:prod"));
637 assert!(!original.tags().has_tag("service:web"));
638
639 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 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 let expected = Context::from_static_parts("metric", &["env:prod", "service:web"]);
655
656 assert_eq!(mutated, expected);
658
659 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 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 #[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 view.retain_tags(|tag| tag.name() == "a");
885 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 let mut direct = base.clone();
901 direct.mutate_tags(|tags| tags.retain(predicate));
902
903 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 let mut direct = base.clone();
923 direct.mutate_origin_tags(|tags| tags.retain(predicate));
924
925 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 assert_eq!(original.tags().len(), 2);
948 assert!(original.tags().has_tag("env:prod"));
949 assert!(original.tags().has_tag("service:web"));
950
951 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); }
967
968 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 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 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 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 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 view.retain_tags(|tag| tag.as_str() != "added:a");
1067 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 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 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 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 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 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 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 let expected = context_with_origin("metric", &["env:prod", "service:web"], &["origin:a", "origin:b"]);
1141 assert_eq!(combined, expected);
1142
1143 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 assert_eq!(Context::from_static_name("metric").to_string(), "metric");
1154
1155 let context = Context::from_static_parts("metric", &["env:prod", "service:web"]);
1158 assert_eq!(context.to_string(), "metric{env:prod, service:web}");
1159 }
1160}