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}