From 16f88b740a3ae883ab51c91744b3220da894bf3f Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Wed, 16 Oct 2024 12:46:56 -0400 Subject: [PATCH 1/8] feat: support APIGW v1 --- .../src/lifecycle/invocation/processor.rs | 5 +- .../src/lifecycle/invocation/span_inferrer.rs | 37 +- .../triggers/api_gateway_rest_event.rs | 318 ++++++++++++++++++ .../src/lifecycle/invocation/triggers/mod.rs | 1 + bottlecap/src/lifecycle/listener.rs | 10 +- 5 files changed, 367 insertions(+), 4 deletions(-) create mode 100644 bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs diff --git a/bottlecap/src/lifecycle/invocation/processor.rs b/bottlecap/src/lifecycle/invocation/processor.rs index 33f94987a..46a2a123b 100644 --- a/bottlecap/src/lifecycle/invocation/processor.rs +++ b/bottlecap/src/lifecycle/invocation/processor.rs @@ -183,12 +183,15 @@ impl Processor { /// Given trace context information, set it to the current span. /// - pub fn on_invocation_end(&mut self, trace_id: u64, span_id: u64, parent_id: u64) { + pub fn on_invocation_end(&mut self, trace_id: u64, span_id: u64, parent_id: u64, status_code: Option) { self.span.trace_id = trace_id; self.span.span_id = span_id; if self.inferrer.get_inferred_span().is_some() { self.inferrer.set_parent_id(parent_id); + if let Some(status_code) = status_code { + self.inferrer.set_status_code(status_code); + } } else { self.span.parent_id = parent_id; } diff --git a/bottlecap/src/lifecycle/invocation/span_inferrer.rs b/bottlecap/src/lifecycle/invocation/span_inferrer.rs index 91bf3b722..1dd72dbde 100644 --- a/bottlecap/src/lifecycle/invocation/span_inferrer.rs +++ b/bottlecap/src/lifecycle/invocation/span_inferrer.rs @@ -1,7 +1,7 @@ use datadog_trace_protobuf::pb::Span; -use log::debug; use rand::Rng; use serde_json::Value; +use tracing::debug; use crate::config::AwsConfig; @@ -9,6 +9,8 @@ use crate::lifecycle::invocation::triggers::{ api_gateway_http_event::APIGatewayHttpEvent, Trigger, }; +use super::triggers::api_gateway_rest_event::APIGatewayRestEvent; + const FUNCTION_TRIGGER_EVENT_SOURCE_TAG: &str = "function_trigger.event_source"; const FUNCTION_TRIGGER_EVENT_SOURCE_ARN_TAG: &str = "function_trigger.event_source_arn"; @@ -58,12 +60,37 @@ impl SpanInferrer { ), ]); + self.is_async_span = t.is_async(); + self.inferred_span = Some(span); + } + } else if APIGatewayRestEvent::is_match(&payload_value){ + debug!("MATCH V1 REST EVENT"); + if let Some(t) = APIGatewayRestEvent::new(payload_value) { + debug!("ASTUYVE PARSING V1 REST EVENT"); + let mut span = Span { + span_id: Self::generate_span_id(), + ..Default::default() + }; + + t.enrich_span(&mut span); + span.meta.extend([ + ( + FUNCTION_TRIGGER_EVENT_SOURCE_TAG.to_string(), + "api_gateway".to_string(), + ), + ( + FUNCTION_TRIGGER_EVENT_SOURCE_ARN_TAG.to_string(), + t.get_arn(&aws_config.region), + ), + ]); + self.is_async_span = t.is_async(); self.inferred_span = Some(span); } } else { debug!("Unable to infer span from payload"); } + } else { debug!("Unable to serialize payload"); } @@ -78,6 +105,13 @@ impl SpanInferrer { } } + pub fn set_status_code(&mut self, status_code: String) { + if let Some(s) = &mut self.inferred_span { + s.meta.insert("http.status_code".to_string(), status_code); + } + } + + // TODO add status tag and other info from response pub fn complete_inferred_span(&mut self, invocation_span: &Span) { if let Some(s) = &mut self.inferred_span { if self.is_async_span { @@ -91,6 +125,7 @@ impl SpanInferrer { } s.trace_id = invocation_span.trace_id; + debug!("Final Span: {:?}", self.inferred_span); } } diff --git a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs new file mode 100644 index 000000000..603dbaaf0 --- /dev/null +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs @@ -0,0 +1,318 @@ +use datadog_trace_protobuf::pb::Span; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; +use tracing::debug; + +use crate::lifecycle::invocation::{ + processor::MS_TO_NS, + triggers::{get_aws_partition_by_region, lowercase_key, Trigger}, +}; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct APIGatewayRestEvent { + #[serde(serialize_with = "lowercase_key")] + pub headers: HashMap, + #[serde(rename = "requestContext")] + pub request_context: RequestContext, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct RequestContext { + pub stage: String, + #[serde(rename = "requestId")] + pub request_id: String, + #[serde(rename = "apiId")] + pub api_id: String, + #[serde(rename = "domainName")] + pub domain_name: String, + #[serde(rename = "requestTimeEpoch")] + pub time_epoch: i64, + #[serde(rename = "httpMethod")] + pub method: String, + #[serde(rename = "resourcePath")] + pub path: String, + pub protocol: String, + pub identity: Identity, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct Identity { + #[serde(rename = "sourceIp")] + pub source_ip: String, + #[serde(rename = "userAgent")] + pub user_agent: String, +} + +impl Trigger for APIGatewayRestEvent { + fn new(payload: Value) -> Option { + match serde_json::from_value(payload) { + Ok(event) => Some(event), + Err(e) => { + debug!("Failed to deserialize APIGatewayRestEvent: {}", e); + None + } + } + } + + fn is_match(payload: &Value) -> bool { + let stage = payload.get("requestContext").and_then(|v| v.get("stage")); + let http_method = payload.get("httpMethod"); + let resource = payload.get("resource"); + debug!( + "Checking if the payload is an API Gateway REST Event: stage: {:?}, http_method: {:?}, resource: {:?}", + stage, http_method, resource + ); + stage.is_some() && http_method.is_some() && resource.is_some() + } + + #[allow(clippy::cast_possible_truncation)] + fn enrich_span(&self, span: &mut Span) { + debug!("Enriching an Inferred Span for an API Gateway REST Event"); + let resource = format!( + "{http_method} {path}", + http_method = self.request_context.method, + path = self.request_context.path + ); + let http_url = format!( + "https://{domain_name}{path}", + domain_name = self.request_context.domain_name, + path = self.request_context.path + ); + let start_time = (self.request_context.time_epoch as f64 * MS_TO_NS) as i64; + // todo: service mapping + let service_name = self.request_context.domain_name.clone(); + + span.name = "aws.apigateway".to_string(); + span.service = service_name; + span.resource.clone_from(&resource); + span.r#type = "http".to_string(); + span.start = start_time; + span.meta.extend(HashMap::from([ + ( + "endpoint".to_string(), + self.request_context.path.clone(), + ), + ("http.url".to_string(), http_url), + ( + "http.method".to_string(), + self.request_context.method.clone(), + ), + ( + "http.protocol".to_string(), + self.request_context.protocol.clone(), + ), + ( + "http.source_ip".to_string(), + self.request_context.identity.source_ip.clone(), + ), + ( + "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(), + ), + ("resource_names".to_string(), resource), + ])); + + // todo: update global(? IsAsync if event payload is `Event` + } + + fn get_tags(&self) -> HashMap { + let mut tags = HashMap::from([ + ( + "http.url".to_string(), + self.request_context.domain_name.clone(), + ), + ( + "http_url_details.path".to_string(), + self.request_context.path.clone(), + ), + ( + "http.method".to_string(), + self.request_context.method.clone(), + ), + ]); + + if let Some(referer) = self.headers.get("referer") { + tags.insert("http.referer".to_string(), referer.to_string()); + } + + if let Some(user_agent) = self.headers.get("user-agent") { + tags.insert("http.user_agent".to_string(), user_agent.to_string()); + } + + tags + } + + fn get_arn(&self, region: &str) -> String { + let partition = get_aws_partition_by_region(region); + format!( + "arn:{partition}:apigateway:{region}::/restapis/{api_id}/stages/{stage}", + partition = partition, + region = region, + api_id = self.request_context.api_id, + stage = self.request_context.stage + ) + } + + fn is_async(&self) -> bool { + self.headers + .get("x-amz-invocation-type") + .is_some_and(|v| v == "Event") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::lifecycle::invocation::triggers::test_utils::read_json_file; + + #[test] + fn test_new() { + let json = read_json_file("api_gateway_http_event.json"); + let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); + let result = APIGatewayRestEvent::new(payload) + .expect("Failed to deserialize into APIGatewayRestEvent"); + + let expected = APIGatewayRestEvent { + headers: HashMap::from([ + ("accept".to_string(), "*/*".to_string()), + ("content-length".to_string(), "0".to_string()), + ( + "host".to_string(), + "x02yirxc7a.execute-api.sa-east-1.amazonaws.com".to_string(), + ), + ("user-agent".to_string(), "curl/7.64.1".to_string()), + ( + "x-amzn-trace-id".to_string(), + "Root=1-613a52fb-4c43cfc95e0241c1471bfa05".to_string(), + ), + ("x-forwarded-for".to_string(), "38.122.226.210".to_string()), + ("x-forwarded-port".to_string(), "443".to_string()), + ("x-forwarded-proto".to_string(), "https".to_string()), + ("x-datadog-trace-id".to_string(), "12345".to_string()), + ("x-datadog-parent-id".to_string(), "67890".to_string()), + ("x-datadog-sampling-priority".to_string(), "2".to_string()), + ]), + request_context: RequestContext { + stage: "$default".to_string(), + request_id: "FaHnXjKCGjQEJ7A=".to_string(), + api_id: "x02yirxc7a".to_string(), + domain_name: "x02yirxc7a.execute-api.sa-east-1.amazonaws.com".to_string(), + time_epoch: 1631212283738, + method: "GET".to_string(), + path: "/httpapi/get".to_string(), + protocol: "HTTP/1.1".to_string(), + identity: Identity { + source_ip: "38.122.226.210".to_string(), + user_agent: "curl/7.64.1".to_string(), + }, + }, + }; + + assert_eq!(result, expected); + } + + #[test] + fn test_is_match() { + let json = read_json_file("api_gateway_http_event.json"); + let payload = + serde_json::from_str(&json).expect("Failed to deserialize APIGatewayRestEvent"); + + assert!(APIGatewayRestEvent::is_match(&payload)); + } + + #[test] + fn test_is_not_match() { + let json = read_json_file("api_gateway_proxy_event.json"); + let payload = + serde_json::from_str(&json).expect("Failed to deserialize APIGatewayRestEvent"); + assert!(!APIGatewayRestEvent::is_match(&payload)); + } + + #[test] + fn test_enrich_span() { + 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 = + APIGatewayRestEvent::new(payload).expect("Failed to deserialize APIGatewayRestEvent"); + let mut span = Span::default(); + event.enrich_span(&mut span); + assert_eq!(span.name, "aws.apigateway"); + assert_eq!( + span.service, + "x02yirxc7a.execute-api.sa-east-1.amazonaws.com" + ); + assert_eq!(span.resource, "GET /httpapi/get"); + assert_eq!(span.r#type, "http"); + assert_eq!( + span.meta, + HashMap::from([ + ("endpoint".to_string(), "/httpapi/get".to_string()), + ( + "http.url".to_string(), + "x02yirxc7a.execute-api.sa-east-1.amazonaws.com/httpapi/get".to_string() + ), + ("http.method".to_string(), "GET".to_string()), + ("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()), + ("resource_names".to_string(), "GET /httpapi/get".to_string()), + ]) + ); + } + + #[test] + fn test_get_tags() { + 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 = + APIGatewayRestEvent::new(payload).expect("Failed to deserialize APIGatewayRestEvent"); + let tags = event.get_tags(); + let sorted_tags_array = tags + .iter() + .map(|(k, v)| format!("{}:{}", k, v)) + .collect::>() + .sort(); + + let expected = HashMap::from([ + ( + "http.url".to_string(), + "x02yirxc7a.execute-api.sa-east-1.amazonaws.com".to_string(), + ), + ( + "http_url_details.path".to_string(), + "/httpapi/get".to_string(), + ), + ("http.method".to_string(), "GET".to_string()), + ("http.route".to_string(), "GET /httpapi/get".to_string()), + ("http.user_agent".to_string(), "curl/7.64.1".to_string()), + ("http.referer".to_string(), "".to_string()), + ]); + let expected_sorted_array = expected + .iter() + .map(|(k, v)| format!("{}:{}", k, v)) + .collect::>() + .sort(); + + assert_eq!(sorted_tags_array, expected_sorted_array); + } + + #[test] + fn test_get_arn() { + 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 = + APIGatewayRestEvent::new(payload).expect("Failed to deserialize APIGatewayRestEvent"); + assert_eq!( + event.get_arn("sa-east-1"), + "arn:aws:apigateway:sa-east-1::/restapis/x02yirxc7a/stages/$default" + ); + } +} diff --git a/bottlecap/src/lifecycle/invocation/triggers/mod.rs b/bottlecap/src/lifecycle/invocation/triggers/mod.rs index ec5860d27..5eb32ec6b 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/mod.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/mod.rs @@ -5,6 +5,7 @@ use serde::{ser::SerializeMap, Serializer}; use serde_json::Value; pub mod api_gateway_http_event; +pub mod api_gateway_rest_event; pub trait Trigger: Sized { fn new(payload: Value) -> Option; diff --git a/bottlecap/src/lifecycle/listener.rs b/bottlecap/src/lifecycle/listener.rs index e52ad9625..eabfb0a1e 100644 --- a/bottlecap/src/lifecycle/listener.rs +++ b/bottlecap/src/lifecycle/listener.rs @@ -116,7 +116,13 @@ impl Listener { invocation_processor: Arc>, ) -> http::Result> { debug!("Received end invocation request"); - let (parts, _) = req.into_parts(); + let (parts, body) = req.into_parts(); + let parsed_body = serde_json::from_slice::(&hyper::body::to_bytes(body).await.unwrap()); + debug!("Parsed body: {:?}", parsed_body); + let mut parsed_status: Option = None; + if let Some(status_code) = parsed_body.unwrap_or_default().get("statusCode") { + parsed_status = Some(status_code.to_string()); + } let headers = parts.headers; let mut processor = invocation_processor.lock().await; @@ -142,7 +148,7 @@ impl Listener { } } - processor.on_invocation_end(trace_id, span_id, parent_id); + processor.on_invocation_end(trace_id, span_id, parent_id, parsed_status); drop(processor); Response::builder() From 588fd793c5577a8e7b7534413637e76f766482b4 Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Thu, 17 Oct 2024 13:55:17 -0400 Subject: [PATCH 2/8] feat: Tests for unparameterized payload working --- .../triggers/api_gateway_http_event.rs | 20 ++-- .../triggers/api_gateway_rest_event.rs | 93 +++++++-------- .../api_gateway_http_event_parameterized.json | 38 ++++++ .../payloads/api_gateway_rest_event.json | 80 +++++++++++++ .../api_gateway_rest_event_parameterized.json | 111 ++++++++++++++++++ 5 files changed, 285 insertions(+), 57 deletions(-) create mode 100644 bottlecap/tests/payloads/api_gateway_http_event_parameterized.json create mode 100644 bottlecap/tests/payloads/api_gateway_rest_event.json create mode 100644 bottlecap/tests/payloads/api_gateway_rest_event_parameterized.json 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 434d636af..ad704faee 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs @@ -63,13 +63,19 @@ impl Trigger for APIGatewayHttpEvent { #[allow(clippy::cast_possible_truncation)] fn enrich_span(&self, span: &mut Span) { debug!("Enriching an Inferred Span for an API Gateway HTTP Event"); - let resource = format!( - "{http_method} {path}", - http_method = self.request_context.http.method, - path = self.request_context.http.path - ); + let resource: String; + if self.route_key.is_empty() { + resource = format!( + "{http_method} {route_key}", + http_method = self.request_context.http.method, + route_key = self.route_key + ); + } else { + resource = self.route_key.clone(); + } + let http_url = format!( - "{domain_name}{path}", + "https://{domain_name}{path}", domain_name = self.request_context.domain_name, path = self.request_context.http.path ); @@ -254,7 +260,7 @@ mod tests { ("endpoint".to_string(), "/httpapi/get".to_string()), ( "http.url".to_string(), - "x02yirxc7a.execute-api.sa-east-1.amazonaws.com/httpapi/get".to_string() + "https://x02yirxc7a.execute-api.sa-east-1.amazonaws.com/httpapi/get".to_string() ), ("http.method".to_string(), "GET".to_string()), ("http.protocol".to_string(), "HTTP/1.1".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 603dbaaf0..58b3bbb3b 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs @@ -31,6 +31,7 @@ pub struct RequestContext { #[serde(rename = "httpMethod")] pub method: String, #[serde(rename = "resourcePath")] + pub resource_path: String, pub path: String, pub protocol: String, pub identity: Identity, @@ -59,10 +60,6 @@ impl Trigger for APIGatewayRestEvent { let stage = payload.get("requestContext").and_then(|v| v.get("stage")); let http_method = payload.get("httpMethod"); let resource = payload.get("resource"); - debug!( - "Checking if the payload is an API Gateway REST Event: stage: {:?}, http_method: {:?}, resource: {:?}", - stage, http_method, resource - ); stage.is_some() && http_method.is_some() && resource.is_some() } @@ -72,7 +69,7 @@ impl Trigger for APIGatewayRestEvent { let resource = format!( "{http_method} {path}", http_method = self.request_context.method, - path = self.request_context.path + path = self.request_context.resource_path ); let http_url = format!( "https://{domain_name}{path}", @@ -118,6 +115,7 @@ impl Trigger for APIGatewayRestEvent { ("resource_names".to_string(), resource), ])); + debug!("Enriched Span: {:?}", span); // todo: update global(? IsAsync if event payload is `Event` } @@ -173,43 +171,29 @@ mod tests { #[test] fn test_new() { - let json = read_json_file("api_gateway_http_event.json"); + let json = read_json_file("api_gateway_rest_event.json"); let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); let result = APIGatewayRestEvent::new(payload) .expect("Failed to deserialize into APIGatewayRestEvent"); let expected = APIGatewayRestEvent { headers: HashMap::from([ - ("accept".to_string(), "*/*".to_string()), - ("content-length".to_string(), "0".to_string()), - ( - "host".to_string(), - "x02yirxc7a.execute-api.sa-east-1.amazonaws.com".to_string(), - ), - ("user-agent".to_string(), "curl/7.64.1".to_string()), - ( - "x-amzn-trace-id".to_string(), - "Root=1-613a52fb-4c43cfc95e0241c1471bfa05".to_string(), - ), - ("x-forwarded-for".to_string(), "38.122.226.210".to_string()), - ("x-forwarded-port".to_string(), "443".to_string()), - ("x-forwarded-proto".to_string(), "https".to_string()), - ("x-datadog-trace-id".to_string(), "12345".to_string()), - ("x-datadog-parent-id".to_string(), "67890".to_string()), - ("x-datadog-sampling-priority".to_string(), "2".to_string()), + ("Header1".to_string(), "value1".to_string()), + ("Header2".to_string(), "value2".to_string()), ]), request_context: RequestContext { stage: "$default".to_string(), - request_id: "FaHnXjKCGjQEJ7A=".to_string(), - api_id: "x02yirxc7a".to_string(), - domain_name: "x02yirxc7a.execute-api.sa-east-1.amazonaws.com".to_string(), - time_epoch: 1631212283738, + request_id: "id=".to_string(), + api_id: "id".to_string(), + domain_name: "id.execute-api.us-east-1.amazonaws.com".to_string(), + time_epoch: 1583349317135, method: "GET".to_string(), - path: "/httpapi/get".to_string(), + path: "/my/path".to_string(), protocol: "HTTP/1.1".to_string(), + resource_path: "/path".to_string(), identity: Identity { - source_ip: "38.122.226.210".to_string(), - user_agent: "curl/7.64.1".to_string(), + source_ip: "IP".to_string(), + user_agent: "user-agent".to_string(), }, }, }; @@ -219,7 +203,7 @@ mod tests { #[test] fn test_is_match() { - let json = read_json_file("api_gateway_http_event.json"); + let json = read_json_file("api_gateway_rest_event.json"); let payload = serde_json::from_str(&json).expect("Failed to deserialize APIGatewayRestEvent"); @@ -228,7 +212,7 @@ mod tests { #[test] fn test_is_not_match() { - let json = read_json_file("api_gateway_proxy_event.json"); + let json = read_json_file("api_gateway_http_event.json"); let payload = serde_json::from_str(&json).expect("Failed to deserialize APIGatewayRestEvent"); assert!(!APIGatewayRestEvent::is_match(&payload)); @@ -236,7 +220,7 @@ mod tests { #[test] fn test_enrich_span() { - let json = read_json_file("api_gateway_http_event.json"); + 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"); @@ -245,32 +229,41 @@ mod tests { assert_eq!(span.name, "aws.apigateway"); assert_eq!( span.service, - "x02yirxc7a.execute-api.sa-east-1.amazonaws.com" + "id.execute-api.us-east-1.amazonaws.com" ); - assert_eq!(span.resource, "GET /httpapi/get"); + assert_eq!(span.resource, "GET /path"); assert_eq!(span.r#type, "http"); - assert_eq!( - span.meta, - HashMap::from([ - ("endpoint".to_string(), "/httpapi/get".to_string()), + let sorted_span_meta = span.meta.iter() + .map(|(k, v)| format!("{}:{}", k, v)) + .collect::>() + .sort(); + let expected = HashMap::from([ + ("endpoint".to_string(), "/my/path".to_string()), ( "http.url".to_string(), - "x02yirxc7a.execute-api.sa-east-1.amazonaws.com/httpapi/get".to_string() + "https://id.execute-api.us-east-1.amazonaws.com".to_string() ), ("http.method".to_string(), "GET".to_string()), ("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()), - ("resource_names".to_string(), "GET /httpapi/get".to_string()), - ]) + ("http.source_ip".to_string(), "IP".to_string()), + ("http.user_agent".to_string(), "user-agent".to_string()), + ("operation_name".to_string(), "aws.api_gateway".to_string()), + ("request_id".to_string(), "id=".to_string()), + ("resource_names".to_string(), "GET /path".to_string()), + ]); + let sorted_expected = expected.iter() + .map(|(k, v)| format!("{}:{}", k, v)) + .collect::>() + .sort(); + assert_eq!( + sorted_span_meta, + sorted_expected ); } #[test] fn test_get_tags() { - let json = read_json_file("api_gateway_http_event.json"); + 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"); @@ -306,13 +299,13 @@ mod tests { #[test] fn test_get_arn() { - let json = read_json_file("api_gateway_http_event.json"); + 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_arn("sa-east-1"), - "arn:aws:apigateway:sa-east-1::/restapis/x02yirxc7a/stages/$default" + event.get_arn("us-east-1"), + "arn:aws:apigateway:us-east-1::/restapis/id/stages/$default" ); } } diff --git a/bottlecap/tests/payloads/api_gateway_http_event_parameterized.json b/bottlecap/tests/payloads/api_gateway_http_event_parameterized.json new file mode 100644 index 000000000..89ff72b9c --- /dev/null +++ b/bottlecap/tests/payloads/api_gateway_http_event_parameterized.json @@ -0,0 +1,38 @@ +{ + "version": "2.0", + "routeKey": "GET /user/{id}", + "rawPath": "/user/42", + "rawQueryString": "", + "headers": { + "accept": "*/*", + "content-length": "0", + "host": "9vj54we5ih.execute-api.sa-east-1.amazonaws.com", + "user-agent": "curl/8.1.2", + "x-amzn-trace-id": "Root=1-65f49d71-505edb3b69b8abd513cfa08b", + "x-forwarded-for": "76.115.124.192", + "x-forwarded-port": "443", + "x-forwarded-proto": "https" + }, + "requestContext": { + "accountId": "425362996713", + "apiId": "9vj54we5ih", + "domainName": "9vj54we5ih.execute-api.sa-east-1.amazonaws.com", + "domainPrefix": "9vj54we5ih", + "http": { + "method": "GET", + "path": "/user/42", + "protocol": "HTTP/1.1", + "sourceIp": "76.115.124.192", + "userAgent": "curl/8.1.2" + }, + "requestId": "Ur2JtjEfGjQEPOg=", + "routeKey": "GET /user/{id}", + "stage": "$default", + "time": "15/Mar/2024:19:11:45 +0000", + "timeEpoch": 1710529905066 + }, + "pathParameters": { + "id": "42" + }, + "isBase64Encoded": false +} diff --git a/bottlecap/tests/payloads/api_gateway_rest_event.json b/bottlecap/tests/payloads/api_gateway_rest_event.json new file mode 100644 index 000000000..df9c5bb88 --- /dev/null +++ b/bottlecap/tests/payloads/api_gateway_rest_event.json @@ -0,0 +1,80 @@ +{ + "version": "1.0", + "resource": "/my/path", + "path": "/my/path", + "httpMethod": "GET", + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "multiValueHeaders": { + "Header1": [ + "value1" + ], + "Header2": [ + "value1", + "value2" + ] + }, + "queryStringParameters": { + "parameter1": "value1", + "parameter2": "value" + }, + "multiValueQueryStringParameters": { + "parameter1": [ + "value1", + "value2" + ], + "parameter2": [ + "value" + ] + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "id", + "authorizer": { + "claims": null, + "scopes": null + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "extendedRequestId": "request-id", + "httpMethod": "GET", + "identity": { + "accessKey": null, + "accountId": null, + "caller": null, + "cognitoAuthenticationProvider": null, + "cognitoAuthenticationType": null, + "cognitoIdentityId": null, + "cognitoIdentityPoolId": null, + "principalOrgId": null, + "sourceIp": "IP", + "user": null, + "userAgent": "user-agent", + "userArn": null, + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "path": "/my/path", + "protocol": "HTTP/1.1", + "requestId": "id=", + "requestTime": "04/Mar/2020:19:15:17 +0000", + "requestTimeEpoch": 1583349317135, + "resourceId": null, + "resourcePath": "/path", + "stage": "$default" + }, + "pathParameters": null, + "stageVariables": null, + "body": "Hello from Lambda!", + "isBase64Encoded": false +} diff --git a/bottlecap/tests/payloads/api_gateway_rest_event_parameterized.json b/bottlecap/tests/payloads/api_gateway_rest_event_parameterized.json new file mode 100644 index 000000000..65527ccb6 --- /dev/null +++ b/bottlecap/tests/payloads/api_gateway_rest_event_parameterized.json @@ -0,0 +1,111 @@ +{ + "resource": "/user/{id}", + "path": "/user/42", + "httpMethod": "GET", + "headers": { + "Accept": "*/*", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-ASN": "7922", + "CloudFront-Viewer-Country": "US", + "Host": "mcwkra0ya4.execute-api.sa-east-1.amazonaws.com", + "User-Agent": "curl/8.1.2", + "Via": "2.0 xxx.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "Tz3yUVcJkwOhQGqZgKTzrEHqAoOd8ZprYAHpg2S6BNxdd-Ym79pb6g==", + "X-Amzn-Trace-Id": "Root=1-65f49d20-7ba106216238dd0078a5db31", + "X-Forwarded-For": "76.115.124.192, 15.158.54.119", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": [ + "*/*" + ], + "CloudFront-Forwarded-Proto": [ + "https" + ], + "CloudFront-Is-Desktop-Viewer": [ + "true" + ], + "CloudFront-Is-Mobile-Viewer": [ + "false" + ], + "CloudFront-Is-SmartTV-Viewer": [ + "false" + ], + "CloudFront-Is-Tablet-Viewer": [ + "false" + ], + "CloudFront-Viewer-ASN": [ + "7922" + ], + "CloudFront-Viewer-Country": [ + "US" + ], + "Host": [ + "mcwkra0ya4.execute-api.sa-east-1.amazonaws.com" + ], + "User-Agent": [ + "curl/8.1.2" + ], + "Via": [ + "2.0 xxx.cloudfront.net (CloudFront)" + ], + "X-Amz-Cf-Id": [ + "Tz3yUVcJkwOhQGqZgKTzrEHqAoOd8ZprYAHpg2S6BNxdd-Ym79pb6g==" + ], + "X-Amzn-Trace-Id": [ + "Root=1-65f49d20-7ba106216238dd0078a5db31" + ], + "X-Forwarded-For": [ + "76.115.124.192, 15.158.54.119" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "queryStringParameters": null, + "multiValueQueryStringParameters": null, + "pathParameters": { + "id": "42" + }, + "stageVariables": null, + "requestContext": { + "resourceId": "ojg3nk", + "resourcePath": "/user/{id}", + "httpMethod": "GET", + "extendedRequestId": "Ur19IHYDmjQEU5A=", + "requestTime": "15/Mar/2024:19:10:24 +0000", + "path": "/dev/user/42", + "accountId": "425362996713", + "protocol": "HTTP/1.1", + "stage": "dev", + "domainPrefix": "mcwkra0ya4", + "requestTimeEpoch": 1710529824520, + "requestId": "e16399f7-e984-463a-9931-745ba021a27f", + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "sourceIp": "76.115.124.192", + "principalOrgId": null, + "accessKey": null, + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "curl/8.1.2", + "user": null + }, + "domainName": "mcwkra0ya4.execute-api.sa-east-1.amazonaws.com", + "apiId": "mcwkra0ya4" + }, + "body": null, + "isBase64Encoded": false +} From 39ea4550cb67e25b67928e97eba52072e30bc062 Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Thu, 17 Oct 2024 14:10:23 -0400 Subject: [PATCH 3/8] feat: parameterized test --- .../triggers/api_gateway_rest_event.rs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) 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 58b3bbb3b..4815b0254 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs @@ -297,6 +297,85 @@ mod tests { assert_eq!(sorted_tags_array, expected_sorted_array); } + #[test] + fn test_enrich_parameterized_span() { + let json = read_json_file("api_gateway_rest_event_parameterized.json"); + let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); + let event = + APIGatewayRestEvent::new(payload).expect("Failed to deserialize APIGatewayRestEvent"); + let mut span = Span::default(); + event.enrich_span(&mut span); + assert_eq!(span.name, "aws.apigateway"); + assert_eq!( + span.service, + "mcwkra0ya4.execute-api.sa-east-1.amazonaws.com" + ); + assert_eq!(span.resource, "GET /user/{id}"); + assert_eq!(span.r#type, "http"); + let sorted_span_meta = span.meta.iter() + .map(|(k, v)| format!("{}:{}", k, v)) + .collect::>() + .sort(); + let expected = HashMap::from([ + ("endpoint".to_string(), "/dev/user/42".to_string()), + ( + "http.url".to_string(), + "mcwkra0ya4.execute-api.sa-east-1.amazonaws.com".to_string() + ), + ("http.method".to_string(), "GET".to_string()), + ("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.api_gateway".to_string()), + ("request_id".to_string(), "mcwkra0ya4".to_string()), + ("resource_names".to_string(), "GET /dev/user/{id}".to_string()), + ]); + let sorted_expected = expected.iter() + .map(|(k, v)| format!("{}:{}", k, v)) + .collect::>() + .sort(); + assert_eq!( + sorted_span_meta, + sorted_expected + ); + } + + #[test] + fn test_get_tags_parameterized() { + let json = read_json_file("api_gateway_rest_event_parameterized.json"); + let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); + let event = + APIGatewayRestEvent::new(payload).expect("Failed to deserialize APIGatewayRestEvent"); + let tags = event.get_tags(); + let sorted_tags_array = tags + .iter() + .map(|(k, v)| format!("{}:{}", k, v)) + .collect::>() + .sort(); + + let expected = HashMap::from([ + ( + "http.url".to_string(), + "x02yirxc7a.execute-api.sa-east-1.amazonaws.com".to_string(), + ), + ( + "http_url_details.path".to_string(), + "/httpapi/get".to_string(), + ), + ("http.method".to_string(), "GET".to_string()), + ("http.route".to_string(), "GET /httpapi/get".to_string()), + ("http.user_agent".to_string(), "curl/7.64.1".to_string()), + ("http.referer".to_string(), "".to_string()), + ]); + let expected_sorted_array = expected + .iter() + .map(|(k, v)| format!("{}:{}", k, v)) + .collect::>() + .sort(); + + assert_eq!(sorted_tags_array, expected_sorted_array); + } + #[test] fn test_get_arn() { let json = read_json_file("api_gateway_rest_event.json"); From 38d30d17d6264d0015cbaedc2cfb8b4f3136dc25 Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Fri, 18 Oct 2024 08:47:45 -0400 Subject: [PATCH 4/8] fix: specs --- .../triggers/api_gateway_http_event.rs | 70 +++++++++++++++++++ .../triggers/api_gateway_rest_event.rs | 66 +++++++---------- 2 files changed, 96 insertions(+), 40 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 ad704faee..4e8e29368 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs @@ -309,6 +309,76 @@ mod tests { assert_eq!(sorted_tags_array, expected_sorted_array); } + #[test] + fn test_enrich_span_parameterized() { + let json = read_json_file("api_gateway_http_event_parameterized.json"); + let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); + let event = + APIGatewayHttpEvent::new(payload).expect("Failed to deserialize APIGatewayHttpEvent"); + let mut span = Span::default(); + event.enrich_span(&mut span); + assert_eq!(span.name, "aws.httpapi"); + assert_eq!( + span.service, + "9vj54we5ih.execute-api.sa-east-1.amazonaws.com" + ); + assert_eq!(span.resource, "GET /user/{id}"); + assert_eq!(span.r#type, "http"); + assert_eq!( + span.meta, + HashMap::from([ + ("endpoint".to_string(), "/user/42".to_string()), + ( + "http.url".to_string(), + "https://9vj54we5ih.execute-api.sa-east-1.amazonaws.com/user/42".to_string() + ), + ("http.method".to_string(), "GET".to_string()), + ("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()), + ("resource_names".to_string(), "GET /user/{id}".to_string()), + ]) + ); + } + + #[test] + fn test_get_tags_parameterized() { + let json = read_json_file("api_gateway_http_event_parameterized.json"); + let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); + let event = + APIGatewayHttpEvent::new(payload).expect("Failed to deserialize APIGatewayHttpEvent"); + let tags = event.get_tags(); + let sorted_tags_array = tags + .iter() + .map(|(k, v)| format!("{}:{}", k, v)) + .collect::>() + .sort(); + + println!("ASTUYVE arr is {:?}", sorted_tags_array); + let expected = HashMap::from([ + ( + "http.url".to_string(), + "x02yirxc7a.execute-api.sa-east-1.amazonaws.com".to_string(), + ), + ( + "http_url_details.path".to_string(), + "/httpapi/get".to_string(), + ), + ("http.method".to_string(), "GET".to_string()), + ("http.route".to_string(), "GET /httpapi/get".to_string()), + ("http.user_agent".to_string(), "curl/7.64.1".to_string()), + ("http.referer".to_string(), "".to_string()), + ]); + let expected_sorted_array = expected + .iter() + .map(|(k, v)| format!("{}:{}", k, v)) + .collect::>() + .sort(); + + assert_eq!(sorted_tags_array, expected_sorted_array); + } #[test] fn test_get_arn() { 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 4815b0254..530d73a51 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs @@ -112,7 +112,8 @@ impl Trigger for APIGatewayRestEvent { "request_id".to_string(), self.request_context.request_id.clone(), ), - ("resource_names".to_string(), resource), + ("resource_names".to_string(), resource.clone()), + ("http.route".to_string(), resource), ])); debug!("Enriched Span: {:?}", span); @@ -133,16 +134,24 @@ impl Trigger for APIGatewayRestEvent { "http.method".to_string(), self.request_context.method.clone(), ), + ( + "http.route".to_string(), + format!( + "{method} {resource}", + method = self.request_context.method.clone(), + resource = self.request_context.resource_path.clone() + ), + ), + ( + "http.user_agent".to_string(), + self.request_context.identity.user_agent.to_string() + ), ]); if let Some(referer) = self.headers.get("referer") { tags.insert("http.referer".to_string(), referer.to_string()); } - if let Some(user_agent) = self.headers.get("user-agent") { - tags.insert("http.user_agent".to_string(), user_agent.to_string()); - } - tags } @@ -233,32 +242,22 @@ mod tests { ); assert_eq!(span.resource, "GET /path"); assert_eq!(span.r#type, "http"); - let sorted_span_meta = span.meta.iter() - .map(|(k, v)| format!("{}:{}", k, v)) - .collect::>() - .sort(); - let expected = HashMap::from([ + + assert_eq!(span.meta, HashMap::from([ ("endpoint".to_string(), "/my/path".to_string()), ( "http.url".to_string(), - "https://id.execute-api.us-east-1.amazonaws.com".to_string() + "https://id.execute-api.us-east-1.amazonaws.com/my/path".to_string() ), ("http.method".to_string(), "GET".to_string()), ("http.protocol".to_string(), "HTTP/1.1".to_string()), ("http.source_ip".to_string(), "IP".to_string()), ("http.user_agent".to_string(), "user-agent".to_string()), - ("operation_name".to_string(), "aws.api_gateway".to_string()), + ("http.route".to_string(), "GET /path".to_string()), + ("operation_name".to_string(), "aws.apigateway".to_string()), ("request_id".to_string(), "id=".to_string()), ("resource_names".to_string(), "GET /path".to_string()), - ]); - let sorted_expected = expected.iter() - .map(|(k, v)| format!("{}:{}", k, v)) - .collect::>() - .sort(); - assert_eq!( - sorted_span_meta, - sorted_expected - ); + ])); } #[test] @@ -347,33 +346,20 @@ mod tests { let event = APIGatewayRestEvent::new(payload).expect("Failed to deserialize APIGatewayRestEvent"); let tags = event.get_tags(); - let sorted_tags_array = tags - .iter() - .map(|(k, v)| format!("{}:{}", k, v)) - .collect::>() - .sort(); - let expected = HashMap::from([ + assert_eq!(tags, HashMap::from([ ( "http.url".to_string(), - "x02yirxc7a.execute-api.sa-east-1.amazonaws.com".to_string(), + "mcwkra0ya4.execute-api.sa-east-1.amazonaws.com".to_string(), ), ( "http_url_details.path".to_string(), - "/httpapi/get".to_string(), + "/dev/user/42".to_string(), ), ("http.method".to_string(), "GET".to_string()), - ("http.route".to_string(), "GET /httpapi/get".to_string()), - ("http.user_agent".to_string(), "curl/7.64.1".to_string()), - ("http.referer".to_string(), "".to_string()), - ]); - let expected_sorted_array = expected - .iter() - .map(|(k, v)| format!("{}:{}", k, v)) - .collect::>() - .sort(); - - assert_eq!(sorted_tags_array, expected_sorted_array); + ("http.route".to_string(), "GET /user/{id}".to_string()), + ("http.user_agent".to_string(), "curl/8.1.2".to_string()), + ])); } #[test] From ba01e42841fa941f6a2667eef94ae9bfa819e8f6 Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Mon, 21 Oct 2024 07:19:30 -0400 Subject: [PATCH 5/8] fix: unwrap_or_default, route has no http verb but is parameterized. --- .../src/lifecycle/invocation/processor.rs | 8 +- .../src/lifecycle/invocation/span_inferrer.rs | 3 +- .../triggers/api_gateway_http_event.rs | 72 ++++----- .../triggers/api_gateway_rest_event.rs | 137 ++++++++---------- bottlecap/src/lifecycle/listener.rs | 5 +- 5 files changed, 101 insertions(+), 124 deletions(-) diff --git a/bottlecap/src/lifecycle/invocation/processor.rs b/bottlecap/src/lifecycle/invocation/processor.rs index 46a2a123b..9b9cc98ef 100644 --- a/bottlecap/src/lifecycle/invocation/processor.rs +++ b/bottlecap/src/lifecycle/invocation/processor.rs @@ -183,7 +183,13 @@ impl Processor { /// Given trace context information, set it to the current span. /// - pub fn on_invocation_end(&mut self, trace_id: u64, span_id: u64, parent_id: u64, status_code: Option) { + pub fn on_invocation_end( + &mut self, + trace_id: u64, + span_id: u64, + parent_id: u64, + status_code: Option, + ) { self.span.trace_id = trace_id; self.span.span_id = span_id; diff --git a/bottlecap/src/lifecycle/invocation/span_inferrer.rs b/bottlecap/src/lifecycle/invocation/span_inferrer.rs index 1dd72dbde..3c1c7e294 100644 --- a/bottlecap/src/lifecycle/invocation/span_inferrer.rs +++ b/bottlecap/src/lifecycle/invocation/span_inferrer.rs @@ -63,7 +63,7 @@ impl SpanInferrer { self.is_async_span = t.is_async(); self.inferred_span = Some(span); } - } else if APIGatewayRestEvent::is_match(&payload_value){ + } else if APIGatewayRestEvent::is_match(&payload_value) { debug!("MATCH V1 REST EVENT"); if let Some(t) = APIGatewayRestEvent::new(payload_value) { debug!("ASTUYVE PARSING V1 REST EVENT"); @@ -90,7 +90,6 @@ impl SpanInferrer { } else { debug!("Unable to infer span from payload"); } - } else { debug!("Unable to serialize 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 4e8e29368..2bb0a10c4 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs @@ -125,10 +125,16 @@ impl Trigger for APIGatewayHttpEvent { let mut tags = HashMap::from([ ( "http.url".to_string(), - self.request_context.domain_name.clone(), + format!( + "https://{domain_name}{path}", + domain_name = self.request_context.domain_name.clone(), + path = self.request_context.http.path.clone() + ), ), + // path and URL are full + // /users/12345/profile ( - "http_url_details.path".to_string(), + "http.url_details.path".to_string(), self.request_context.http.path.clone(), ), ( @@ -136,9 +142,18 @@ impl Trigger for APIGatewayHttpEvent { self.request_context.http.method.clone(), ), ]); - + // route is parameterized + // /users/{id}/profile if !self.route_key.is_empty() { - tags.insert("http.route".to_string(), self.route_key.clone()); + tags.insert( + "http.route".to_string(), + self.route_key + .clone() + .split_whitespace() + .last() + .unwrap_or(&self.route_key.clone()) + .to_string(), + ); } if let Some(referer) = self.headers.get("referer") { @@ -260,7 +275,8 @@ mod tests { ("endpoint".to_string(), "/httpapi/get".to_string()), ( "http.url".to_string(), - "https://x02yirxc7a.execute-api.sa-east-1.amazonaws.com/httpapi/get".to_string() + "https://x02yirxc7a.execute-api.sa-east-1.amazonaws.com/httpapi/get" + .to_string() ), ("http.method".to_string(), "GET".to_string()), ("http.protocol".to_string(), "HTTP/1.1".to_string()), @@ -280,33 +296,21 @@ mod tests { let event = APIGatewayHttpEvent::new(payload).expect("Failed to deserialize APIGatewayHttpEvent"); let tags = event.get_tags(); - let sorted_tags_array = tags - .iter() - .map(|(k, v)| format!("{}:{}", k, v)) - .collect::>() - .sort(); - let expected = HashMap::from([ ( "http.url".to_string(), - "x02yirxc7a.execute-api.sa-east-1.amazonaws.com".to_string(), + "https://x02yirxc7a.execute-api.sa-east-1.amazonaws.com/httpapi/get".to_string(), ), ( - "http_url_details.path".to_string(), + "http.url_details.path".to_string(), "/httpapi/get".to_string(), ), ("http.method".to_string(), "GET".to_string()), - ("http.route".to_string(), "GET /httpapi/get".to_string()), + ("http.route".to_string(), "/httpapi/get".to_string()), ("http.user_agent".to_string(), "curl/7.64.1".to_string()), - ("http.referer".to_string(), "".to_string()), ]); - let expected_sorted_array = expected - .iter() - .map(|(k, v)| format!("{}:{}", k, v)) - .collect::>() - .sort(); - assert_eq!(sorted_tags_array, expected_sorted_array); + assert_eq!(tags, expected); } #[test] @@ -350,34 +354,18 @@ mod tests { let event = APIGatewayHttpEvent::new(payload).expect("Failed to deserialize APIGatewayHttpEvent"); let tags = event.get_tags(); - let sorted_tags_array = tags - .iter() - .map(|(k, v)| format!("{}:{}", k, v)) - .collect::>() - .sort(); - println!("ASTUYVE arr is {:?}", sorted_tags_array); let expected = HashMap::from([ ( "http.url".to_string(), - "x02yirxc7a.execute-api.sa-east-1.amazonaws.com".to_string(), - ), - ( - "http_url_details.path".to_string(), - "/httpapi/get".to_string(), + "https://9vj54we5ih.execute-api.sa-east-1.amazonaws.com/user/42".to_string(), ), + ("http.url_details.path".to_string(), "/user/42".to_string()), ("http.method".to_string(), "GET".to_string()), - ("http.route".to_string(), "GET /httpapi/get".to_string()), - ("http.user_agent".to_string(), "curl/7.64.1".to_string()), - ("http.referer".to_string(), "".to_string()), + ("http.route".to_string(), "/user/{id}".to_string()), + ("http.user_agent".to_string(), "curl/8.1.2".to_string()), ]); - let expected_sorted_array = expected - .iter() - .map(|(k, v)| format!("{}:{}", k, v)) - .collect::>() - .sort(); - - assert_eq!(sorted_tags_array, expected_sorted_array); + assert_eq!(tags, expected); } #[test] fn test_get_arn() { 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 530d73a51..a568bc08d 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs @@ -86,10 +86,7 @@ impl Trigger for APIGatewayRestEvent { span.r#type = "http".to_string(); span.start = start_time; span.meta.extend(HashMap::from([ - ( - "endpoint".to_string(), - self.request_context.path.clone(), - ), + ("endpoint".to_string(), self.request_context.path.clone()), ("http.url".to_string(), http_url), ( "http.method".to_string(), @@ -113,7 +110,10 @@ impl Trigger for APIGatewayRestEvent { self.request_context.request_id.clone(), ), ("resource_names".to_string(), resource.clone()), - ("http.route".to_string(), resource), + ( + "http.route".to_string(), + self.request_context.resource_path.clone(), + ), ])); debug!("Enriched Span: {:?}", span); @@ -124,10 +124,14 @@ impl Trigger for APIGatewayRestEvent { let mut tags = HashMap::from([ ( "http.url".to_string(), - self.request_context.domain_name.clone(), + format!( + "https://{domain_name}{path}", + domain_name = self.request_context.domain_name, + path = self.request_context.path + ), ), ( - "http_url_details.path".to_string(), + "http.url_details.path".to_string(), self.request_context.path.clone(), ), ( @@ -136,15 +140,11 @@ impl Trigger for APIGatewayRestEvent { ), ( "http.route".to_string(), - format!( - "{method} {resource}", - method = self.request_context.method.clone(), - resource = self.request_context.resource_path.clone() - ), + self.request_context.resource_path.clone(), ), ( "http.user_agent".to_string(), - self.request_context.identity.user_agent.to_string() + self.request_context.identity.user_agent.to_string(), ), ]); @@ -236,14 +236,13 @@ mod tests { let mut span = Span::default(); event.enrich_span(&mut span); assert_eq!(span.name, "aws.apigateway"); - assert_eq!( - span.service, - "id.execute-api.us-east-1.amazonaws.com" - ); + assert_eq!(span.service, "id.execute-api.us-east-1.amazonaws.com"); assert_eq!(span.resource, "GET /path"); assert_eq!(span.r#type, "http"); - assert_eq!(span.meta, HashMap::from([ + assert_eq!( + span.meta, + HashMap::from([ ("endpoint".to_string(), "/my/path".to_string()), ( "http.url".to_string(), @@ -253,11 +252,12 @@ mod tests { ("http.protocol".to_string(), "HTTP/1.1".to_string()), ("http.source_ip".to_string(), "IP".to_string()), ("http.user_agent".to_string(), "user-agent".to_string()), - ("http.route".to_string(), "GET /path".to_string()), + ("http.route".to_string(), "/path".to_string()), ("operation_name".to_string(), "aws.apigateway".to_string()), ("request_id".to_string(), "id=".to_string()), ("resource_names".to_string(), "GET /path".to_string()), - ])); + ]) + ); } #[test] @@ -267,33 +267,19 @@ mod tests { let event = APIGatewayRestEvent::new(payload).expect("Failed to deserialize APIGatewayRestEvent"); let tags = event.get_tags(); - let sorted_tags_array = tags - .iter() - .map(|(k, v)| format!("{}:{}", k, v)) - .collect::>() - .sort(); let expected = HashMap::from([ ( "http.url".to_string(), - "x02yirxc7a.execute-api.sa-east-1.amazonaws.com".to_string(), - ), - ( - "http_url_details.path".to_string(), - "/httpapi/get".to_string(), + "https://id.execute-api.us-east-1.amazonaws.com/my/path".to_string(), ), + ("http.url_details.path".to_string(), "/my/path".to_string()), ("http.method".to_string(), "GET".to_string()), - ("http.route".to_string(), "GET /httpapi/get".to_string()), - ("http.user_agent".to_string(), "curl/7.64.1".to_string()), - ("http.referer".to_string(), "".to_string()), + ("http.route".to_string(), "/path".to_string()), + ("http.user_agent".to_string(), "user-agent".to_string()), ]); - let expected_sorted_array = expected - .iter() - .map(|(k, v)| format!("{}:{}", k, v)) - .collect::>() - .sort(); - assert_eq!(sorted_tags_array, expected_sorted_array); + assert_eq!(tags, expected); } #[test] @@ -311,32 +297,25 @@ mod tests { ); assert_eq!(span.resource, "GET /user/{id}"); assert_eq!(span.r#type, "http"); - let sorted_span_meta = span.meta.iter() - .map(|(k, v)| format!("{}:{}", k, v)) - .collect::>() - .sort(); let expected = HashMap::from([ - ("endpoint".to_string(), "/dev/user/42".to_string()), - ( - "http.url".to_string(), - "mcwkra0ya4.execute-api.sa-east-1.amazonaws.com".to_string() - ), - ("http.method".to_string(), "GET".to_string()), - ("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.api_gateway".to_string()), - ("request_id".to_string(), "mcwkra0ya4".to_string()), - ("resource_names".to_string(), "GET /dev/user/{id}".to_string()), - ]); - let sorted_expected = expected.iter() - .map(|(k, v)| format!("{}:{}", k, v)) - .collect::>() - .sort(); - assert_eq!( - sorted_span_meta, - sorted_expected - ); + ("endpoint".to_string(), "/dev/user/42".to_string()), + ( + "http.url".to_string(), + "https://mcwkra0ya4.execute-api.sa-east-1.amazonaws.com/dev/user/42".to_string(), + ), + ("http.method".to_string(), "GET".to_string()), + ("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()), + ("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(), + ), + ("resource_names".to_string(), "GET /user/{id}".to_string()), + ]); + assert_eq!(span.meta, expected); } #[test] @@ -347,19 +326,23 @@ mod tests { APIGatewayRestEvent::new(payload).expect("Failed to deserialize APIGatewayRestEvent"); let tags = event.get_tags(); - assert_eq!(tags, HashMap::from([ - ( - "http.url".to_string(), - "mcwkra0ya4.execute-api.sa-east-1.amazonaws.com".to_string(), - ), - ( - "http_url_details.path".to_string(), - "/dev/user/42".to_string(), - ), - ("http.method".to_string(), "GET".to_string()), - ("http.route".to_string(), "GET /user/{id}".to_string()), - ("http.user_agent".to_string(), "curl/8.1.2".to_string()), - ])); + assert_eq!( + tags, + HashMap::from([ + ( + "http.url".to_string(), + "https://mcwkra0ya4.execute-api.sa-east-1.amazonaws.com/dev/user/42" + .to_string(), + ), + ( + "http.url_details.path".to_string(), + "/dev/user/42".to_string(), + ), + ("http.method".to_string(), "GET".to_string()), + ("http.route".to_string(), "/user/{id}".to_string()), + ("http.user_agent".to_string(), "curl/8.1.2".to_string()), + ]) + ); } #[test] diff --git a/bottlecap/src/lifecycle/listener.rs b/bottlecap/src/lifecycle/listener.rs index eabfb0a1e..4b11717f1 100644 --- a/bottlecap/src/lifecycle/listener.rs +++ b/bottlecap/src/lifecycle/listener.rs @@ -117,8 +117,9 @@ impl Listener { ) -> http::Result> { debug!("Received end invocation request"); let (parts, body) = req.into_parts(); - let parsed_body = serde_json::from_slice::(&hyper::body::to_bytes(body).await.unwrap()); - debug!("Parsed body: {:?}", parsed_body); + let parsed_body = serde_json::from_slice::( + &hyper::body::to_bytes(body).await.unwrap_or_default(), + ); let mut parsed_status: Option = None; if let Some(status_code) = parsed_body.unwrap_or_default().get("statusCode") { parsed_status = Some(status_code.to_string()); From 01d11a0623d0b3d27f50168b29f38aa09b16c867 Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Mon, 21 Oct 2024 09:28:00 -0400 Subject: [PATCH 6/8] fix: lint --- .../invocation/triggers/api_gateway_http_event.rs | 13 ++++++------- .../invocation/triggers/api_gateway_rest_event.rs | 2 +- 2 files changed, 7 insertions(+), 8 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 2bb0a10c4..effc3e3c8 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs @@ -63,16 +63,15 @@ impl Trigger for APIGatewayHttpEvent { #[allow(clippy::cast_possible_truncation)] fn enrich_span(&self, span: &mut Span) { debug!("Enriching an Inferred Span for an API Gateway HTTP Event"); - let resource: String; - if self.route_key.is_empty() { - resource = format!( + let resource = if self.route_key.is_empty() { + format!( "{http_method} {route_key}", http_method = self.request_context.http.method, route_key = self.route_key - ); + ) } else { - resource = self.route_key.clone(); - } + self.route_key.clone() + }; let http_url = format!( "https://{domain_name}{path}", @@ -223,7 +222,7 @@ mod tests { request_id: "FaHnXjKCGjQEJ7A=".to_string(), api_id: "x02yirxc7a".to_string(), domain_name: "x02yirxc7a.execute-api.sa-east-1.amazonaws.com".to_string(), - time_epoch: 1631212283738, + time_epoch: 1_631_212_283_738, http: RequestContextHTTP { method: "GET".to_string(), path: "/httpapi/get".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 a568bc08d..7a737d576 100644 --- a/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs @@ -195,7 +195,7 @@ mod tests { request_id: "id=".to_string(), api_id: "id".to_string(), domain_name: "id.execute-api.us-east-1.amazonaws.com".to_string(), - time_epoch: 1583349317135, + time_epoch: 1_583_349_317_135, method: "GET".to_string(), path: "/my/path".to_string(), protocol: "HTTP/1.1".to_string(), From c053e5da94e92a21a52a86d2ab564722a3a2dbf0 Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Mon, 21 Oct 2024 10:45:45 -0400 Subject: [PATCH 7/8] fix: Remove debugs, consolidate import --- bottlecap/src/lifecycle/invocation/span_inferrer.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/bottlecap/src/lifecycle/invocation/span_inferrer.rs b/bottlecap/src/lifecycle/invocation/span_inferrer.rs index 3c1c7e294..491db1475 100644 --- a/bottlecap/src/lifecycle/invocation/span_inferrer.rs +++ b/bottlecap/src/lifecycle/invocation/span_inferrer.rs @@ -6,11 +6,11 @@ use tracing::debug; use crate::config::AwsConfig; use crate::lifecycle::invocation::triggers::{ - api_gateway_http_event::APIGatewayHttpEvent, Trigger, + api_gateway_http_event::APIGatewayHttpEvent, + api_gateway_rest_event::APIGatewayRestEvent, + Trigger, }; -use super::triggers::api_gateway_rest_event::APIGatewayRestEvent; - const FUNCTION_TRIGGER_EVENT_SOURCE_TAG: &str = "function_trigger.event_source"; const FUNCTION_TRIGGER_EVENT_SOURCE_ARN_TAG: &str = "function_trigger.event_source_arn"; @@ -64,9 +64,7 @@ impl SpanInferrer { self.inferred_span = Some(span); } } else if APIGatewayRestEvent::is_match(&payload_value) { - debug!("MATCH V1 REST EVENT"); if let Some(t) = APIGatewayRestEvent::new(payload_value) { - debug!("ASTUYVE PARSING V1 REST EVENT"); let mut span = Span { span_id: Self::generate_span_id(), ..Default::default() @@ -124,7 +122,6 @@ impl SpanInferrer { } s.trace_id = invocation_span.trace_id; - debug!("Final Span: {:?}", self.inferred_span); } } From 1414ffdb20619a2361bf2d970e123311ea33bc11 Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Mon, 21 Oct 2024 10:51:09 -0400 Subject: [PATCH 8/8] fix: oneline --- bottlecap/src/lifecycle/invocation/span_inferrer.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bottlecap/src/lifecycle/invocation/span_inferrer.rs b/bottlecap/src/lifecycle/invocation/span_inferrer.rs index 491db1475..7b2a0eefc 100644 --- a/bottlecap/src/lifecycle/invocation/span_inferrer.rs +++ b/bottlecap/src/lifecycle/invocation/span_inferrer.rs @@ -6,8 +6,7 @@ use tracing::debug; use crate::config::AwsConfig; use crate::lifecycle::invocation::triggers::{ - api_gateway_http_event::APIGatewayHttpEvent, - api_gateway_rest_event::APIGatewayRestEvent, + api_gateway_http_event::APIGatewayHttpEvent, api_gateway_rest_event::APIGatewayRestEvent, Trigger, };