@@ -411,13 +411,21 @@ impl Metric {
411411 let names_dict = parse_dict_strings ( & data. dictNameStr ) ?;
412412 let tags_dict = parse_dict_strings ( & data. dictTagStr ) ?;
413413 let tagsets_dict = parse_tagsets ( & data. dictTagsets , & tags_dict) ?;
414+ let resources_dict = parse_resources (
415+ & data. dictResourceLen ,
416+ & data. dictResourceType ,
417+ & data. dictResourceName ,
418+ & parse_dict_strings ( & data. dictResourceStr ) ?,
419+ ) ?;
414420
415421 // Delta-decode index arrays.
416422 let mut name_refs = data. nameRefs ;
417423 let mut tagset_refs = data. tagsetRefs ;
424+ let mut resources_refs = data. resourcesRefs ;
418425 let mut timestamps = data. timestamps ;
419426 delta_decode ( & mut name_refs) ;
420427 delta_decode ( & mut tagset_refs) ;
428+ delta_decode ( & mut resources_refs) ;
421429 delta_decode ( & mut timestamps) ;
422430
423431 // Delta-decode sketch bin keys (per-sketch sequences are individually delta-encoded,
@@ -446,7 +454,7 @@ impl Metric {
446454
447455 // Resolve tags (1-based index).
448456 let tagset_ref = tagset_refs[ i] as usize ;
449- let tags = if tagset_ref == 0 {
457+ let mut tags = if tagset_ref == 0 {
450458 Vec :: new ( )
451459 } else {
452460 tagsets_dict
@@ -457,6 +465,23 @@ impl Metric {
457465 . clone ( )
458466 } ;
459467
468+ let resource_ref = resources_refs. get ( i) . copied ( ) . unwrap_or ( 0 ) as usize ;
469+ if resource_ref != 0 {
470+ let resources = resources_dict. get ( resource_ref - 1 ) . ok_or_else ( || {
471+ generic_error ! (
472+ "Invalid resource ref {} (dict size {})" ,
473+ resource_ref,
474+ resources_dict. len( )
475+ )
476+ } ) ?;
477+ if let Some ( ( _, host_name) ) = resources
478+ . iter ( )
479+ . find ( |( resource_type, resource_name) | resource_type == "host" && !resource_name. is_empty ( ) )
480+ {
481+ tags. push ( format ! ( "host:{}" , host_name) ) ;
482+ }
483+ }
484+
460485 let mut values = Vec :: with_capacity ( num_points) ;
461486
462487 if metric_type == V3_METRIC_TYPE_SKETCH {
@@ -468,14 +493,8 @@ impl Metric {
468493 let timestamp = u64:: try_from ( ts) . map_err ( |_| generic_error ! ( "Invalid timestamp: {}" , ts) ) ?;
469494 cursors. timestamp += 1 ;
470495
471- // Sketch count is always in valsSint64.
472- let cnt = * data
473- . valsSint64
474- . get ( cursors. sint64 )
475- . ok_or_else ( || generic_error ! ( "Ran out of sint64 values for sketch count" ) ) ?;
476- cursors. sint64 += 1 ;
477-
478- // Sum, min, max are stored as 3 consecutive values based on value_type.
496+ // The Agent writes sketch summaries as sum, min, max, then count. Count is always in valsSint64,
497+ // but integer summaries can share that column, so count must be read after the summary values.
479498 let sum = read_value (
480499 value_type,
481500 & mut cursors,
@@ -497,6 +516,11 @@ impl Metric {
497516 & data. valsFloat32 ,
498517 & data. valsFloat64 ,
499518 ) ?;
519+ let cnt = * data
520+ . valsSint64
521+ . get ( cursors. sint64 )
522+ . ok_or_else ( || generic_error ! ( "Ran out of sint64 values for sketch count" ) ) ?;
523+ cursors. sint64 += 1 ;
500524 let avg = if cnt != 0 { sum / cnt as f64 } else { 0.0 } ;
501525
502526 // Read bin data.
@@ -669,6 +693,52 @@ fn parse_tagsets(dict_tagsets: &[i64], tags_dict: &[String]) -> Result<Vec<Vec<S
669693 Ok ( tagsets)
670694}
671695
696+ /// Parse resource sets from V3 resource dictionaries.
697+ ///
698+ /// Each resource set is encoded as one length entry plus that many locally delta-encoded type/name dictionary indexes.
699+ fn parse_resources (
700+ dict_resource_len : & [ i64 ] , dict_resource_type : & [ i64 ] , dict_resource_name : & [ i64 ] , resource_strings : & [ String ] ,
701+ ) -> Result < Vec < Vec < ( String , String ) > > , GenericError > {
702+ let mut resources = Vec :: with_capacity ( dict_resource_len. len ( ) ) ;
703+ let mut offset = 0 ;
704+
705+ for & count in dict_resource_len {
706+ let count = usize:: try_from ( count) . map_err ( |_| generic_error ! ( "Invalid negative resource count: {}" , count) ) ?;
707+ if offset + count > dict_resource_type. len ( ) || offset + count > dict_resource_name. len ( ) {
708+ return Err ( generic_error ! ( "Resource set extends past resource dictionary arrays" ) ) ;
709+ }
710+
711+ let mut type_indices = dict_resource_type[ offset..offset + count] . to_vec ( ) ;
712+ let mut name_indices = dict_resource_name[ offset..offset + count] . to_vec ( ) ;
713+ delta_decode ( & mut type_indices) ;
714+ delta_decode ( & mut name_indices) ;
715+
716+ let mut resource_set = Vec :: with_capacity ( count) ;
717+ for ( & type_idx, & name_idx) in type_indices. iter ( ) . zip ( name_indices. iter ( ) ) {
718+ let resource_type = resource_strings
719+ . get ( resource_index ( type_idx) ?)
720+ . ok_or_else ( || generic_error ! ( "Invalid resource type index {}" , type_idx) ) ?
721+ . clone ( ) ;
722+ let resource_name = resource_strings
723+ . get ( resource_index ( name_idx) ?)
724+ . ok_or_else ( || generic_error ! ( "Invalid resource name index {}" , name_idx) ) ?
725+ . clone ( ) ;
726+ resource_set. push ( ( resource_type, resource_name) ) ;
727+ }
728+
729+ resources. push ( resource_set) ;
730+ offset += count;
731+ }
732+
733+ Ok ( resources)
734+ }
735+
736+ fn resource_index ( idx : i64 ) -> Result < usize , GenericError > {
737+ let idx = usize:: try_from ( idx) . map_err ( |_| generic_error ! ( "Invalid negative resource index: {}" , idx) ) ?;
738+ idx. checked_sub ( 1 )
739+ . ok_or_else ( || generic_error ! ( "Invalid zero resource index" ) )
740+ }
741+
672742/// Read the next f64 value from the appropriate value array based on `value_type`.
673743fn read_value (
674744 value_type : u64 , cursors : & mut V3ValueCursors , vals_sint64 : & [ i64 ] , vals_float32 : & [ f32 ] , vals_float64 : & [ f64 ] ,
@@ -834,4 +904,78 @@ mod tests {
834904 assert ! ( metrics[ 0 ] . context. tags. contains( & "host:server-1" . to_string( ) ) ) ;
835905 assert ! ( metrics[ 0 ] . context. tags. contains( & "env:prod" . to_string( ) ) ) ;
836906 }
907+
908+ #[ test]
909+ fn try_from_v3_folds_host_resource_into_tags ( ) {
910+ use datadog_protos:: metrics:: v3:: { MetricData , Payload } ;
911+
912+ let mut data = MetricData :: new ( ) ;
913+ data. dictNameStr = length_prefixed_strings ( [ "my.metric" ] ) ;
914+ data. dictTagStr = length_prefixed_strings ( [ "env:prod" ] ) ;
915+ data. dictTagsets = vec ! [ 1 , 1 ] ;
916+ data. dictResourceStr = length_prefixed_strings ( [ "host" , "server-1" , "device" , "eth0" ] ) ;
917+ data. dictResourceLen = vec ! [ 2 ] ;
918+ data. dictResourceType = vec ! [ 1 , 2 ] ;
919+ data. dictResourceName = vec ! [ 2 , 2 ] ;
920+ data. types = vec ! [ V3_METRIC_TYPE_COUNT | V3_VALUE_TYPE_ZERO ] ;
921+ data. nameRefs = vec ! [ 1 ] ;
922+ data. tagsetRefs = vec ! [ 1 ] ;
923+ data. resourcesRefs = vec ! [ 1 ] ;
924+ data. intervals = vec ! [ 0 ] ;
925+ data. numPoints = vec ! [ 1 ] ;
926+ data. timestamps = vec ! [ 1 ] ;
927+
928+ let mut payload = Payload :: new ( ) ;
929+ payload. metricData = Some ( data) . into ( ) ;
930+
931+ let metrics = Metric :: try_from_v3 ( payload) . expect ( "parse should succeed" ) ;
932+ assert_eq ! ( metrics. len( ) , 1 ) ;
933+ assert ! ( metrics[ 0 ] . context. tags. contains( & "env:prod" . to_string( ) ) ) ;
934+ assert ! ( metrics[ 0 ] . context. tags. contains( & "host:server-1" . to_string( ) ) ) ;
935+ assert ! ( !metrics[ 0 ] . context. tags. iter( ) . any( |tag| tag. starts_with( "device:" ) ) ) ;
936+ }
937+
938+ #[ test]
939+ fn try_from_v3_decodes_integer_sketch_summary_order ( ) {
940+ use datadog_protos:: metrics:: v3:: { MetricData , Payload } ;
941+
942+ let mut data = MetricData :: new ( ) ;
943+ data. dictNameStr = length_prefixed_strings ( [ "my.sketch" ] ) ;
944+ data. types = vec ! [ V3_METRIC_TYPE_SKETCH | V3_VALUE_TYPE_SINT64 ] ;
945+ data. nameRefs = vec ! [ 1 ] ;
946+ data. tagsetRefs = vec ! [ 0 ] ;
947+ data. resourcesRefs = vec ! [ 0 ] ;
948+ data. intervals = vec ! [ 0 ] ;
949+ data. numPoints = vec ! [ 1 ] ;
950+ data. timestamps = vec ! [ 123 ] ;
951+ // Agent V3 sketch ordering is sum, min, max, count when integer summaries share valsSint64.
952+ data. valsSint64 = vec ! [ 10 , 1 , 4 , 4 ] ;
953+ data. sketchNumBins = vec ! [ 1 ] ;
954+ data. sketchBinKeys = vec ! [ 0 ] ;
955+ data. sketchBinCnts = vec ! [ 4 ] ;
956+
957+ let mut payload = Payload :: new ( ) ;
958+ payload. metricData = Some ( data) . into ( ) ;
959+
960+ let metrics = Metric :: try_from_v3 ( payload) . expect ( "parse should succeed" ) ;
961+ assert_eq ! ( metrics. len( ) , 1 ) ;
962+
963+ let MetricValue :: Sketch { sketch } = & metrics[ 0 ] . values [ 0 ] . 1 else {
964+ panic ! ( "expected sketch value" ) ;
965+ } ;
966+ assert_eq ! ( sketch. count( ) , 4 ) ;
967+ assert_eq ! ( sketch. sum( ) , Some ( 10.0 ) ) ;
968+ assert_eq ! ( sketch. min( ) , Some ( 1.0 ) ) ;
969+ assert_eq ! ( sketch. max( ) , Some ( 4.0 ) ) ;
970+ assert_eq ! ( sketch. avg( ) , Some ( 2.5 ) ) ;
971+ }
972+
973+ fn length_prefixed_strings ( strings : impl IntoIterator < Item = & ' static str > ) -> Vec < u8 > {
974+ let mut bytes = Vec :: new ( ) ;
975+ for s in strings {
976+ bytes. push ( s. len ( ) as u8 ) ;
977+ bytes. extend_from_slice ( s. as_bytes ( ) ) ;
978+ }
979+ bytes
980+ }
837981}
0 commit comments