-
Notifications
You must be signed in to change notification settings - Fork 20
feat(bottlecap): add EventBridge inferred spans #436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
alexgallotta
merged 16 commits into
jordan.gonzalez/bottlecap/universal-instrumentation
from
alexgallotta/event-bridge
Nov 11, 2024
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
0579339
add eventbridge event
alexgallotta 4c811e8
fix test path
alexgallotta c8be100
add comments with code ref and fix metadata api-gateway
alexgallotta 13d5b50
fix error message
alexgallotta 3fd3b7c
clean import
alexgallotta 375a216
make build faster using host network
alexgallotta 4d88fbd
fix conflicts and tests
alexgallotta 5e6992b
fix test conflicts
alexgallotta 13947e9
Merge branch 'jordan.gonzalez/bottlecap/universal-instrumentation' in…
duncanista 6ab1450
resolve merge conflicts
duncanista 129590d
minor changes
duncanista e95ed81
add missing unit test
duncanista a65df82
update events for testing
duncanista bb3b6e8
account for millisecond resolution and resource name
duncanista f53aca8
fix unit tests
duncanista 03c4199
remove `network` tag for runners
duncanista File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
242 changes: 242 additions & 0 deletions
242
bottlecap/src/lifecycle/invocation/triggers/event_bridge_event.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,242 @@ | ||
| use chrono::{DateTime, Utc}; | ||
| 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, S_TO_NS}, | ||
| triggers::{Trigger, DATADOG_CARRIER_KEY, FUNCTION_TRIGGER_EVENT_SOURCE_TAG}, | ||
| }; | ||
|
|
||
| const DATADOG_START_TIME_KEY: &str = "x-datadog-start-time"; | ||
| const DATADOG_RESOURCE_NAME_KEY: &str = "x-datadog-resource-name"; | ||
|
|
||
| #[derive(Serialize, Deserialize, Debug, PartialEq)] | ||
| pub struct EventBridgeEvent { | ||
| pub id: String, | ||
| pub version: String, | ||
| pub account: String, | ||
| pub time: DateTime<Utc>, | ||
| pub region: String, | ||
| pub resources: Vec<String>, | ||
| pub source: String, | ||
| #[serde(rename = "detail-type")] | ||
| pub detail_type: String, | ||
| pub detail: Value, | ||
| #[serde(rename = "replay-name")] | ||
| pub replay_name: Option<String>, | ||
| } | ||
|
|
||
| impl Trigger for EventBridgeEvent { | ||
| fn new(payload: Value) -> Option<Self> { | ||
| match serde_json::from_value(payload) { | ||
| Ok(event) => Some(event), | ||
| Err(e) => { | ||
| debug!("Failed to deserialize EventBridge Event: {}", e); | ||
| None | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn is_match(payload: &Value) -> bool { | ||
| payload.get("detail-type").is_some() | ||
| && payload | ||
| .get("source") | ||
| .and_then(Value::as_str) | ||
| .map_or(false, |s| s != "aws.events") | ||
|
Comment on lines
+45
to
+48
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added this missed check |
||
| } | ||
|
|
||
| #[allow(clippy::cast_possible_truncation)] | ||
| fn enrich_span(&self, span: &mut Span) { | ||
| // EventBridge events have a timestamp resolution in seconds | ||
| let start_time_seconds = self | ||
| .time | ||
| .timestamp_nanos_opt() | ||
| .unwrap_or((self.time.timestamp_millis() as f64 * S_TO_NS) as i64); | ||
|
|
||
| let carrier = self.get_carrier(); | ||
| let resource_name = carrier | ||
| .get(DATADOG_RESOURCE_NAME_KEY) | ||
| .unwrap_or(&self.source) | ||
| .clone(); | ||
| let start_time = carrier | ||
| .get(DATADOG_START_TIME_KEY) | ||
| .and_then(|s| s.parse::<f64>().ok()) | ||
| .map_or(start_time_seconds, |s| (s * MS_TO_NS) as i64); | ||
|
Comment on lines
+60
to
+67
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added resource name and start time accurate resolution when available |
||
|
|
||
| // todo: service mapping and peer service | ||
| let service_name = "eventbridge"; | ||
|
|
||
| span.name = String::from("aws.eventbridge"); | ||
| span.service = service_name.to_string(); | ||
| span.resource = resource_name; | ||
| span.r#type = String::from("web"); | ||
| span.start = start_time; | ||
| span.meta.extend(HashMap::from([ | ||
| ("operation_name".to_string(), "aws.eventbridge".to_string()), | ||
| ("detail_type".to_string(), self.detail_type.clone()), | ||
| ])); | ||
| } | ||
|
|
||
| fn get_tags(&self) -> HashMap<String, String> { | ||
| HashMap::from([( | ||
| FUNCTION_TRIGGER_EVENT_SOURCE_TAG.to_string(), | ||
| "eventbridge".to_string(), | ||
| )]) | ||
| } | ||
|
|
||
| fn get_arn(&self, _region: &str) -> String { | ||
| self.source.clone() | ||
| } | ||
|
|
||
| fn get_carrier(&self) -> HashMap<String, String> { | ||
| if let Ok(detail) = serde_json::from_value::<HashMap<String, Value>>(self.detail.clone()) { | ||
| if let Some(carrier) = detail.get(DATADOG_CARRIER_KEY) { | ||
| return serde_json::from_value(carrier.clone()).unwrap_or_default(); | ||
| } | ||
| } | ||
| HashMap::new() | ||
| } | ||
|
|
||
| fn is_async(&self) -> bool { | ||
| true | ||
| } | ||
| } | ||
|
|
||
| #[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("eventbridge_event.json"); | ||
| let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); | ||
| let result = | ||
| EventBridgeEvent::new(payload).expect("Failed to deserialize into EventBridgeEvent"); | ||
|
|
||
| let expected = EventBridgeEvent { | ||
| id: "bd3c8258-8d30-007c-2562-64715b2d0ea8".to_string(), | ||
| version: "0".to_string(), | ||
| account: "601427279990".to_string(), | ||
| time: DateTime::parse_from_rfc3339("2024-11-09T08:22:15Z") | ||
| .expect("Failed to parse time") | ||
| .with_timezone(&Utc), | ||
| region: "eu-west-1".to_string(), | ||
| resources: vec![], | ||
| source: "my.event".to_string(), | ||
| detail_type: "UserSignUp".to_string(), | ||
| detail: serde_json::json!({ | ||
| "hello": "there", | ||
| "_datadog": { | ||
| "x-datadog-trace-id": "5827606813695714842", | ||
| "x-datadog-parent-id": "4726693487091824375", | ||
| "x-datadog-sampled": "1", | ||
| "x-datadog-sampling-priority": "1", | ||
| "x-datadog-resource-name": "testBus", | ||
| "x-datadog-start-time": "1731183820135" | ||
| } | ||
| }), | ||
| replay_name: None, | ||
| }; | ||
|
|
||
| assert_eq!(result, expected); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_is_match() { | ||
| let json = read_json_file("eventbridge_event.json"); | ||
| let payload = serde_json::from_str(&json).expect("Failed to deserialize EventBridgeEvent"); | ||
|
|
||
| assert!(EventBridgeEvent::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 EventBridgeEvent"); | ||
| assert!(!EventBridgeEvent::is_match(&payload)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_enrich_span() { | ||
| let json = read_json_file("eventbridge_event.json"); | ||
| let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); | ||
| let event = | ||
| EventBridgeEvent::new(payload).expect("Failed to deserialize into EventBridgeEvent"); | ||
|
|
||
| let mut span = Span::default(); | ||
| event.enrich_span(&mut span); | ||
|
|
||
| let expected = serde_json::from_str(&read_json_file("eventbridge_span.json")) | ||
| .expect("Failed to deserialize into Span"); | ||
| assert_eq!(span, expected); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_enrich_span_no_resource_name() { | ||
| let json = read_json_file("eventbridge_no_resource_name_event.json"); | ||
| let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); | ||
| let event = | ||
| EventBridgeEvent::new(payload).expect("Failed to deserialize into EventBridgeEvent"); | ||
|
|
||
| let mut span = Span::default(); | ||
| event.enrich_span(&mut span); | ||
|
|
||
| assert_eq!(span.resource, "my.event"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_enrich_span_no_timestamp() { | ||
| let json = read_json_file("eventbridge_no_timestamp_event.json"); | ||
| let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); | ||
| let event = | ||
| EventBridgeEvent::new(payload).expect("Failed to deserialize into EventBridgeEvent"); | ||
|
|
||
| let mut span = Span::default(); | ||
| event.enrich_span(&mut span); | ||
|
|
||
| assert_eq!(span.resource, "testBus"); | ||
| // Seconds resolution | ||
| assert_eq!(span.start, 1731140535000000000); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_get_arn() { | ||
| let json = read_json_file("eventbridge_event.json"); | ||
| let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); | ||
| let event = EventBridgeEvent::new(payload).expect("Failed to deserialize EventBridgeEvent"); | ||
| assert_eq!(event.get_arn("us-east-1"), "my.event"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_get_carrier() { | ||
| let json = read_json_file("eventbridge_event.json"); | ||
| let payload = serde_json::from_str(&json).expect("Failed to deserialize into Value"); | ||
| let event = | ||
| EventBridgeEvent::new(payload).expect("Failed to deserialize EventBridge Event"); | ||
| let carrier = event.get_carrier(); | ||
|
|
||
| let expected = HashMap::from([ | ||
| ( | ||
| "x-datadog-trace-id".to_string(), | ||
| "5827606813695714842".to_string(), | ||
| ), | ||
| ( | ||
| "x-datadog-parent-id".to_string(), | ||
| "4726693487091824375".to_string(), | ||
| ), | ||
| ("x-datadog-sampling-priority".to_string(), "1".to_string()), | ||
| ("x-datadog-sampled".to_string(), "1".to_string()), | ||
| ("x-datadog-resource-name".to_string(), "testBus".to_string()), | ||
| ( | ||
| "x-datadog-start-time".to_string(), | ||
| "1731183820135".to_string(), | ||
| ), | ||
| ]); | ||
|
|
||
| assert_eq!(carrier, expected); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| { | ||
| "version": "0", | ||
| "id": "bd3c8258-8d30-007c-2562-64715b2d0ea8", | ||
| "detail-type": "UserSignUp", | ||
| "source": "my.event", | ||
| "account": "601427279990", | ||
| "time": "2024-11-09T08:22:15Z", | ||
| "region": "eu-west-1", | ||
| "resources": [], | ||
| "detail": { | ||
| "hello": "there", | ||
| "_datadog": { | ||
| "x-datadog-trace-id": "5827606813695714842", | ||
| "x-datadog-parent-id": "4726693487091824375", | ||
| "x-datadog-sampled": "1", | ||
| "x-datadog-sampling-priority": "1", | ||
| "x-datadog-resource-name": "testBus", | ||
| "x-datadog-start-time": "1731183820135" | ||
| } | ||
| } | ||
| } |
19 changes: 19 additions & 0 deletions
19
bottlecap/tests/payloads/eventbridge_no_resource_name_event.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "version": "0", | ||
| "id": "bd3c8258-8d30-007c-2562-64715b2d0ea8", | ||
| "detail-type": "UserSignUp", | ||
| "source": "my.event", | ||
| "account": "601427279990", | ||
| "time": "2024-11-09T08:22:15Z", | ||
| "region": "eu-west-1", | ||
| "resources": [], | ||
| "detail": { | ||
| "hello": "there", | ||
| "_datadog": { | ||
| "x-datadog-trace-id": "5827606813695714842", | ||
| "x-datadog-parent-id": "4726693487091824375", | ||
| "x-datadog-sampling-priority": "1", | ||
| "x-datadog-start-time": "1731183820135" | ||
| } | ||
| } | ||
| } |
19 changes: 19 additions & 0 deletions
19
bottlecap/tests/payloads/eventbridge_no_timestamp_event.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "version": "0", | ||
| "id": "bd3c8258-8d30-007c-2562-64715b2d0ea8", | ||
| "detail-type": "UserSignUp", | ||
| "source": "my.event", | ||
| "account": "601427279990", | ||
| "time": "2024-11-09T08:22:15Z", | ||
| "region": "eu-west-1", | ||
| "resources": [], | ||
| "detail": { | ||
| "hello": "there", | ||
| "_datadog": { | ||
| "x-datadog-trace-id": "5827606813695714842", | ||
| "x-datadog-parent-id": "4726693487091824375", | ||
| "x-datadog-sampling-priority": "1", | ||
| "x-datadog-resource-name": "testBus" | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Converted this into
DateTime<Utc>