diff --git a/bottlecap/src/config/env.rs b/bottlecap/src/config/env.rs index ad91db2f2..46b2717f4 100644 --- a/bottlecap/src/config/env.rs +++ b/bottlecap/src/config/env.rs @@ -178,6 +178,11 @@ pub struct EnvConfig { /// #[serde(deserialize_with = "deserialize_additional_endpoints")] pub apm_additional_endpoints: HashMap>, + /// @env `DD_TRACE_AWS_SERVICE_REPRESENTATION_ENABLED` + /// + /// Enable the new AWS-resource naming logic in the tracer. + #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] + pub trace_aws_service_representation_enabled: Option, // // Trace Propagation /// @env `DD_TRACE_PROPAGATION_STYLE` @@ -362,6 +367,7 @@ fn merge_config(config: &mut Config, env_config: &EnvConfig) { merge_option_to_value!(config, env_config, apm_config_compression_level); merge_vec!(config, env_config, apm_features); merge_hashmap!(config, env_config, apm_additional_endpoints); + merge_option_to_value!(config, env_config, trace_aws_service_representation_enabled); // Trace Propagation merge_vec!(config, env_config, trace_propagation_style); @@ -560,6 +566,7 @@ mod tests { jail.set_env("DD_TRACE_PROPAGATION_STYLE_EXTRACT", "b3"); jail.set_env("DD_TRACE_PROPAGATION_EXTRACT_FIRST", "true"); jail.set_env("DD_TRACE_PROPAGATION_HTTP_BAGGAGE_ENABLED", "true"); + jail.set_env("DD_TRACE_AWS_SERVICE_REPRESENTATION_ENABLED", "true"); // OTLP jail.set_env("DD_OTLP_CONFIG_TRACES_ENABLED", "false"); @@ -709,6 +716,7 @@ mod tests { trace_propagation_style_extract: vec![TracePropagationStyle::B3], trace_propagation_extract_first: true, trace_propagation_http_baggage_enabled: true, + trace_aws_service_representation_enabled: true, otlp_config_traces_enabled: false, otlp_config_traces_span_name_as_resource_name: true, otlp_config_traces_span_name_remappings: HashMap::from([( diff --git a/bottlecap/src/config/mod.rs b/bottlecap/src/config/mod.rs index 2f7d43085..3a5f2f845 100644 --- a/bottlecap/src/config/mod.rs +++ b/bottlecap/src/config/mod.rs @@ -283,6 +283,7 @@ pub struct Config { pub trace_propagation_style_extract: Vec, pub trace_propagation_extract_first: bool, pub trace_propagation_http_baggage_enabled: bool, + pub trace_aws_service_representation_enabled: bool, // OTLP // @@ -371,6 +372,7 @@ impl Default for Config { apm_config_compression_level: 6, apm_features: vec![], apm_additional_endpoints: HashMap::new(), + trace_aws_service_representation_enabled: true, trace_propagation_style: vec![ TracePropagationStyle::Datadog, TracePropagationStyle::TraceContext, diff --git a/bottlecap/src/config/yaml.rs b/bottlecap/src/config/yaml.rs index 1045fd660..7620f5ab5 100644 --- a/bottlecap/src/config/yaml.rs +++ b/bottlecap/src/config/yaml.rs @@ -64,6 +64,8 @@ pub struct YamlConfig { pub apm_config: ApmConfig, #[serde(deserialize_with = "deserialize_service_mapping")] pub service_mapping: HashMap, + #[serde(deserialize_with = "deserialize_optional_bool_from_anything")] + pub trace_aws_service_representation_enabled: Option, // Trace Propagation #[serde(deserialize_with = "deserialize_trace_propagation_style")] pub trace_propagation_style: Vec, @@ -451,6 +453,11 @@ fn merge_config(config: &mut Config, yaml_config: &YamlConfig) { merge_vec!(config, yaml_config, trace_propagation_style_extract); merge_option_to_value!(config, yaml_config, trace_propagation_extract_first); merge_option_to_value!(config, yaml_config, trace_propagation_http_baggage_enabled); + merge_option_to_value!( + config, + yaml_config, + trace_aws_service_representation_enabled + ); // OTLP if let Some(otlp_config) = &yaml_config.otlp_config { @@ -711,6 +718,7 @@ trace_propagation_style: "datadog" trace_propagation_style_extract: "b3" trace_propagation_extract_first: true trace_propagation_http_baggage_enabled: true +trace_aws_service_representation_enabled: true # OTLP otlp_config: @@ -839,6 +847,7 @@ extension_version: "compatibility" trace_propagation_style_extract: vec![TracePropagationStyle::B3], trace_propagation_extract_first: true, trace_propagation_http_baggage_enabled: true, + trace_aws_service_representation_enabled: true, otlp_config_traces_enabled: false, otlp_config_traces_span_name_as_resource_name: true, otlp_config_traces_span_name_remappings: HashMap::from([( diff --git a/bottlecap/src/lifecycle/invocation/mod.rs b/bottlecap/src/lifecycle/invocation/mod.rs index 70cca8efd..34d264b5f 100644 --- a/bottlecap/src/lifecycle/invocation/mod.rs +++ b/bottlecap/src/lifecycle/invocation/mod.rs @@ -32,13 +32,19 @@ pub fn base64_to_string(base64_string: &str) -> Result { } fn create_empty_span(name: String, resource: &str, service: &str) -> Span { - Span { + let mut span = Span { name, resource: resource.to_string(), service: service.to_string(), r#type: String::from("serverless"), ..Default::default() - } + }; + + // Add span.kind to the span to enable other server based features for serverless + span.meta + .insert("span.kind".to_string(), "server".to_string()); + + span } #[must_use] diff --git a/bottlecap/src/lifecycle/invocation/processor.rs b/bottlecap/src/lifecycle/invocation/processor.rs index 289a98dd9..88a30d343 100644 --- a/bottlecap/src/lifecycle/invocation/processor.rs +++ b/bottlecap/src/lifecycle/invocation/processor.rs @@ -39,6 +39,8 @@ use crate::{ }, }; +use crate::lifecycle::invocation::triggers::get_default_service_name; + pub const MS_TO_NS: f64 = 1_000_000.0; pub const S_TO_MS: u64 = 1_000; pub const S_TO_NS: f64 = 1_000_000_000.0; @@ -89,16 +91,23 @@ impl Processor { aws_config: Arc, metrics_aggregator: Arc>, ) -> Self { - let service = config.service.clone().unwrap_or(String::from("aws.lambda")); let resource = tags_provider .get_canonical_resource_name() .unwrap_or(String::from("aws.lambda")); + let service = get_default_service_name( + &config.service.clone().unwrap_or(resource.clone()), + "aws.lambda", + config.trace_aws_service_representation_enabled, + ); let propagator = DatadogCompositePropagator::new(Arc::clone(&config)); Processor { context_buffer: ContextBuffer::default(), - inferrer: SpanInferrer::new(config.service_mapping.clone()), + inferrer: SpanInferrer::new( + config.service_mapping.clone(), + config.trace_aws_service_representation_enabled, + ), propagator, enhanced_metrics: EnhancedMetrics::new(metrics_aggregator, Arc::clone(&config)), aws_config, @@ -195,6 +204,7 @@ impl Processor { self.dynamic_tags .insert(String::from("cold_start"), cold_start.to_string()); + if proactive_initialization { self.dynamic_tags.insert( String::from("proactive_initialization"), @@ -238,7 +248,6 @@ impl Processor { ); cold_start_span.span_id = generate_span_id(); cold_start_span.start = start_time; - context.cold_start_span = Some(cold_start_span); } diff --git a/bottlecap/src/lifecycle/invocation/span_inferrer.rs b/bottlecap/src/lifecycle/invocation/span_inferrer.rs index ce6651e17..12973c781 100644 --- a/bottlecap/src/lifecycle/invocation/span_inferrer.rs +++ b/bottlecap/src/lifecycle/invocation/span_inferrer.rs @@ -32,6 +32,7 @@ use crate::{ #[derive(Default)] pub struct SpanInferrer { service_mapping: HashMap, + aws_service_representation_enabled: bool, // Span inferred from the Lambda incoming request payload pub inferred_span: Option, // Nested span inferred from the Lambda incoming request payload @@ -50,9 +51,13 @@ pub struct SpanInferrer { impl SpanInferrer { #[must_use] - pub fn new(service_mapping: HashMap) -> Self { + pub fn new( + service_mapping: HashMap, + aws_service_representation_enabled: bool, + ) -> Self { Self { service_mapping, + aws_service_representation_enabled, inferred_span: None, wrapped_inferred_span: None, is_async_span: false, @@ -87,19 +92,31 @@ impl SpanInferrer { if APIGatewayHttpEvent::is_match(payload_value) { if let Some(t) = APIGatewayHttpEvent::new(payload_value.clone()) { - t.enrich_span(&mut inferred_span, &self.service_mapping); + t.enrich_span( + &mut inferred_span, + &self.service_mapping, + self.aws_service_representation_enabled, + ); trigger = Some(Box::new(t)); } } else if APIGatewayRestEvent::is_match(payload_value) { if let Some(t) = APIGatewayRestEvent::new(payload_value.clone()) { - t.enrich_span(&mut inferred_span, &self.service_mapping); + t.enrich_span( + &mut inferred_span, + &self.service_mapping, + self.aws_service_representation_enabled, + ); trigger = Some(Box::new(t)); } } else if APIGatewayWebSocketEvent::is_match(payload_value) { if let Some(t) = APIGatewayWebSocketEvent::new(payload_value.clone()) { - t.enrich_span(&mut inferred_span, &self.service_mapping); + t.enrich_span( + &mut inferred_span, + &self.service_mapping, + self.aws_service_representation_enabled, + ); trigger = Some(Box::new(t)); } @@ -110,19 +127,31 @@ impl SpanInferrer { } } else if LambdaFunctionUrlEvent::is_match(payload_value) { if let Some(t) = LambdaFunctionUrlEvent::new(payload_value.clone()) { - t.enrich_span(&mut inferred_span, &self.service_mapping); + t.enrich_span( + &mut inferred_span, + &self.service_mapping, + self.aws_service_representation_enabled, + ); trigger = Some(Box::new(t)); } } else if MSKEvent::is_match(payload_value) { if let Some(t) = MSKEvent::new(payload_value.clone()) { - t.enrich_span(&mut inferred_span, &self.service_mapping); + t.enrich_span( + &mut inferred_span, + &self.service_mapping, + self.aws_service_representation_enabled, + ); trigger = Some(Box::new(t)); } } else if SqsRecord::is_match(payload_value) { if let Some(t) = SqsRecord::new(payload_value.clone()) { - t.enrich_span(&mut inferred_span, &self.service_mapping); + t.enrich_span( + &mut inferred_span, + &self.service_mapping, + self.aws_service_representation_enabled, + ); self.generated_span_context = extract_trace_context_from_aws_trace_header( t.attributes.aws_trace_header.clone(), @@ -140,7 +169,11 @@ impl SpanInferrer { sns: sns_entity, event_subscription_arn: None, }; - wt.enrich_span(&mut wrapped_inferred_span, &self.service_mapping); + wt.enrich_span( + &mut wrapped_inferred_span, + &self.service_mapping, + self.aws_service_representation_enabled, + ); inferred_span.meta.extend(wt.get_tags()); wrapped_inferred_span.duration = @@ -155,8 +188,11 @@ impl SpanInferrer { ..Default::default() }; - event_bridge_entity - .enrich_span(&mut wrapped_inferred_span, &self.service_mapping); + event_bridge_entity.enrich_span( + &mut wrapped_inferred_span, + &self.service_mapping, + self.aws_service_representation_enabled, + ); inferred_span.meta.extend(event_bridge_entity.get_tags()); wrapped_inferred_span.duration = @@ -169,7 +205,11 @@ impl SpanInferrer { } } else if SnsRecord::is_match(payload_value) { if let Some(t) = SnsRecord::new(payload_value.clone()) { - t.enrich_span(&mut inferred_span, &self.service_mapping); + t.enrich_span( + &mut inferred_span, + &self.service_mapping, + self.aws_service_representation_enabled, + ); if let Some(message) = &t.sns.message { if let Ok(event_bridge_wrapper_message) = @@ -180,8 +220,11 @@ impl SpanInferrer { ..Default::default() }; - event_bridge_wrapper_message - .enrich_span(&mut wrapped_inferred_span, &self.service_mapping); + event_bridge_wrapper_message.enrich_span( + &mut wrapped_inferred_span, + &self.service_mapping, + self.aws_service_representation_enabled, + ); inferred_span .meta .extend(event_bridge_wrapper_message.get_tags()); @@ -197,27 +240,43 @@ impl SpanInferrer { } } else if DynamoDbRecord::is_match(payload_value) { if let Some(t) = DynamoDbRecord::new(payload_value.clone()) { - t.enrich_span(&mut inferred_span, &self.service_mapping); + t.enrich_span( + &mut inferred_span, + &self.service_mapping, + self.aws_service_representation_enabled, + ); self.span_pointers = t.get_span_pointers(); trigger = Some(Box::new(t)); } } else if S3Record::is_match(payload_value) { if let Some(t) = S3Record::new(payload_value.clone()) { - t.enrich_span(&mut inferred_span, &self.service_mapping); + t.enrich_span( + &mut inferred_span, + &self.service_mapping, + self.aws_service_representation_enabled, + ); self.span_pointers = t.get_span_pointers(); trigger = Some(Box::new(t)); } } else if EventBridgeEvent::is_match(payload_value) { if let Some(t) = EventBridgeEvent::new(payload_value.clone()) { - t.enrich_span(&mut inferred_span, &self.service_mapping); + t.enrich_span( + &mut inferred_span, + &self.service_mapping, + self.aws_service_representation_enabled, + ); trigger = Some(Box::new(t)); } } else if KinesisRecord::is_match(payload_value) { if let Some(t) = KinesisRecord::new(payload_value.clone()) { - t.enrich_span(&mut inferred_span, &self.service_mapping); + t.enrich_span( + &mut inferred_span, + &self.service_mapping, + self.aws_service_representation_enabled, + ); trigger = Some(Box::new(t)); } @@ -282,6 +341,7 @@ impl SpanInferrer { String::from("peer.service"), invocation_span.service.clone(), ); + s.meta.insert("span.kind".to_string(), "server".to_string()); if let Some(ws) = &mut self.wrapped_inferred_span { ws.trace_id = invocation_span.trace_id; diff --git a/bottlecap/src/lifecycle/invocation/triggers/alb_event.rs b/bottlecap/src/lifecycle/invocation/triggers/alb_event.rs index 9405bcbc7..81ffe6c32 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/alb_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/alb_event.rs @@ -47,7 +47,13 @@ impl Trigger for ALBEvent { target_group_arn.is_some() } - fn enrich_span(&self, _span: &mut Span, _service_mapping: &HashMap) {} + fn enrich_span( + &self, + _span: &mut Span, + _service_mapping: &HashMap, + _aws_service_representation_enabled: bool, + ) { + } fn get_tags(&self) -> HashMap { HashMap::from([ @@ -210,12 +216,68 @@ mod tests { } #[test] - fn test_resolve_service_name() { + fn test_resolve_service_name_with_representation_enabled() { + let json = read_json_file("application_load_balancer.json"); + let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); + let event = ALBEvent::new(payload).expect("Failed to deserialize ALBEvent"); + + // Test 1: Specific mapping takes priority + let specific_service_mapping = HashMap::from([ + ( + "nhulston-alb-test".to_string(), + "specific-service".to_string(), + ), + ( + "lambda_application_load_balancer".to_string(), + "generic-service".to_string(), + ), + ]); + + assert_eq!( + event.resolve_service_name( + &specific_service_mapping, + &event.request_context.elb.target_group_arn, + "lambda_application_load_balancer", + true // aws_service_representation_enabled + ), + "specific-service" + ); + + // Test 2: Generic mapping is used when specific not found + let generic_service_mapping = HashMap::from([( + "lambda_application_load_balancer".to_string(), + "generic-service".to_string(), + )]); + assert_eq!( + event.resolve_service_name( + &generic_service_mapping, + &event.request_context.elb.target_group_arn, + "lambda_application_load_balancer", + true // aws_service_representation_enabled + ), + "generic-service" + ); + + // Test 3: When no mapping exists, uses instance name + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + &event.request_context.elb.target_group_arn, + "lambda_application_load_balancer", + true // aws_service_representation_enabled + ), + event.request_context.elb.target_group_arn // instance name + ); + } + + #[test] + fn test_resolve_service_name_with_representation_disabled() { let json = read_json_file("application_load_balancer.json"); let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); let event = ALBEvent::new(payload).expect("Failed to deserialize ALBEvent"); - // Priority is given to the specific key + // Test 1: With specific mapping - still respects mapping let specific_service_mapping = HashMap::from([ ( "nhulston-alb-test".to_string(), @@ -230,11 +292,14 @@ mod tests { assert_eq!( event.resolve_service_name( &specific_service_mapping, - &event.request_context.elb.target_group_arn + &event.request_context.elb.target_group_arn, + "lambda_application_load_balancer", + false // aws_service_representation_enabled = false ), "specific-service" ); + // Test 2: With generic mapping - still respects mapping let generic_service_mapping = HashMap::from([( "lambda_application_load_balancer".to_string(), "generic-service".to_string(), @@ -242,10 +307,24 @@ mod tests { assert_eq!( event.resolve_service_name( &generic_service_mapping, - &event.request_context.elb.target_group_arn + &event.request_context.elb.target_group_arn, + "lambda_application_load_balancer", + false // aws_service_representation_enabled = false ), "generic-service" ); + + // Test 3: When no mapping exists, uses fallback value + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + &event.request_context.elb.target_group_arn, + "lambda_application_load_balancer", + false // aws_service_representation_enabled = false + ), + "lambda_application_load_balancer" // fallback value + ); } #[test] 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 3d6003ae9..792e6881b 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs @@ -64,7 +64,12 @@ impl Trigger for APIGatewayHttpEvent { } #[allow(clippy::cast_possible_truncation)] - fn enrich_span(&self, span: &mut Span, service_mapping: &HashMap) { + fn enrich_span( + &self, + span: &mut Span, + service_mapping: &HashMap, + aws_service_representation_enabled: bool, + ) { debug!("Enriching an Inferred Span for an API Gateway HTTP Event"); let resource = format!( "{http_method} {parameterized_route}", @@ -78,8 +83,12 @@ impl Trigger for APIGatewayHttpEvent { ); let start_time = (self.request_context.time_epoch as f64 * MS_TO_NS) as i64; - let service_name = - self.resolve_service_name(service_mapping, &self.request_context.domain_name); + let service_name = self.resolve_service_name( + service_mapping, + &self.request_context.domain_name, + &self.request_context.domain_name, + aws_service_representation_enabled, + ); span.name = "aws.httpapi".to_string(); span.service = service_name; @@ -275,7 +284,7 @@ mod tests { APIGatewayHttpEvent::new(payload).expect("Failed to deserialize APIGatewayHttpEvent"); let mut span = Span::default(); let service_mapping = HashMap::new(); - event.enrich_span(&mut span, &service_mapping); + event.enrich_span(&mut span, &service_mapping, true); assert_eq!(span.name, "aws.httpapi"); assert_eq!( span.service, @@ -339,7 +348,7 @@ mod tests { APIGatewayHttpEvent::new(payload).expect("Failed to deserialize APIGatewayHttpEvent"); let mut span = Span::default(); let service_mapping = HashMap::new(); - event.enrich_span(&mut span, &service_mapping); + event.enrich_span(&mut span, &service_mapping, true); assert_eq!(span.name, "aws.httpapi"); assert_eq!( span.service, @@ -403,13 +412,13 @@ mod tests { } #[test] - fn test_resolve_service_name() { + fn test_resolve_service_name_with_representation_enabled() { 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"); - // Priority is given to the specific key + // Test 1: Specific mapping takes priority let specific_service_mapping = HashMap::from([ ("x02yirxc7a".to_string(), "specific-service".to_string()), ( @@ -421,19 +430,92 @@ mod tests { assert_eq!( event.resolve_service_name( &specific_service_mapping, - &event.request_context.domain_name + &event.request_context.domain_name, + &event.request_context.domain_name, + true // aws_service_representation_enabled ), "specific-service" ); + // Test 2: Generic mapping is used when specific not found let generic_service_mapping = HashMap::from([( "lambda_api_gateway".to_string(), "generic-service".to_string(), )]); assert_eq!( - event - .resolve_service_name(&generic_service_mapping, &event.request_context.domain_name), + event.resolve_service_name( + &generic_service_mapping, + &event.request_context.domain_name, + &event.request_context.domain_name, + true // aws_service_representation_enabled + ), "generic-service" ); + + // Test 3: When no mapping exists, uses instance name (domain_name) + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + &event.request_context.domain_name, + &event.request_context.domain_name, + true // aws_service_representation_enabled + ), + event.request_context.domain_name // instance name + ); + } + + #[test] + fn test_resolve_service_name_with_representation_disabled() { + 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"); + + // Test 1: With specific mapping - still respects mapping + let specific_service_mapping = HashMap::from([ + ("x02yirxc7a".to_string(), "specific-service".to_string()), + ( + "lambda_api_gateway".to_string(), + "generic-service".to_string(), + ), + ]); + + assert_eq!( + event.resolve_service_name( + &specific_service_mapping, + &event.request_context.domain_name, + &event.request_context.domain_name, + false // aws_service_representation_enabled = false + ), + "specific-service" + ); + + // Test 2: With generic mapping - still respects mapping + let generic_service_mapping = HashMap::from([( + "lambda_api_gateway".to_string(), + "generic-service".to_string(), + )]); + assert_eq!( + event.resolve_service_name( + &generic_service_mapping, + &event.request_context.domain_name, + &event.request_context.domain_name, + false // aws_service_representation_enabled = false + ), + "generic-service" + ); + + // Test 3: When no mapping exists, uses fallback value (domain_name) + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + &event.request_context.domain_name, + &event.request_context.domain_name, + false // aws_service_representation_enabled = false + ), + event.request_context.domain_name // fallback value + ); } } 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 99daf7b4e..52f2eb8c9 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs @@ -67,7 +67,12 @@ impl Trigger for APIGatewayRestEvent { } #[allow(clippy::cast_possible_truncation)] - fn enrich_span(&self, span: &mut Span, service_mapping: &HashMap) { + fn enrich_span( + &self, + span: &mut Span, + service_mapping: &HashMap, + aws_service_representation_enabled: bool, + ) { debug!("Enriching an Inferred Span for an API Gateway REST Event"); let resource = format!( "{http_method} {path}", @@ -81,8 +86,12 @@ impl Trigger for APIGatewayRestEvent { ); let start_time = (self.request_context.time_epoch as f64 * MS_TO_NS) as i64; - let service_name = - self.resolve_service_name(service_mapping, &self.request_context.domain_name); + let service_name = self.resolve_service_name( + service_mapping, + &self.request_context.domain_name, + &self.request_context.domain_name, + aws_service_representation_enabled, + ); span.name = "aws.apigateway".to_string(); span.service = service_name; @@ -287,7 +296,7 @@ mod tests { APIGatewayRestEvent::new(payload).expect("Failed to deserialize APIGatewayRestEvent"); let mut span = Span::default(); let service_mapping = HashMap::new(); - event.enrich_span(&mut span, &service_mapping); + event.enrich_span(&mut span, &service_mapping, true); 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"); @@ -346,7 +355,7 @@ mod tests { APIGatewayRestEvent::new(payload).expect("Failed to deserialize APIGatewayRestEvent"); let mut span = Span::default(); let service_mapping = HashMap::new(); - event.enrich_span(&mut span, &service_mapping); + event.enrich_span(&mut span, &service_mapping, true); assert_eq!(span.name, "aws.apigateway"); assert_eq!( span.service, @@ -419,13 +428,13 @@ mod tests { } #[test] - fn test_resolve_service_name() { + fn test_resolve_service_name_with_representation_enabled() { 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"); - // Priority is given to the specific key + // Test 1: Specific mapping takes priority let specific_service_mapping = HashMap::from([ ("id".to_string(), "specific-service".to_string()), ( @@ -437,19 +446,92 @@ mod tests { assert_eq!( event.resolve_service_name( &specific_service_mapping, - &event.request_context.domain_name + &event.request_context.domain_name, + &event.request_context.domain_name, + true // aws_service_representation_enabled ), "specific-service" ); + // Test 2: Generic mapping is used when specific not found let generic_service_mapping = HashMap::from([( "lambda_api_gateway".to_string(), "generic-service".to_string(), )]); assert_eq!( - event - .resolve_service_name(&generic_service_mapping, &event.request_context.domain_name), + event.resolve_service_name( + &generic_service_mapping, + &event.request_context.domain_name, + &event.request_context.domain_name, + true // aws_service_representation_enabled + ), "generic-service" ); + + // Test 3: When no mapping exists, uses instance name (domain_name) + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + &event.request_context.domain_name, + &event.request_context.domain_name, + true // aws_service_representation_enabled + ), + event.request_context.domain_name // instance name + ); + } + + #[test] + fn test_resolve_service_name_with_representation_disabled() { + 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"); + + // Test 1: With specific mapping - still respects mapping + let specific_service_mapping = HashMap::from([ + ("id".to_string(), "specific-service".to_string()), + ( + "lambda_api_gateway".to_string(), + "generic-service".to_string(), + ), + ]); + + assert_eq!( + event.resolve_service_name( + &specific_service_mapping, + &event.request_context.domain_name, + &event.request_context.domain_name, + false // aws_service_representation_enabled = false + ), + "specific-service" + ); + + // Test 2: With generic mapping - still respects mapping + let generic_service_mapping = HashMap::from([( + "lambda_api_gateway".to_string(), + "generic-service".to_string(), + )]); + assert_eq!( + event.resolve_service_name( + &generic_service_mapping, + &event.request_context.domain_name, + &event.request_context.domain_name, + false // aws_service_representation_enabled = false + ), + "generic-service" + ); + + // Test 3: When no mapping exists, uses fallback value (domain_name) + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + &event.request_context.domain_name, + &event.request_context.domain_name, + false // aws_service_representation_enabled = false + ), + event.request_context.domain_name // fallback value + ); } } 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 ffd705202..53b97c91b 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_websocket_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_websocket_event.rs @@ -61,7 +61,12 @@ impl Trigger for APIGatewayWebSocketEvent { } #[allow(clippy::cast_possible_truncation)] - fn enrich_span(&self, span: &mut Span, service_mapping: &HashMap) { + fn enrich_span( + &self, + span: &mut Span, + service_mapping: &HashMap, + aws_service_representation_enabled: bool, + ) { debug!("Enriching an Inferred Span for an API Gateway WebSocket Event"); let resource = &self.request_context.route_key; let http_url = format!( @@ -71,8 +76,12 @@ impl Trigger for APIGatewayWebSocketEvent { ); let start_time = (self.request_context.time_epoch as f64 * MS_TO_NS) as i64; - let service_name = - self.resolve_service_name(service_mapping, &self.request_context.domain_name); + let service_name = self.resolve_service_name( + service_mapping, + &self.request_context.domain_name, + "api_gateway_websocket", + aws_service_representation_enabled, + ); span.name = "aws.apigateway".to_string(); span.service = service_name; @@ -277,7 +286,7 @@ mod tests { let mut span = Span::default(); let service_mapping = HashMap::new(); - event.enrich_span(&mut span, &service_mapping); + event.enrich_span(&mut span, &service_mapping, true); assert_eq!(span.name, "aws.apigateway"); assert_eq!( @@ -344,13 +353,68 @@ mod tests { } #[test] - fn test_resolve_service_name() { + fn test_resolve_service_name_with_representation_enabled() { + let json = read_json_file("api_gateway_websocket_connect_event.json"); + let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); + let event = APIGatewayWebSocketEvent::new(payload) + .expect("Failed to deserialize APIGatewayWebSocketEvent"); + + // Test 1: Specific mapping takes priority + let specific_service_mapping = HashMap::from([ + ("85fj5nw29d".to_string(), "specific-service".to_string()), + ( + "lambda_api_gateway".to_string(), + "generic-service".to_string(), + ), + ]); + + assert_eq!( + event.resolve_service_name( + &specific_service_mapping, + &event.request_context.domain_name, + &event.request_context.domain_name, + true // aws_service_representation_enabled + ), + "specific-service" + ); + + // Test 2: Generic mapping is used when specific not found + let generic_service_mapping = HashMap::from([( + "lambda_api_gateway".to_string(), + "generic-service".to_string(), + )]); + + assert_eq!( + event.resolve_service_name( + &generic_service_mapping, + &event.request_context.domain_name, + &event.request_context.domain_name, + true // aws_service_representation_enabled + ), + "generic-service" + ); + + // Test 3: When no mapping exists, uses instance name (domain_name) + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + &event.request_context.domain_name, + &event.request_context.domain_name, + true // aws_service_representation_enabled + ), + event.request_context.domain_name // instance name + ); + } + + #[test] + fn test_resolve_service_name_with_representation_disabled() { let json = read_json_file("api_gateway_websocket_connect_event.json"); let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); let event = APIGatewayWebSocketEvent::new(payload) .expect("Failed to deserialize APIGatewayWebSocketEvent"); - // Priority is given to the specific key + // Test 1: With specific mapping - still respects mapping let specific_service_mapping = HashMap::from([ ("85fj5nw29d".to_string(), "specific-service".to_string()), ( @@ -362,20 +426,39 @@ mod tests { assert_eq!( event.resolve_service_name( &specific_service_mapping, - &event.request_context.domain_name + &event.request_context.domain_name, + &event.request_context.domain_name, + false // aws_service_representation_enabled = false ), "specific-service" ); + // Test 2: With generic mapping - still respects mapping let generic_service_mapping = HashMap::from([( "lambda_api_gateway".to_string(), "generic-service".to_string(), )]); assert_eq!( - event - .resolve_service_name(&generic_service_mapping, &event.request_context.domain_name), + event.resolve_service_name( + &generic_service_mapping, + &event.request_context.domain_name, + &event.request_context.domain_name, + false // aws_service_representation_enabled = false + ), "generic-service" ); + + // Test 3: When no mapping exists, uses fallback value (domain_name) + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + &event.request_context.domain_name, + &event.request_context.domain_name, + false // aws_service_representation_enabled = false + ), + event.request_context.domain_name // fallback value + ); } } diff --git a/bottlecap/src/lifecycle/invocation/triggers/dynamodb_event.rs b/bottlecap/src/lifecycle/invocation/triggers/dynamodb_event.rs index 7bbb878c7..38b3f4dfc 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/dynamodb_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/dynamodb_event.rs @@ -100,14 +100,24 @@ impl Trigger for DynamoDbRecord { } #[allow(clippy::cast_possible_truncation)] - fn enrich_span(&self, span: &mut Span, service_mapping: &HashMap) { + fn enrich_span( + &self, + span: &mut Span, + service_mapping: &HashMap, + aws_service_representation_enabled: bool, + ) { debug!("Enriching an Inferred Span for a DynamoDB event"); let table_name = self.get_specific_identifier(); let resource = format!("{} {}", self.event_name.clone(), table_name); let start_time = (self.dynamodb.approximate_creation_date_time * S_TO_NS) as i64; - let service_name = self.resolve_service_name(service_mapping, "dynamodb"); + let service_name = self.resolve_service_name( + service_mapping, + &table_name, + "dynamodb", + aws_service_representation_enabled, + ); span.name = String::from("aws.dynamodb"); span.service = service_name.to_string(); @@ -278,9 +288,9 @@ mod tests { let event = DynamoDbRecord::new(payload).expect("Failed to deserialize DynamoDbRecord"); let mut span = Span::default(); let service_mapping = HashMap::new(); - event.enrich_span(&mut span, &service_mapping); + event.enrich_span(&mut span, &service_mapping, true); assert_eq!(span.name, "aws.dynamodb"); - assert_eq!(span.service, "dynamodb"); + assert_eq!(span.service, "ExampleTableWithStream"); assert_eq!(span.resource, "INSERT ExampleTableWithStream"); assert_eq!(span.r#type, "web"); @@ -341,12 +351,12 @@ mod tests { } #[test] - fn test_resolve_service_name() { + fn test_resolve_service_name_with_representation_enabled() { let json = read_json_file("dynamodb_event.json"); let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); let event = DynamoDbRecord::new(payload).expect("Failed to deserialize DynamoDbRecord"); - // Priority is given to the specific key + // Test 1: Specific mapping takes priority let specific_service_mapping = HashMap::from([ ( "ExampleTableWithStream".to_string(), @@ -356,16 +366,90 @@ mod tests { ]); assert_eq!( - event.resolve_service_name(&specific_service_mapping, "dynamodb"), + event.resolve_service_name( + &specific_service_mapping, + &event.get_specific_identifier(), + "dynamodb", + true // aws_service_representation_enabled + ), "specific-service" ); + // Test 2: Generic mapping is used when specific not found let generic_service_mapping = HashMap::from([("lambda_dynamodb".to_string(), "generic-service".to_string())]); assert_eq!( - event.resolve_service_name(&generic_service_mapping, "dynamodb"), + event.resolve_service_name( + &generic_service_mapping, + &event.get_specific_identifier(), + "dynamodb", + true // aws_service_representation_enabled + ), "generic-service" ); + + // Test 3: When no mapping exists, uses instance name (table name) + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + &event.get_specific_identifier(), + "dynamodb", + true // aws_service_representation_enabled + ), + event.get_specific_identifier() // instance name + ); + } + + #[test] + fn test_resolve_service_name_with_representation_disabled() { + let json = read_json_file("dynamodb_event.json"); + let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); + let event = DynamoDbRecord::new(payload).expect("Failed to deserialize DynamoDbRecord"); + + // Test 1: With specific mapping - still respects mapping + let specific_service_mapping = HashMap::from([ + ( + "ExampleTableWithStream".to_string(), + "specific-service".to_string(), + ), + ("lambda_dynamodb".to_string(), "generic-service".to_string()), + ]); + + assert_eq!( + event.resolve_service_name( + &specific_service_mapping, + &event.get_specific_identifier(), + "dynamodb", + false // aws_service_representation_enabled = false + ), + "specific-service" + ); + + // Test 2: With generic mapping - still respects mapping + let generic_service_mapping = + HashMap::from([("lambda_dynamodb".to_string(), "generic-service".to_string())]); + assert_eq!( + event.resolve_service_name( + &generic_service_mapping, + &event.get_specific_identifier(), + "dynamodb", + false // aws_service_representation_enabled = false + ), + "generic-service" + ); + + // Test 3: When no mapping exists, uses fallback value + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + &event.get_specific_identifier(), + "dynamodb", + false // aws_service_representation_enabled = false + ), + "dynamodb" // fallback value + ); } #[test] diff --git a/bottlecap/src/lifecycle/invocation/triggers/event_bridge_event.rs b/bottlecap/src/lifecycle/invocation/triggers/event_bridge_event.rs index 8cffea3fa..f4754cef5 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/event_bridge_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/event_bridge_event.rs @@ -51,7 +51,12 @@ impl Trigger for EventBridgeEvent { } #[allow(clippy::cast_possible_truncation)] - fn enrich_span(&self, span: &mut Span, service_mapping: &HashMap) { + fn enrich_span( + &self, + span: &mut Span, + service_mapping: &HashMap, + aws_service_representation_enabled: bool, + ) { // EventBridge events have a timestamp resolution in seconds let start_time_seconds = self .time @@ -65,7 +70,12 @@ impl Trigger for EventBridgeEvent { .and_then(|s| s.parse::().ok()) .map_or(start_time_seconds, |s| (s * MS_TO_NS) as i64); - let service_name = self.resolve_service_name(service_mapping, "eventbridge"); + let service_name = self.resolve_service_name( + service_mapping, + &self.get_specific_identifier(), + "eventbridge", + aws_service_representation_enabled, + ); span.name = String::from("aws.eventbridge"); span.service = service_name.to_string(); @@ -181,7 +191,7 @@ mod tests { let mut span = Span::default(); let service_mapping = HashMap::new(); - event.enrich_span(&mut span, &service_mapping); + event.enrich_span(&mut span, &service_mapping, true); let expected = serde_json::from_str(&read_json_file("eventbridge_span.json")) .expect("Failed to deserialize into Span"); @@ -197,7 +207,7 @@ mod tests { let mut span = Span::default(); let service_mapping = HashMap::new(); - event.enrich_span(&mut span, &service_mapping); + event.enrich_span(&mut span, &service_mapping, true); assert_eq!(span.resource, "my.event"); } @@ -211,7 +221,7 @@ mod tests { let mut span = Span::default(); let service_mapping = HashMap::new(); - event.enrich_span(&mut span, &service_mapping); + event.enrich_span(&mut span, &service_mapping, true); assert_eq!(span.resource, "testBus"); // Seconds resolution @@ -256,12 +266,12 @@ mod tests { } #[test] - fn test_resolve_service_name() { + fn test_resolve_service_name_with_representation_enabled() { let json = read_json_file("eventbridge_event.json"); let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); let event = EventBridgeEvent::new(payload).expect("Failed to deserialize EventBridgeEvent"); - // Priority is given to the specific key + // Test 1: Specific mapping takes priority let specific_service_mapping = HashMap::from([ ("testBus".to_string(), "specific-service".to_string()), ( @@ -271,17 +281,93 @@ mod tests { ]); assert_eq!( - event.resolve_service_name(&specific_service_mapping, "eventbridge"), + event.resolve_service_name( + &specific_service_mapping, + &event.get_specific_identifier(), + "eventbridge", + true // aws_service_representation_enabled + ), "specific-service" ); + // Test 2: Generic mapping is used when specific not found let generic_service_mapping = HashMap::from([( "lambda_eventbridge".to_string(), "generic-service".to_string(), )]); assert_eq!( - event.resolve_service_name(&generic_service_mapping, "eventbridge"), + event.resolve_service_name( + &generic_service_mapping, + &event.get_specific_identifier(), + "eventbridge", + true // aws_service_representation_enabled + ), "generic-service" ); + + // Test 3: When no mapping exists, uses instance name + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + &event.get_specific_identifier(), + "eventbridge", + true // aws_service_representation_enabled + ), + event.get_specific_identifier() // instance name + ); + } + + #[test] + fn test_resolve_service_name_with_representation_disabled() { + let json = read_json_file("eventbridge_event.json"); + let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); + let event = EventBridgeEvent::new(payload).expect("Failed to deserialize EventBridgeEvent"); + + // Test 1: With specific mapping - still respects mapping + let specific_service_mapping = HashMap::from([ + ("testBus".to_string(), "specific-service".to_string()), + ( + "lambda_eventbridge".to_string(), + "generic-service".to_string(), + ), + ]); + + assert_eq!( + event.resolve_service_name( + &specific_service_mapping, + &event.get_specific_identifier(), + "eventbridge", + false // aws_service_representation_enabled = false + ), + "specific-service" + ); + + // Test 2: With generic mapping - still respects mapping + let generic_service_mapping = HashMap::from([( + "lambda_eventbridge".to_string(), + "generic-service".to_string(), + )]); + assert_eq!( + event.resolve_service_name( + &generic_service_mapping, + &event.get_specific_identifier(), + "eventbridge", + false // aws_service_representation_enabled = false + ), + "generic-service" + ); + + // Test 3: When no mapping exists, uses fallback value + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + &event.get_specific_identifier(), + "eventbridge", + false // aws_service_representation_enabled = false + ), + "eventbridge" // fallback value + ); } } diff --git a/bottlecap/src/lifecycle/invocation/triggers/kinesis_event.rs b/bottlecap/src/lifecycle/invocation/triggers/kinesis_event.rs index e7a127501..6ccf24fcf 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/kinesis_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/kinesis_event.rs @@ -70,10 +70,20 @@ impl Trigger for KinesisRecord { } #[allow(clippy::cast_possible_truncation)] - fn enrich_span(&self, span: &mut Span, service_mapping: &HashMap) { + fn enrich_span( + &self, + span: &mut Span, + service_mapping: &HashMap, + aws_service_representation_enabled: bool, + ) { let stream_name = self.get_specific_identifier(); let shard_id = self.event_id.split(':').next().unwrap_or_default(); - let service_name = self.resolve_service_name(service_mapping, "kinesis"); + let service_name = self.resolve_service_name( + service_mapping, + &stream_name, + "kinesis", + aws_service_representation_enabled, + ); span.name = String::from("aws.kinesis"); span.service = service_name; @@ -190,9 +200,9 @@ mod tests { let event = KinesisRecord::new(payload).expect("Failed to deserialize S3Record"); let mut span = Span::default(); let service_mapping = HashMap::new(); - event.enrich_span(&mut span, &service_mapping); + event.enrich_span(&mut span, &service_mapping, true); assert_eq!(span.name, "aws.kinesis"); - assert_eq!(span.service, "kinesis"); + assert_eq!(span.service, "kinesisStream"); assert_eq!(span.resource, "kinesisStream"); assert_eq!(span.r#type, "web"); @@ -267,27 +277,98 @@ mod tests { } #[test] - fn test_resolve_service_name() { + fn test_resolve_service_name_with_representation_enabled() { + let json = read_json_file("kinesis_event.json"); + let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); + let event = KinesisRecord::new(payload).expect("Failed to deserialize KinesisRecord"); + + // Test 1: Specific mapping takes priority + let specific_service_mapping = HashMap::from([ + ("kinesisStream".to_string(), "specific-service".to_string()), + ("lambda_kinesis".to_string(), "generic-service".to_string()), + ]); + + assert_eq!( + event.resolve_service_name( + &specific_service_mapping, + &event.get_specific_identifier(), + "kinesis", + true // aws_service_representation_enabled + ), + "specific-service" + ); + + // Test 2: Generic mapping is used when specific not found + let generic_service_mapping = + HashMap::from([("lambda_kinesis".to_string(), "generic-service".to_string())]); + assert_eq!( + event.resolve_service_name( + &generic_service_mapping, + &event.get_specific_identifier(), + "kinesis", + true // aws_service_representation_enabled + ), + "generic-service" + ); + + // Test 3: When no mapping exists, uses instance name (stream name) + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + &event.get_specific_identifier(), + "kinesis", + true // aws_service_representation_enabled + ), + event.get_specific_identifier() // instance name + ); + } + + #[test] + fn test_resolve_service_name_with_representation_disabled() { let json = read_json_file("kinesis_event.json"); let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); let event = KinesisRecord::new(payload).expect("Failed to deserialize KinesisRecord"); - // Priority is given to the specific key + // Test 1: With specific mapping - still respects mapping let specific_service_mapping = HashMap::from([ ("kinesisStream".to_string(), "specific-service".to_string()), ("lambda_kinesis".to_string(), "generic-service".to_string()), ]); assert_eq!( - event.resolve_service_name(&specific_service_mapping, "kinesis"), + event.resolve_service_name( + &specific_service_mapping, + &event.get_specific_identifier(), + "kinesis", + false // aws_service_representation_enabled = false + ), "specific-service" ); + // Test 2: With generic mapping - still respects mapping let generic_service_mapping = HashMap::from([("lambda_kinesis".to_string(), "generic-service".to_string())]); assert_eq!( - event.resolve_service_name(&generic_service_mapping, "kinesis"), + event.resolve_service_name( + &generic_service_mapping, + &event.get_specific_identifier(), + "kinesis", + false // aws_service_representation_enabled = false + ), "generic-service" ); + + // Test 3: When no mapping exists, uses fallback value + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + &event.get_specific_identifier(), + "kinesis", + false // aws_service_representation_enabled = false + ), + "kinesis" // fallback value + ); } } diff --git a/bottlecap/src/lifecycle/invocation/triggers/lambda_function_url_event.rs b/bottlecap/src/lifecycle/invocation/triggers/lambda_function_url_event.rs index f1b1ca278..7159244df 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/lambda_function_url_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/lambda_function_url_event.rs @@ -63,7 +63,12 @@ impl Trigger for LambdaFunctionUrlEvent { } #[allow(clippy::cast_possible_truncation)] - fn enrich_span(&self, span: &mut Span, service_mapping: &HashMap) { + fn enrich_span( + &self, + span: &mut Span, + service_mapping: &HashMap, + aws_service_representation_enabled: bool, + ) { let resource = format!( "{} {}", self.request_context.http.method, self.request_context.http.path @@ -77,8 +82,12 @@ impl Trigger for LambdaFunctionUrlEvent { let start_time = (self.request_context.time_epoch as f64 * MS_TO_NS) as i64; - let service_name = - self.resolve_service_name(service_mapping, &self.request_context.domain_name); + let service_name = self.resolve_service_name( + service_mapping, + &self.request_context.domain_name, + &self.request_context.domain_name, + aws_service_representation_enabled, + ); span.name = String::from("aws.lambda.url"); span.service = service_name; @@ -267,7 +276,7 @@ mod tests { .expect("Failed to deserialize LambdaFunctionUrlEvent"); let mut span = Span::default(); let service_mapping = HashMap::new(); - event.enrich_span(&mut span, &service_mapping); + event.enrich_span(&mut span, &service_mapping, true); assert_eq!(span.name, "aws.lambda.url"); assert_eq!( span.service, @@ -323,28 +332,100 @@ mod tests { } #[test] - fn test_resolve_service_name() { + fn test_resolve_service_name_with_representation_enabled() { let json = read_json_file("lambda_function_url_event.json"); let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); let event = LambdaFunctionUrlEvent::new(payload) .expect("Failed to deserialize LambdaFunctionUrlEvent"); - // Priority is given to the specific key + // Test 1: Specific mapping takes priority let specific_service_mapping = HashMap::from([ ("a8hyhsshac".to_string(), "specific-service".to_string()), ("lambda_url".to_string(), "generic-service".to_string()), ]); assert_eq!( - event.resolve_service_name(&specific_service_mapping, "domain-name"), + event.resolve_service_name( + &specific_service_mapping, + "domain-name", + "lambda_url", + true // aws_service_representation_enabled + ), "specific-service" ); + // Test 2: Generic mapping is used when specific not found let generic_service_mapping = HashMap::from([("lambda_url".to_string(), "generic-service".to_string())]); assert_eq!( - event.resolve_service_name(&generic_service_mapping, "domain-name"), + event.resolve_service_name( + &generic_service_mapping, + "domain-name", + "lambda_url", + true // aws_service_representation_enabled + ), "generic-service" ); + + // Test 3: When no mapping exists, uses instance name + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + "domain-name", + "lambda_url", + true // aws_service_representation_enabled + ), + "domain-name" // instance name + ); + } + + #[test] + fn test_resolve_service_name_with_representation_disabled() { + let json = read_json_file("lambda_function_url_event.json"); + let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); + let event = LambdaFunctionUrlEvent::new(payload) + .expect("Failed to deserialize LambdaFunctionUrlEvent"); + + // Test 1: With specific mapping - still respects mapping + let specific_service_mapping = HashMap::from([ + ("a8hyhsshac".to_string(), "specific-service".to_string()), + ("lambda_url".to_string(), "generic-service".to_string()), + ]); + + assert_eq!( + event.resolve_service_name( + &specific_service_mapping, + "domain-name", + "lambda_url", + false // aws_service_representation_enabled = false + ), + "specific-service" + ); + + // Test 2: With generic mapping - still respects mapping + let generic_service_mapping = + HashMap::from([("lambda_url".to_string(), "generic-service".to_string())]); + assert_eq!( + event.resolve_service_name( + &generic_service_mapping, + "domain-name", + "lambda_url", + false // aws_service_representation_enabled = false + ), + "generic-service" + ); + + // Test 3: When no mapping exists, uses fallback value + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + "domain-name", + "lambda_url", + false // aws_service_representation_enabled = false + ), + "lambda_url" // fallback value + ); } } diff --git a/bottlecap/src/lifecycle/invocation/triggers/mod.rs b/bottlecap/src/lifecycle/invocation/triggers/mod.rs index a92c46f38..02d424d84 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/mod.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/mod.rs @@ -96,6 +96,19 @@ pub fn parameterize_api_resource(resource: String) -> String { result.join("/") } +#[must_use] +pub fn get_default_service_name( + instance_name: &str, + fallback: &str, + aws_service_representation_enabled: bool, +) -> String { + if !aws_service_representation_enabled { + return fallback.to_string(); + } + + instance_name.to_string() +} + pub trait Trigger: ServiceNameResolver { fn new(payload: Value) -> Option where @@ -103,7 +116,12 @@ pub trait Trigger: ServiceNameResolver { fn is_match(payload: &Value) -> bool where Self: Sized; - fn enrich_span(&self, span: &mut Span, service_mapping: &HashMap); + fn enrich_span( + &self, + span: &mut Span, + service_mapping: &HashMap, + aws_service_representation_enabled: bool, + ); fn get_tags(&self) -> HashMap; fn get_arn(&self, region: &str) -> String; fn get_carrier(&self) -> HashMap; @@ -113,13 +131,21 @@ pub trait Trigger: ServiceNameResolver { fn resolve_service_name( &self, service_mapping: &HashMap, + instance_name: &str, fallback: &str, + aws_service_representation_enabled: bool, ) -> String { service_mapping .get(&self.get_specific_identifier()) .or_else(|| service_mapping.get(self.get_generic_identifier())) - .unwrap_or(&fallback.to_string()) - .to_string() + .cloned() + .unwrap_or_else(|| { + get_default_service_name( + instance_name, + fallback, + aws_service_representation_enabled, + ) + }) } } diff --git a/bottlecap/src/lifecycle/invocation/triggers/msk_event.rs b/bottlecap/src/lifecycle/invocation/triggers/msk_event.rs index ab610da67..6523387ee 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/msk_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/msk_event.rs @@ -59,11 +59,21 @@ impl Trigger for MSKEvent { } #[allow(clippy::cast_possible_truncation)] - fn enrich_span(&self, span: &mut Span, service_mapping: &HashMap) { + fn enrich_span( + &self, + span: &mut Span, + service_mapping: &HashMap, + aws_service_representation_enabled: bool, + ) { debug!("Enriching an Inferred Span for an MSK event"); span.name = String::from("aws.msk"); - span.service = self.resolve_service_name(service_mapping, "msk"); + span.service = self.resolve_service_name( + service_mapping, + &self.get_specific_identifier(), + "msk", + aws_service_representation_enabled, + ); span.r#type = String::from("web"); let first_value = self.records.values().find_map(|arr| arr.first()); @@ -169,10 +179,10 @@ mod tests { let event = MSKEvent::new(payload).expect("Failed to deserialize MSKEvent"); let mut span = Span::default(); let service_mapping = HashMap::new(); - event.enrich_span(&mut span, &service_mapping); + event.enrich_span(&mut span, &service_mapping, true); assert_eq!(span.name, "aws.msk"); - assert_eq!(span.service, "msk"); + assert_eq!(span.service, "demo-cluster"); assert_eq!(span.r#type, "web"); assert_eq!(span.resource, "topic1"); assert_eq!(span.start, 1_745_846_213_022_000_128); @@ -231,27 +241,98 @@ mod tests { } #[test] - fn test_resolve_service_name() { + fn test_resolve_service_name_with_representation_enabled() { + let json = read_json_file("msk_event.json"); + let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); + let event = MSKEvent::new(payload).expect("Failed to deserialize MSKEvent"); + + // Test 1: Specific mapping takes priority + let specific_service_mapping = HashMap::from([ + ("demo-cluster".to_string(), "specific-service".to_string()), + ("lambda_msk".to_string(), "generic-service".to_string()), + ]); + + assert_eq!( + event.resolve_service_name( + &specific_service_mapping, + &event.get_specific_identifier(), + "msk", + true // aws_service_representation_enabled + ), + "specific-service" + ); + + // Test 2: Generic mapping is used when specific not found + let generic_service_mapping = + HashMap::from([("lambda_msk".to_string(), "generic-service".to_string())]); + assert_eq!( + event.resolve_service_name( + &generic_service_mapping, + &event.get_specific_identifier(), + "msk", + true // aws_service_representation_enabled + ), + "generic-service" + ); + + // Test 3: When no mapping exists, uses instance name (cluster name) + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + &event.get_specific_identifier(), + "msk", + true // aws_service_representation_enabled + ), + event.get_specific_identifier() // instance name + ); + } + + #[test] + fn test_resolve_service_name_with_representation_disabled() { let json = read_json_file("msk_event.json"); let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); let event = MSKEvent::new(payload).expect("Failed to deserialize MSKEvent"); - // Priority is given to the specific key + // Test 1: With specific mapping - still respects mapping let specific_service_mapping = HashMap::from([ ("demo-cluster".to_string(), "specific-service".to_string()), ("lambda_msk".to_string(), "generic-service".to_string()), ]); assert_eq!( - event.resolve_service_name(&specific_service_mapping, "msk"), + event.resolve_service_name( + &specific_service_mapping, + &event.get_specific_identifier(), + "msk", + false // aws_service_representation_enabled = false + ), "specific-service" ); + // Test 2: With generic mapping - still respects mapping let generic_service_mapping = HashMap::from([("lambda_msk".to_string(), "generic-service".to_string())]); assert_eq!( - event.resolve_service_name(&generic_service_mapping, "msk"), + event.resolve_service_name( + &generic_service_mapping, + &event.get_specific_identifier(), + "msk", + false // aws_service_representation_enabled = false + ), "generic-service" ); + + // Test 3: When no mapping exists, uses fallback value + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + &event.get_specific_identifier(), + "msk", + false // aws_service_representation_enabled = false + ), + "msk" // fallback value + ); } } diff --git a/bottlecap/src/lifecycle/invocation/triggers/s3_event.rs b/bottlecap/src/lifecycle/invocation/triggers/s3_event.rs index 4f3a6e232..6e2237d7a 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/s3_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/s3_event.rs @@ -77,7 +77,12 @@ impl Trigger for S3Record { } #[allow(clippy::cast_possible_truncation)] - fn enrich_span(&self, span: &mut Span, service_mapping: &HashMap) { + fn enrich_span( + &self, + span: &mut Span, + service_mapping: &HashMap, + aws_service_representation_enabled: bool, + ) { debug!("Enriching an InferredSpan span with S3 event"); let bucket_name = self.get_specific_identifier(); let start_time = self @@ -85,10 +90,15 @@ impl Trigger for S3Record { .timestamp_nanos_opt() .unwrap_or((self.event_time.timestamp_millis() as f64 * MS_TO_NS) as i64); - let service_name = self.resolve_service_name(service_mapping, "s3"); + let service_name = self.resolve_service_name( + service_mapping, + &bucket_name, + "s3", + aws_service_representation_enabled, + ); span.name = String::from("aws.s3"); - span.service = service_name.to_string(); + span.service = service_name; span.resource.clone_from(&bucket_name); span.r#type = String::from("web"); span.start = start_time; @@ -211,30 +221,12 @@ mod tests { let event = S3Record::new(payload).expect("Failed to deserialize S3Record"); let mut span = Span::default(); let service_mapping = HashMap::new(); - event.enrich_span(&mut span, &service_mapping); + event.enrich_span(&mut span, &service_mapping, true); assert_eq!(span.name, "aws.s3"); - assert_eq!(span.service, "s3"); + assert_eq!(span.service, "example-bucket"); assert_eq!(span.resource, "example-bucket"); assert_eq!(span.r#type, "web"); - - assert_eq!( - span.meta, - HashMap::from([ - ("operation_name".to_string(), "aws.s3".to_string()), - ("event_name".to_string(), "ObjectCreated:Put".to_string()), - ("bucketname".to_string(), "example-bucket".to_string()), - ( - "bucket_arn".to_string(), - "arn:aws:s3:::example-bucket".to_string() - ), - ("object_key".to_string(), "test/key".to_string()), - ("object_size".to_string(), "1024".to_string()), - ( - "object_etag".to_string(), - "0123456789abcdef0123456789abcdef".to_string() - ) - ]) - ); + assert_eq!(span.start, 1_673_049_600_000_000_000); } #[test] @@ -273,28 +265,87 @@ mod tests { } #[test] - fn test_resolve_service_name() { + fn test_resolve_service_name_with_representation_enabled() { + let json = read_json_file("s3_event.json"); + let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); + let event = S3Record::new(payload).expect("Failed to deserialize S3Record"); + + // Test 1: Specific mapping takes priority + let specific_service_mapping = HashMap::from([ + ("example-bucket".to_string(), "specific-service".to_string()), + ("lambda_s3".to_string(), "generic-service".to_string()), + ]); + + let service = event.resolve_service_name( + &specific_service_mapping, + &event.get_specific_identifier(), + "s3", + true, // aws_service_representation_enabled + ); + assert_eq!(service, "specific-service"); + + // Test 2: Generic mapping is used when specific not found + let generic_service_mapping = + HashMap::from([("lambda_s3".to_string(), "generic-service".to_string())]); + let service = event.resolve_service_name( + &generic_service_mapping, + &event.get_specific_identifier(), + "s3", + true, // aws_service_representation_enabled + ); + assert_eq!(service, "generic-service"); + + // Test 3: When no mapping exists, uses instance name (bucket name) + let empty_mapping = HashMap::new(); + let service = event.resolve_service_name( + &empty_mapping, + &event.get_specific_identifier(), + "s3", + true, // aws_service_representation_enabled + ); + assert_eq!(service, event.get_specific_identifier()); // instance name + } + + #[test] + fn test_resolve_service_name_with_representation_disabled() { let json = read_json_file("s3_event.json"); let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); let event = S3Record::new(payload).expect("Failed to deserialize S3Record"); - // Priority is given to the specific key + // Test 1: With specific mapping - still respects mapping let specific_service_mapping = HashMap::from([ ("example-bucket".to_string(), "specific-service".to_string()), ("lambda_s3".to_string(), "generic-service".to_string()), ]); - assert_eq!( - event.resolve_service_name(&specific_service_mapping, "s3"), - "specific-service" + let service = event.resolve_service_name( + &specific_service_mapping, + &event.get_specific_identifier(), + "s3", + false, // aws_service_representation_enabled = false ); + assert_eq!(service, "specific-service"); + // Test 2: With generic mapping - still respects mapping let generic_service_mapping = HashMap::from([("lambda_s3".to_string(), "generic-service".to_string())]); - assert_eq!( - event.resolve_service_name(&generic_service_mapping, "s3"), - "generic-service" + let service = event.resolve_service_name( + &generic_service_mapping, + &event.get_specific_identifier(), + "s3", + false, // aws_service_representation_enabled = false + ); + assert_eq!(service, "generic-service"); + + // Test 3: When no mapping exists, uses fallback value + let empty_mapping = HashMap::new(); + let service = event.resolve_service_name( + &empty_mapping, + &event.get_specific_identifier(), + "s3", + false, // aws_service_representation_enabled = false ); + assert_eq!(service, "s3"); // fallback value } #[test] diff --git a/bottlecap/src/lifecycle/invocation/triggers/sns_event.rs b/bottlecap/src/lifecycle/invocation/triggers/sns_event.rs index ddff7aae1..95df01083 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/sns_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/sns_event.rs @@ -82,7 +82,12 @@ impl Trigger for SnsRecord { } #[allow(clippy::cast_possible_truncation)] - fn enrich_span(&self, span: &mut Span, service_mapping: &HashMap) { + fn enrich_span( + &self, + span: &mut Span, + service_mapping: &HashMap, + aws_service_representation_enabled: bool, + ) { debug!("Enriching an Inferred Span for an SNS Event"); let resource_name = self.get_specific_identifier(); @@ -92,7 +97,12 @@ impl Trigger for SnsRecord { .timestamp_nanos_opt() .unwrap_or((self.sns.timestamp.timestamp_millis() as f64 * MS_TO_NS) as i64); - let service_name = self.resolve_service_name(service_mapping, "sns"); + let service_name = self.resolve_service_name( + service_mapping, + &self.get_specific_identifier(), + "sns", + aws_service_representation_enabled, + ); span.name = "aws.sns".to_string(); span.service = service_name.to_string(); @@ -234,9 +244,9 @@ mod tests { let event = SnsRecord::new(payload).expect("Failed to deserialize SnsRecord"); let mut span = Span::default(); let service_mapping = HashMap::new(); - event.enrich_span(&mut span, &service_mapping); + event.enrich_span(&mut span, &service_mapping, true); assert_eq!(span.name, "aws.sns"); - assert_eq!(span.service, "sns"); + assert_eq!(span.service, "serverlessTracingTopicPy"); assert_eq!(span.resource, "serverlessTracingTopicPy"); assert_eq!(span.r#type, "web"); @@ -353,12 +363,12 @@ mod tests { } #[test] - fn test_resolve_service_name() { + fn test_resolve_service_name_with_representation_enabled() { let json = read_json_file("sns_event.json"); let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); let event = SnsRecord::new(payload).expect("Failed to deserialize SnsRecord"); - // Priority is given to the specific key + // Test 1: Specific mapping takes priority let specific_service_mapping = HashMap::from([ ( "serverlessTracingTopicPy".to_string(), @@ -368,15 +378,89 @@ mod tests { ]); assert_eq!( - event.resolve_service_name(&specific_service_mapping, "sns"), + event.resolve_service_name( + &specific_service_mapping, + &event.get_specific_identifier(), + "sns", + true // aws_service_representation_enabled + ), "specific-service" ); + // Test 2: Generic mapping is used when specific not found let generic_service_mapping = HashMap::from([("lambda_sns".to_string(), "generic-service".to_string())]); assert_eq!( - event.resolve_service_name(&generic_service_mapping, "sns"), + event.resolve_service_name( + &generic_service_mapping, + &event.get_specific_identifier(), + "sns", + true // aws_service_representation_enabled + ), "generic-service" ); + + // Test 3: When no mapping exists, uses instance name (topic name) + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + &event.get_specific_identifier(), + "sns", + true // aws_service_representation_enabled + ), + event.get_specific_identifier() // instance name + ); + } + + #[test] + fn test_resolve_service_name_with_representation_disabled() { + let json = read_json_file("sns_event.json"); + let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); + let event = SnsRecord::new(payload).expect("Failed to deserialize SnsRecord"); + + // Test 1: With specific mapping - still respects mapping + let specific_service_mapping = HashMap::from([ + ( + "serverlessTracingTopicPy".to_string(), + "specific-service".to_string(), + ), + ("lambda_sns".to_string(), "generic-service".to_string()), + ]); + + assert_eq!( + event.resolve_service_name( + &specific_service_mapping, + &event.get_specific_identifier(), + "sns", + false // aws_service_representation_enabled = false + ), + "specific-service" + ); + + // Test 2: With generic mapping - still respects mapping + let generic_service_mapping = + HashMap::from([("lambda_sns".to_string(), "generic-service".to_string())]); + assert_eq!( + event.resolve_service_name( + &generic_service_mapping, + &event.get_specific_identifier(), + "sns", + false // aws_service_representation_enabled = false + ), + "generic-service" + ); + + // Test 3: When no mapping exists, uses fallback value + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + &event.get_specific_identifier(), + "sns", + false // aws_service_representation_enabled = false + ), + "sns" // fallback value + ); } } diff --git a/bottlecap/src/lifecycle/invocation/triggers/sqs_event.rs b/bottlecap/src/lifecycle/invocation/triggers/sqs_event.rs index f271d6bf1..834e9c143 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/sqs_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/sqs_event.rs @@ -99,7 +99,12 @@ impl Trigger for SqsRecord { } #[allow(clippy::cast_possible_truncation)] - fn enrich_span(&self, span: &mut Span, service_mapping: &HashMap) { + fn enrich_span( + &self, + span: &mut Span, + service_mapping: &HashMap, + aws_service_representation_enabled: bool, + ) { debug!("Enriching an Inferred Span for an SQS Event"); let resource = self.get_specific_identifier(); let start_time = (self @@ -109,7 +114,12 @@ impl Trigger for SqsRecord { .unwrap_or_default() as f64 * MS_TO_NS) as i64; - let service_name = self.resolve_service_name(service_mapping, "sqs"); + let service_name = self.resolve_service_name( + service_mapping, + &self.get_specific_identifier(), + "sqs", + aws_service_representation_enabled, + ); span.name = "aws.sqs".to_string(); span.service = service_name.to_string(); @@ -326,9 +336,9 @@ mod tests { let event = SqsRecord::new(payload).expect("Failed to deserialize SqsRecord"); let mut span = Span::default(); let service_mapping = HashMap::new(); - event.enrich_span(&mut span, &service_mapping); + event.enrich_span(&mut span, &service_mapping, true); assert_eq!(span.name, "aws.sqs"); - assert_eq!(span.service, "sqs"); + assert_eq!(span.service, "MyQueue"); assert_eq!(span.resource, "MyQueue"); assert_eq!(span.r#type, "web"); @@ -493,28 +503,99 @@ mod tests { } #[test] - fn test_resolve_service_name() { + fn test_resolve_service_name_with_representation_enabled() { + let json = read_json_file("sqs_event.json"); + let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); + let event = SqsRecord::new(payload).expect("Failed to deserialize SqsRecord"); + + // Test 1: Specific mapping takes priority + let specific_service_mapping = HashMap::from([ + ("MyQueue".to_string(), "specific-service".to_string()), + ("lambda_sqs".to_string(), "generic-service".to_string()), + ]); + + assert_eq!( + event.resolve_service_name( + &specific_service_mapping, + &event.get_specific_identifier(), + "sqs", + true // aws_service_representation_enabled + ), + "specific-service" + ); + + // Test 2: Generic mapping is used when specific not found + let generic_service_mapping = + HashMap::from([("lambda_sqs".to_string(), "generic-service".to_string())]); + assert_eq!( + event.resolve_service_name( + &generic_service_mapping, + &event.get_specific_identifier(), + "sqs", + true // aws_service_representation_enabled + ), + "generic-service" + ); + + // Test 3: When no mapping exists, uses instance name (queue name) + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + &event.get_specific_identifier(), + "sqs", + true // aws_service_representation_enabled + ), + event.get_specific_identifier() // instance name + ); + } + + #[test] + fn test_resolve_service_name_with_representation_disabled() { let json = read_json_file("sqs_event.json"); let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); let event = SqsRecord::new(payload).expect("Failed to deserialize SqsRecord"); - // Priority is given to the specific key + // Test 1: With specific mapping - still respects mapping let specific_service_mapping = HashMap::from([ ("MyQueue".to_string(), "specific-service".to_string()), ("lambda_sqs".to_string(), "generic-service".to_string()), ]); assert_eq!( - event.resolve_service_name(&specific_service_mapping, "sqs"), + event.resolve_service_name( + &specific_service_mapping, + &event.get_specific_identifier(), + "sqs", + false // aws_service_representation_enabled = false + ), "specific-service" ); + // Test 2: With generic mapping - still respects mapping let generic_service_mapping = HashMap::from([("lambda_sqs".to_string(), "generic-service".to_string())]); assert_eq!( - event.resolve_service_name(&generic_service_mapping, "sqs"), + event.resolve_service_name( + &generic_service_mapping, + &event.get_specific_identifier(), + "sqs", + false // aws_service_representation_enabled = false + ), "generic-service" ); + + // Test 3: When no mapping exists, uses fallback value + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + &event.get_specific_identifier(), + "sqs", + false // aws_service_representation_enabled = false + ), + "sqs" // fallback value + ); } #[test] diff --git a/bottlecap/src/lifecycle/invocation/triggers/step_function_event.rs b/bottlecap/src/lifecycle/invocation/triggers/step_function_event.rs index a15913064..aaf5cfe2b 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/step_function_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/step_function_event.rs @@ -116,6 +116,7 @@ impl Trigger for StepFunctionEvent { &self, _span: &mut datadog_trace_protobuf::pb::Span, _service_mapping: &HashMap, + _aws_service_representation_enabled: bool, ) { } @@ -637,4 +638,76 @@ mod tests { assert_eq!(hex_tid, "1914fe7789eb32be"); } + + #[test] + fn test_resolve_service_name_with_representation_enabled() { + let json = read_json_file("step_function_event.json"); + let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); + let event = + StepFunctionEvent::new(payload).expect("Failed to deserialize StepFunctionEvent"); + + // Test 1: Generic mapping is used for Step Functions + let generic_service_mapping = HashMap::from([( + "lambda_stepfunction".to_string(), + "generic-service".to_string(), + )]); + + assert_eq!( + event.resolve_service_name( + &generic_service_mapping, + "stepfunction", + "stepfunction", + true // aws_service_representation_enabled + ), + "generic-service" + ); + + // Test 2: When no mapping exists, uses instance name + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + "stepfunction", + "stepfunction", + true // aws_service_representation_enabled + ), + "stepfunction" // instance name + ); + } + + #[test] + fn test_resolve_service_name_with_representation_disabled() { + let json = read_json_file("step_function_event.json"); + let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); + let event = + StepFunctionEvent::new(payload).expect("Failed to deserialize StepFunctionEvent"); + + // Test 1: With generic mapping - still respects mapping + let generic_service_mapping = HashMap::from([( + "lambda_stepfunction".to_string(), + "generic-service".to_string(), + )]); + + assert_eq!( + event.resolve_service_name( + &generic_service_mapping, + "stepfunction", + "stepfunction", + false // aws_service_representation_enabled = false + ), + "generic-service" + ); + + // Test 2: When no mapping exists, uses fallback value + let empty_mapping = HashMap::new(); + assert_eq!( + event.resolve_service_name( + &empty_mapping, + "stepfunction", + "stepfunction", + false // aws_service_representation_enabled = false + ), + "stepfunction" // fallback value + ); + } } diff --git a/bottlecap/src/traces/trace_processor.rs b/bottlecap/src/traces/trace_processor.rs index dcbb030a8..ccba8fd72 100644 --- a/bottlecap/src/traces/trace_processor.rs +++ b/bottlecap/src/traces/trace_processor.rs @@ -53,6 +53,9 @@ impl TraceChunkProcessor for ChunkProcessor { } } + // Remove the _dd.base_service tag for unintentional service name override + span.meta.remove("_dd.base_service"); + self.tags_provider.get_tags_map().iter().for_each(|(k, v)| { span.meta.insert(k.clone(), v.clone()); }); diff --git a/bottlecap/tests/payloads/eventbridge_span.json b/bottlecap/tests/payloads/eventbridge_span.json index 0515abd69..9d74502c2 100644 --- a/bottlecap/tests/payloads/eventbridge_span.json +++ b/bottlecap/tests/payloads/eventbridge_span.json @@ -1,5 +1,5 @@ { - "service": "eventbridge", + "service": "testBus", "name": "aws.eventbridge", "resource": "testBus", "trace_id": 0,