From 557081c797bdeccf96e845f0aa65b3443b92d371 Mon Sep 17 00:00:00 2001 From: Flavien Darche Date: Mon, 15 Dec 2025 15:52:29 +0100 Subject: [PATCH 1/8] change http to web --- .../lifecycle/invocation/triggers/api_gateway_http_event.rs | 6 +++--- .../lifecycle/invocation/triggers/api_gateway_rest_event.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs index 807e4e7ab..70d945d86 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs @@ -107,7 +107,7 @@ impl Trigger for APIGatewayHttpEvent { span.name = "aws.httpapi".to_string(); span.service = service_name; span.resource.clone_from(&resource); - span.r#type = "http".to_string(); + span.r#type = "web".to_string(); span.start = start_time; span.meta.extend(HashMap::from([ ( @@ -309,7 +309,7 @@ mod tests { "x02yirxc7a.execute-api.sa-east-1.amazonaws.com" ); assert_eq!(span.resource, "GET /httpapi/get"); - assert_eq!(span.r#type, "http"); + assert_eq!(span.r#type, "web"); assert_eq!( span.meta, HashMap::from([ @@ -373,7 +373,7 @@ mod tests { "9vj54we5ih.execute-api.sa-east-1.amazonaws.com" ); assert_eq!(span.resource, "GET /user/{user_id}"); - assert_eq!(span.r#type, "http"); + assert_eq!(span.r#type, "web"); assert_eq!( span.meta, HashMap::from([ diff --git a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs index 9bebc7744..a800ac4fe 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs @@ -104,7 +104,7 @@ impl Trigger for APIGatewayRestEvent { span.name = "aws.apigateway".to_string(); span.service = service_name; span.resource = resource; - span.r#type = "http".to_string(); + span.r#type = "web".to_string(); span.start = start_time; span.meta.extend(HashMap::from([ ("endpoint".to_string(), self.request_context.path.clone()), @@ -327,7 +327,7 @@ mod tests { assert_eq!(span.name, "aws.apigateway"); assert_eq!(span.service, "id.execute-api.us-east-1.amazonaws.com"); assert_eq!(span.resource, "GET /my/path"); - assert_eq!(span.r#type, "http"); + assert_eq!(span.r#type, "web"); assert_eq!( span.meta, @@ -389,7 +389,7 @@ mod tests { "mcwkra0ya4.execute-api.sa-east-1.amazonaws.com" ); assert_eq!(span.resource, "GET /dev/user/{user_id}/id/{id}"); - assert_eq!(span.r#type, "http"); + assert_eq!(span.r#type, "web"); let expected = HashMap::from([ ("endpoint".to_string(), "/dev/user/42/id/50".to_string()), ( From bf6790d267deb6d1c47664726772e4df8d456094 Mon Sep 17 00:00:00 2001 From: Flavien Darche Date: Mon, 15 Dec 2025 15:57:05 +0100 Subject: [PATCH 2/8] removed operation_name and apiname --- .../lifecycle/invocation/triggers/api_gateway_http_event.rs | 3 --- .../lifecycle/invocation/triggers/api_gateway_rest_event.rs | 3 --- .../invocation/triggers/api_gateway_websocket_event.rs | 4 ---- 3 files changed, 10 deletions(-) diff --git a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs index 70d945d86..746ac8363 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs @@ -131,7 +131,6 @@ impl Trigger for APIGatewayHttpEvent { "http.user_agent".to_string(), self.request_context.http.user_agent.clone(), ), - ("operation_name".to_string(), "aws.httpapi".to_string()), ( "request_id".to_string(), self.request_context.request_id.clone(), @@ -323,7 +322,6 @@ mod tests { ("http.protocol".to_string(), "HTTP/1.1".to_string()), ("http.source_ip".to_string(), "38.122.226.210".to_string()), ("http.user_agent".to_string(), "curl/7.64.1".to_string()), - ("operation_name".to_string(), "aws.httpapi".to_string()), ("request_id".to_string(), "FaHnXjKCGjQEJ7A=".to_string()), ]) ); @@ -386,7 +384,6 @@ mod tests { ("http.protocol".to_string(), "HTTP/1.1".to_string()), ("http.source_ip".to_string(), "76.115.124.192".to_string()), ("http.user_agent".to_string(), "curl/8.1.2".to_string()), - ("operation_name".to_string(), "aws.httpapi".to_string()), ("request_id".to_string(), "Ur2JtjEfGjQEPOg=".to_string()), ]) ); diff --git a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs index a800ac4fe..90d842406 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs @@ -125,7 +125,6 @@ impl Trigger for APIGatewayRestEvent { "http.user_agent".to_string(), self.request_context.identity.user_agent.clone(), ), - ("operation_name".to_string(), "aws.apigateway".to_string()), ( "request_id".to_string(), self.request_context.request_id.clone(), @@ -342,7 +341,6 @@ mod tests { ("http.source_ip".to_string(), "IP".to_string()), ("http.user_agent".to_string(), "user-agent".to_string()), ("http.route".to_string(), "/path".to_string()), - ("operation_name".to_string(), "aws.apigateway".to_string()), ("request_id".to_string(), "id=".to_string()), ]) ); @@ -402,7 +400,6 @@ mod tests { ("http.source_ip".to_string(), "76.115.124.192".to_string()), ("http.user_agent".to_string(), "curl/8.1.2".to_string()), ("http.route".to_string(), "/user/{id}".to_string()), - ("operation_name".to_string(), "aws.apigateway".to_string()), ( "request_id".to_string(), "e16399f7-e984-463a-9931-745ba021a27f".to_string(), diff --git a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_websocket_event.rs b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_websocket_event.rs index 62d767366..02aaf820a 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_websocket_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_websocket_event.rs @@ -111,13 +111,11 @@ impl Trigger for APIGatewayWebSocketEvent { self.request_context.route_key.clone(), ), ("http.url".to_string(), http_url), - ("operation_name".to_string(), "aws.apigateway".to_string()), ( "request_id".to_string(), self.request_context.request_id.clone(), ), ("apiid".to_string(), self.request_context.api_id.clone()), - ("apiname".to_string(), self.request_context.api_id.clone()), ("stage".to_string(), self.request_context.stage.clone()), ( "connection_id".to_string(), @@ -353,10 +351,8 @@ mod tests { "http.url".to_string(), "https://85fj5nw29d.execute-api.eu-west-1.amazonaws.comhello".to_string() ), - ("operation_name".to_string(), "aws.apigateway".to_string()), ("request_id".to_string(), "ahVmYGOMmjQFhyg=".to_string()), ("apiid".to_string(), "85fj5nw29d".to_string()), - ("apiname".to_string(), "85fj5nw29d".to_string()), ("stage".to_string(), "dev".to_string()), ("connection_id".to_string(), "ahVWscZqmjQCI1w=".to_string()), ("event_type".to_string(), "MESSAGE".to_string()), From f0b53e739d7fb339749ffc2a157def55584030d6 Mon Sep 17 00:00:00 2001 From: Flavien Darche Date: Mon, 15 Dec 2025 16:03:08 +0100 Subject: [PATCH 3/8] propagate appsec (enablement + json) --- .../src/lifecycle/invocation/span_inferrer.rs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/bottlecap/src/lifecycle/invocation/span_inferrer.rs b/bottlecap/src/lifecycle/invocation/span_inferrer.rs index d93f52d1d..c2e0c7977 100644 --- a/bottlecap/src/lifecycle/invocation/span_inferrer.rs +++ b/bottlecap/src/lifecycle/invocation/span_inferrer.rs @@ -278,12 +278,15 @@ impl SpanInferrer { invocation_span.service.clone(), ); s.meta.insert("span.kind".to_string(), "server".to_string()); + let appsec_enabled = self.config.serverless_appsec_enabled; + propagate_appsec(appsec_enabled, invocation_span, s); if let Some(ws) = &mut self.wrapped_inferred_span { ws.trace_id = invocation_span.trace_id; ws.error = invocation_span.error; ws.meta .insert(String::from("peer.service"), s.service.clone()); + propagate_appsec(appsec_enabled, invocation_span, ws); // The wrapper span should be the parent of the inferred span, // therefore the `parent_id` of the inferred span should be the @@ -325,6 +328,36 @@ impl SpanInferrer { } } +fn propagate_appsec( + serverless_appsec_enabled: bool, + invocation_span: &Span, + target_span: &mut Span, +) { + let has_appsec = invocation_span + .metrics + .get("_dd.appsec.enabled") + .copied() + .or_else(|| { + if serverless_appsec_enabled { + Some(1.0) + } else { + None + } + }); + + if let Some(enabled) = has_appsec { + target_span + .metrics + .insert("_dd.appsec.enabled".to_string(), enabled); + } + + if let Some(json) = invocation_span.meta.get("_dd.appsec.json") { + target_span + .meta + .insert("_dd.appsec.json".to_string(), json.clone()); + } +} + pub fn extract_span_context( payload_value: &Value, propagator: Arc, From 0f02b425626abaf42062a86aa64883a5f9950492 Mon Sep 17 00:00:00 2001 From: Flavien Darche Date: Mon, 15 Dec 2025 17:33:04 +0100 Subject: [PATCH 4/8] build apigw arn and set dd_resource_key --- .../src/lifecycle/invocation/span_inferrer.rs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/bottlecap/src/lifecycle/invocation/span_inferrer.rs b/bottlecap/src/lifecycle/invocation/span_inferrer.rs index c2e0c7977..c29a286af 100644 --- a/bottlecap/src/lifecycle/invocation/span_inferrer.rs +++ b/bottlecap/src/lifecycle/invocation/span_inferrer.rs @@ -6,6 +6,7 @@ use serde_json::Value; use tracing::debug; use crate::config::Config; +use crate::config::aws::get_aws_partition_by_region; use crate::lifecycle::invocation::triggers::IdentifiedTrigger; use crate::traces::span_pointers::SpanPointer; use crate::traces::{context::SpanContext, propagation::Propagator}; @@ -39,6 +40,17 @@ pub struct SpanInferrer { pub span_pointers: Option>, } +#[derive(Default)] +struct ApiGatewayContext { + dd_resource_key: Option, + aws_user: Option, +} + +enum ApiGatewayType { + Rest, + Http, +} + impl SpanInferrer { #[must_use] pub fn new(config: Arc) -> Self { @@ -209,6 +221,8 @@ impl SpanInferrer { }; let identified_trigger = IdentifiedTrigger::from_value(payload_value); + let api_gateway_context = + Self::get_api_gateway_context(&identified_trigger, &aws_config.region); let should_enrich_span = Self::should_enrich_span(&identified_trigger); let should_skip_inferred_span = Self::should_skip_inferred_span(&identified_trigger); let wrapped_inferred_span = @@ -227,6 +241,12 @@ impl SpanInferrer { ); } + if let Some(dd_resource_key) = api_gateway_context.dd_resource_key { + inferred_span + .meta + .insert("dd_resource_key".to_string(), dd_resource_key); + } + self.wrapped_inferred_span = wrapped_inferred_span; self.span_pointers = span_pointers; @@ -326,6 +346,51 @@ impl SpanInferrer { pub fn get_trigger_tags(&self) -> Option> { self.trigger_tags.clone() } + + fn get_api_gateway_context( + trigger: &IdentifiedTrigger, + region: &str, + ) -> ApiGatewayContext { + match trigger { + IdentifiedTrigger::APIGatewayRestEvent(event) => ApiGatewayContext { + dd_resource_key: Self::build_api_gateway_arn( + &event.request_context.api_id, + region, + ApiGatewayType::Rest, + ), + aws_user: None, + }, + IdentifiedTrigger::APIGatewayHttpEvent(event) => ApiGatewayContext { + dd_resource_key: Self::build_api_gateway_arn( + &event.request_context.api_id, + region, + ApiGatewayType::Http, + ), + aws_user: None, + }, + _ => ApiGatewayContext::default(), + } + } + + fn build_api_gateway_arn( + api_id: &str, + region: &str, + api_type: ApiGatewayType, + ) -> Option { + if api_id.is_empty() { + return None; + } + + let partition = get_aws_partition_by_region(region); + let path = match api_type { + ApiGatewayType::Rest => "restapis", + ApiGatewayType::Http => "apis", + }; + + Some(format!( + "arn:{partition}:apigateway:{region}::/{path}/{api_id}", + )) + } } fn propagate_appsec( From a542a0bb19ad7c423c389c51dada6621dd45123a Mon Sep 17 00:00:00 2001 From: Flavien Darche Date: Tue, 16 Dec 2025 15:27:52 +0100 Subject: [PATCH 5/8] refactor dd_resource_key + add tests (appsec propagation and resource key) --- .../src/lifecycle/invocation/span_inferrer.rs | 181 ++++++++++++++---- .../triggers/api_gateway_http_event.rs | 1 + 2 files changed, 144 insertions(+), 38 deletions(-) diff --git a/bottlecap/src/lifecycle/invocation/span_inferrer.rs b/bottlecap/src/lifecycle/invocation/span_inferrer.rs index c29a286af..136f51d30 100644 --- a/bottlecap/src/lifecycle/invocation/span_inferrer.rs +++ b/bottlecap/src/lifecycle/invocation/span_inferrer.rs @@ -40,17 +40,6 @@ pub struct SpanInferrer { pub span_pointers: Option>, } -#[derive(Default)] -struct ApiGatewayContext { - dd_resource_key: Option, - aws_user: Option, -} - -enum ApiGatewayType { - Rest, - Http, -} - impl SpanInferrer { #[must_use] pub fn new(config: Arc) -> Self { @@ -221,8 +210,8 @@ impl SpanInferrer { }; let identified_trigger = IdentifiedTrigger::from_value(payload_value); - let api_gateway_context = - Self::get_api_gateway_context(&identified_trigger, &aws_config.region); + let dd_resource_key = + Self::get_api_gateway_resource_key(&identified_trigger, &aws_config.region); let should_enrich_span = Self::should_enrich_span(&identified_trigger); let should_skip_inferred_span = Self::should_skip_inferred_span(&identified_trigger); let wrapped_inferred_span = @@ -241,7 +230,7 @@ impl SpanInferrer { ); } - if let Some(dd_resource_key) = api_gateway_context.dd_resource_key { + if let Some(dd_resource_key) = dd_resource_key { inferred_span .meta .insert("dd_resource_key".to_string(), dd_resource_key); @@ -347,45 +336,32 @@ impl SpanInferrer { self.trigger_tags.clone() } - fn get_api_gateway_context( + #[must_use] + fn get_api_gateway_resource_key( trigger: &IdentifiedTrigger, region: &str, - ) -> ApiGatewayContext { + ) -> Option { match trigger { - IdentifiedTrigger::APIGatewayRestEvent(event) => ApiGatewayContext { - dd_resource_key: Self::build_api_gateway_arn( - &event.request_context.api_id, - region, - ApiGatewayType::Rest, - ), - aws_user: None, - }, - IdentifiedTrigger::APIGatewayHttpEvent(event) => ApiGatewayContext { - dd_resource_key: Self::build_api_gateway_arn( - &event.request_context.api_id, - region, - ApiGatewayType::Http, - ), - aws_user: None, - }, - _ => ApiGatewayContext::default(), + IdentifiedTrigger::APIGatewayRestEvent(event) => { + Self::build_api_gateway_arn(&event.request_context.api_id, region, "restapis") + } + IdentifiedTrigger::APIGatewayHttpEvent(event) => { + Self::build_api_gateway_arn(&event.request_context.api_id, region, "apis") + } + _ => None, } } fn build_api_gateway_arn( api_id: &str, region: &str, - api_type: ApiGatewayType, + path: &str, ) -> Option { if api_id.is_empty() { return None; } let partition = get_aws_partition_by_region(region); - let path = match api_type { - ApiGatewayType::Rest => "restapis", - ApiGatewayType::Http => "apis", - }; Some(format!( "arn:{partition}:apigateway:{region}::/{path}/{api_id}", @@ -466,6 +442,7 @@ pub fn extract_generated_span_context( #[cfg(test)] mod tests { use super::*; + use crate::lifecycle::invocation::triggers::test_utils::read_json_file; use crate::traces::propagation::text_map_propagator::DatadogHeaderPropagator; use serde_json::json; use std::sync::Arc; @@ -669,4 +646,132 @@ mod tests { "Should have SQS as event source" ); } + + fn api_gateway_rest_payload() -> serde_json::Value { + let json = read_json_file("api_gateway_rest_event.json"); + serde_json::from_str(&json).expect("Failed to deserialize API Gateway REST payload") + } + + fn aws_config(region: &str) -> Arc { + Arc::new(AwsConfig { + region: region.to_string(), + aws_lwa_proxy_lambda_runtime_api: Some(String::new()), + runtime_api: String::new(), + function_name: String::new(), + sandbox_init_time: Instant::now(), + exec_wrapper: None, + initialization_type: "on-demand".into(), + }) + } + + #[test] + fn test_api_gateway_sets_dd_resource_key_for_rest_event() { + let payload = api_gateway_rest_payload(); + let aws_config = aws_config("us-east-1"); + let mut inferrer = SpanInferrer::new(Arc::new(Config::default())); + + inferrer.infer_span(&payload, &aws_config); + + let inferred_span = inferrer + .inferred_span + .as_ref() + .expect("Should have inferred API Gateway span"); + + assert_eq!( + inferred_span + .meta + .get("dd_resource_key") + .cloned() + .unwrap_or_default(), + "arn:aws:apigateway:us-east-1::/restapis/id" + ); + } + + #[test] + fn test_complete_inferred_spans_propagates_appsec_from_invocation() { + let payload = api_gateway_rest_payload(); + let aws_config = aws_config("us-east-1"); + let mut inferrer = SpanInferrer::new(Arc::new(Config::default())); + + inferrer.infer_span(&payload, &aws_config); + + let mut invocation_span = Span::default(); + invocation_span.trace_id = 42; + invocation_span.span_id = 100; + invocation_span.service = "lambda-service".to_string(); + if let Some(inferred_span) = &inferrer.inferred_span { + invocation_span.start = inferred_span.start; + } + invocation_span.duration = 1; + invocation_span + .metrics + .insert("_dd.appsec.enabled".to_string(), 1.0); + invocation_span.meta.insert( + "_dd.appsec.json".to_string(), + r#"{"triggers":["rule"]}"#.to_string(), + ); + + inferrer.complete_inferred_spans(&invocation_span); + + let inferred_span = inferrer + .inferred_span + .as_ref() + .expect("Inferred span should still be present"); + + assert_eq!( + inferred_span + .metrics + .get("_dd.appsec.enabled") + .copied() + .unwrap_or_default(), + 1.0 + ); + assert_eq!( + inferred_span + .meta + .get("_dd.appsec.json") + .cloned() + .unwrap_or_default(), + r#"{"triggers":["rule"]}"# + ); + } + + #[test] + fn test_complete_inferred_spans_sets_appsec_when_enabled_in_config() { + let mut config = Config::default(); + config.serverless_appsec_enabled = true; + let mut inferrer = SpanInferrer::new(Arc::new(config)); + + let payload = api_gateway_rest_payload(); + let aws_config = aws_config("us-east-1"); + inferrer.infer_span(&payload, &aws_config); + + let mut invocation_span = Span::default(); + invocation_span.trace_id = 7; + invocation_span.service = "lambda-service".to_string(); + if let Some(inferred_span) = &inferrer.inferred_span { + invocation_span.start = inferred_span.start; + } + invocation_span.duration = 1; + + inferrer.complete_inferred_spans(&invocation_span); + + let inferred_span = inferrer + .inferred_span + .as_ref() + .expect("Inferred span should still be present"); + + assert_eq!( + inferred_span + .metrics + .get("_dd.appsec.enabled") + .copied() + .unwrap_or_default(), + 1.0 + ); + assert!( + !inferred_span.meta.contains_key("_dd.appsec.json"), + "AppSec JSON should not be added when invocation span has none" + ); + } } diff --git a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs index 746ac8363..ac32a3634 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs @@ -219,6 +219,7 @@ impl ServiceNameResolver for APIGatewayHttpEvent { "lambda_api_gateway" } } + #[cfg(test)] mod tests { use super::*; From 156c74e438f829c24ee01e93926942ae7d58e8b7 Mon Sep 17 00:00:00 2001 From: Flavien Darche Date: Wed, 17 Dec 2025 14:55:23 +0100 Subject: [PATCH 6/8] linter fix --- .../src/lifecycle/invocation/span_inferrer.rs | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/bottlecap/src/lifecycle/invocation/span_inferrer.rs b/bottlecap/src/lifecycle/invocation/span_inferrer.rs index 136f51d30..405b2db98 100644 --- a/bottlecap/src/lifecycle/invocation/span_inferrer.rs +++ b/bottlecap/src/lifecycle/invocation/span_inferrer.rs @@ -337,10 +337,7 @@ impl SpanInferrer { } #[must_use] - fn get_api_gateway_resource_key( - trigger: &IdentifiedTrigger, - region: &str, - ) -> Option { + fn get_api_gateway_resource_key(trigger: &IdentifiedTrigger, region: &str) -> Option { match trigger { IdentifiedTrigger::APIGatewayRestEvent(event) => { Self::build_api_gateway_arn(&event.request_context.api_id, region, "restapis") @@ -352,11 +349,8 @@ impl SpanInferrer { } } - fn build_api_gateway_arn( - api_id: &str, - region: &str, - path: &str, - ) -> Option { + #[must_use] + fn build_api_gateway_arn(api_id: &str, region: &str, path: &str) -> Option { if api_id.is_empty() { return None; } @@ -378,12 +372,10 @@ fn propagate_appsec( .metrics .get("_dd.appsec.enabled") .copied() - .or_else(|| { - if serverless_appsec_enabled { - Some(1.0) - } else { - None - } + .or(if serverless_appsec_enabled { + Some(1.0) + } else { + None }); if let Some(enabled) = has_appsec { @@ -695,10 +687,12 @@ mod tests { inferrer.infer_span(&payload, &aws_config); - let mut invocation_span = Span::default(); - invocation_span.trace_id = 42; - invocation_span.span_id = 100; - invocation_span.service = "lambda-service".to_string(); + let mut invocation_span = Span { + trace_id: 42, + span_id: 100, + service: "lambda-service".to_string(), + ..Span::default() + }; if let Some(inferred_span) = &inferrer.inferred_span { invocation_span.start = inferred_span.start; } @@ -718,13 +712,14 @@ mod tests { .as_ref() .expect("Inferred span should still be present"); - assert_eq!( - inferred_span - .metrics - .get("_dd.appsec.enabled") - .copied() - .unwrap_or_default(), - 1.0 + let appsec_enabled = inferred_span + .metrics + .get("_dd.appsec.enabled") + .copied() + .unwrap_or_default(); + assert!( + (appsec_enabled - 1.0).abs() < f64::EPSILON, + "Expected appsec enabled metric to be 1.0" ); assert_eq!( inferred_span @@ -738,17 +733,21 @@ mod tests { #[test] fn test_complete_inferred_spans_sets_appsec_when_enabled_in_config() { - let mut config = Config::default(); - config.serverless_appsec_enabled = true; + let config = Config { + serverless_appsec_enabled: true, + ..Config::default() + }; let mut inferrer = SpanInferrer::new(Arc::new(config)); let payload = api_gateway_rest_payload(); let aws_config = aws_config("us-east-1"); inferrer.infer_span(&payload, &aws_config); - let mut invocation_span = Span::default(); - invocation_span.trace_id = 7; - invocation_span.service = "lambda-service".to_string(); + let mut invocation_span = Span { + trace_id: 7, + service: "lambda-service".to_string(), + ..Span::default() + }; if let Some(inferred_span) = &inferrer.inferred_span { invocation_span.start = inferred_span.start; } @@ -761,13 +760,14 @@ mod tests { .as_ref() .expect("Inferred span should still be present"); - assert_eq!( - inferred_span - .metrics - .get("_dd.appsec.enabled") - .copied() - .unwrap_or_default(), - 1.0 + let appsec_enabled = inferred_span + .metrics + .get("_dd.appsec.enabled") + .copied() + .unwrap_or_default(); + assert!( + (appsec_enabled - 1.0).abs() < f64::EPSILON, + "Expected appsec enabled metric to be 1.0" ); assert!( !inferred_span.meta.contains_key("_dd.appsec.json"), From 9acc70f5e65b5db50831210e420cd0eb91a882f4 Mon Sep 17 00:00:00 2001 From: Flavien Darche Date: Wed, 17 Dec 2025 17:02:59 +0100 Subject: [PATCH 7/8] move dd_resource_key in the trigger --- .../src/lifecycle/invocation/span_inferrer.rs | 31 +------------------ .../triggers/api_gateway_http_event.rs | 14 +++++++++ .../triggers/api_gateway_rest_event.rs | 14 +++++++++ .../src/lifecycle/invocation/triggers/mod.rs | 4 +++ 4 files changed, 33 insertions(+), 30 deletions(-) diff --git a/bottlecap/src/lifecycle/invocation/span_inferrer.rs b/bottlecap/src/lifecycle/invocation/span_inferrer.rs index 405b2db98..cdc159f6d 100644 --- a/bottlecap/src/lifecycle/invocation/span_inferrer.rs +++ b/bottlecap/src/lifecycle/invocation/span_inferrer.rs @@ -6,7 +6,6 @@ use serde_json::Value; use tracing::debug; use crate::config::Config; -use crate::config::aws::get_aws_partition_by_region; use crate::lifecycle::invocation::triggers::IdentifiedTrigger; use crate::traces::span_pointers::SpanPointer; use crate::traces::{context::SpanContext, propagation::Propagator}; @@ -210,8 +209,6 @@ impl SpanInferrer { }; let identified_trigger = IdentifiedTrigger::from_value(payload_value); - let dd_resource_key = - Self::get_api_gateway_resource_key(&identified_trigger, &aws_config.region); let should_enrich_span = Self::should_enrich_span(&identified_trigger); let should_skip_inferred_span = Self::should_skip_inferred_span(&identified_trigger); let wrapped_inferred_span = @@ -230,7 +227,7 @@ impl SpanInferrer { ); } - if let Some(dd_resource_key) = dd_resource_key { + if let Some(dd_resource_key) = t.get_dd_resource_key(&aws_config.region) { inferred_span .meta .insert("dd_resource_key".to_string(), dd_resource_key); @@ -335,32 +332,6 @@ impl SpanInferrer { pub fn get_trigger_tags(&self) -> Option> { self.trigger_tags.clone() } - - #[must_use] - fn get_api_gateway_resource_key(trigger: &IdentifiedTrigger, region: &str) -> Option { - match trigger { - IdentifiedTrigger::APIGatewayRestEvent(event) => { - Self::build_api_gateway_arn(&event.request_context.api_id, region, "restapis") - } - IdentifiedTrigger::APIGatewayHttpEvent(event) => { - Self::build_api_gateway_arn(&event.request_context.api_id, region, "apis") - } - _ => None, - } - } - - #[must_use] - fn build_api_gateway_arn(api_id: &str, region: &str, path: &str) -> Option { - if api_id.is_empty() { - return None; - } - - let partition = get_aws_partition_by_region(region); - - Some(format!( - "arn:{partition}:apigateway:{region}::/{path}/{api_id}", - )) - } } fn propagate_appsec( diff --git a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs index ac32a3634..13375b684 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs @@ -199,6 +199,20 @@ impl Trigger for APIGatewayHttpEvent { ) } + fn get_dd_resource_key(&self, region: &str) -> Option { + if self.request_context.api_id.is_empty() { + return None; + } + + let partition = get_aws_partition_by_region(region); + Some(format!( + "arn:{partition}:apigateway:{region}::/apis/{api_id}", + partition = partition, + region = region, + api_id = self.request_context.api_id + )) + } + fn is_async(&self) -> bool { self.headers .get("x-amz-invocation-type") diff --git a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs index 90d842406..53fc2aa5f 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs @@ -186,6 +186,20 @@ impl Trigger for APIGatewayRestEvent { ) } + fn get_dd_resource_key(&self, region: &str) -> Option { + if self.request_context.api_id.is_empty() { + return None; + } + + let partition = get_aws_partition_by_region(region); + Some(format!( + "arn:{partition}:apigateway:{region}::/restapis/{api_id}", + partition = partition, + region = region, + api_id = self.request_context.api_id + )) + } + fn is_async(&self) -> bool { self.headers .get("x-amz-invocation-type") diff --git a/bottlecap/src/lifecycle/invocation/triggers/mod.rs b/bottlecap/src/lifecycle/invocation/triggers/mod.rs index cd28b523f..04f73c498 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/mod.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/mod.rs @@ -130,6 +130,10 @@ pub trait Trigger: ServiceNameResolver { fn get_carrier(&self) -> HashMap; fn is_async(&self) -> bool; + fn get_dd_resource_key(&self, _region: &str) -> Option { + None + } + /// Default implementation for service name resolution fn resolve_service_name( &self, From cddf7fc798f94cae7d96f44e523feeaca997a738 Mon Sep 17 00:00:00 2001 From: Flavien Darche Date: Wed, 17 Dec 2025 17:06:17 +0100 Subject: [PATCH 8/8] move test to event trigger files --- .../src/lifecycle/invocation/span_inferrer.rs | 23 ------------------- .../triggers/api_gateway_http_event.rs | 12 ++++++++++ .../triggers/api_gateway_rest_event.rs | 12 ++++++++++ 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/bottlecap/src/lifecycle/invocation/span_inferrer.rs b/bottlecap/src/lifecycle/invocation/span_inferrer.rs index cdc159f6d..14cba1ab0 100644 --- a/bottlecap/src/lifecycle/invocation/span_inferrer.rs +++ b/bottlecap/src/lifecycle/invocation/span_inferrer.rs @@ -627,29 +627,6 @@ mod tests { }) } - #[test] - fn test_api_gateway_sets_dd_resource_key_for_rest_event() { - let payload = api_gateway_rest_payload(); - let aws_config = aws_config("us-east-1"); - let mut inferrer = SpanInferrer::new(Arc::new(Config::default())); - - inferrer.infer_span(&payload, &aws_config); - - let inferred_span = inferrer - .inferred_span - .as_ref() - .expect("Should have inferred API Gateway span"); - - assert_eq!( - inferred_span - .meta - .get("dd_resource_key") - .cloned() - .unwrap_or_default(), - "arn:aws:apigateway:us-east-1::/restapis/id" - ); - } - #[test] fn test_complete_inferred_spans_propagates_appsec_from_invocation() { let payload = api_gateway_rest_payload(); diff --git a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs index 13375b684..0078cec4d 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs @@ -441,6 +441,18 @@ mod tests { ); } + #[test] + fn test_get_dd_resource_key() { + let json = read_json_file("api_gateway_http_event.json"); + let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); + let event = + APIGatewayHttpEvent::new(payload).expect("Failed to deserialize APIGatewayHttpEvent"); + assert_eq!( + event.get_dd_resource_key("sa-east-1"), + Some("arn:aws:apigateway:sa-east-1::/apis/x02yirxc7a".to_string()) + ); + } + #[test] fn test_resolve_service_name_with_representation_enabled() { let json = read_json_file("api_gateway_http_event.json"); diff --git a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs index 53fc2aa5f..b52d8aad3 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs @@ -465,6 +465,18 @@ mod tests { ); } + #[test] + fn test_get_dd_resource_key() { + let json = read_json_file("api_gateway_rest_event.json"); + let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); + let event = + APIGatewayRestEvent::new(payload).expect("Failed to deserialize APIGatewayRestEvent"); + assert_eq!( + event.get_dd_resource_key("us-east-1"), + Some("arn:aws:apigateway:us-east-1::/restapis/id".to_string()) + ); + } + #[test] fn test_resolve_service_name_with_representation_enabled() { let json = read_json_file("api_gateway_rest_event.json");