@@ -92,6 +92,17 @@ pub fn extract_trigger_chain_from_json(json_val: &serde_json::Value) -> TriggerC
9292 return extract_kafka_chain ( & json_val) ;
9393 }
9494
95+ // ALB / ELB: target-group events also carry "requestContext", but with an
96+ // "elb" sub-key. Must be checked before the API Gateway branch, which keys
97+ // off "requestContext" alone and would otherwise misclassify ALB as API GW.
98+ if let Some ( elb) = json_val
99+ . get ( "requestContext" )
100+ . and_then ( |rc| rc. get ( "elb" ) )
101+ . filter ( |elb| elb. is_object ( ) )
102+ {
103+ return extract_alb_chain ( elb) ;
104+ }
105+
95106 // API Gateway: has "requestContext"
96107 if json_val. get ( "requestContext" ) . is_some ( ) {
97108 return extract_api_gateway_chain ( & json_val) ;
@@ -412,6 +423,45 @@ fn extract_kafka_chain(json_val: &serde_json::Value) -> TriggerChainResult {
412423 }
413424}
414425
426+ // ── ALB / ELB ─────────────────────────────────────────────────────────
427+
428+ /// Extracts the target group name from an ALB target-group ARN.
429+ ///
430+ /// `arn:aws:elasticloadbalancing:us-east-2:123:targetgroup/my-tg/abc123` -> `my-tg`
431+ ///
432+ /// `extract_name_from_arn` would return the trailing random id, so target groups
433+ /// need their own parse: the name is the segment after `targetgroup/`.
434+ fn extract_target_group_name ( arn : & str ) -> Option < String > {
435+ let parts: Vec < & str > = arn. splitn ( 6 , ':' ) . collect ( ) ;
436+ if parts. len ( ) < 6 {
437+ return None ;
438+ }
439+ let segments: Vec < & str > = parts[ 5 ] . split ( '/' ) . collect ( ) ;
440+ if segments. len ( ) >= 2 && segments[ 0 ] == "targetgroup" {
441+ Some ( segments[ 1 ] . to_string ( ) )
442+ } else {
443+ None
444+ }
445+ }
446+
447+ fn extract_alb_chain ( elb : & serde_json:: Value ) -> TriggerChainResult {
448+ let arn = elb
449+ . get ( "targetGroupArn" )
450+ . and_then ( |v| v. as_str ( ) )
451+ . map ( |s| s. to_string ( ) ) ;
452+ let name = arn. as_deref ( ) . and_then ( extract_target_group_name) ;
453+
454+ TriggerChainResult {
455+ hops : vec ! [ TriggerHop {
456+ trigger_type: "aws:load_balancer" . to_string( ) ,
457+ arn,
458+ name,
459+ timestamp: None ,
460+ } ] ,
461+ truncated : false ,
462+ }
463+ }
464+
415465// ── API Gateway ──────────────────────────────────────────────────────
416466
417467fn extract_api_gateway_chain ( json_val : & serde_json:: Value ) -> TriggerChainResult {
@@ -810,6 +860,57 @@ mod tests {
810860 assert ! ( result. hops[ 0 ] . arn. is_none( ) ) ;
811861 }
812862
863+ // ── ALB / ELB ─────────────────────────────────────────────────────
864+
865+ #[ test]
866+ fn alb_event_classified_as_load_balancer ( ) {
867+ let payload = r#"{
868+ "requestContext": {
869+ "elb": {
870+ "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-target/abcdef0123456789"
871+ }
872+ },
873+ "httpMethod": "GET", "path": "/lambda",
874+ "headers": {"host": "my-alb-1234567890.us-east-2.elb.amazonaws.com"},
875+ "body": ""
876+ }"# ;
877+
878+ let result = extract_trigger_chain ( payload) ;
879+ assert_eq ! ( result. hops. len( ) , 1 ) ;
880+ assert_eq ! ( result. hops[ 0 ] . trigger_type, "aws:load_balancer" ) ;
881+ assert_eq ! (
882+ result. hops[ 0 ] . arn. as_deref( ) ,
883+ Some ( "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-target/abcdef0123456789" )
884+ ) ;
885+ assert_eq ! ( result. hops[ 0 ] . name. as_deref( ) , Some ( "lambda-target" ) ) ;
886+ }
887+
888+ #[ test]
889+ fn alb_event_not_misclassified_as_api_gateway ( ) {
890+ let payload = r#"{
891+ "requestContext": {"elb": {"targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123:targetgroup/tg/abc"}},
892+ "httpMethod": "POST", "path": "/orders"
893+ }"# ;
894+
895+ let result = extract_trigger_chain ( payload) ;
896+ assert_eq ! ( result. hops. len( ) , 1 ) ;
897+ assert_eq ! ( result. hops[ 0 ] . trigger_type, "aws:load_balancer" ) ;
898+ }
899+
900+ #[ test]
901+ fn alb_event_without_target_group_arn_still_load_balancer ( ) {
902+ let payload = r#"{
903+ "requestContext": {"elb": {}},
904+ "httpMethod": "GET", "path": "/lambda"
905+ }"# ;
906+
907+ let result = extract_trigger_chain ( payload) ;
908+ assert_eq ! ( result. hops. len( ) , 1 ) ;
909+ assert_eq ! ( result. hops[ 0 ] . trigger_type, "aws:load_balancer" ) ;
910+ assert ! ( result. hops[ 0 ] . arn. is_none( ) ) ;
911+ assert ! ( result. hops[ 0 ] . name. is_none( ) ) ;
912+ }
913+
813914 // ── Edge cases ───────────────────────────────────────────────────
814915
815916 #[ test]
0 commit comments