saluki_context/hash.rs
1use std::hash::{Hash as _, Hasher as _};
2
3use saluki_common::{
4 collections::PrehashedHashSet,
5 hash::{get_fast_hasher, hash_single_fast},
6};
7
8/// Hashes a metric context.
9///
10/// Takes a metric name, an iterator of tags, and an iterator of origin tags, and returns a tuple containing a unique
11/// hash key for the overall context, and a unique hash key for the non-origin tags by themselves.
12///
13/// All tags are hashed in an order-oblivious (XOR) manner, which allows tags to be hashed in any order while still
14/// resulting in the same overall hash. This function is _not_ oblivious to the actual tag values themselves, though, so
15/// differences such as case (lower vs upper) or leading/trailing whitespace will influence the resulting hash.
16///
17/// If a tag is seen more than once, it will be ignored and not included in the overall hash. This function allocates a
18/// hash set in order to track which tags have already been hashed, so it's preferable to allocate a single
19/// [`PrehashedHashSet`] and use it with [`hash_context_with_host_and_seen`] in order to amortize the cost of allocating
20/// the hash set.
21///
22/// Returns a hash that uniquely identifies the combination of name, tags, and origin of the value.
23pub fn hash_context<I, I2, T, T2>(name: &str, tags: I, origin_tags: I2) -> (ContextKey, TagSetKey)
24where
25 I: IntoIterator<Item = T>,
26 T: AsRef<str>,
27 I2: IntoIterator<Item = T2>,
28 T2: AsRef<str>,
29{
30 let mut seen = PrehashedHashSet::default();
31 hash_context_with_host_and_seen(name, None, tags, origin_tags, &mut seen)
32}
33
34/// Hashes a metric context with an explicit host dimension, using a provided set to track duplicate tags.
35///
36/// Takes a metric name, optional host, an iterator of tags, and an iterator of origin tags, and returns a tuple
37/// containing a unique hash key for the overall context, and a unique hash key for the non-origin tags by themselves.
38/// The host is considered part of overall context identity, but not part of the non-origin tag set.
39///
40/// All tags are hashed in an order-oblivious (XOR) manner, which allows tags to be hashed in any order while still
41/// resulting in the same overall hash. This function is _not_ oblivious to the actual tag values themselves, though, so
42/// differences such as case (lower vs upper) or leading/trailing whitespace will influence the resulting hash. The host
43/// and metric name are order-dependent scalar context fields, not tags.
44///
45/// If a tag is seen more than once, it will be ignored and not included in the overall hash. This function requires the
46/// caller to provide the hash set used for tracking duplicates, and is more efficient than `hash_context` which
47/// allocates a new hash set each time.
48///
49/// Returns a hash that uniquely identifies the combination of name, host, tags, and origin of the value. An unset host
50/// and an explicitly empty host are distinct contexts.
51pub fn hash_context_with_host<I, I2, T, T2>(
52 name: &str, host: Option<&str>, tags: I, origin_tags: I2,
53) -> (ContextKey, TagSetKey)
54where
55 I: IntoIterator<Item = T>,
56 T: AsRef<str>,
57 I2: IntoIterator<Item = T2>,
58 T2: AsRef<str>,
59{
60 let mut seen = PrehashedHashSet::default();
61 hash_context_with_host_and_seen(name, host, tags, origin_tags, &mut seen)
62}
63
64pub(super) fn hash_context_with_host_and_seen<I, I2, T, T2>(
65 name: &str, host: Option<&str>, tags: I, origin_tags: I2, seen: &mut PrehashedHashSet<u64>,
66) -> (ContextKey, TagSetKey)
67where
68 I: IntoIterator<Item = T>,
69 T: AsRef<str>,
70 I2: IntoIterator<Item = T2>,
71 T2: AsRef<str>,
72{
73 seen.clear();
74
75 let mut hasher = get_fast_hasher();
76
77 // Hash the metric name and host.
78 name.hash(&mut hasher);
79 host.hash(&mut hasher);
80
81 // Hash the metric tags individually and XOR their hashes together, which allows us to be order-oblivious:
82 let mut combined_tags_hash = 0;
83
84 for tag in tags {
85 let tag_hash = hash_single_fast(tag.as_ref());
86
87 // If we've already seen this tag before, skip combining it again.
88 if !seen.insert(tag_hash) {
89 continue;
90 }
91
92 combined_tags_hash ^= tag_hash;
93 }
94
95 hasher.write_u64(combined_tags_hash);
96
97 // Finally, hash the origin tags.
98 //
99 // We also hash these in an order-oblivious manner.
100 seen.clear();
101 let mut combined_origin_tags_hash = 0;
102
103 for tag in origin_tags {
104 let tag_hash = hash_single_fast(tag.as_ref());
105
106 // If we've already seen this tag before, skip combining it again.
107 if !seen.insert(tag_hash) {
108 continue;
109 }
110
111 combined_origin_tags_hash ^= tag_hash;
112 }
113
114 hasher.write_u64(combined_origin_tags_hash);
115
116 let context_key = ContextKey { hash: hasher.finish() };
117 let tagset_key = TagSetKey {
118 hash: combined_tags_hash,
119 };
120
121 (context_key, tagset_key)
122}
123
124/// A compact hash key that identifies a metric context.
125///
126/// The key is process-local and collision-prone by nature because it stores a 64-bit hash.
127#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
128pub struct ContextKey {
129 hash: u64,
130}
131
132#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
133pub struct TagSetKey {
134 hash: u64,
135}