@@ -645,21 +645,31 @@ mod tests {
645645
646646 /// Build a concentrator with `max_entries_per_bucket = 1` pre-seeded with four distinct spans
647647 /// so that three spans are collapsed into the overflow bucket.
648+ ///
649+ /// per_key_collapsed: enable small per-key limits to also collapse on `resource` and
650+ /// `http_endpoint`
648651 #[ cfg( any( feature = "telemetry" , feature = "dogstatsd" ) ) ]
649- fn get_collapsed_concentrator ( ) -> SpanConcentrator {
652+ fn get_collapsed_concentrator ( per_key_collapsed : bool ) -> SpanConcentrator {
650653 use crate :: span_concentrator:: CardinalityLimitConfig ;
651- use libdd_trace_utils:: span:: { trace_utils, v04:: SpanSlice } ;
654+ use libdd_trace_utils:: span:: {
655+ trace_utils,
656+ v04:: { SpanSlice , VecMap } ,
657+ } ;
652658
659+ let mut cardinality_limit_config = CardinalityLimitConfig {
660+ whole_key_limit : 2 , // max 2 distinct key → third distinct span collapses
661+ ..Default :: default ( )
662+ } ;
663+ if per_key_collapsed {
664+ cardinality_limit_config. resource_limit = 1 ;
665+ cardinality_limit_config. http_endpoint_limit = 1 ;
666+ }
653667 let mut concentrator = SpanConcentrator :: new (
654668 BUCKETS_DURATION ,
655669 SystemTime :: now ( ) ,
656670 vec ! [ ] ,
657671 vec ! [ ] ,
658- Some ( CardinalityLimitConfig {
659- whole_key_limit : 2 , // max 2 distinct key → third distinct span collapses
660- resource_limit : 1 , // max 1 distinct resource values
661- ..Default :: default ( )
662- } ) ,
672+ Some ( cardinality_limit_config) ,
663673 #[ cfg( feature = "stats-obfuscation" ) ]
664674 None ,
665675 ) ;
@@ -669,20 +679,26 @@ mod tests {
669679 service: "svc-a" ,
670680 resource: "resource-a" ,
671681 duration: 10 ,
682+ meta: VecMap :: from_iter( [ ( "http.endpoint" , "/" ) ] ) ,
672683 ..Default :: default ( )
673684 } ,
685+ // only resource get collapsed if per-key limits are enabled
674686 SpanSlice {
675687 service: "svc-a" ,
676688 resource: "resource-b" ,
677689 duration: 20 ,
690+ meta: VecMap :: from_iter( [ ( "http.endpoint" , "/" ) ] ) ,
678691 ..Default :: default ( )
679692 } ,
693+ // both resource and http_endpoint get collapsed if per-key limits are enabled
680694 SpanSlice {
681695 service: "svc-b" ,
682- resource: "resource-a " ,
696+ resource: "resource-c " ,
683697 duration: 20 ,
698+ meta: VecMap :: from_iter( [ ( "http.endpoint" , "/hello.txt" ) ] ) ,
684699 ..Default :: default ( )
685700 } ,
701+ // both resource and http_endpoint get collapsed if per-key limits are enabled
686702 SpanSlice {
687703 service: "svc-b" ,
688704 resource: "resource-b" ,
@@ -776,7 +792,59 @@ mod tests {
776792
777793 let stats_exporter = StatsExporter :: < NativeCapabilities > :: new (
778794 BUCKETS_DURATION ,
779- Arc :: new ( Mutex :: new ( get_collapsed_concentrator ( ) ) ) ,
795+ Arc :: new ( Mutex :: new ( get_collapsed_concentrator ( false ) ) ) ,
796+ get_test_metadata ( ) ,
797+ Endpoint :: from_url ( stats_url_from_agent_url ( & server. url ( "/" ) ) . unwrap ( ) ) ,
798+ NativeCapabilities :: new_client ( ) ,
799+ #[ cfg( feature = "stats-obfuscation" ) ]
800+ "1" ,
801+ #[ cfg( feature = "telemetry" ) ]
802+ None ,
803+ Some ( dogstatsd_client) ,
804+ ) ;
805+
806+ stats_exporter. send ( true ) . await . unwrap ( ) ;
807+
808+ let mut buf = [ 0u8 ; 256 ] ;
809+ let n = socket
810+ . recv ( & mut buf)
811+ . expect ( "expected a DogStatsD datagram" ) ;
812+ let datagram = std:: str:: from_utf8 ( & buf[ ..n] ) . expect ( "valid utf-8" ) ;
813+ assert_eq ! (
814+ datagram, "datadog.tracer.stats.collapsed_spans:2|c|#collapsed_spans:whole_key" ,
815+ "DogStatsD datagram must match the expected format"
816+ ) ;
817+ }
818+
819+ /// Verify that `COLLAPSED_SPANS_METRIC` is emitted to DogStatsD when spans are collapsed by
820+ /// per-key limits.
821+ #[ cfg( feature = "dogstatsd" ) ]
822+ #[ cfg_attr( miri, ignore) ]
823+ #[ tokio:: test]
824+ async fn test_collapsed_spans_per_key_dogstatsd ( ) {
825+ use std:: net;
826+
827+ let server = MockServer :: start_async ( ) . await ;
828+ server
829+ . mock_async ( |_when, then| {
830+ then. status ( 200 ) . body ( "" ) ;
831+ } )
832+ . await ;
833+
834+ let socket = net:: UdpSocket :: bind ( "127.0.0.1:0" ) . expect ( "failed to bind UDP socket" ) ;
835+ socket
836+ . set_read_timeout ( Some ( std:: time:: Duration :: from_millis ( 500 ) ) )
837+ . unwrap ( ) ;
838+ let addr = socket. local_addr ( ) . unwrap ( ) . to_string ( ) ;
839+
840+ let dogstatsd_client = Arc :: new (
841+ libdd_dogstatsd_client:: new ( libdd_common:: Endpoint :: from_slice ( & addr) )
842+ . expect ( "failed to create dogstatsd client" ) ,
843+ ) ;
844+
845+ let stats_exporter = StatsExporter :: < NativeCapabilities > :: new (
846+ BUCKETS_DURATION ,
847+ Arc :: new ( Mutex :: new ( get_collapsed_concentrator ( true ) ) ) ,
780848 get_test_metadata ( ) ,
781849 Endpoint :: from_url ( stats_url_from_agent_url ( & server. url ( "/" ) ) . unwrap ( ) ) ,
782850 NativeCapabilities :: new_client ( ) ,
@@ -790,6 +858,7 @@ mod tests {
790858 stats_exporter. send ( true ) . await . unwrap ( ) ;
791859
792860 let mut buf = [ 0u8 ; 256 ] ;
861+
793862 // Whole-key metric emitted first
794863 let n = socket
795864 . recv ( & mut buf)
@@ -800,13 +869,21 @@ mod tests {
800869 "DogStatsD datagram must match the expected format"
801870 ) ;
802871
803- // Then comes the per-field collapse metrics
872+ // Then comes the per-key collapse telemetry
873+ let n = socket
874+ . recv ( & mut buf)
875+ . expect ( "expected a DogStatsD datagram" ) ;
876+ let datagram = std:: str:: from_utf8 ( & buf[ ..n] ) . expect ( "valid utf-8" ) ;
877+ assert_eq ! (
878+ datagram, "datadog.tracer.stats.collapsed_spans:1|c|#collapsed_spans:resource" ,
879+ "DogStatsD datagram must match the expected format"
880+ ) ;
804881 let n = socket
805882 . recv ( & mut buf)
806883 . expect ( "expected a DogStatsD datagram" ) ;
807884 let datagram = std:: str:: from_utf8 ( & buf[ ..n] ) . expect ( "valid utf-8" ) ;
808885 assert_eq ! (
809- datagram, "datadog.tracer.stats.collapsed_spans:2|c|#collapsed_spans:resource" ,
886+ datagram, "datadog.tracer.stats.collapsed_spans:2|c|#collapsed_spans:resource,collapsed_spans:http_endpoint " ,
810887 "DogStatsD datagram must match the expected format"
811888 ) ;
812889 }
@@ -837,7 +914,7 @@ mod tests {
837914
838915 let stats_exporter = StatsExporter :: < NativeCapabilities > :: new (
839916 BUCKETS_DURATION ,
840- Arc :: new ( Mutex :: new ( get_collapsed_concentrator ( ) ) ) ,
917+ Arc :: new ( Mutex :: new ( get_collapsed_concentrator ( true ) ) ) ,
841918 get_test_metadata ( ) ,
842919 Endpoint :: from_url ( stats_url_from_agent_url ( & server. url ( "/" ) ) . unwrap ( ) ) ,
843920 NativeCapabilities :: new_client ( ) ,
@@ -867,8 +944,8 @@ mod tests {
867944 "exactly one metric context (COLLAPSED_SPANS_METRIC) should be registered"
868945 ) ;
869946 assert_eq ! (
870- stats. metric_buckets. buckets, 2 ,
871- "exactly one metric bucket expected after collapsing both on whole-key and on the resource field "
947+ stats. metric_buckets. buckets, 3 ,
948+ "exactly 3 metric bucket expected after one whole-key collapsed-spans, one resource key collapsed-span and one resource key+http_endpoint key emissions "
872949 ) ;
873950 }
874951}
0 commit comments