@@ -20,6 +20,9 @@ use super::protocol::{MetricsPayloadInfo, MetricsProtocolVersion};
2020
2121static DD_URL_REGEX : LazyLock < Regex > =
2222 LazyLock :: new ( || Regex :: new ( r"^app(\.mrf)?(\.[a-z]{2}\d)?\.(datad(oghq|0g)\.(com|eu)|ddog-gov\.com)$" ) . unwrap ( ) ) ;
23+ static DD_SITE_FROM_HOSTNAME_REGEX : LazyLock < Regex > = LazyLock :: new ( || {
24+ Regex :: new ( r"(?:^|\.)([a-z]{2,}\d{1,2}\.)?(datad(?:oghq|0g)\.(?:com|eu)|ddog-gov\.com)\.?$" ) . unwrap ( )
25+ } ) ;
2326
2427pub const DEFAULT_SITE : & str = "datadoghq.com" ;
2528
@@ -45,6 +48,9 @@ pub struct EndpointV3Settings {
4548
4649 /// Whether validation mode is enabled for sketches (send both V2 and V3).
4750 pub sketches_validation_mode : bool ,
51+
52+ /// Whether this endpoint accepts sampled V3 beta series shadow payloads.
53+ pub series_shadow_mode : bool ,
4854}
4955
5056impl EndpointV3Settings {
@@ -53,17 +59,22 @@ impl EndpointV3Settings {
5359 /// The `v3_series_endpoints` and `v3_sketches_endpoints` are lists of configured endpoint names.
5460 /// If the endpoint name matches any entry, V3 is enabled for that metric type.
5561 pub fn from_endpoint_url (
56- configured_endpoint : & str , v3_series_endpoints : & [ String ] , v3_sketches_endpoints : & [ String ] ,
57- series_validate : bool , sketches_validate : bool ,
62+ configured_endpoint : & str , resolved_endpoint : & Url , v3_series_endpoints : & [ String ] ,
63+ v3_sketches_endpoints : & [ String ] , series_validate : bool , sketches_validate : bool ,
64+ series_shadow_sites : & [ String ] ,
5865 ) -> Self {
5966 let use_v3_series = v3_series_endpoints. iter ( ) . any ( |e| configured_endpoint == e) ;
6067 let use_v3_sketches = v3_sketches_endpoints. iter ( ) . any ( |e| configured_endpoint == e) ;
68+ let series_shadow_mode = !use_v3_series
69+ && extract_site_from_url ( resolved_endpoint. as_str ( ) )
70+ . is_some_and ( |site| series_shadow_sites. iter ( ) . any ( |shadow_site| shadow_site == & site) ) ;
6171
6272 Self {
6373 use_v3_series,
6474 use_v3_sketches,
6575 series_validation_mode : use_v3_series && series_validate,
6676 sketches_validation_mode : use_v3_sketches && sketches_validate,
77+ series_shadow_mode,
6778 }
6879 }
6980
@@ -100,8 +111,11 @@ impl EndpointV3Settings {
100111 if is_sketch {
101112 // V3 sketches: accept if V3 sketches is enabled
102113 self . use_v3_sketches
114+ } else if info. is_shadow ( ) {
115+ // V3 shadow series: accept only when this V2-authoritative endpoint is shadow-enabled.
116+ self . series_shadow_mode
103117 } else {
104- // V3 series: accept if V3 series is enabled
118+ // V3 series: accept if V3 series is enabled.
105119 self . use_v3_series
106120 }
107121 }
@@ -117,14 +131,25 @@ impl EndpointV3Settings {
117131 return false ;
118132 } ;
119133
120- if info. is_sketch ( ) {
134+ if info. is_shadow ( ) {
135+ self . series_shadow_mode
136+ } else if info. is_sketch ( ) {
121137 self . sketches_validation_mode
122138 } else {
123139 self . series_validation_mode
124140 }
125141 }
126142}
127143
144+ pub ( crate ) fn extract_site_from_url ( raw_url : & str ) -> Option < String > {
145+ let url = Url :: parse ( raw_url) . ok ( ) ?;
146+ let hostname = url. host_str ( ) ?. trim_end_matches ( '.' ) . to_ascii_lowercase ( ) ;
147+ let captures = DD_SITE_FROM_HOSTNAME_REGEX . captures ( & hostname) ?;
148+ let datacenter = captures. get ( 1 ) . map_or ( "" , |m| m. as_str ( ) ) ;
149+ let domain = captures. get ( 2 ) ?. as_str ( ) ;
150+ Some ( format ! ( "{datacenter}{domain}" ) )
151+ }
152+
128153/// Error type for invalid endpoints.
129154#[ derive( Debug , Snafu ) ]
130155#[ snafu( context( suffix( false ) ) ) ]
@@ -929,6 +954,7 @@ mod tests {
929954 use_v3_sketches : false ,
930955 series_validation_mode : true ,
931956 sketches_validation_mode : false ,
957+ series_shadow_mode : false ,
932958 } ;
933959
934960 assert ! ( settings. should_receive_validation_headers( Some ( MetricsPayloadInfo :: v2_series( ) ) ) ) ;
@@ -938,6 +964,79 @@ mod tests {
938964 assert ! ( !settings. should_receive_validation_headers( None ) ) ;
939965 }
940966
967+ #[ test]
968+ fn extract_site_from_url_matches_datadog_domains ( ) {
969+ assert_eq ! (
970+ Some ( "datadoghq.com" . to_string( ) ) ,
971+ extract_site_from_url( "https://1-2-3-agent.datadoghq.com/api/v2/series" )
972+ ) ;
973+ assert_eq ! (
974+ Some ( "us3.datadoghq.com" . to_string( ) ) ,
975+ extract_site_from_url( "https://intake.profile.us3.datadoghq.com/v1/input" )
976+ ) ;
977+ assert_eq ! ( None , extract_site_from_url( "https://vector.example.test/api/v2/series" ) ) ;
978+ }
979+
980+ #[ test]
981+ fn shadow_payloads_are_endpoint_scoped ( ) {
982+ let resolved = ResolvedEndpoint :: from_raw_endpoint ( "https://app.datadoghq.com" , "fake-api-key" )
983+ . expect ( "endpoint should resolve" ) ;
984+ let settings = EndpointV3Settings :: from_endpoint_url (
985+ resolved. configured_endpoint ( ) ,
986+ resolved. endpoint ( ) ,
987+ & [ ] ,
988+ & [ ] ,
989+ false ,
990+ false ,
991+ & [ "datadoghq.com" . to_string ( ) ] ,
992+ ) ;
993+
994+ assert ! ( settings. should_receive_payload( Some ( MetricsPayloadInfo :: v2_shadow_series( ) ) ) ) ;
995+ assert ! ( settings. should_receive_payload( Some ( MetricsPayloadInfo :: v3_shadow_series( ) ) ) ) ;
996+ assert ! ( !settings. should_receive_payload( Some ( MetricsPayloadInfo :: v3_series( ) ) ) ) ;
997+ assert ! ( settings. should_receive_validation_headers( Some ( MetricsPayloadInfo :: v2_shadow_series( ) ) ) ) ;
998+ assert ! ( settings. should_receive_validation_headers( Some ( MetricsPayloadInfo :: v3_shadow_series( ) ) ) ) ;
999+ }
1000+
1001+ #[ test]
1002+ fn shadow_payloads_require_allowed_site_and_v2_authoritative_endpoint ( ) {
1003+ let us3 = ResolvedEndpoint :: from_raw_endpoint ( "https://app.us3.datadoghq.com" , "fake-api-key" )
1004+ . expect ( "endpoint should resolve" ) ;
1005+ let settings = EndpointV3Settings :: from_endpoint_url (
1006+ us3. configured_endpoint ( ) ,
1007+ us3. endpoint ( ) ,
1008+ & [ ] ,
1009+ & [ ] ,
1010+ false ,
1011+ false ,
1012+ & [ "datadoghq.com" . to_string ( ) ] ,
1013+ ) ;
1014+ assert ! ( !settings. should_receive_payload( Some ( MetricsPayloadInfo :: v3_shadow_series( ) ) ) ) ;
1015+
1016+ let settings = EndpointV3Settings :: from_endpoint_url (
1017+ us3. configured_endpoint ( ) ,
1018+ us3. endpoint ( ) ,
1019+ & [ ] ,
1020+ & [ ] ,
1021+ false ,
1022+ false ,
1023+ & [ "us3.datadoghq.com" . to_string ( ) ] ,
1024+ ) ;
1025+ assert ! ( settings. should_receive_payload( Some ( MetricsPayloadInfo :: v3_shadow_series( ) ) ) ) ;
1026+
1027+ let v3_series_endpoints = vec ! [ us3. configured_endpoint( ) . to_string( ) ] ;
1028+ let settings = EndpointV3Settings :: from_endpoint_url (
1029+ us3. configured_endpoint ( ) ,
1030+ us3. endpoint ( ) ,
1031+ & v3_series_endpoints,
1032+ & [ ] ,
1033+ false ,
1034+ false ,
1035+ & [ "us3.datadoghq.com" . to_string ( ) ] ,
1036+ ) ;
1037+ assert ! ( !settings. should_receive_payload( Some ( MetricsPayloadInfo :: v3_shadow_series( ) ) ) ) ;
1038+ }
1039+
9411040 #[ test]
9421041 fn v3_endpoint_matching_uses_configured_endpoint_before_version_prefix ( ) {
9431042 let resolved = ResolvedEndpoint :: from_raw_endpoint ( "https://app.datadoghq.com" , "fake-api-key" )
@@ -949,10 +1048,12 @@ mod tests {
9491048 let v3_series_endpoints = vec ! [ "https://app.datadoghq.com" . to_string( ) ] ;
9501049 let settings = EndpointV3Settings :: from_endpoint_url (
9511050 resolved. configured_endpoint ( ) ,
1051+ resolved. endpoint ( ) ,
9521052 & v3_series_endpoints,
9531053 & [ ] ,
9541054 false ,
9551055 false ,
1056+ & [ "datadoghq.com" . to_string ( ) ] ,
9561057 ) ;
9571058
9581059 assert ! ( settings. use_v3_series) ;
@@ -961,12 +1062,16 @@ mod tests {
9611062 #[ test]
9621063 fn v3_endpoint_matching_is_endpoint_based ( ) {
9631064 let v3_series_endpoints = vec ! [ "https://app.us" . to_string( ) ] ;
1065+ let resolved = ResolvedEndpoint :: from_raw_endpoint ( "https://app.us5.datadoghq.com" , "fake-api-key" )
1066+ . expect ( "endpoint should resolve" ) ;
9641067 let settings = EndpointV3Settings :: from_endpoint_url (
965- "https://app.us5.datadoghq.com" ,
1068+ resolved. configured_endpoint ( ) ,
1069+ resolved. endpoint ( ) ,
9661070 & v3_series_endpoints,
9671071 & [ ] ,
9681072 false ,
9691073 false ,
1074+ & [ "datadoghq.com" . to_string ( ) ] ,
9701075 ) ;
9711076
9721077 assert ! ( !settings. use_v3_series) ;
@@ -975,8 +1080,17 @@ mod tests {
9751080 #[ test]
9761081 fn v3_endpoint_matching_requires_exact_configured_endpoint ( ) {
9771082 let v3_series_endpoints = vec ! [ "app.datadoghq.com/" . to_string( ) ] ;
978- let settings =
979- EndpointV3Settings :: from_endpoint_url ( "https://app.datadoghq.com" , & v3_series_endpoints, & [ ] , false , false ) ;
1083+ let resolved = ResolvedEndpoint :: from_raw_endpoint ( "https://app.datadoghq.com" , "fake-api-key" )
1084+ . expect ( "endpoint should resolve" ) ;
1085+ let settings = EndpointV3Settings :: from_endpoint_url (
1086+ resolved. configured_endpoint ( ) ,
1087+ resolved. endpoint ( ) ,
1088+ & v3_series_endpoints,
1089+ & [ ] ,
1090+ false ,
1091+ false ,
1092+ & [ "datadoghq.com" . to_string ( ) ] ,
1093+ ) ;
9801094
9811095 assert ! ( !settings. use_v3_series) ;
9821096 }
0 commit comments