@@ -25,7 +25,6 @@ use opentelemetry_proto::tonic::metrics::v1::{
2525use serde_json:: { Map , Value } ;
2626
2727use rustc_hash:: FxHasher ;
28- use std:: borrow:: Cow ;
2928use std:: collections:: HashSet ;
3029use std:: hash:: Hasher ;
3130use tracing:: info_span;
@@ -87,6 +86,37 @@ pub const OTEL_METRICS_KNOWN_FIELD_LIST: [&str; 37] = [
8786static OTEL_METRICS_KNOWN_FIELDS : Lazy < HashSet < & ' static str > > =
8887 Lazy :: new ( || OTEL_METRICS_KNOWN_FIELD_LIST . iter ( ) . copied ( ) . collect ( ) ) ;
8988
89+ /// To avoid heap allocations for non-string attributes (int/float/bool)
90+ /// values are formatted via a zero-alloc [`StackBuf`] backed by a 32-byte
91+ /// stack array. This limit is safely bounded by the maximum string length
92+ /// of OTel `i64` (20 bytes) and `f64` (24 bytes) values
93+ struct StackBuf {
94+ data : [ u8 ; 32 ] ,
95+ len : usize ,
96+ }
97+
98+ impl StackBuf {
99+ fn new ( ) -> Self {
100+ Self {
101+ data : [ 0u8 ; 32 ] ,
102+ len : 0 ,
103+ }
104+ }
105+ fn as_bytes ( & self ) -> & [ u8 ] {
106+ & self . data [ ..self . len ]
107+ }
108+ }
109+
110+ impl std:: fmt:: Write for StackBuf {
111+ fn write_str ( & mut self , s : & str ) -> std:: fmt:: Result {
112+ let bytes = s. as_bytes ( ) ;
113+ let end = std:: cmp:: min ( self . len + bytes. len ( ) , 32 ) ;
114+ self . data [ self . len ..end] . copy_from_slice ( & bytes[ ..end - self . len ] ) ;
115+ self . len = end;
116+ Ok ( ( ) )
117+ }
118+ }
119+
90120/// Compute a stable u64 identifier for the physical series a sample
91121/// belongs to. Hashes `metric_name` plus every attribute key/value pair
92122/// that survived OTel flattening — everything in the flattened data
@@ -97,18 +127,13 @@ static OTEL_METRICS_KNOWN_FIELDS: Lazy<HashSet<&'static str>> =
97127/// non-cryptographic) and feeds keys in sorted order so the hash
98128/// doesn't depend on JSON Map iteration order.
99129fn compute_series_hash ( dp : & Map < String , Value > ) -> u64 {
100- let mut label_pairs : Vec < ( & str , Cow < ' _ , str > ) > = Vec :: with_capacity ( dp. len ( ) ) ;
101- for ( key, value ) in dp {
102- if OTEL_METRICS_KNOWN_FIELDS . contains ( key. as_str ( ) ) {
103- continue ;
130+ let mut keys : Vec < & str > = Vec :: with_capacity ( dp. len ( ) ) ;
131+ for key in dp. keys ( ) {
132+ if ! OTEL_METRICS_KNOWN_FIELDS . contains ( key. as_str ( ) ) {
133+ keys . push ( key ) ;
104134 }
105- let value = match value {
106- Value :: String ( s) => Cow :: Borrowed ( s. as_str ( ) ) ,
107- other => Cow :: Owned ( other. to_string ( ) ) ,
108- } ;
109- label_pairs. push ( ( key. as_str ( ) , value) ) ;
110135 }
111- label_pairs . sort_by ( |a , b| a . 0 . cmp ( b . 0 ) ) ;
136+ keys . sort_unstable ( ) ;
112137
113138 let mut hasher = FxHasher :: default ( ) ;
114139 // Include metric_name in the identity. Without it, two different
@@ -117,10 +142,28 @@ fn compute_series_hash(dp: &Map<String, Value>) -> u64 {
117142 hasher. write ( name. as_bytes ( ) ) ;
118143 hasher. write ( b"\0 " ) ;
119144 }
120- for ( k , v ) in & label_pairs {
121- hasher. write ( k . as_bytes ( ) ) ;
145+ for key in keys {
146+ hasher. write ( key . as_bytes ( ) ) ;
122147 hasher. write ( b"=" ) ;
123- hasher. write ( v. as_bytes ( ) ) ;
148+ if let Some ( val) = dp. get ( key) {
149+ match val {
150+ Value :: String ( s) => hasher. write ( s. as_bytes ( ) ) ,
151+ Value :: Bool ( b) => {
152+ if * b {
153+ hasher. write ( b"true" ) ;
154+ } else {
155+ hasher. write ( b"false" ) ;
156+ }
157+ }
158+ Value :: Number ( n) => {
159+ // Zero- Alloc number formatting
160+ let mut buf = StackBuf :: new ( ) ;
161+ let _ = std:: fmt:: Write :: write_fmt ( & mut buf, format_args ! ( "{}" , n) ) ;
162+ hasher. write ( buf. as_bytes ( ) ) ;
163+ }
164+ _ => hasher. write ( b"null" ) ,
165+ }
166+ }
124167 hasher. write ( b"\0 " ) ;
125168 }
126169 hasher. finish ( )
@@ -172,7 +215,9 @@ fn flatten_number_data_points(data_points: &[NumberDataPoint]) -> Vec<Map<String
172215 data_points
173216 . iter ( )
174217 . map ( |data_point| {
175- let mut data_point_json = Map :: with_capacity ( data_point. attributes . len ( ) + 8 ) ;
218+ let mut data_point_json = Map :: with_capacity (
219+ data_point. attributes . len ( ) + 8 + ( data_point. exemplars . len ( ) * 4 ) ,
220+ ) ;
176221 insert_attributes ( & mut data_point_json, & data_point. attributes ) ;
177222 data_point_json. insert (
178223 "start_time_unix_nano" . to_string ( ) ,
0 commit comments