diff --git a/bottlecap/src/lifecycle/invocation/processor.rs b/bottlecap/src/lifecycle/invocation/processor.rs index 33f94987a..9b9cc98ef 100644 --- a/bottlecap/src/lifecycle/invocation/processor.rs +++ b/bottlecap/src/lifecycle/invocation/processor.rs @@ -183,12 +183,21 @@ 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..7b2a0eefc 100644 --- a/bottlecap/src/lifecycle/invocation/span_inferrer.rs +++ b/bottlecap/src/lifecycle/invocation/span_inferrer.rs @@ -1,12 +1,13 @@ use datadog_trace_protobuf::pb::Span; -use log::debug; use rand::Rng; use serde_json::Value; +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, }; const FUNCTION_TRIGGER_EVENT_SOURCE_TAG: &str = "function_trigger.event_source"; @@ -58,6 +59,28 @@ impl SpanInferrer { ), ]); + self.is_async_span = t.is_async(); + self.inferred_span = Some(span); + } + } else if APIGatewayRestEvent::is_match(&payload_value) { + if let Some(t) = APIGatewayRestEvent::new(payload_value) { + 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); } @@ -78,6 +101,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 { 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..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,13 +63,18 @@ 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 = if self.route_key.is_empty() { + format!( + "{http_method} {route_key}", + http_method = self.request_context.http.method, + route_key = self.route_key + ) + } else { + 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 ); @@ -119,10 +124,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(), ), ( @@ -130,9 +141,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") { @@ -202,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(), @@ -254,7 +274,8 @@ 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()), @@ -274,35 +295,77 @@ 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] + 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 expected = HashMap::from([ + ( + "http.url".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(), "/user/{id}".to_string()), + ("http.user_agent".to_string(), "curl/8.1.2".to_string()), + ]); + assert_eq!(tags, expected); + } #[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 new file mode 100644 index 000000000..7a737d576 --- /dev/null +++ b/bottlecap/src/lifecycle/invocation/triggers/api_gateway_rest_event.rs @@ -0,0 +1,359 @@ +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 resource_path: String, + 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"); + 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.resource_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.clone()), + ( + "http.route".to_string(), + self.request_context.resource_path.clone(), + ), + ])); + + debug!("Enriched Span: {:?}", span); + // todo: update global(? IsAsync if event payload is `Event` + } + + fn get_tags(&self) -> HashMap { + let mut tags = HashMap::from([ + ( + "http.url".to_string(), + format!( + "https://{domain_name}{path}", + domain_name = self.request_context.domain_name, + path = self.request_context.path + ), + ), + ( + "http.url_details.path".to_string(), + self.request_context.path.clone(), + ), + ( + "http.method".to_string(), + self.request_context.method.clone(), + ), + ( + "http.route".to_string(), + 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()); + } + + 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_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([ + ("Header1".to_string(), "value1".to_string()), + ("Header2".to_string(), "value2".to_string()), + ]), + request_context: RequestContext { + stage: "$default".to_string(), + request_id: "id=".to_string(), + api_id: "id".to_string(), + domain_name: "id.execute-api.us-east-1.amazonaws.com".to_string(), + time_epoch: 1_583_349_317_135, + method: "GET".to_string(), + path: "/my/path".to_string(), + protocol: "HTTP/1.1".to_string(), + resource_path: "/path".to_string(), + identity: Identity { + source_ip: "IP".to_string(), + user_agent: "user-agent".to_string(), + }, + }, + }; + + assert_eq!(result, expected); + } + + #[test] + fn test_is_match() { + let json = read_json_file("api_gateway_rest_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_http_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_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"); + 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.resource, "GET /path"); + assert_eq!(span.r#type, "http"); + + 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/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()), + ("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] + fn test_get_tags() { + 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"); + let tags = event.get_tags(); + + let expected = HashMap::from([ + ( + "http.url".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(), "/path".to_string()), + ("http.user_agent".to_string(), "user-agent".to_string()), + ]); + + assert_eq!(tags, expected); + } + + #[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 expected = HashMap::from([ + ("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] + 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(); + + 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] + fn test_get_arn() { + 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("us-east-1"), + "arn:aws:apigateway:us-east-1::/restapis/id/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..4b11717f1 100644 --- a/bottlecap/src/lifecycle/listener.rs +++ b/bottlecap/src/lifecycle/listener.rs @@ -116,7 +116,14 @@ 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_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()); + } let headers = parts.headers; let mut processor = invocation_processor.lock().await; @@ -142,7 +149,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() 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 +}