1use std::{num::NonZeroUsize, sync::Arc, time::Duration};
2
3use saluki_common::{
4 cache::{weight::ItemCountWeighter, Cache, CacheBuilder},
5 collections::PrehashedHashSet,
6 hash::NoopU64BuildHasher,
7};
8use saluki_error::{generic_error, GenericError};
9use saluki_metrics::{static_metrics, Counter, Gauge};
10use stringtheory::{
11 interning::{GenericMapInterner, Interner as _},
12 CheapMetaString, MetaString,
13};
14use tokio::time::sleep;
15use tracing::debug;
16
17use crate::{
18 context::{Context, ContextInner},
19 hash::{hash_context_with_host_and_seen, ContextKey, TagSetKey},
20 origin::{OriginTagsResolver, RawOrigin},
21 tags::{SharedTagSet, TagSet},
22};
23
24const DEFAULT_CONTEXT_RESOLVER_CACHED_CONTEXTS_LIMIT: NonZeroUsize = NonZeroUsize::new(500_000).unwrap();
26
27const DEFAULT_CONTEXT_RESOLVER_INTERNER_CAPACITY_BYTES: NonZeroUsize = NonZeroUsize::new(2 * 1024 * 1024).unwrap();
29
30const SEEN_HASHSET_INITIAL_CAPACITY: usize = 128;
31
32type ContextCache = Cache<ContextKey, Context, ItemCountWeighter, NoopU64BuildHasher>;
33type TagSetCache = Cache<TagSetKey, SharedTagSet, ItemCountWeighter, NoopU64BuildHasher>;
34
35#[static_metrics(prefix = context_resolver, labels(resolver_id))]
36#[derive(Clone)]
37struct Telemetry {
38 interner_capacity_bytes: Gauge,
39 interner_len_bytes: Gauge,
40 interner_entries: Gauge,
41 #[metric(level = debug)]
42 intern_fallback_total: Counter,
43 #[metric(level = debug)]
44 resolved_existing_context_total: Counter,
45 #[metric(level = debug)]
46 resolved_new_context_total: Counter,
47 active_contexts: Gauge,
48 #[metric(level = debug)]
49 resolved_existing_tagset_total: Counter,
50 #[metric(level = debug)]
51 resolved_new_tagset_total: Counter,
52}
53
54pub struct ContextResolverBuilder {
60 name: String,
61 caching_enabled: bool,
62 cached_contexts_limit: Option<NonZeroUsize>,
63 idle_context_expiration: Option<Duration>,
64 interner_capacity_bytes: Option<NonZeroUsize>,
65 allow_heap_allocations: Option<bool>,
66 tags_resolver: Option<TagsResolver>,
67 interner: Option<GenericMapInterner>,
68 origin_tags_resolver: Option<Arc<dyn OriginTagsResolver>>,
69 telemetry_enabled: bool,
70}
71
72impl ContextResolverBuilder {
73 pub fn from_name<S: Into<String>>(name: S) -> Result<Self, GenericError> {
83 let name = name.into();
84 if name.is_empty() {
85 return Err(generic_error!("resolver name must not be empty"));
86 }
87
88 Ok(Self {
89 name,
90 caching_enabled: true,
91 cached_contexts_limit: None,
92 idle_context_expiration: None,
93 interner_capacity_bytes: None,
94 allow_heap_allocations: None,
95 tags_resolver: None,
96 interner: None,
97 origin_tags_resolver: None,
98 telemetry_enabled: true,
99 })
100 }
101
102 pub fn without_caching(mut self) -> Self {
117 self.caching_enabled = false;
118 self.idle_context_expiration = None;
119 self
120 }
121
122 pub fn with_cached_contexts_limit(mut self, limit: usize) -> Self {
137 match NonZeroUsize::new(limit) {
138 Some(limit) => {
139 self.cached_contexts_limit = Some(limit);
140 self
141 }
142 None => self.without_caching(),
143 }
144 }
145
146 pub fn with_idle_context_expiration(mut self, time_to_idle: Duration) -> Self {
155 self.idle_context_expiration = Some(time_to_idle);
156 self
157 }
158
159 pub fn with_interner_capacity_bytes(mut self, capacity: NonZeroUsize) -> Self {
175 self.interner_capacity_bytes = Some(capacity);
176 self
177 }
178
179 pub fn with_heap_allocations(mut self, allow: bool) -> Self {
188 self.allow_heap_allocations = Some(allow);
189 self
190 }
191
192 pub fn with_tags_resolver(mut self, resolver: Option<TagsResolver>) -> Self {
196 self.tags_resolver = resolver;
197 self
198 }
199
200 pub fn without_telemetry(mut self) -> Self {
209 self.telemetry_enabled = false;
210 self
211 }
212
213 pub fn with_interner(mut self, interner: GenericMapInterner) -> Self {
217 self.interner = Some(interner);
218 self
219 }
220
221 pub fn for_tests() -> Self {
234 ContextResolverBuilder::from_name("noop")
235 .expect("resolver name not empty")
236 .with_cached_contexts_limit(usize::MAX)
237 .with_interner_capacity_bytes(NonZeroUsize::new(1).expect("not zero"))
238 .with_heap_allocations(true)
239 .with_tags_resolver(Some(TagsResolverBuilder::for_tests().build()))
240 .without_telemetry()
241 }
242
243 pub fn build(self) -> ContextResolver {
245 let interner_capacity_bytes = self
246 .interner_capacity_bytes
247 .unwrap_or(DEFAULT_CONTEXT_RESOLVER_INTERNER_CAPACITY_BYTES);
248
249 let interner = match self.interner {
250 Some(interner) => interner,
251 None => GenericMapInterner::new(interner_capacity_bytes),
252 };
253
254 let cached_context_limit = self
255 .cached_contexts_limit
256 .unwrap_or(DEFAULT_CONTEXT_RESOLVER_CACHED_CONTEXTS_LIMIT);
257
258 let allow_heap_allocations = self.allow_heap_allocations.unwrap_or(true);
259
260 let telemetry = Telemetry::new(&self.name);
261 telemetry
262 .interner_capacity_bytes()
263 .set(interner.capacity_bytes() as f64);
264
265 let context_cache = CacheBuilder::from_identifier(format!("{}/contexts", self.name))
267 .expect("cache identifier cannot possibly be empty")
268 .with_capacity(cached_context_limit)
269 .with_time_to_idle(self.idle_context_expiration)
270 .with_hasher::<NoopU64BuildHasher>()
271 .with_telemetry(self.telemetry_enabled)
272 .build();
273
274 let tags_resolver = match self.tags_resolver {
276 Some(tags_resolver) => tags_resolver,
277 None => TagsResolverBuilder::new(format!("{}/tags", self.name), interner.clone())
278 .expect("tags resolver name not empty")
279 .with_cached_tagsets_limit(cached_context_limit.get())
280 .with_idle_tagsets_expiration(self.idle_context_expiration.unwrap_or_default())
281 .with_heap_allocations(allow_heap_allocations)
282 .with_origin_tags_resolver(self.origin_tags_resolver.clone())
283 .build(),
284 };
285
286 if self.telemetry_enabled {
287 tokio::spawn(drive_telemetry(interner.clone(), telemetry.clone()));
288 }
289
290 ContextResolver {
291 telemetry,
292 interner,
293 caching_enabled: self.caching_enabled,
294 context_cache,
295 hash_seen_buffer: PrehashedHashSet::with_capacity_and_hasher(
296 SEEN_HASHSET_INITIAL_CAPACITY,
297 NoopU64BuildHasher,
298 ),
299 allow_heap_allocations,
300 tags_resolver,
301 }
302 }
303}
304
305pub struct ContextResolver {
329 telemetry: Telemetry,
330 interner: GenericMapInterner,
331 caching_enabled: bool,
332 context_cache: ContextCache,
333 hash_seen_buffer: PrehashedHashSet<u64>,
334 allow_heap_allocations: bool,
335 tags_resolver: TagsResolver,
336}
337
338impl ContextResolver {
339 fn intern<S>(&self, s: S) -> Option<MetaString>
340 where
341 S: AsRef<str> + CheapMetaString,
342 {
343 s.try_cheap_clone()
346 .or_else(|| self.interner.try_intern(s.as_ref()).map(MetaString::from))
347 .or_else(|| {
348 self.allow_heap_allocations.then(|| {
349 saluki_antithesis::sometimes!(
353 true,
354 "context string interner spilled to the heap (unbounded under default config)"
355 );
356 self.telemetry.intern_fallback_total().increment(1);
357 MetaString::from(s.as_ref())
358 })
359 })
360 }
361
362 fn create_context_key_with_host<N, H, I, I2, T, T2>(
363 &mut self, name: N, host: Option<&H>, tags: I, origin_tags: I2,
364 ) -> (ContextKey, TagSetKey)
365 where
366 N: AsRef<str>,
367 H: AsRef<str>,
368 I: IntoIterator<Item = T>,
369 T: AsRef<str>,
370 I2: IntoIterator<Item = T2>,
371 T2: AsRef<str>,
372 {
373 hash_context_with_host_and_seen(
374 name.as_ref(),
375 host.map(AsRef::as_ref),
376 tags,
377 origin_tags,
378 &mut self.hash_seen_buffer,
379 )
380 }
381
382 fn create_context<N, H>(
383 &self, key: ContextKey, name: N, host: Option<H>, context_tags: SharedTagSet, origin_tags: SharedTagSet,
384 ) -> Option<Context>
385 where
386 N: AsRef<str> + CheapMetaString,
387 H: AsRef<str> + CheapMetaString,
388 {
389 let context_name = self.intern(name)?;
391 let context_host = match host {
392 Some(host) => Some(self.intern(host)?),
393 None => None,
394 };
395
396 self.telemetry.resolved_new_context_total().increment(1);
397 self.telemetry.active_contexts().increment(1);
398
399 Some(Context::from_inner(ContextInner::from_parts(
400 key,
401 context_name,
402 context_host,
403 context_tags.into(),
404 origin_tags.into(),
405 self.telemetry.active_contexts().clone(),
406 )))
407 }
408
409 pub fn resolve<N, I, T>(&mut self, name: N, tags: I, maybe_origin: Option<RawOrigin<'_>>) -> Option<Context>
418 where
419 N: AsRef<str> + CheapMetaString,
420 I: IntoIterator<Item = T> + Clone,
421 T: AsRef<str> + CheapMetaString,
422 {
423 let origin_tags = self.tags_resolver.resolve_origin_tags(maybe_origin);
425
426 self.resolve_inner(name, tags, origin_tags)
427 }
428
429 pub fn resolve_with_origin_tags<N, I, T>(
448 &mut self, name: N, tags: I, origin_tags: impl Into<SharedTagSet>,
449 ) -> Option<Context>
450 where
451 N: AsRef<str> + CheapMetaString,
452 I: IntoIterator<Item = T> + Clone,
453 T: AsRef<str> + CheapMetaString,
454 {
455 self.resolve_inner(name, tags, origin_tags.into())
456 }
457
458 pub fn resolve_with_host_and_origin_tags<N, H, I, T>(
460 &mut self, name: N, host: H, tags: I, origin_tags: impl Into<SharedTagSet>,
461 ) -> Option<Context>
462 where
463 N: AsRef<str> + CheapMetaString,
464 H: AsRef<str> + CheapMetaString,
465 I: IntoIterator<Item = T> + Clone,
466 T: AsRef<str> + CheapMetaString,
467 {
468 self.resolve_inner_with_host(name, Some(host), tags, origin_tags.into())
469 }
470
471 pub fn resolve_with_optional_host_and_origin_tags<N, H, I, T>(
473 &mut self, name: N, host: Option<H>, tags: I, origin_tags: impl Into<SharedTagSet>,
474 ) -> Option<Context>
475 where
476 N: AsRef<str> + CheapMetaString,
477 H: AsRef<str> + CheapMetaString,
478 I: IntoIterator<Item = T> + Clone,
479 T: AsRef<str> + CheapMetaString,
480 {
481 self.resolve_inner_with_host(name, host, tags, origin_tags.into())
482 }
483
484 pub fn resolve_with_host<N, H, I, T>(
488 &mut self, name: N, host: H, tags: I, maybe_origin: Option<RawOrigin<'_>>,
489 ) -> Option<Context>
490 where
491 N: AsRef<str> + CheapMetaString,
492 H: AsRef<str> + CheapMetaString,
493 I: IntoIterator<Item = T> + Clone,
494 T: AsRef<str> + CheapMetaString,
495 {
496 self.resolve_with_optional_host(name, Some(host), tags, maybe_origin)
497 }
498
499 pub fn resolve_with_optional_host<N, H, I, T>(
501 &mut self, name: N, host: Option<H>, tags: I, maybe_origin: Option<RawOrigin<'_>>,
502 ) -> Option<Context>
503 where
504 N: AsRef<str> + CheapMetaString,
505 H: AsRef<str> + CheapMetaString,
506 I: IntoIterator<Item = T> + Clone,
507 T: AsRef<str> + CheapMetaString,
508 {
509 let origin_tags = self.tags_resolver.resolve_origin_tags(maybe_origin);
510
511 self.resolve_inner_with_host(name, host, tags, origin_tags)
512 }
513
514 fn resolve_inner<N, I, T>(&mut self, name: N, tags: I, origin_tags: SharedTagSet) -> Option<Context>
515 where
516 N: AsRef<str> + CheapMetaString,
517 I: IntoIterator<Item = T> + Clone,
518 T: AsRef<str> + CheapMetaString,
519 {
520 self.resolve_inner_with_host(name, None::<&str>, tags, origin_tags)
521 }
522
523 fn resolve_inner_with_host<N, H, I, T>(
524 &mut self, name: N, host: Option<H>, tags: I, origin_tags: SharedTagSet,
525 ) -> Option<Context>
526 where
527 N: AsRef<str> + CheapMetaString,
528 H: AsRef<str> + CheapMetaString,
529 I: IntoIterator<Item = T> + Clone,
530 T: AsRef<str> + CheapMetaString,
531 {
532 let (context_key, tagset_key) =
533 self.create_context_key_with_host(&name, host.as_ref(), tags.clone(), &origin_tags);
534
535 if !self.caching_enabled {
537 let tag_set = self.tags_resolver.create_tag_set(tags).unwrap_or_default();
538
539 let context = self.create_context(context_key, name, host, tag_set, origin_tags)?;
540
541 debug!(?context_key, ?context, "Resolved new non-cached context.");
542 return Some(context);
543 }
544
545 match self.context_cache.get(&context_key) {
546 Some(context) => {
547 self.telemetry.resolved_existing_context_total().increment(1);
548 Some(context)
549 }
550 None => {
551 let tag_set = match self.tags_resolver.get_tag_set(tagset_key) {
553 Some(tag_set) => {
554 self.telemetry.resolved_existing_tagset_total().increment(1);
555 tag_set
556 }
557 None => {
558 let tag_set = self.tags_resolver.create_tag_set(tags.clone()).unwrap_or_default();
560
561 self.tags_resolver.insert_tag_set(tagset_key, tag_set.clone());
562
563 tag_set
564 }
565 };
566
567 let context = self.create_context(context_key, name, host, tag_set, origin_tags)?;
568 self.context_cache.insert(context_key, context.clone());
569
570 debug!(?context_key, ?context, "Resolved new context.");
571 Some(context)
572 }
573 }
574 }
575}
576
577impl Clone for ContextResolver {
578 fn clone(&self) -> Self {
579 Self {
580 telemetry: self.telemetry.clone(),
581 interner: self.interner.clone(),
582 caching_enabled: self.caching_enabled,
583 context_cache: self.context_cache.clone(),
584 hash_seen_buffer: PrehashedHashSet::with_capacity_and_hasher(
585 SEEN_HASHSET_INITIAL_CAPACITY,
586 NoopU64BuildHasher,
587 ),
588 allow_heap_allocations: self.allow_heap_allocations,
589 tags_resolver: self.tags_resolver.clone(),
590 }
591 }
592}
593
594async fn drive_telemetry(interner: GenericMapInterner, telemetry: Telemetry) {
595 loop {
596 sleep(Duration::from_secs(1)).await;
597
598 telemetry.interner_entries().set(interner.len() as f64);
599 telemetry
600 .interner_capacity_bytes()
601 .set(interner.capacity_bytes() as f64);
602 telemetry.interner_len_bytes().set(interner.len_bytes() as f64);
603 }
604}
605
606pub struct TagsResolverBuilder {
608 name: String,
609 caching_enabled: bool,
610 cached_tagset_limit: Option<NonZeroUsize>,
611 idle_tagset_expiration: Option<Duration>,
612 allow_heap_allocations: Option<bool>,
613 origin_tags_resolver: Option<Arc<dyn OriginTagsResolver>>,
614 telemetry_enabled: bool,
615 interner: GenericMapInterner,
616}
617
618impl TagsResolverBuilder {
619 pub fn new<S: Into<String>>(name: S, interner: GenericMapInterner) -> Result<Self, GenericError> {
621 let name = name.into();
622 if name.is_empty() {
623 return Err(generic_error!("resolver name must not be empty"));
624 }
625
626 Ok(Self {
627 name,
628 caching_enabled: true,
629 cached_tagset_limit: None,
630 idle_tagset_expiration: None,
631 allow_heap_allocations: None,
632 origin_tags_resolver: None,
633 telemetry_enabled: true,
634 interner,
635 })
636 }
637
638 pub fn with_interner(mut self, interner: GenericMapInterner) -> Self {
644 self.interner = interner;
645 self
646 }
647
648 pub fn without_caching(mut self) -> Self {
663 self.caching_enabled = false;
664 self.idle_tagset_expiration = None;
665 self
666 }
667
668 pub fn with_cached_tagsets_limit(mut self, limit: usize) -> Self {
683 match NonZeroUsize::new(limit) {
684 Some(limit) => {
685 self.cached_tagset_limit = Some(limit);
686 self
687 }
688 None => self.without_caching(),
689 }
690 }
691
692 pub fn with_idle_tagsets_expiration(mut self, time_to_idle: Duration) -> Self {
701 self.idle_tagset_expiration = Some(time_to_idle);
702 self
703 }
704
705 pub fn with_heap_allocations(mut self, allow: bool) -> Self {
714 self.allow_heap_allocations = Some(allow);
715 self
716 }
717
718 pub fn with_origin_tags_resolver(mut self, resolver: Option<Arc<dyn OriginTagsResolver>>) -> Self {
731 self.origin_tags_resolver = resolver;
732 self
733 }
734
735 pub fn without_telemetry(mut self) -> Self {
744 self.telemetry_enabled = false;
745 self
746 }
747
748 pub fn build(self) -> TagsResolver {
750 let cached_tagsets_limit = self
751 .cached_tagset_limit
752 .unwrap_or(DEFAULT_CONTEXT_RESOLVER_CACHED_CONTEXTS_LIMIT);
753
754 let allow_heap_allocations = self.allow_heap_allocations.unwrap_or(true);
755
756 let telemetry = Telemetry::new(self.name.clone());
757 telemetry
758 .interner_capacity_bytes()
759 .set(self.interner.capacity_bytes() as f64);
760
761 let tagset_cache = CacheBuilder::from_identifier(format!("{}/tagsets", self.name))
762 .expect("cache identifier cannot possibly be empty")
763 .with_capacity(cached_tagsets_limit)
764 .with_time_to_idle(self.idle_tagset_expiration)
765 .with_hasher::<NoopU64BuildHasher>()
766 .with_telemetry(self.telemetry_enabled)
767 .build();
768
769 TagsResolver {
770 telemetry,
771 interner: self.interner,
772 caching_enabled: self.caching_enabled,
773 tagset_cache,
774 origin_tags_resolver: self.origin_tags_resolver,
775 allow_heap_allocations,
776 }
777 }
778
779 pub fn for_tests() -> Self {
792 TagsResolverBuilder::new("noop", GenericMapInterner::new(NonZeroUsize::new(1).expect("not zero")))
793 .expect("resolver name not empty")
794 .with_cached_tagsets_limit(usize::MAX)
795 .with_heap_allocations(true)
796 .without_telemetry()
797 }
798}
799
800pub struct TagsResolver {
802 telemetry: Telemetry,
803 interner: GenericMapInterner,
804 caching_enabled: bool,
805 tagset_cache: TagSetCache,
806 origin_tags_resolver: Option<Arc<dyn OriginTagsResolver>>,
807 allow_heap_allocations: bool,
808}
809
810impl TagsResolver {
811 fn intern<S>(&self, s: S) -> Option<MetaString>
812 where
813 S: AsRef<str> + CheapMetaString,
814 {
815 s.try_cheap_clone()
818 .or_else(|| self.interner.try_intern(s.as_ref()).map(MetaString::from))
819 .or_else(|| {
820 self.allow_heap_allocations.then(|| {
821 saluki_antithesis::sometimes!(
825 true,
826 "tag string interner spilled to the heap (unbounded under default config)"
827 );
828 self.telemetry.intern_fallback_total().increment(1);
829 MetaString::from(s.as_ref())
830 })
831 })
832 }
833
834 pub fn create_tag_set<I, T>(&mut self, tags: I) -> Option<SharedTagSet>
841 where
842 I: IntoIterator<Item = T>,
843 T: AsRef<str> + CheapMetaString,
844 {
845 let mut tag_set = TagSet::default();
846 for tag in tags {
847 let tag = self.intern(tag)?;
848 tag_set.insert_tag(tag);
849 }
850
851 self.telemetry.resolved_new_tagset_total().increment(1);
852
853 Some(tag_set.into_shared())
854 }
855
856 pub fn resolve_origin_tags(&self, maybe_origin: Option<RawOrigin<'_>>) -> SharedTagSet {
860 self.origin_tags_resolver
861 .as_ref()
862 .and_then(|resolver| maybe_origin.map(|origin| resolver.resolve_origin_tags(origin)))
863 .unwrap_or_default()
864 }
865
866 fn get_tag_set(&self, key: TagSetKey) -> Option<SharedTagSet> {
867 self.tagset_cache.get(&key)
868 }
869
870 fn insert_tag_set(&self, key: TagSetKey, tag_set: SharedTagSet) {
871 self.tagset_cache.insert(key, tag_set);
872 }
873}
874
875impl Clone for TagsResolver {
876 fn clone(&self) -> Self {
877 Self {
878 telemetry: self.telemetry.clone(),
879 interner: self.interner.clone(),
880 caching_enabled: self.caching_enabled,
881 tagset_cache: self.tagset_cache.clone(),
882 origin_tags_resolver: self.origin_tags_resolver.clone(),
883 allow_heap_allocations: self.allow_heap_allocations,
884 }
885 }
886}
887
888#[cfg(test)]
889mod tests {
890 use metrics::{SharedString, Unit};
891 use metrics_util::{
892 debugging::{DebugValue, DebuggingRecorder},
893 CompositeKey,
894 };
895 use saluki_common::hash::hash_single_fast;
896
897 use super::*;
898 use crate::tags::Tag;
899
900 fn get_gauge_value(metrics: &[(CompositeKey, Option<Unit>, Option<SharedString>, DebugValue)], key: &str) -> f64 {
901 metrics
902 .iter()
903 .find(|(k, _, _, _)| k.key().name() == key)
904 .map(|(_, _, _, value)| match value {
905 DebugValue::Gauge(value) => value.into_inner(),
906 other => panic!("expected a gauge, got: {:?}", other),
907 })
908 .unwrap_or_else(|| panic!("no metric found with key: {}", key))
909 }
910
911 struct DummyOriginTagsResolver;
912
913 impl OriginTagsResolver for DummyOriginTagsResolver {
914 fn resolve_origin_tags(&self, origin: RawOrigin<'_>) -> SharedTagSet {
915 let origin_key = hash_single_fast(origin);
916
917 let mut tags = TagSet::default();
918 tags.insert_tag(format!("origin_key:{}", origin_key));
919 tags.into_shared()
920 }
921 }
922
923 #[test]
924 fn basic() {
925 let mut resolver = ContextResolverBuilder::for_tests().build();
926
927 let name = "metric_name";
929 let tags1: [&str; 0] = [];
930 let tags2 = ["tag1"];
931
932 assert_ne!(&tags1[..], &tags2[..]);
933
934 let context1 = resolver
935 .resolve(name, &tags1[..], None)
936 .expect("should not fail to resolve");
937 let context2 = resolver
938 .resolve(name, &tags2[..], None)
939 .expect("should not fail to resolve");
940
941 assert_ne!(context1, context2);
944 assert!(!context1.ptr_eq(&context2));
945
946 let context1_redo = resolver
948 .resolve(name, &tags1[..], None)
949 .expect("should not fail to resolve");
950 let context2_redo = resolver
951 .resolve(name, &tags2[..], None)
952 .expect("should not fail to resolve");
953
954 assert_ne!(context1_redo, context2_redo);
955 assert_eq!(context1, context1_redo);
956 assert_eq!(context2, context2_redo);
957 assert!(context1.ptr_eq(&context1_redo));
958 assert!(context2.ptr_eq(&context2_redo));
959 }
960
961 #[test]
962 fn tag_order() {
963 let mut resolver = ContextResolverBuilder::for_tests().build();
964
965 let name = "metric_name";
967 let tags1 = ["tag1", "tag2"];
968 let tags2 = ["tag2", "tag1"];
969
970 assert_ne!(&tags1[..], &tags2[..]);
971
972 let context1 = resolver
973 .resolve(name, &tags1[..], None)
974 .expect("should not fail to resolve");
975 let context2 = resolver
976 .resolve(name, &tags2[..], None)
977 .expect("should not fail to resolve");
978
979 assert_eq!(context1, context2);
982 assert!(context1.ptr_eq(&context2));
983 }
984
985 #[test]
986 fn host_affects_identity_but_not_visible_tags() {
987 let mut resolver = ContextResolverBuilder::for_tests().build();
988
989 let context1 = resolver
990 .resolve_with_host("metric_name", "host-a", &[] as &[&str], None)
991 .expect("should not fail to resolve");
992 let context2 = resolver
993 .resolve_with_host("metric_name", "host-b", &[] as &[&str], None)
994 .expect("should not fail to resolve");
995 let context1_redo = resolver
996 .resolve_with_host("metric_name", "host-a", &[] as &[&str], None)
997 .expect("should not fail to resolve");
998
999 assert_ne!(context1, context2);
1000 assert_eq!(context1, context1_redo);
1001 assert!(context1.ptr_eq(&context1_redo));
1002 assert_eq!(context1.host(), Some("host-a"));
1003 assert_eq!(context2.host(), Some("host-b"));
1004 assert!(context1.tags().is_empty());
1005 assert!(context2.tags().is_empty());
1006
1007 let mut uncached_resolver = ContextResolverBuilder::for_tests().without_caching().build();
1008 let uncached1 = uncached_resolver
1009 .resolve_with_host("metric_name", "host-a", &[] as &[&str], None)
1010 .expect("should not fail to resolve");
1011 let uncached2 = uncached_resolver
1012 .resolve_with_host("metric_name", "host-b", &[] as &[&str], None)
1013 .expect("should not fail to resolve");
1014
1015 assert_ne!(uncached1, uncached2);
1016 assert!(!uncached1.ptr_eq(&uncached2));
1017 }
1018
1019 #[test]
1020 fn host_survives_rewrites() {
1021 let mut resolver = ContextResolverBuilder::for_tests().build();
1022
1023 let context1 = resolver
1024 .resolve_with_host("metric_name", "host-a", &["env:prod"][..], None)
1025 .expect("should not fail to resolve");
1026 let context2 = resolver
1027 .resolve_with_host("metric_name", "host-b", &["env:prod"][..], None)
1028 .expect("should not fail to resolve");
1029
1030 assert_ne!(context1, context2);
1031 let service_tag_set = TagSet::from_iter([Tag::from("service:api")]);
1032 assert_ne!(context1.with_name("renamed"), context2.with_name("renamed"));
1033 assert_ne!(
1034 context1.with_tags(service_tag_set.clone()),
1035 context2.with_tags(service_tag_set)
1036 );
1037
1038 let mut context1_filtered = context1.clone();
1039 let mut context2_filtered = context2.clone();
1040 let mut state1 = crate::context::TagSetMutViewState::new();
1041 let mut state2 = crate::context::TagSetMutViewState::new();
1042 {
1043 let mut view = context1_filtered.tags_mut_view(&mut state1);
1044 view.retain_tags(|_| false);
1045 view.finish();
1046 }
1047 {
1048 let mut view = context2_filtered.tags_mut_view(&mut state2);
1049 view.retain_tags(|_| false);
1050 view.finish();
1051 }
1052
1053 assert_ne!(context1_filtered, context2_filtered);
1054 assert!(context1_filtered.tags().is_empty());
1055 assert!(context2_filtered.tags().is_empty());
1056 assert_eq!(context1_filtered.host(), Some("host-a"));
1057 assert_eq!(context2_filtered.host(), Some("host-b"));
1058 }
1059
1060 #[test]
1061 fn active_contexts() {
1062 let recorder = DebuggingRecorder::new();
1063 let snapshotter = recorder.snapshotter();
1064
1065 let context = metrics::with_local_recorder(&recorder, || {
1067 let mut resolver = ContextResolverBuilder::for_tests().build();
1068 resolver
1069 .resolve("name", &["tag"][..], None)
1070 .expect("should not fail to resolve")
1071 });
1072
1073 let metrics_before = snapshotter.snapshot().into_vec();
1075 let active_contexts = get_gauge_value(&metrics_before, Telemetry::active_contexts_name());
1076 assert_eq!(active_contexts, 1.0);
1077
1078 drop(context);
1080 let metrics_after = snapshotter.snapshot().into_vec();
1081 let active_contexts = get_gauge_value(&metrics_after, Telemetry::active_contexts_name());
1082 assert_eq!(active_contexts, -1.0);
1083 }
1084
1085 #[test]
1086 fn duplicate_tags() {
1087 let mut resolver = ContextResolverBuilder::for_tests().build();
1088
1089 let name = "metric_name";
1091 let tags1 = ["tag1"];
1092 let tags1_duplicated = ["tag1", "tag1"];
1093 let tags2 = ["tag2"];
1094 let tags2_duplicated = ["tag2", "tag2"];
1095
1096 let context1 = resolver
1097 .resolve(name, &tags1[..], None)
1098 .expect("should not fail to resolve");
1099 let context1_duplicated = resolver
1100 .resolve(name, &tags1_duplicated[..], None)
1101 .expect("should not fail to resolve");
1102 let context2 = resolver
1103 .resolve(name, &tags2[..], None)
1104 .expect("should not fail to resolve");
1105 let context2_duplicated = resolver
1106 .resolve(name, &tags2_duplicated[..], None)
1107 .expect("should not fail to resolve");
1108
1109 assert_eq!(context1, context1_duplicated);
1111 assert_eq!(context2, context2_duplicated);
1112
1113 assert_ne!(context1, context2);
1122 assert_ne!(context1_duplicated, context2_duplicated);
1123 assert_ne!(context1, context2_duplicated);
1124 assert_ne!(context2, context1_duplicated);
1125 }
1126
1127 #[test]
1128 fn differing_origins_with_without_resolver() {
1129 let mut resolver = ContextResolverBuilder::for_tests().build();
1132
1133 let name = "metric_name";
1134 let tags = ["tag1"];
1135 let mut origin1 = RawOrigin::default();
1136 origin1.set_local_data("container1");
1137 let mut origin2 = RawOrigin::default();
1138 origin2.set_local_data("container2");
1139
1140 let context1 = resolver
1141 .resolve(name, &tags[..], Some(origin1.clone()))
1142 .expect("should not fail to resolve");
1143 let context2 = resolver
1144 .resolve(name, &tags[..], Some(origin2.clone()))
1145 .expect("should not fail to resolve");
1146
1147 assert_eq!(context1, context2);
1148
1149 let tags_resolver = TagsResolverBuilder::for_tests()
1150 .with_origin_tags_resolver(Some(Arc::new(DummyOriginTagsResolver)))
1151 .build();
1152 let mut resolver = ContextResolverBuilder::for_tests()
1156 .with_tags_resolver(Some(tags_resolver))
1157 .build();
1158
1159 let context1 = resolver
1160 .resolve(name, &tags[..], Some(origin1))
1161 .expect("should not fail to resolve");
1162 let context2 = resolver
1163 .resolve(name, &tags[..], Some(origin2))
1164 .expect("should not fail to resolve");
1165
1166 assert_ne!(context1, context2);
1167 }
1168
1169 #[test]
1170 fn caching_disabled() {
1171 let tags_resolver = TagsResolverBuilder::for_tests()
1172 .with_origin_tags_resolver(Some(Arc::new(DummyOriginTagsResolver)))
1173 .build();
1174 let mut resolver = ContextResolverBuilder::for_tests()
1175 .without_caching()
1176 .with_tags_resolver(Some(tags_resolver))
1177 .build();
1178
1179 let name = "metric_name";
1180 let tags = ["tag1"];
1181 let mut origin1 = RawOrigin::default();
1182 origin1.set_local_data("container1");
1183
1184 let context1 = resolver
1186 .resolve(name, &tags[..], Some(origin1.clone()))
1187 .expect("should not fail to resolve");
1188 assert_eq!(resolver.context_cache.len(), 0);
1189
1190 let context2 = resolver
1192 .resolve(name, &tags[..], Some(origin1))
1193 .expect("should not fail to resolve");
1194 assert_eq!(resolver.context_cache.len(), 0);
1195
1196 assert_eq!(context1, context2);
1199 assert!(!context1.ptr_eq(&context2));
1200 }
1201
1202 #[test]
1203 fn cheaply_cloneable_name_and_tags() {
1204 const BIG_TAG_ONE: &str = "long-tag-that-cannot-be-inlined-just-to-be-doubly-sure-on-top-of-being-static";
1205 const BIG_TAG_TWO: &str = "another-long-boye-that-we-are-also-sure-wont-be-inlined-and-we-stand-on-that";
1206
1207 let mut resolver = ContextResolverBuilder::for_tests()
1209 .with_interner_capacity_bytes(NonZeroUsize::new(1024).expect("not zero"))
1210 .build();
1211
1212 let name = MetaString::from_static("long-metric-name-that-shouldnt-be-inlined-and-should-end-up-interned");
1214 let tags = [
1215 MetaString::from_static(BIG_TAG_ONE),
1216 MetaString::from_static(BIG_TAG_TWO),
1217 ];
1218 assert!(tags[0].is_cheaply_cloneable());
1219 assert!(tags[1].is_cheaply_cloneable());
1220
1221 assert_eq!(resolver.interner.len(), 0);
1224 assert_eq!(resolver.interner.len_bytes(), 0);
1225
1226 let context = resolver
1227 .resolve(&name, &tags[..], None)
1228 .expect("should not fail to resolve");
1229 assert_eq!(resolver.interner.len(), 0);
1230 assert_eq!(resolver.interner.len_bytes(), 0);
1231
1232 assert_eq!(context.name(), &name);
1234
1235 let context_tags = context.tags();
1236 assert_eq!(context_tags.len(), 2);
1237 assert!(context_tags.has_tag(&tags[0]));
1238 assert!(context_tags.has_tag(&tags[1]));
1239 }
1240}