Skip to content

Commit

Permalink
APIGW v1 + httpAPI/v1 API parameterized routes (#419)
Browse files Browse the repository at this point in the history
* feat: support APIGW v1

* feat: Tests for unparameterized payload working

* feat: parameterized test

* fix: specs

* fix: unwrap_or_default, route has no http verb but is parameterized.

* fix: lint

* fix: Remove debugs, consolidate import

* fix: oneline
  • Loading branch information
astuyve authored Oct 21, 2024
1 parent d6ba035 commit ee42f4f
Show file tree
Hide file tree
Showing 9 changed files with 731 additions and 33 deletions.
11 changes: 10 additions & 1 deletion bottlecap/src/lifecycle/invocation/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
) {
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;
}
Expand Down
34 changes: 32 additions & 2 deletions bottlecap/src/lifecycle/invocation/span_inferrer.rs
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
}
Expand All @@ -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 {
Expand Down
119 changes: 91 additions & 28 deletions bottlecap/src/lifecycle/invocation/triggers/api_gateway_http_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Expand Down Expand Up @@ -119,20 +124,35 @@ 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(),
),
(
"http.method".to_string(),
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") {
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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()),
Expand All @@ -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::<Vec<String>>()
.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::<Vec<String>>()
.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");
Expand Down
Loading

0 comments on commit ee42f4f

Please sign in to comment.