Skip to main content

metrics/
key.rs

1use crate::{cow::Cow, IntoLabels, Label, SharedString};
2use rapidhash::v3::{rapidhash_v3_nano_inline, RapidSecrets};
3use std::{
4    borrow::Borrow,
5    cmp, fmt,
6    hash::{Hash, Hasher},
7    slice::Iter,
8};
9
10const NO_LABELS: [Label; 0] = [];
11
12/// Name component of a key.
13#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
14pub struct KeyName(SharedString);
15
16impl KeyName {
17    /// Creates a `KeyName` from a static string.
18    pub const fn from_const_str(name: &'static str) -> Self {
19        KeyName(SharedString::const_str(name))
20    }
21
22    /// Gets a reference to the string used for this name.
23    pub const fn as_str(&self) -> &str {
24        self.0.as_const_str()
25    }
26
27    /// Consumes this [`KeyName`], returning the inner [`SharedString`].
28    pub fn into_inner(self) -> SharedString {
29        self.0
30    }
31
32    /// Returns a version of this name that is cheap to clone for long-lived retention.
33    ///
34    /// *NOTE:* This will allocate if this `KeyName` was created from a non-`'static`
35    /// string, however, the returned `KeyName` will not require allocation when cloned
36    /// or `to_retained` is invoked again.
37    pub fn to_retained(&self) -> Self {
38        KeyName(self.0.to_retained())
39    }
40}
41
42impl<T> From<T> for KeyName
43where
44    T: Into<SharedString>,
45{
46    fn from(name: T) -> Self {
47        KeyName(name.into())
48    }
49}
50
51impl Borrow<str> for KeyName {
52    fn borrow(&self) -> &str {
53        self.0.borrow()
54    }
55}
56
57impl From<KeyName> for std::borrow::Cow<'static, str> {
58    fn from(name: KeyName) -> Self {
59        name.0.into()
60    }
61}
62
63/// A metric identifier.
64///
65/// A key represents both the name and labels of a metric.
66///
67/// # Safety
68/// Clippy will report any usage of `Key` as the key of a map/set as "mutable key type", meaning
69/// that it believes that there is interior mutability present which could lead to a key being
70/// hashed different over time.  That behavior could lead to unexpected behavior, as standard
71/// maps/sets depend on keys having stable hashes over time, related to times when they must be
72/// recomputed as the internal storage is resized and items are moved around.
73///
74/// In this case, the `Hash` implementation of `Key` does _not_ depend on the fields which Clippy
75/// considers mutable (the atomics) and so it is actually safe against differing hashes being
76/// generated.  We personally allow this Clippy lint in places where we store the key, such as
77/// helper types in the `metrics-util` crate, and you may need to do the same if you're using it in
78/// such a way as well.
79#[derive(Debug, Clone)]
80pub struct Key {
81    name: KeyName,
82    labels: Cow<'static, [Label]>,
83    hash: u64,
84}
85
86impl Key {
87    /// Creates a [`Key`] from a name.
88    pub fn from_name<N>(name: N) -> Self
89    where
90        N: Into<KeyName>,
91    {
92        let name = name.into();
93        let labels = Cow::from_owned(Vec::new());
94
95        Self::builder(name, labels)
96    }
97
98    /// Creates a [`Key`] from a name and set of labels.
99    pub fn from_parts<N, L>(name: N, labels: L) -> Self
100    where
101        N: Into<KeyName>,
102        L: IntoLabels,
103    {
104        let name = name.into();
105        let labels = Cow::from_owned(labels.into_labels());
106
107        Self::builder(name, labels)
108    }
109
110    /// Creates a [`Key`] from a non-static name and a static set of labels.
111    pub fn from_static_labels<N>(name: N, labels: &'static [Label]) -> Self
112    where
113        N: Into<KeyName>,
114    {
115        Self::builder(name.into(), Cow::const_slice(labels))
116    }
117
118    /// Creates a [`Key`] from a static name.
119    ///
120    /// This function is `const`, so it can be used in a static context.
121    pub const fn from_static_name(name: &'static str) -> Self {
122        Self::from_static_parts(name, &NO_LABELS)
123    }
124
125    /// Creates a [`Key`] from a static name and static set of labels.
126    ///
127    /// This function is `const`, so it can be used in a static context.
128    pub const fn from_static_parts(name: &'static str, labels: &'static [Label]) -> Self {
129        Self::builder(KeyName::from_const_str(name), Cow::const_slice(labels))
130    }
131
132    const fn builder(name: KeyName, labels: Cow<'static, [Label]>) -> Self {
133        let hash = generate_key_hash(&name, &labels);
134        Self { name, labels, hash }
135    }
136
137    /// Name of this key.
138    pub fn name(&self) -> &str {
139        self.name.0.as_ref()
140    }
141
142    /// Name of this key as a [`KeyName`]
143    pub fn name_shared(&self) -> KeyName {
144        self.name.clone()
145    }
146
147    /// Labels of this key, if they exist.
148    pub fn labels(&self) -> Iter<'_, Label> {
149        self.labels.iter()
150    }
151
152    /// Consumes this [`Key`], returning the name parts and any labels.
153    pub fn into_parts(self) -> (KeyName, Vec<Label>) {
154        (self.name, self.labels.into_owned())
155    }
156
157    /// Returns a version of this key that is cheap to clone for long-lived retention.
158    ///
159    /// *NOTE:* This will allocate if this `Key` was created from non-`'static`
160    /// parts, however, the returned `Key` will not require allocation when cloned
161    /// or `to_retained` is invoked again.
162    pub fn to_retained(&self) -> Self {
163        Self { name: self.name.to_retained(), labels: self.labels.to_retained(), hash: self.hash }
164    }
165
166    /// Clones this [`Key`], and expands the existing set of labels.
167    pub fn with_extra_labels(&self, extra_labels: Vec<Label>) -> Self {
168        if extra_labels.is_empty() {
169            return self.clone();
170        }
171
172        let name = self.name.clone();
173        let mut labels = self.labels.clone().into_owned();
174        labels.extend(extra_labels);
175
176        Self::builder(name, labels.into())
177    }
178
179    /// Gets the hash value for this key.
180    #[inline]
181    pub const fn get_hash(&self) -> u64 {
182        self.hash
183    }
184}
185
186/// Generate a hash value for a `Key`.
187#[inline]
188const fn generate_key_hash(name: &KeyName, labels: &Cow<'static, [Label]>) -> u64 {
189    // Here we explicitly use a static seed. We believe HashDoS should not be a concern here, as
190    // the primary use case is static metric keys coming from the application code itself. Users
191    // could allow untrusted input to form a metric key, but it's not a use case we are designing
192    // for. If this ever changes, using const_random!(u64) could be a clean solution to randomize
193    // the seed at _build_ time.
194    // github issue: https://github.com/metrics-rs/metrics/pull/651#issuecomment-3744517372
195    // const_random: https://crates.io/crates/const-random
196    const SECRETS: RapidSecrets = RapidSecrets::seed_cpp(0);
197
198    // The name uses an independent seed to avoid simple substitution errors (where a user swaps a
199    // label with the key name for example). We use `seed_cpp` to ensure the secrets arrays are the
200    // same const array so the compiler can optimise the loading of secrets as the same immediates.
201    let mut hash = hash_bytes(name.0.as_const_str().as_bytes(), &SECRETS);
202
203    // Label order should _not_ change the resulting hash. The use of addition here is a hack,
204    // this is both faster than sorting the labels and feasible in `const`. We use an ADD chain
205    // as opposed to an XOR chain to avoid duplicate labels cancelling out.
206    let mut i = 0;
207    let labels = labels.as_const_slice();
208    while i < labels.len() {
209        let label_hash = hash_label(&labels[i]);
210        hash = hash.wrapping_add(label_hash);
211        i += 1;
212    }
213
214    hash
215}
216
217/// The underlying hash function used for keys and labels.
218#[inline(always)]
219const fn hash_bytes(slice: &[u8], secrets: &RapidSecrets) -> u64 {
220    // We expect keys and labels to typically be <48 bytes, and so we choose rapidhash nano. We
221    // skip the AVALANCHE mix step for a slightly lower-quality hash, as speed is preferably over
222    // the highest hash "quality".
223    rapidhash_v3_nano_inline::<false, false>(slice, secrets)
224}
225
226/// Hash a label, taking care to hash keys and values with independent seeds.
227#[inline(always)]
228const fn hash_label(Label(key, value): &Label) -> u64 {
229    // hash the key and value with independent seeds to avoid substitution errors, but use
230    // seed_cpp to ensure the secrets arrays are the same const array
231    const KEY: RapidSecrets = RapidSecrets::seed_cpp(1);
232    const VALUE: RapidSecrets = RapidSecrets::seed_cpp(2);
233
234    let key = hash_bytes(key.as_const_str().as_bytes(), &KEY);
235    let value = hash_bytes(value.as_const_str().as_bytes(), &VALUE);
236
237    key ^ value
238}
239
240impl PartialEq for Key {
241    fn eq(&self, other: &Self) -> bool {
242        if self.hash != other.hash {
243            return false;
244        }
245        if self.name != other.name {
246            return false;
247        }
248        if self.labels.len() != other.labels.len() {
249            return false;
250        }
251        match self.labels.len() {
252            0 => true,
253            1 => self.labels[0] == other.labels[0],
254            2 => {
255                if self.labels[0] == other.labels[0] {
256                    self.labels[1] == other.labels[1]
257                } else if self.labels[0] == other.labels[1] {
258                    self.labels[1] == other.labels[0]
259                } else {
260                    false
261                }
262            }
263            n if n < 8 => {
264                let mut labels_sort_map: [u8; 8] = [0, 1, 2, 3, 4, 5, 6, 7];
265                labels_sort_map[..n].sort_by_key(|i| self.labels[*i as usize].key());
266                let mut his_labels_sort_map: [u8; 8] = [0, 1, 2, 3, 4, 5, 6, 7];
267                his_labels_sort_map[..n].sort_by_key(|i| other.labels[*i as usize].key());
268                for i in 0..n {
269                    if self.labels[labels_sort_map[i] as usize]
270                        != other.labels[his_labels_sort_map[i] as usize]
271                    {
272                        return false;
273                    }
274                }
275                true
276            }
277            n => {
278                let mut labels_sort_map: Vec<usize> = (0..n).collect();
279                labels_sort_map.sort_by_key(|i| self.labels[*i].key());
280                let mut his_labels_sort_map: Vec<usize> = (0..n).collect();
281                his_labels_sort_map.sort_by_key(|i| other.labels[*i].key());
282                for i in 0..n {
283                    if self.labels[labels_sort_map[i]] != other.labels[his_labels_sort_map[i]] {
284                        return false;
285                    }
286                }
287                true
288            }
289        }
290    }
291}
292
293impl Eq for Key {}
294
295impl PartialOrd for Key {
296    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
297        Some(self.cmp(other))
298    }
299}
300
301impl Ord for Key {
302    fn cmp(&self, other: &Self) -> cmp::Ordering {
303        match (&self.name, self.labels.len()).cmp(&(&other.name, other.labels.len())) {
304            cmp::Ordering::Less => return cmp::Ordering::Less,
305            cmp::Ordering::Equal => {}
306            cmp::Ordering::Greater => return cmp::Ordering::Greater,
307        }
308        match self.labels.len() {
309            0 => cmp::Ordering::Equal,
310            1 => self.labels[0].cmp(&other.labels[0]),
311            n if n < 8 => {
312                let mut labels_sort_map: [u8; 8] = [0, 1, 2, 3, 4, 5, 6, 7];
313                labels_sort_map[..n].sort_by_key(|i| self.labels[*i as usize].key());
314                let mut his_labels_sort_map: [u8; 8] = [0, 1, 2, 3, 4, 5, 6, 7];
315                his_labels_sort_map[..n].sort_by_key(|i| other.labels[*i as usize].key());
316                for i in 0..n {
317                    match self.labels[labels_sort_map[i] as usize]
318                        .cmp(&other.labels[his_labels_sort_map[i] as usize])
319                    {
320                        cmp::Ordering::Less => return cmp::Ordering::Less,
321                        cmp::Ordering::Equal => {}
322                        cmp::Ordering::Greater => return cmp::Ordering::Greater,
323                    }
324                }
325                cmp::Ordering::Equal
326            }
327            n => {
328                let mut labels_sort_map: Vec<usize> = (0..n).collect();
329                labels_sort_map.sort_by_key(|i| self.labels[*i].key());
330                let mut his_labels_sort_map: Vec<usize> = (0..n).collect();
331                his_labels_sort_map.sort_by_key(|i| other.labels[*i].key());
332                for i in 0..n {
333                    match self.labels[labels_sort_map[i]].cmp(&other.labels[his_labels_sort_map[i]])
334                    {
335                        cmp::Ordering::Less => return cmp::Ordering::Less,
336                        cmp::Ordering::Equal => {}
337                        cmp::Ordering::Greater => return cmp::Ordering::Greater,
338                    }
339                }
340                cmp::Ordering::Equal
341            }
342        }
343    }
344}
345
346impl Hash for Key {
347    #[inline]
348    fn hash<H: Hasher>(&self, state: &mut H) {
349        // MUST only call `write_u64(self.hash)`. The companion no-op `metrics_util::common::KeyHasher`
350        // (and the no-op fast path in the deprecated `metrics::KeyHasher`) depend on this being the
351        // sole write call — any byte write would route through a byte hasher and produce a value
352        // that disagrees with `Key::get_hash()` / `Hashable::hashable(&key)`, causing duplicate
353        // entries in `metrics_util::registry::Registry`'s internal HashMap after resize (#694).
354        state.write_u64(self.hash)
355    }
356}
357
358impl fmt::Display for Key {
359    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
360        if self.labels.is_empty() {
361            write!(f, "Key({})", self.name.as_str())
362        } else {
363            write!(f, "Key({}, [", self.name.as_str())?;
364            let mut first = true;
365            for label in self.labels.as_ref() {
366                if first {
367                    write!(f, "{} = {}", label.0, label.1)?;
368                    first = false;
369                } else {
370                    write!(f, ", {} = {}", label.0, label.1)?;
371                }
372            }
373            write!(f, "])")
374        }
375    }
376}
377
378impl<T> From<T> for Key
379where
380    T: Into<KeyName>,
381{
382    fn from(name: T) -> Self {
383        Self::from_name(name)
384    }
385}
386
387impl<N, L> From<(N, L)> for Key
388where
389    N: Into<KeyName>,
390    L: IntoLabels,
391{
392    fn from(parts: (N, L)) -> Self {
393        Self::from_parts(parts.0, parts.1)
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::Key;
400    use crate::{KeyName, Label};
401    use std::{cmp, collections::HashMap, ops::Deref, sync::Arc};
402
403    static BORROWED_NAME: &str = "name";
404    static FOOBAR_NAME: &str = "foobar";
405    static BORROWED_BASIC: Key = Key::from_static_name(BORROWED_NAME);
406    static LABELS: [Label; 1] = [Label::from_static_parts("key", "value")];
407    static BORROWED_LABELS: Key = Key::from_static_parts(BORROWED_NAME, &LABELS);
408
409    #[test]
410    fn test_key_ord_and_partialord() {
411        let keys_expected: Vec<Key> =
412            vec![Key::from_name("aaaa"), Key::from_name("bbbb"), Key::from_name("cccc")];
413
414        let keys_unsorted: Vec<Key> =
415            vec![Key::from_name("bbbb"), Key::from_name("cccc"), Key::from_name("aaaa")];
416
417        let keys = {
418            let mut keys = keys_unsorted.clone();
419            keys.sort();
420            keys
421        };
422        assert_eq!(keys, keys_expected);
423
424        let keys = {
425            let mut keys = keys_unsorted.clone();
426            keys.sort_by(|a, b| a.partial_cmp(b).unwrap());
427            keys
428        };
429        assert_eq!(keys, keys_expected);
430    }
431
432    #[test]
433    fn test_key_ord_total() {
434        let mut keys: Vec<_> = (1..16)
435            .flat_map(|i| {
436                let names: Vec<Label> =
437                    (0..i).map(|s| Label::new(format!("{}", s), format!("{}", s))).collect();
438                let key1 = Key::from_parts("key", names.clone());
439
440                // test that changing the order doesn't affect the value
441                let mut names_alt = names.clone();
442                names_alt.rotate_left(1);
443                let key2 = Key::from_parts("key", names_alt);
444                assert_eq!(key1, key2);
445
446                // check that the first element affects the order
447                names_alt = names.clone();
448                names_alt[0] = Label::new("x".to_string(), "0".to_string());
449                let key3 = Key::from_parts("key", names_alt);
450                assert_ne!(key1, key3);
451
452                // check that the last element affects the order
453                names_alt = names.clone();
454                names_alt[i - 1] = Label::new("x".to_string(), "0".to_string());
455                let key4 = Key::from_parts("key", names_alt);
456                assert_ne!(key1, key4);
457
458                [key1, key2, key3, key4]
459            })
460            .collect();
461        keys.push(Key::from_parts("key", vec![]));
462        keys.sort();
463        for i in 0..keys.len() {
464            for j in 0..keys.len() {
465                // check that the order is a total order
466                match (keys[i] == keys[j], keys[i].cmp(&keys[j])) {
467                    (true, cmp::Ordering::Equal) => {}
468                    (true, cmp::Ordering::Less) => panic!("at {} {} equal keys compared lt", i, j),
469                    (true, cmp::Ordering::Greater) => {
470                        panic!("at {} {} equal keys compared gt", i, j)
471                    }
472                    (false, cmp::Ordering::Equal) => {
473                        panic!("at {} {} unequal keys compared equal", i, j)
474                    }
475                    (false, cmp::Ordering::Less) => assert!(i < j),
476                    (false, cmp::Ordering::Greater) => assert!(i > j),
477                }
478            }
479        }
480    }
481
482    #[test]
483    fn test_key_eq_and_hash() {
484        let mut keys = HashMap::new();
485
486        let owned_basic: Key = Key::from_name("name");
487        assert_eq!(&owned_basic, &BORROWED_BASIC);
488
489        let previous = keys.insert(owned_basic, 42);
490        assert!(previous.is_none());
491
492        let previous = keys.get(&BORROWED_BASIC);
493        assert_eq!(previous, Some(&42));
494
495        let labels = LABELS.to_vec();
496        let owned_labels = Key::from_parts(BORROWED_NAME, labels);
497        assert_eq!(&owned_labels, &BORROWED_LABELS);
498
499        let previous = keys.insert(owned_labels, 43);
500        assert!(previous.is_none());
501
502        let previous = keys.get(&BORROWED_LABELS);
503        assert_eq!(previous, Some(&43));
504
505        let basic: Key = "constant_key".into();
506        let cloned_basic = basic.clone();
507        assert_eq!(basic, cloned_basic);
508
509        for i in 1..16 {
510            let names: Vec<Label> =
511                (0..i).map(|s| Label::new(format!("{}", s), format!("{}", s))).collect();
512            let key1 = Key::from_parts("key", names.clone());
513            let mut names_alt = names.clone();
514
515            // test that changing the order doesn't affect the hash
516            names_alt.rotate_left(1);
517            let key2 = Key::from_parts("key", names_alt);
518            assert_eq!(key1, key2);
519            assert_eq!(key1.get_hash(), key2.get_hash());
520
521            // check that the first element affects the hash
522            names_alt = names.clone();
523            names_alt[0] = Label::new("x".to_string(), "0".to_string());
524            let key2 = Key::from_parts("key", names_alt);
525            assert_ne!(key1, key2);
526            assert_ne!(key1.get_hash(), key2.get_hash());
527
528            // check that the last element affects the hash
529            names_alt = names.clone();
530            names_alt[i - 1] = Label::new("x".to_string(), "0".to_string());
531            let key2 = Key::from_parts("key", names_alt);
532            assert_ne!(key1, key2);
533            assert_ne!(key1.get_hash(), key2.get_hash());
534
535            // check that it differs from a key with no labels
536            let key2 = Key::from_parts("key", vec![]);
537            assert_ne!(key1, key2);
538            assert_ne!(key1.get_hash(), key2.get_hash());
539        }
540    }
541
542    #[test]
543    fn test_key_data_proper_display() {
544        let key1 = Key::from_name("foobar");
545        let result1 = key1.to_string();
546        assert_eq!(result1, "Key(foobar)");
547
548        let key2 = Key::from_parts(FOOBAR_NAME, vec![Label::new("system", "http")]);
549        let result2 = key2.to_string();
550        assert_eq!(result2, "Key(foobar, [system = http])");
551
552        let key3 = Key::from_parts(
553            FOOBAR_NAME,
554            vec![Label::new("system", "http"), Label::new("user", "joe")],
555        );
556        let result3 = key3.to_string();
557        assert_eq!(result3, "Key(foobar, [system = http, user = joe])");
558
559        let key4 = Key::from_parts(
560            FOOBAR_NAME,
561            vec![
562                Label::new("black", "black"),
563                Label::new("lives", "lives"),
564                Label::new("matter", "matter"),
565            ],
566        );
567        let result4 = key4.to_string();
568        assert_eq!(result4, "Key(foobar, [black = black, lives = lives, matter = matter])");
569    }
570
571    #[test]
572    fn test_key_name_equality() {
573        static KEY_NAME: &str = "key_name";
574
575        let borrowed_const = KeyName::from_const_str(KEY_NAME);
576        let borrowed_nonconst = KeyName::from(KEY_NAME);
577        let owned = KeyName::from(KEY_NAME.to_owned());
578
579        let shared_arc = Arc::from(KEY_NAME);
580        let shared = KeyName::from(Arc::clone(&shared_arc));
581
582        assert_eq!(borrowed_const, borrowed_nonconst);
583        assert_eq!(borrowed_const.as_str(), borrowed_nonconst.as_str());
584        assert_eq!(borrowed_const, owned);
585        assert_eq!(borrowed_const.as_str(), owned.as_str());
586        assert_eq!(borrowed_const, shared);
587        assert_eq!(borrowed_const.as_str(), shared.as_str());
588    }
589
590    #[test]
591    fn test_shared_key_name_drop_logic() {
592        let shared_arc = Arc::from("foo");
593        let shared = KeyName::from(Arc::clone(&shared_arc));
594
595        assert_eq!(shared_arc.deref(), shared.as_str());
596
597        assert_eq!(Arc::strong_count(&shared_arc), 2);
598        drop(shared);
599        assert_eq!(Arc::strong_count(&shared_arc), 1);
600
601        let shared_weak = Arc::downgrade(&shared_arc);
602        assert_eq!(Arc::strong_count(&shared_arc), 1);
603
604        let shared = KeyName::from(Arc::clone(&shared_arc));
605        assert_eq!(shared_arc.deref(), shared.as_str());
606        assert_eq!(Arc::strong_count(&shared_arc), 2);
607
608        drop(shared_arc);
609        assert_eq!(shared_weak.strong_count(), 1);
610
611        drop(shared);
612        assert_eq!(shared_weak.strong_count(), 0);
613    }
614
615    #[test]
616    fn test_key_to_retained_preserves_equality_and_hash() {
617        let key = Key::from_parts(
618            String::from("retained"),
619            vec![Label::new(String::from("service"), String::from("api"))],
620        );
621
622        let retained = key.to_retained();
623
624        assert_eq!(key, retained);
625        assert_eq!(key.get_hash(), retained.get_hash());
626        assert_eq!(retained, retained.clone());
627    }
628
629    #[test]
630    fn test_buildhasherdefault_keyhasher_agrees_with_get_hash() {
631        use std::hash::{BuildHasher, BuildHasherDefault};
632        #[allow(deprecated)]
633        use crate::KeyHasher;
634
635        // The Registry in `metrics-util` versions 0.19.0..=0.20.1 uses
636        // `BuildHasherDefault<metrics::KeyHasher>` as the HashMap's BuildHasher, while looking
637        // up entries via `Hashable::hashable(&key)` (which returns `Key::get_hash()` directly).
638        // If those two paths produce different u64s, hashbrown's resize-time rebucketing places
639        // entries at a different bucket than the lookup queries, and the Registry inserts a
640        // fresh entry on every call. See https://github.com/metrics-rs/metrics/issues/694.
641        let key = Key::from_parts("regression", vec![Label::new("a", "1")]);
642        #[allow(deprecated)]
643        let via_buildhasher = BuildHasherDefault::<KeyHasher>::default().hash_one(&key);
644        assert_eq!(via_buildhasher, key.get_hash());
645    }
646
647    #[test]
648    fn test_keyhasher_byte_mode_distinguishes_inputs() {
649        use std::hash::{BuildHasher, BuildHasherDefault};
650        #[allow(deprecated)]
651        use crate::KeyHasher;
652
653        // Validates the byte-mode fallback path used by `metrics_util::DefaultHashable<H>` in
654        // `metrics-util 0.19.x`: when `Hash::hash` writes bytes (not just `write_u64`), the
655        // hasher must still produce deterministic, input-distinguishing output.
656        #[allow(deprecated)]
657        let bh = BuildHasherDefault::<KeyHasher>::default();
658        let h_foo_1 = bh.hash_one("foo");
659        let h_foo_2 = bh.hash_one("foo");
660        let h_bar = bh.hash_one("bar");
661        assert_eq!(h_foo_1, h_foo_2);
662        assert_ne!(h_foo_1, h_bar);
663    }
664}