1313// limitations under the License.
1414
1515use std:: collections:: { BTreeMap , HashMap } ;
16- use std:: sync:: { Arc , LazyLock , OnceLock } ;
16+ use std:: hash:: { BuildHasherDefault , Hasher } ;
17+ use std:: sync:: { Arc , LazyLock , OnceLock , RwLock } ;
1718use std:: time:: Instant ;
1819
20+ use fnv:: FnvHasher ;
1921use opentelemetry:: metrics:: Meter ;
2022use opentelemetry:: { Key , KeyValue } ;
2123use prometheus:: { HistogramOpts , Opts , TextEncoder } ;
@@ -57,6 +59,68 @@ fn build_prometheus_labels(const_labels: &[(&str, &str)]) -> HashMap<String, Str
5759 . collect ( )
5860}
5961
62+ /// Identity hasher for pre-hashed `u64` keys. The metric cache keys are
63+ /// already FNV-hashed, so re-hashing them inside the HashMap (default SipHash)
64+ /// would be redundant.
65+ #[ derive( Default ) ]
66+ struct NoHashHasher ( u64 ) ;
67+
68+ impl Hasher for NoHashHasher {
69+ fn finish ( & self ) -> u64 {
70+ self . 0
71+ }
72+
73+ fn write ( & mut self , _bytes : & [ u8 ] ) {
74+ unreachable ! ( "NoHashHasher only supports write_u64" ) ;
75+ }
76+
77+ fn write_u64 ( & mut self , i : u64 ) {
78+ self . 0 = i;
79+ }
80+ }
81+
82+ type BuildNoHashHasher = BuildHasherDefault < NoHashHasher > ;
83+
84+ fn hash_label_values ( values : & [ & str ] ) -> u64 {
85+ let mut hasher = FnvHasher :: default ( ) ;
86+ for value in values {
87+ // Length prefix prevents collisions when label boundaries shift,
88+ // e.g. ["ab", "c"] vs ["a", "bc"] produce the same byte stream
89+ // without this.
90+ hasher. write_u64 ( value. len ( ) as u64 ) ;
91+ hasher. write ( value. as_bytes ( ) ) ;
92+ }
93+ hasher. finish ( )
94+ }
95+
96+ fn new_metric_cache < T > ( ) -> Arc < RwLock < HashMap < u64 , T , BuildNoHashHasher > > > {
97+ Arc :: new ( RwLock :: new ( HashMap :: with_hasher (
98+ BuildNoHashHasher :: default ( ) ,
99+ ) ) )
100+ }
101+
102+ fn get_or_insert_cached < T : Clone > (
103+ cache : & RwLock < HashMap < u64 , T , BuildNoHashHasher > > ,
104+ label_values : & [ & str ] ,
105+ build : impl FnOnce ( ) -> T ,
106+ ) -> T {
107+ let hash = hash_label_values ( label_values) ;
108+ {
109+ let cache = cache. read ( ) . expect ( "metric cache lock poisoned" ) ;
110+ if let Some ( metric) = cache. get ( & hash) {
111+ return metric. clone ( ) ;
112+ }
113+ }
114+ let metric = build ( ) ;
115+ let mut cache = cache. write ( ) . expect ( "metric cache lock poisoned" ) ;
116+ // Another thread may have inserted between the read lock and the write lock acquisition.
117+ if let Some ( cached) = cache. get ( & hash) {
118+ return cached. clone ( ) ;
119+ }
120+ cache. insert ( hash, metric. clone ( ) ) ;
121+ metric
122+ }
123+
60124struct OtelState < T > {
61125 build_instrument : Box < dyn Fn ( & Meter ) -> T + Send + Sync > ,
62126 instrument : OnceLock < T > ,
@@ -221,6 +285,7 @@ pub struct IntCounterVec<const N: usize> {
221285 prometheus : prometheus:: IntCounterVec ,
222286 otel : OtelMetric < opentelemetry:: metrics:: Counter < u64 > > ,
223287 label_names : Arc < [ Key ] > ,
288+ cache : Arc < RwLock < HashMap < u64 , IntCounter , BuildNoHashHasher > > > ,
224289}
225290
226291impl < const N : usize > IntCounterVec < N > {
@@ -242,14 +307,15 @@ impl<const N: usize> IntCounterVec<N> {
242307 prometheus : prom,
243308 otel : OtelMetric :: new ( None , build_otel_attributes ( const_labels) ) ,
244309 label_names : build_otel_label_names ( label_names) ,
310+ cache : new_metric_cache ( ) ,
245311 }
246312 }
247313
248314 pub fn with_label_values ( & self , label_values : [ & str ; N ] ) -> IntCounter {
249- IntCounter {
315+ get_or_insert_cached ( & self . cache , & label_values , || IntCounter {
250316 prometheus : self . prometheus . with_label_values ( & label_values) ,
251317 otel : self . otel . with_attributes ( & self . label_names , label_values) ,
252- }
318+ } )
253319 }
254320}
255321
@@ -275,14 +341,15 @@ pub struct IntGaugeVec<const N: usize> {
275341 prometheus : prometheus:: IntGaugeVec ,
276342 otel : OtelMetric < opentelemetry:: metrics:: Gauge < i64 > > ,
277343 label_names : Arc < [ Key ] > ,
344+ cache : Arc < RwLock < HashMap < u64 , IntGauge , BuildNoHashHasher > > > ,
278345}
279346
280347impl < const N : usize > IntGaugeVec < N > {
281348 pub fn with_label_values ( & self , label_values : [ & str ; N ] ) -> IntGauge {
282- IntGauge {
349+ get_or_insert_cached ( & self . cache , & label_values , || IntGauge {
283350 prometheus : self . prometheus . with_label_values ( & label_values) ,
284351 otel : self . otel . with_attributes ( & self . label_names , label_values) ,
285- }
352+ } )
286353 }
287354}
288355
@@ -323,14 +390,15 @@ pub struct IntUpDownCounterVec<const N: usize> {
323390 prometheus : prometheus:: IntGaugeVec ,
324391 otel : OtelMetric < opentelemetry:: metrics:: UpDownCounter < i64 > > ,
325392 label_names : Arc < [ Key ] > ,
393+ cache : Arc < RwLock < HashMap < u64 , IntUpDownCounter , BuildNoHashHasher > > > ,
326394}
327395
328396impl < const N : usize > IntUpDownCounterVec < N > {
329397 pub fn with_label_values ( & self , label_values : [ & str ; N ] ) -> IntUpDownCounter {
330- IntUpDownCounter {
398+ get_or_insert_cached ( & self . cache , & label_values , || IntUpDownCounter {
331399 prometheus : self . prometheus . with_label_values ( & label_values) ,
332400 otel : self . otel . with_attributes ( & self . label_names , label_values) ,
333- }
401+ } )
334402 }
335403}
336404
@@ -397,14 +465,15 @@ pub struct HistogramVec<const N: usize> {
397465 prometheus : prometheus:: HistogramVec ,
398466 otel : OtelMetric < opentelemetry:: metrics:: Histogram < f64 > > ,
399467 label_names : Arc < [ Key ] > ,
468+ cache : Arc < RwLock < HashMap < u64 , Histogram , BuildNoHashHasher > > > ,
400469}
401470
402471impl < const N : usize > HistogramVec < N > {
403472 pub fn with_label_values ( & self , label_values : [ & str ; N ] ) -> Histogram {
404- Histogram {
473+ get_or_insert_cached ( & self . cache , & label_values , || Histogram {
405474 prometheus : self . prometheus . with_label_values ( & label_values) ,
406475 otel : self . otel . with_attributes ( & self . label_names , label_values) ,
407- }
476+ } )
408477 }
409478}
410479
@@ -550,6 +619,7 @@ pub fn new_counter_vec<const N: usize>(
550619 build_otel_attributes ( const_labels) ,
551620 ) ,
552621 label_names : build_otel_label_names ( label_names) ,
622+ cache : new_metric_cache ( ) ,
553623 }
554624}
555625
@@ -597,6 +667,7 @@ pub fn new_gauge_vec<const N: usize>(
597667 build_otel_attributes ( const_labels) ,
598668 ) ,
599669 label_names : build_otel_label_names ( label_names) ,
670+ cache : new_metric_cache ( ) ,
600671 }
601672}
602673
@@ -644,6 +715,7 @@ pub fn new_up_down_counter_vec<const N: usize>(
644715 build_otel_attributes ( const_labels) ,
645716 ) ,
646717 label_names : build_otel_label_names ( label_names) ,
718+ cache : new_metric_cache ( ) ,
647719 }
648720}
649721
@@ -721,6 +793,7 @@ pub fn new_histogram_vec<const N: usize>(
721793 build_otel_attributes ( const_labels) ,
722794 ) ,
723795 label_names : build_otel_label_names ( label_names) ,
796+ cache : new_metric_cache ( ) ,
724797 }
725798}
726799
@@ -1371,4 +1444,24 @@ mod tests {
13711444 assert ! ( payload. contains( "quickwit_test_test_payload_ctr" ) ) ;
13721445 assert ! ( payload. contains( "42" ) ) ;
13731446 }
1447+
1448+ #[ test]
1449+ fn test_hash_label_values ( ) {
1450+ // Same values produce the same hash.
1451+ assert_eq ! (
1452+ hash_label_values( & [ "foo" , "bar" ] ) ,
1453+ hash_label_values( & [ "foo" , "bar" ] ) ,
1454+ ) ;
1455+ // Different values produce different hashes.
1456+ assert_ne ! (
1457+ hash_label_values( & [ "foo" , "bar" ] ) ,
1458+ hash_label_values( & [ "foo" , "baz" ] ) ,
1459+ ) ;
1460+ // Shifted label boundaries produce different hashes
1461+ // (length prefix prevents "ab"+"c" from colliding with "a"+"bc").
1462+ assert_ne ! (
1463+ hash_label_values( & [ "ab" , "c" ] ) ,
1464+ hash_label_values( & [ "a" , "bc" ] ) ,
1465+ ) ;
1466+ }
13741467}
0 commit comments