diff --git a/instrumentation/aws-lambda-java-events-3.11/src/main/java/com/nr/instrumentation/lambda/LambdaConstants.java b/instrumentation/aws-lambda-java-events-3.11/src/main/java/com/nr/instrumentation/lambda/LambdaConstants.java index 823db9954a..f2a615842b 100644 --- a/instrumentation/aws-lambda-java-events-3.11/src/main/java/com/nr/instrumentation/lambda/LambdaConstants.java +++ b/instrumentation/aws-lambda-java-events-3.11/src/main/java/com/nr/instrumentation/lambda/LambdaConstants.java @@ -28,6 +28,30 @@ public class LambdaConstants { public static final String EVENT_TYPE_API_GATEWAY = "apiGateway"; public static final String EVENT_TYPE_CLOUDFRONT = "cloudFront"; + // Event-specific metadata attribute constants + public static final String EVENT_SOURCE_ACCOUNT = "aws.lambda.eventSource.account"; + public static final String EVENT_SOURCE_ACCOUNT_ID = "aws.lambda.eventSource.accountId"; + public static final String EVENT_SOURCE_API_ID = "aws.lambda.eventSource.apiId"; + public static final String EVENT_SOURCE_BUCKET_NAME = "aws.lambda.eventSource.bucketName"; + public static final String EVENT_SOURCE_EVENT_NAME = "aws.lambda.eventSource.eventName"; + public static final String EVENT_SOURCE_EVENT_TIME = "aws.lambda.eventSource.eventTime"; + public static final String EVENT_SOURCE_ID = "aws.lambda.eventSource.id"; + public static final String EVENT_SOURCE_LENGTH = "aws.lambda.eventSource.length"; + public static final String EVENT_SOURCE_MESSAGE_ID = "aws.lambda.eventSource.messageId"; + public static final String EVENT_SOURCE_OBJECT_KEY = "aws.lambda.eventSource.objectKey"; + public static final String EVENT_SOURCE_OBJECT_SEQUENCER = "aws.lambda.eventSource.objectSequencer"; + public static final String EVENT_SOURCE_OBJECT_SIZE = "aws.lambda.eventSource.objectSize"; + public static final String EVENT_SOURCE_REGION = "aws.lambda.eventSource.region"; + public static final String EVENT_SOURCE_RESOURCE = "aws.lambda.eventSource.resource"; + public static final String EVENT_SOURCE_RESOURCE_ID = "aws.lambda.eventSource.resourceId"; + public static final String EVENT_SOURCE_RESOURCE_PATH = "aws.lambda.eventSource.resourcePath"; + public static final String EVENT_SOURCE_STAGE = "aws.lambda.eventSource.stage"; + public static final String EVENT_SOURCE_TIME = "aws.lambda.eventSource.time"; + public static final String EVENT_SOURCE_TIMESTAMP = "aws.lambda.eventSource.timestamp"; + public static final String EVENT_SOURCE_TOPIC_ARN = "aws.lambda.eventSource.topicArn"; + public static final String EVENT_SOURCE_TYPE = "aws.lambda.eventSource.type"; + public static final String EVENT_SOURCE_X_AMZ_ID_2 = "aws.lambda.eventSource.xAmzId2"; + private LambdaConstants() { // Prevent instantiation } diff --git a/instrumentation/aws-lambda-java-events-3.11/src/main/java/com/nr/instrumentation/lambda/LambdaInstrumentationHelper.java b/instrumentation/aws-lambda-java-events-3.11/src/main/java/com/nr/instrumentation/lambda/LambdaInstrumentationHelper.java index 8faf677816..e8e723366b 100644 --- a/instrumentation/aws-lambda-java-events-3.11/src/main/java/com/nr/instrumentation/lambda/LambdaInstrumentationHelper.java +++ b/instrumentation/aws-lambda-java-events-3.11/src/main/java/com/nr/instrumentation/lambda/LambdaInstrumentationHelper.java @@ -24,13 +24,38 @@ import com.newrelic.agent.bridge.Transaction; import com.newrelic.api.agent.NewRelic; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; import java.util.logging.Level; import static com.nr.instrumentation.lambda.LambdaConstants.AWS_REQUEST_ID_ATTRIBUTE; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_ACCOUNT; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_ACCOUNT_ID; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_API_ID; import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_ARN_ATTRIBUTE; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_BUCKET_NAME; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_EVENT_NAME; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_EVENT_TIME; import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_EVENT_TYPE_ATTRIBUTE; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_ID; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_LENGTH; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_MESSAGE_ID; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_OBJECT_KEY; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_OBJECT_SEQUENCER; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_OBJECT_SIZE; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_REGION; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_RESOURCE; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_RESOURCE_ID; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_RESOURCE_PATH; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_STAGE; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_TIME; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_TIMESTAMP; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_TOPIC_ARN; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_TYPE; +import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_SOURCE_X_AMZ_ID_2; import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_TYPE_ALB; import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_TYPE_API_GATEWAY; import static com.nr.instrumentation.lambda.LambdaConstants.EVENT_TYPE_CLOUDFRONT; @@ -54,6 +79,24 @@ public class LambdaInstrumentationHelper { // Track whether this is the first invocation (cold start) private static final AtomicBoolean COLD_START = new AtomicBoolean(true); + // Map of event types to their extraction handlers + private static final Map, BiConsumer> EVENT_EXTRACTORS = new HashMap<>(); + + static { + EVENT_EXTRACTORS.put(S3Event.class, (event, txn) -> extractS3Metadata((S3Event) event, txn)); + EVENT_EXTRACTORS.put(SNSEvent.class, (event, txn) -> extractSNSMetadata((SNSEvent) event, txn)); + EVENT_EXTRACTORS.put(SQSEvent.class, (event, txn) -> extractSQSMetadata((SQSEvent) event, txn)); + EVENT_EXTRACTORS.put(DynamodbEvent.class, (event, txn) -> extractDynamodbMetadata((DynamodbEvent) event, txn)); + EVENT_EXTRACTORS.put(KinesisEvent.class, (event, txn) -> extractKinesisMetadata((KinesisEvent) event, txn)); + EVENT_EXTRACTORS.put(KinesisFirehoseEvent.class, (event, txn) -> extractKinesisFirehoseMetadata((KinesisFirehoseEvent) event, txn)); + EVENT_EXTRACTORS.put(CodeCommitEvent.class, (event, txn) -> extractCodeCommitMetadata((CodeCommitEvent) event, txn)); + EVENT_EXTRACTORS.put(ScheduledEvent.class, (event, txn) -> extractScheduledEventMetadata((ScheduledEvent) event, txn)); + EVENT_EXTRACTORS.put(ApplicationLoadBalancerRequestEvent.class, (event, txn) -> extractALBMetadata((ApplicationLoadBalancerRequestEvent) event, txn)); + EVENT_EXTRACTORS.put(APIGatewayProxyRequestEvent.class, (event, txn) -> extractAPIGatewayProxyMetadata((APIGatewayProxyRequestEvent) event, txn)); + EVENT_EXTRACTORS.put(APIGatewayV2HTTPEvent.class, (event, txn) -> extractAPIGatewayV2HTTPMetadata((APIGatewayV2HTTPEvent) event, txn)); + EVENT_EXTRACTORS.put(CloudFrontEvent.class, (event, txn) -> extractCloudFrontMetadata((CloudFrontEvent) event, txn)); + } + /** * Captures Lambda metadata and stores it via AgentBridge for the serverless payload. * @@ -163,8 +206,7 @@ private static void handleColdStart(Transaction transaction) { /** * Extracts event source metadata (ARN and event type) from various AWS Lambda event types. - * Each event type is handled independently with its own error handling. - * Adds the aws.lambda.eventSource.arn and aws.lambda.eventSource.eventType attributes when successfully extracted. + * Uses a map-based dispatcher to delegate to specific extraction methods for each event type. * * @param event The Lambda event object (can be any supported event type) * @param transaction The current transaction @@ -174,169 +216,318 @@ public static void extractEventSourceMetadata(Object event, Transaction transact return; } - String arn = null; - String eventType = null; + BiConsumer extractor = EVENT_EXTRACTORS.get(event.getClass()); + if (extractor != null) { + extractor.accept(event, transaction); + } + } - // S3Event + /** + * Helper method to safely add a string attribute to the transaction. + * + * @param transaction The current transaction + * @param key The attribute key + * @param value The attribute value + */ + private static void addAttribute(Transaction transaction, String key, String value) { + if (value != null && !value.isEmpty()) { + try { + transaction.getAgentAttributes().put(key, value); + } catch (Throwable t) { + NewRelic.getAgent().getLogger().log(Level.FINE, t, "Error adding attribute: " + key); + } + } + } + + /** + * Helper method to safely add an integer attribute to the transaction. + * + * @param transaction The current transaction + * @param key The attribute key + * @param value The attribute value + */ + private static void addAttribute(Transaction transaction, String key, Integer value) { + if (value != null) { + try { + transaction.getAgentAttributes().put(key, value); + } catch (Throwable t) { + NewRelic.getAgent().getLogger().log(Level.FINE, t, "Error adding attribute: " + key); + } + } + } + + /** + * Helper method to safely add a long attribute to the transaction. + * + * @param transaction The current transaction + * @param key The attribute key + * @param value The attribute value + */ + private static void addAttribute(Transaction transaction, String key, Long value) { + if (value != null) { + try { + transaction.getAgentAttributes().put(key, value); + } catch (Throwable t) { + NewRelic.getAgent().getLogger().log(Level.FINE, t, "Error adding attribute: " + key); + } + } + } + + /** + * Extracts metadata from S3Event. + * Adds event source ARN, event type, and S3-specific attributes. + */ + private static void extractS3Metadata(S3Event event, Transaction transaction) { try { - if (event instanceof S3Event) { - S3Event s3Event = (S3Event) event; - if (s3Event.getRecords() != null && !s3Event.getRecords().isEmpty()) { - arn = s3Event.getRecords().get(0).getS3().getBucket().getArn(); + if (event.getRecords() != null && !event.getRecords().isEmpty()) { + S3Event.S3EventNotificationRecord firstRecord = event.getRecords().get(0); + + // ARN and event type + if (firstRecord.getS3() != null && firstRecord.getS3().getBucket() != null) { + addAttribute(transaction, EVENT_SOURCE_ARN_ATTRIBUTE, firstRecord.getS3().getBucket().getArn()); + } + addAttribute(transaction, EVENT_SOURCE_EVENT_TYPE_ATTRIBUTE, EVENT_TYPE_S3); + + // S3-specific attributes + addAttribute(transaction, EVENT_SOURCE_LENGTH, event.getRecords().size()); + addAttribute(transaction, EVENT_SOURCE_REGION, firstRecord.getAwsRegion()); + addAttribute(transaction, EVENT_SOURCE_EVENT_NAME, firstRecord.getEventName()); + addAttribute(transaction, EVENT_SOURCE_EVENT_TIME, firstRecord.getEventTime() != null ? firstRecord.getEventTime().toString() : null); + + if (firstRecord.getResponseElements() != null) { + addAttribute(transaction, EVENT_SOURCE_X_AMZ_ID_2, firstRecord.getResponseElements().getxAmzId2()); + } + + if (firstRecord.getS3() != null) { + if (firstRecord.getS3().getBucket() != null) { + addAttribute(transaction, EVENT_SOURCE_BUCKET_NAME, firstRecord.getS3().getBucket().getName()); + } + if (firstRecord.getS3().getObject() != null) { + addAttribute(transaction, EVENT_SOURCE_OBJECT_KEY, firstRecord.getS3().getObject().getKey()); + addAttribute(transaction, EVENT_SOURCE_OBJECT_SEQUENCER, firstRecord.getS3().getObject().getSequencer()); + addAttribute(transaction, EVENT_SOURCE_OBJECT_SIZE, firstRecord.getS3().getObject().getSizeAsLong()); + } } - eventType = EVENT_TYPE_S3; } } catch (Throwable t) { NewRelic.getAgent().getLogger().log(Level.FINE, t, "Error extracting metadata from S3Event"); } + } - // SNSEvent + /** + * Extracts metadata from SNSEvent. + * Adds event source ARN, event type, and SNS-specific attributes. + */ + private static void extractSNSMetadata(SNSEvent event, Transaction transaction) { try { - if (event instanceof SNSEvent) { - SNSEvent snsEvent = (SNSEvent) event; - if (snsEvent.getRecords() != null && !snsEvent.getRecords().isEmpty()) { - arn = snsEvent.getRecords().get(0).getEventSubscriptionArn(); + if (event.getRecords() != null && !event.getRecords().isEmpty()) { + SNSEvent.SNSRecord firstRecord = event.getRecords().get(0); + + // ARN and event type + addAttribute(transaction, EVENT_SOURCE_ARN_ATTRIBUTE, firstRecord.getEventSubscriptionArn()); + addAttribute(transaction, EVENT_SOURCE_EVENT_TYPE_ATTRIBUTE, EVENT_TYPE_SNS); + + // SNS-specific attributes + addAttribute(transaction, EVENT_SOURCE_LENGTH, event.getRecords().size()); + if (firstRecord.getSNS() != null) { + addAttribute(transaction, EVENT_SOURCE_MESSAGE_ID, firstRecord.getSNS().getMessageId()); + addAttribute(transaction, EVENT_SOURCE_TIMESTAMP, firstRecord.getSNS().getTimestamp() != null ? firstRecord.getSNS().getTimestamp().toString() : null); + addAttribute(transaction, EVENT_SOURCE_TOPIC_ARN, firstRecord.getSNS().getTopicArn()); + addAttribute(transaction, EVENT_SOURCE_TYPE, firstRecord.getSNS().getType()); } - eventType = EVENT_TYPE_SNS; } } catch (Throwable t) { NewRelic.getAgent().getLogger().log(Level.FINE, t, "Error extracting metadata from SNSEvent"); } + } - // SQSEvent + /** + * Extracts metadata from SQSEvent. + * Adds event source ARN, event type, and SQS-specific attributes. + */ + private static void extractSQSMetadata(SQSEvent event, Transaction transaction) { try { - if (event instanceof SQSEvent) { - SQSEvent sqsEvent = (SQSEvent) event; - if (sqsEvent.getRecords() != null && !sqsEvent.getRecords().isEmpty()) { - arn = sqsEvent.getRecords().get(0).getEventSourceArn(); - } - eventType = EVENT_TYPE_SQS; + if (event.getRecords() != null && !event.getRecords().isEmpty()) { + // ARN and event type + addAttribute(transaction, EVENT_SOURCE_ARN_ATTRIBUTE, event.getRecords().get(0).getEventSourceArn()); + addAttribute(transaction, EVENT_SOURCE_EVENT_TYPE_ATTRIBUTE, EVENT_TYPE_SQS); + + // SQS-specific attributes + addAttribute(transaction, EVENT_SOURCE_LENGTH, event.getRecords().size()); } } catch (Throwable t) { NewRelic.getAgent().getLogger().log(Level.FINE, t, "Error extracting metadata from SQSEvent"); } + } - // DynamodbEvent + /** + * Extracts metadata from DynamodbEvent. + * Adds event source ARN and event type. + */ + private static void extractDynamodbMetadata(DynamodbEvent event, Transaction transaction) { try { - if (event instanceof DynamodbEvent) { - DynamodbEvent dynamodbEvent = (DynamodbEvent) event; - if (dynamodbEvent.getRecords() != null && !dynamodbEvent.getRecords().isEmpty()) { - arn = dynamodbEvent.getRecords().get(0).getEventSourceARN(); - } - eventType = EVENT_TYPE_DYNAMO_STREAMS; + if (event.getRecords() != null && !event.getRecords().isEmpty()) { + // ARN and event type + addAttribute(transaction, EVENT_SOURCE_ARN_ATTRIBUTE, event.getRecords().get(0).getEventSourceARN()); + addAttribute(transaction, EVENT_SOURCE_EVENT_TYPE_ATTRIBUTE, EVENT_TYPE_DYNAMO_STREAMS); } } catch (Throwable t) { NewRelic.getAgent().getLogger().log(Level.FINE, t, "Error extracting metadata from DynamodbEvent"); } + } - // KinesisEvent + /** + * Extracts metadata from KinesisEvent. + * Adds event source ARN, event type, and Kinesis-specific attributes. + */ + private static void extractKinesisMetadata(KinesisEvent event, Transaction transaction) { try { - if (event instanceof KinesisEvent) { - KinesisEvent kinesisEvent = (KinesisEvent) event; - if (kinesisEvent.getRecords() != null && !kinesisEvent.getRecords().isEmpty()) { - arn = kinesisEvent.getRecords().get(0).getEventSourceARN(); - } - eventType = EVENT_TYPE_KINESIS; + if (event.getRecords() != null && !event.getRecords().isEmpty()) { + KinesisEvent.KinesisEventRecord firstRecord = event.getRecords().get(0); + + // ARN and event type + addAttribute(transaction, EVENT_SOURCE_ARN_ATTRIBUTE, firstRecord.getEventSourceARN()); + addAttribute(transaction, EVENT_SOURCE_EVENT_TYPE_ATTRIBUTE, EVENT_TYPE_KINESIS); + + // Kinesis-specific attributes + addAttribute(transaction, EVENT_SOURCE_LENGTH, event.getRecords().size()); + addAttribute(transaction, EVENT_SOURCE_REGION, firstRecord.getAwsRegion()); } } catch (Throwable t) { NewRelic.getAgent().getLogger().log(Level.FINE, t, "Error extracting metadata from KinesisEvent"); } + } - // KinesisFirehoseEvent + /** + * Extracts metadata from KinesisFirehoseEvent. + * Adds event source ARN, event type, and Kinesis Firehose-specific attributes. + */ + private static void extractKinesisFirehoseMetadata(KinesisFirehoseEvent event, Transaction transaction) { try { - if (event instanceof KinesisFirehoseEvent) { - KinesisFirehoseEvent firehoseEvent = (KinesisFirehoseEvent) event; - arn = firehoseEvent.getDeliveryStreamArn(); - eventType = EVENT_TYPE_FIREHOSE; + // ARN and event type + addAttribute(transaction, EVENT_SOURCE_ARN_ATTRIBUTE, event.getDeliveryStreamArn()); + addAttribute(transaction, EVENT_SOURCE_EVENT_TYPE_ATTRIBUTE, EVENT_TYPE_FIREHOSE); + + // Kinesis Firehose-specific attributes + if (event.getRecords() != null) { + addAttribute(transaction, EVENT_SOURCE_LENGTH, event.getRecords().size()); } + addAttribute(transaction, EVENT_SOURCE_REGION, event.getRegion()); } catch (Throwable t) { NewRelic.getAgent().getLogger().log(Level.FINE, t, "Error extracting metadata from KinesisFirehoseEvent"); } + } - // CodeCommitEvent - // Note: CodeCommit is not in the spec, so we extract ARN but omit eventType + /** + * Extracts metadata from CodeCommitEvent. + * Note: CodeCommit is not in the spec, so we only extract ARN without event type. + */ + private static void extractCodeCommitMetadata(CodeCommitEvent event, Transaction transaction) { try { - if (event instanceof CodeCommitEvent) { - CodeCommitEvent codeCommitEvent = (CodeCommitEvent) event; - if (codeCommitEvent.getRecords() != null && !codeCommitEvent.getRecords().isEmpty()) { - arn = codeCommitEvent.getRecords().get(0).getEventSourceArn(); - } + if (event.getRecords() != null && !event.getRecords().isEmpty()) { + // ARN only (no event type in spec) + addAttribute(transaction, EVENT_SOURCE_ARN_ATTRIBUTE, event.getRecords().get(0).getEventSourceArn()); } } catch (Throwable t) { NewRelic.getAgent().getLogger().log(Level.FINE, t, "Error extracting metadata from CodeCommitEvent"); } + } - // ScheduledEvent + /** + * Extracts metadata from ScheduledEvent (CloudWatch Scheduled). + * Adds event source ARN, event type, and CloudWatch Scheduled-specific attributes. + */ + private static void extractScheduledEventMetadata(ScheduledEvent event, Transaction transaction) { try { - if (event instanceof ScheduledEvent) { - ScheduledEvent scheduledEvent = (ScheduledEvent) event; - List resources = scheduledEvent.getResources(); - if (resources != null && !resources.isEmpty()) { - arn = resources.get(0); - } - eventType = EVENT_TYPE_CLOUDWATCH_SCHEDULED; + // ARN and event type + List resources = event.getResources(); + if (resources != null && !resources.isEmpty()) { + String arn = resources.get(0); + addAttribute(transaction, EVENT_SOURCE_ARN_ATTRIBUTE, arn); + addAttribute(transaction, EVENT_SOURCE_RESOURCE, arn); } + addAttribute(transaction, EVENT_SOURCE_EVENT_TYPE_ATTRIBUTE, EVENT_TYPE_CLOUDWATCH_SCHEDULED); + + // CloudWatch Scheduled-specific attributes + addAttribute(transaction, EVENT_SOURCE_ACCOUNT, event.getAccount()); + addAttribute(transaction, EVENT_SOURCE_ID, event.getId()); + addAttribute(transaction, EVENT_SOURCE_REGION, event.getRegion()); + addAttribute(transaction, EVENT_SOURCE_TIME, event.getTime() != null ? event.getTime().toString() : null); } catch (Throwable t) { NewRelic.getAgent().getLogger().log(Level.FINE, t, "Error extracting metadata from ScheduledEvent"); } + } - // ApplicationLoadBalancerRequestEvent + /** + * Extracts metadata from ApplicationLoadBalancerRequestEvent. + * Adds event source ARN and event type. + */ + private static void extractALBMetadata(ApplicationLoadBalancerRequestEvent event, Transaction transaction) { try { - if (event instanceof ApplicationLoadBalancerRequestEvent) { - ApplicationLoadBalancerRequestEvent albEvent = (ApplicationLoadBalancerRequestEvent) event; - if (albEvent.getRequestContext() != null && albEvent.getRequestContext().getElb() != null) { - arn = albEvent.getRequestContext().getElb().getTargetGroupArn(); - } - eventType = EVENT_TYPE_ALB; + // ARN and event type + if (event.getRequestContext() != null && event.getRequestContext().getElb() != null) { + addAttribute(transaction, EVENT_SOURCE_ARN_ATTRIBUTE, event.getRequestContext().getElb().getTargetGroupArn()); } + addAttribute(transaction, EVENT_SOURCE_EVENT_TYPE_ATTRIBUTE, EVENT_TYPE_ALB); } catch (Throwable t) { NewRelic.getAgent().getLogger().log(Level.FINE, t, "Error extracting metadata from ApplicationLoadBalancerRequestEvent"); } + } - // APIGatewayProxyRequestEvent + /** + * Extracts metadata from APIGatewayProxyRequestEvent. + * Adds event type and API Gateway-specific attributes. + */ + private static void extractAPIGatewayProxyMetadata(APIGatewayProxyRequestEvent event, Transaction transaction) { try { - if (event instanceof APIGatewayProxyRequestEvent) { - eventType = EVENT_TYPE_API_GATEWAY; + // Event type + addAttribute(transaction, EVENT_SOURCE_EVENT_TYPE_ATTRIBUTE, EVENT_TYPE_API_GATEWAY); + + // API Gateway-specific attributes + if (event.getRequestContext() != null) { + addAttribute(transaction, EVENT_SOURCE_ACCOUNT_ID, event.getRequestContext().getAccountId()); + addAttribute(transaction, EVENT_SOURCE_API_ID, event.getRequestContext().getApiId()); + addAttribute(transaction, EVENT_SOURCE_RESOURCE_ID, event.getRequestContext().getResourceId()); + addAttribute(transaction, EVENT_SOURCE_RESOURCE_PATH, event.getRequestContext().getResourcePath()); + addAttribute(transaction, EVENT_SOURCE_STAGE, event.getRequestContext().getStage()); } } catch (Throwable t) { NewRelic.getAgent().getLogger().log(Level.FINE, t, "Error extracting metadata from APIGatewayProxyRequestEvent"); } + } - // APIGatewayV2HTTPEvent + /** + * Extracts metadata from APIGatewayV2HTTPEvent. + * Adds event type and API Gateway V2-specific attributes. + * Note: V2 uses routeKey instead of resourceId/resourcePath from V1. + */ + private static void extractAPIGatewayV2HTTPMetadata(APIGatewayV2HTTPEvent event, Transaction transaction) { try { - if (event instanceof APIGatewayV2HTTPEvent) { - eventType = EVENT_TYPE_API_GATEWAY; + // Event type + addAttribute(transaction, EVENT_SOURCE_EVENT_TYPE_ATTRIBUTE, EVENT_TYPE_API_GATEWAY); + + // API Gateway V2-specific attributes (no resourceId/resourcePath in V2) + if (event.getRequestContext() != null) { + addAttribute(transaction, EVENT_SOURCE_ACCOUNT_ID, event.getRequestContext().getAccountId()); + addAttribute(transaction, EVENT_SOURCE_API_ID, event.getRequestContext().getApiId()); + addAttribute(transaction, EVENT_SOURCE_STAGE, event.getRequestContext().getStage()); } } catch (Throwable t) { NewRelic.getAgent().getLogger().log(Level.FINE, t, "Error extracting metadata from APIGatewayV2HTTPEvent"); } + } - // CloudFrontEvent + /** + * Extracts metadata from CloudFrontEvent. + * Adds event type only (no ARN available). + */ + private static void extractCloudFrontMetadata(CloudFrontEvent event, Transaction transaction) { try { - if (event instanceof CloudFrontEvent) { - eventType = EVENT_TYPE_CLOUDFRONT; - } + // Event type only + addAttribute(transaction, EVENT_SOURCE_EVENT_TYPE_ATTRIBUTE, EVENT_TYPE_CLOUDFRONT); } catch (Throwable t) { NewRelic.getAgent().getLogger().log(Level.FINE, t, "Error extracting metadata from CloudFrontEvent"); } - - // Add the ARN attribute if we successfully extracted one - if (arn != null && !arn.isEmpty()) { - try { - transaction.getAgentAttributes().put(EVENT_SOURCE_ARN_ATTRIBUTE, arn); - } catch (Throwable t) { - NewRelic.getAgent().getLogger().log(Level.WARNING, t, "Error adding event source ARN attribute"); - } - } - - // Add the event type attribute if we identified one - if (eventType != null) { - try { - transaction.getAgentAttributes().put(EVENT_SOURCE_EVENT_TYPE_ATTRIBUTE, eventType); - } catch (Throwable t) { - NewRelic.getAgent().getLogger().log(Level.WARNING, t, "Error adding event source event type attribute"); - } - } } /** diff --git a/instrumentation/aws-lambda-java-events-3.11/src/test/java/com/amazonaws/services/lambda/runtime/LambdaEventMetadataTest.java b/instrumentation/aws-lambda-java-events-3.11/src/test/java/com/amazonaws/services/lambda/runtime/LambdaEventMetadataTest.java new file mode 100644 index 0000000000..293fe11c3c --- /dev/null +++ b/instrumentation/aws-lambda-java-events-3.11/src/test/java/com/amazonaws/services/lambda/runtime/LambdaEventMetadataTest.java @@ -0,0 +1,484 @@ +/* + * + * * Copyright 2026 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.amazonaws.services.lambda.runtime; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent; +import com.amazonaws.services.lambda.runtime.events.S3Event; +import com.amazonaws.services.lambda.runtime.events.SNSEvent; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.amazonaws.services.lambda.runtime.events.ScheduledEvent; +import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification; +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.Introspector; +import com.newrelic.agent.introspec.TransactionEvent; +import com.nr.instrumentation.lambda.LambdaInstrumentationHelper; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.joda.time.DateTime; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import static com.nr.instrumentation.lambda.LambdaConstants.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for event-specific metadata extraction in Lambda instrumentation. + * Verifies that event-specific attributes are captured correctly per the Lambda spec. + */ +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = {"com.amazonaws.services.lambda.runtime"}) +public class LambdaEventMetadataTest { + + @Before + public void setUp() { + LambdaInstrumentationHelper.resetColdStartForTesting(); + } + + @Test + public void testS3EventMetadata() { + Context mockContext = createMockContext(); + + // Create S3 event with full metadata + S3EventNotification.UserIdentityEntity userIdentity = mock(S3EventNotification.UserIdentityEntity.class); + when(userIdentity.getPrincipalId()).thenReturn("AIDAI123456789"); + + S3EventNotification.RequestParametersEntity requestParameters = mock(S3EventNotification.RequestParametersEntity.class); + when(requestParameters.getSourceIPAddress()).thenReturn("192.168.1.1"); + + S3EventNotification.ResponseElementsEntity responseElements = mock(S3EventNotification.ResponseElementsEntity.class); + when(responseElements.getxAmzId2()).thenReturn("xAmzId2Value"); + when(responseElements.getxAmzRequestId()).thenReturn("xAmzRequestIdValue"); + + S3EventNotification.S3BucketEntity bucket = mock(S3EventNotification.S3BucketEntity.class); + when(bucket.getName()).thenReturn("my-test-bucket"); + when(bucket.getArn()).thenReturn("arn:aws:s3:::my-test-bucket"); + + S3EventNotification.S3ObjectEntity object = mock(S3EventNotification.S3ObjectEntity.class); + when(object.getKey()).thenReturn("test-folder/test-file.txt"); + when(object.getSizeAsLong()).thenReturn(1024L); + when(object.getSequencer()).thenReturn("0055AED6DCD90281E5"); + + S3EventNotification.S3Entity s3Entity = mock(S3EventNotification.S3Entity.class); + when(s3Entity.getBucket()).thenReturn(bucket); + when(s3Entity.getObject()).thenReturn(object); + when(s3Entity.getConfigurationId()).thenReturn("testConfigRule"); + + S3EventNotification.S3EventNotificationRecord record = mock(S3EventNotification.S3EventNotificationRecord.class); + when(record.getEventName()).thenReturn("ObjectCreated:Put"); + when(record.getEventTime()).thenReturn(DateTime.parse("2021-01-01T12:00:00.000Z")); + when(record.getAwsRegion()).thenReturn("us-east-1"); + when(record.getUserIdentity()).thenReturn(userIdentity); + when(record.getRequestParameters()).thenReturn(requestParameters); + when(record.getResponseElements()).thenReturn(responseElements); + when(record.getS3()).thenReturn(s3Entity); + + S3Event s3Event = new S3Event(Collections.singletonList(record)); + + TestS3Handler handler = new TestS3Handler(); + handler.handleRequest(s3Event, mockContext); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + assertEquals(1, introspector.getFinishedTransactionCount()); + + Collection events = introspector.getTransactionEvents( + "OtherTransaction/Java/com.amazonaws.services.lambda.runtime.LambdaEventMetadataTest$TestS3Handler/handleRequest"); + assertEquals(1, events.size()); + + TransactionEvent event = events.iterator().next(); + Map attributes = event.getAttributes(); + + // Verify S3-specific metadata + assertTrue("Event source length should be present", attributes.containsKey(EVENT_SOURCE_LENGTH)); + assertEquals(1, attributes.get(EVENT_SOURCE_LENGTH)); + + assertTrue("Event source region should be present", attributes.containsKey(EVENT_SOURCE_REGION)); + assertEquals("us-east-1", attributes.get(EVENT_SOURCE_REGION)); + + assertTrue("Event name should be present", attributes.containsKey(EVENT_SOURCE_EVENT_NAME)); + assertEquals("ObjectCreated:Put", attributes.get(EVENT_SOURCE_EVENT_NAME)); + + assertTrue("Event time should be present", attributes.containsKey(EVENT_SOURCE_EVENT_TIME)); + assertEquals("2021-01-01T12:00:00.000Z", attributes.get(EVENT_SOURCE_EVENT_TIME)); + + assertTrue("xAmzId2 should be present", attributes.containsKey(EVENT_SOURCE_X_AMZ_ID_2)); + assertEquals("xAmzId2Value", attributes.get(EVENT_SOURCE_X_AMZ_ID_2)); + + assertTrue("Bucket name should be present", attributes.containsKey(EVENT_SOURCE_BUCKET_NAME)); + assertEquals("my-test-bucket", attributes.get(EVENT_SOURCE_BUCKET_NAME)); + + assertTrue("Object key should be present", attributes.containsKey(EVENT_SOURCE_OBJECT_KEY)); + assertEquals("test-folder/test-file.txt", attributes.get(EVENT_SOURCE_OBJECT_KEY)); + + assertTrue("Object sequencer should be present", attributes.containsKey(EVENT_SOURCE_OBJECT_SEQUENCER)); + assertEquals("0055AED6DCD90281E5", attributes.get(EVENT_SOURCE_OBJECT_SEQUENCER)); + + assertTrue("Object size should be present", attributes.containsKey(EVENT_SOURCE_OBJECT_SIZE)); + assertEquals(1024L, attributes.get(EVENT_SOURCE_OBJECT_SIZE)); + } + + @Test + public void testSNSEventMetadata() { + Context mockContext = createMockContext(); + + SNSEvent.SNS sns = new SNSEvent.SNS(); + sns.setMessageId("12345678-1234-1234-1234-123456789012"); + sns.setTimestamp(DateTime.parse("2021-01-01T12:00:00.000Z")); + sns.setTopicArn("arn:aws:sns:us-east-1:123456789012:my-topic"); + sns.setType("Notification"); + + SNSEvent.SNSRecord record = new SNSEvent.SNSRecord(); + record.setSns(sns); + record.setEventSubscriptionArn("arn:aws:sns:us-east-1:123456789012:my-topic:subscription-id"); + + SNSEvent snsEvent = new SNSEvent(); + snsEvent.setRecords(Collections.singletonList(record)); + + TestSNSHandler handler = new TestSNSHandler(); + handler.handleRequest(snsEvent, mockContext); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + assertEquals(1, introspector.getFinishedTransactionCount()); + + Collection events = introspector.getTransactionEvents( + "OtherTransaction/Java/com.amazonaws.services.lambda.runtime.LambdaEventMetadataTest$TestSNSHandler/handleRequest"); + assertEquals(1, events.size()); + + TransactionEvent event = events.iterator().next(); + Map attributes = event.getAttributes(); + + // Verify SNS-specific metadata + assertTrue("Event source length should be present", attributes.containsKey(EVENT_SOURCE_LENGTH)); + assertEquals(1, attributes.get(EVENT_SOURCE_LENGTH)); + + assertTrue("Message ID should be present", attributes.containsKey(EVENT_SOURCE_MESSAGE_ID)); + assertEquals("12345678-1234-1234-1234-123456789012", attributes.get(EVENT_SOURCE_MESSAGE_ID)); + + assertTrue("Timestamp should be present", attributes.containsKey(EVENT_SOURCE_TIMESTAMP)); + assertEquals("2021-01-01T12:00:00.000Z", attributes.get(EVENT_SOURCE_TIMESTAMP)); + + assertTrue("Topic ARN should be present", attributes.containsKey(EVENT_SOURCE_TOPIC_ARN)); + assertEquals("arn:aws:sns:us-east-1:123456789012:my-topic", attributes.get(EVENT_SOURCE_TOPIC_ARN)); + + assertTrue("Type should be present", attributes.containsKey(EVENT_SOURCE_TYPE)); + assertEquals("Notification", attributes.get(EVENT_SOURCE_TYPE)); + } + + @Test + public void testSQSEventMetadata() { + Context mockContext = createMockContext(); + + SQSEvent.SQSMessage message1 = new SQSEvent.SQSMessage(); + message1.setEventSourceArn("arn:aws:sqs:us-east-1:123456789012:my-queue"); + + SQSEvent.SQSMessage message2 = new SQSEvent.SQSMessage(); + message2.setEventSourceArn("arn:aws:sqs:us-east-1:123456789012:my-queue"); + + SQSEvent.SQSMessage message3 = new SQSEvent.SQSMessage(); + message3.setEventSourceArn("arn:aws:sqs:us-east-1:123456789012:my-queue"); + + SQSEvent sqsEvent = new SQSEvent(); + sqsEvent.setRecords(java.util.Arrays.asList(message1, message2, message3)); + + TestSQSHandler handler = new TestSQSHandler(); + handler.handleRequest(sqsEvent, mockContext); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + assertEquals(1, introspector.getFinishedTransactionCount()); + + Collection events = introspector.getTransactionEvents( + "OtherTransaction/Java/com.amazonaws.services.lambda.runtime.LambdaEventMetadataTest$TestSQSHandler/handleRequest"); + assertEquals(1, events.size()); + + TransactionEvent event = events.iterator().next(); + Map attributes = event.getAttributes(); + + // Verify SQS-specific metadata (length only) + assertTrue("Event source length should be present", attributes.containsKey(EVENT_SOURCE_LENGTH)); + assertEquals(3, attributes.get(EVENT_SOURCE_LENGTH)); + } + + @Test + public void testKinesisEventMetadata() { + Context mockContext = createMockContext(); + + KinesisEvent.KinesisEventRecord record1 = new KinesisEvent.KinesisEventRecord(); + record1.setEventSourceARN("arn:aws:kinesis:us-west-2:123456789012:stream/my-stream"); + record1.setAwsRegion("us-west-2"); + + KinesisEvent.KinesisEventRecord record2 = new KinesisEvent.KinesisEventRecord(); + record2.setEventSourceARN("arn:aws:kinesis:us-west-2:123456789012:stream/my-stream"); + record2.setAwsRegion("us-west-2"); + + KinesisEvent kinesisEvent = new KinesisEvent(); + kinesisEvent.setRecords(java.util.Arrays.asList(record1, record2)); + + TestKinesisHandler handler = new TestKinesisHandler(); + handler.handleRequest(kinesisEvent, mockContext); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + assertEquals(1, introspector.getFinishedTransactionCount()); + + Collection events = introspector.getTransactionEvents( + "OtherTransaction/Java/com.amazonaws.services.lambda.runtime.LambdaEventMetadataTest$TestKinesisHandler/handleRequest"); + assertEquals(1, events.size()); + + TransactionEvent event = events.iterator().next(); + Map attributes = event.getAttributes(); + + // Verify Kinesis-specific metadata + assertTrue("Event source length should be present", attributes.containsKey(EVENT_SOURCE_LENGTH)); + assertEquals(2, attributes.get(EVENT_SOURCE_LENGTH)); + + assertTrue("Event source region should be present", attributes.containsKey(EVENT_SOURCE_REGION)); + assertEquals("us-west-2", attributes.get(EVENT_SOURCE_REGION)); + } + + @Test + public void testKinesisFirehoseEventMetadata() { + Context mockContext = createMockContext(); + + KinesisFirehoseEvent.Record record1 = new KinesisFirehoseEvent.Record(); + KinesisFirehoseEvent.Record record2 = new KinesisFirehoseEvent.Record(); + KinesisFirehoseEvent.Record record3 = new KinesisFirehoseEvent.Record(); + + KinesisFirehoseEvent firehoseEvent = new KinesisFirehoseEvent(); + firehoseEvent.setDeliveryStreamArn("arn:aws:firehose:eu-west-1:123456789012:deliverystream/my-stream"); + firehoseEvent.setRegion("eu-west-1"); + firehoseEvent.setRecords(java.util.Arrays.asList(record1, record2, record3)); + + TestKinesisFirehoseHandler handler = new TestKinesisFirehoseHandler(); + handler.handleRequest(firehoseEvent, mockContext); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + assertEquals(1, introspector.getFinishedTransactionCount()); + + Collection events = introspector.getTransactionEvents( + "OtherTransaction/Java/com.amazonaws.services.lambda.runtime.LambdaEventMetadataTest$TestKinesisFirehoseHandler/handleRequest"); + assertEquals(1, events.size()); + + TransactionEvent event = events.iterator().next(); + Map attributes = event.getAttributes(); + + // Verify Kinesis Firehose-specific metadata + assertTrue("Event source length should be present", attributes.containsKey(EVENT_SOURCE_LENGTH)); + assertEquals(3, attributes.get(EVENT_SOURCE_LENGTH)); + + assertTrue("Event source region should be present", attributes.containsKey(EVENT_SOURCE_REGION)); + assertEquals("eu-west-1", attributes.get(EVENT_SOURCE_REGION)); + } + + @Test + public void testScheduledEventMetadata() { + Context mockContext = createMockContext(); + + ScheduledEvent scheduledEvent = new ScheduledEvent(); + scheduledEvent.setAccount("123456789012"); + scheduledEvent.setId("cdc73f9d-aea9-11e3-9d5a-835b769c0d9c"); + scheduledEvent.setRegion("us-east-1"); + scheduledEvent.setResources(Collections.singletonList("arn:aws:events:us-east-1:123456789012:rule/my-scheduled-rule")); + scheduledEvent.setTime(DateTime.parse("2021-01-01T12:00:00.000Z")); + + TestScheduledHandler handler = new TestScheduledHandler(); + handler.handleRequest(scheduledEvent, mockContext); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + assertEquals(1, introspector.getFinishedTransactionCount()); + + Collection events = introspector.getTransactionEvents( + "OtherTransaction/Java/com.amazonaws.services.lambda.runtime.LambdaEventMetadataTest$TestScheduledHandler/handleRequest"); + assertEquals(1, events.size()); + + TransactionEvent event = events.iterator().next(); + Map attributes = event.getAttributes(); + + // Verify CloudWatch Scheduled-specific metadata + assertTrue("Account should be present", attributes.containsKey(EVENT_SOURCE_ACCOUNT)); + assertEquals("123456789012", attributes.get(EVENT_SOURCE_ACCOUNT)); + + assertTrue("ID should be present", attributes.containsKey(EVENT_SOURCE_ID)); + assertEquals("cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", attributes.get(EVENT_SOURCE_ID)); + + assertTrue("Region should be present", attributes.containsKey(EVENT_SOURCE_REGION)); + assertEquals("us-east-1", attributes.get(EVENT_SOURCE_REGION)); + + assertTrue("Resource should be present", attributes.containsKey(EVENT_SOURCE_RESOURCE)); + assertEquals("arn:aws:events:us-east-1:123456789012:rule/my-scheduled-rule", attributes.get(EVENT_SOURCE_RESOURCE)); + + assertTrue("Time should be present", attributes.containsKey(EVENT_SOURCE_TIME)); + assertEquals("2021-01-01T12:00:00.000Z", attributes.get(EVENT_SOURCE_TIME)); + } + + @Test + public void testAPIGatewayProxyEventMetadata() { + Context mockContext = createMockContext(); + + APIGatewayProxyRequestEvent.ProxyRequestContext requestContext = new APIGatewayProxyRequestEvent.ProxyRequestContext(); + requestContext.setAccountId("123456789012"); + requestContext.setApiId("abcd1234"); + requestContext.setResourceId("xyz789"); + requestContext.setResourcePath("/users/{id}"); + requestContext.setStage("prod"); + + APIGatewayProxyRequestEvent apiGatewayEvent = new APIGatewayProxyRequestEvent(); + apiGatewayEvent.setRequestContext(requestContext); + + TestAPIGatewayHandler handler = new TestAPIGatewayHandler(); + handler.handleRequest(apiGatewayEvent, mockContext); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + assertEquals(1, introspector.getFinishedTransactionCount()); + + Collection events = introspector.getTransactionEvents( + "OtherTransaction/Java/com.amazonaws.services.lambda.runtime.LambdaEventMetadataTest$TestAPIGatewayHandler/handleRequest"); + assertEquals(1, events.size()); + + TransactionEvent event = events.iterator().next(); + Map attributes = event.getAttributes(); + + // Verify API Gateway V1-specific metadata + assertTrue("Account ID should be present", attributes.containsKey(EVENT_SOURCE_ACCOUNT_ID)); + assertEquals("123456789012", attributes.get(EVENT_SOURCE_ACCOUNT_ID)); + + assertTrue("API ID should be present", attributes.containsKey(EVENT_SOURCE_API_ID)); + assertEquals("abcd1234", attributes.get(EVENT_SOURCE_API_ID)); + + assertTrue("Resource ID should be present", attributes.containsKey(EVENT_SOURCE_RESOURCE_ID)); + assertEquals("xyz789", attributes.get(EVENT_SOURCE_RESOURCE_ID)); + + assertTrue("Resource path should be present", attributes.containsKey(EVENT_SOURCE_RESOURCE_PATH)); + assertEquals("/users/{id}", attributes.get(EVENT_SOURCE_RESOURCE_PATH)); + + assertTrue("Stage should be present", attributes.containsKey(EVENT_SOURCE_STAGE)); + assertEquals("prod", attributes.get(EVENT_SOURCE_STAGE)); + } + + @Test + public void testAPIGatewayV2HTTPEventMetadata() { + Context mockContext = createMockContext(); + + APIGatewayV2HTTPEvent.RequestContext requestContext = new APIGatewayV2HTTPEvent.RequestContext(); + requestContext.setAccountId("987654321098"); + requestContext.setApiId("v2api123"); + requestContext.setStage("beta"); + + APIGatewayV2HTTPEvent apiGatewayV2Event = new APIGatewayV2HTTPEvent(); + apiGatewayV2Event.setRequestContext(requestContext); + + TestAPIGatewayV2HTTPHandler handler = new TestAPIGatewayV2HTTPHandler(); + handler.handleRequest(apiGatewayV2Event, mockContext); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + assertEquals(1, introspector.getFinishedTransactionCount()); + + Collection events = introspector.getTransactionEvents( + "OtherTransaction/Java/com.amazonaws.services.lambda.runtime.LambdaEventMetadataTest$TestAPIGatewayV2HTTPHandler/handleRequest"); + assertEquals(1, events.size()); + + TransactionEvent event = events.iterator().next(); + Map attributes = event.getAttributes(); + + // Verify API Gateway V2-specific metadata (no resourceId or resourcePath) + assertTrue("Account ID should be present", attributes.containsKey(EVENT_SOURCE_ACCOUNT_ID)); + assertEquals("987654321098", attributes.get(EVENT_SOURCE_ACCOUNT_ID)); + + assertTrue("API ID should be present", attributes.containsKey(EVENT_SOURCE_API_ID)); + assertEquals("v2api123", attributes.get(EVENT_SOURCE_API_ID)); + + assertTrue("Stage should be present", attributes.containsKey(EVENT_SOURCE_STAGE)); + assertEquals("beta", attributes.get(EVENT_SOURCE_STAGE)); + + // V2 should NOT have resourceId or resourcePath + assertTrue("Resource ID should NOT be present for V2", + !attributes.containsKey(EVENT_SOURCE_RESOURCE_ID) || attributes.get(EVENT_SOURCE_RESOURCE_ID) == null); + assertTrue("Resource path should NOT be present for V2", + !attributes.containsKey(EVENT_SOURCE_RESOURCE_PATH) || attributes.get(EVENT_SOURCE_RESOURCE_PATH) == null); + } + + /** + * Creates a mock Lambda Context for testing. + */ + private Context createMockContext() { + Context context = mock(Context.class); + when(context.getInvokedFunctionArn()).thenReturn("arn:aws:lambda:us-east-1:123456789012:function:test-function"); + when(context.getFunctionVersion()).thenReturn("$LATEST"); + when(context.getFunctionName()).thenReturn("test-function"); + when(context.getAwsRequestId()).thenReturn("request-123"); + when(context.getMemoryLimitInMB()).thenReturn(512); + when(context.getRemainingTimeInMillis()).thenReturn(30000); + when(context.getLogGroupName()).thenReturn("/aws/lambda/test-function"); + when(context.getLogStreamName()).thenReturn("2026/01/01/[$LATEST]abc123"); + return context; + } + + // Test handler implementations + public static class TestS3Handler implements RequestHandler { + @Override + public Void handleRequest(S3Event event, Context context) { + return null; + } + } + + public static class TestSNSHandler implements RequestHandler { + @Override + public Void handleRequest(SNSEvent event, Context context) { + return null; + } + } + + public static class TestSQSHandler implements RequestHandler { + @Override + public Void handleRequest(SQSEvent event, Context context) { + return null; + } + } + + public static class TestKinesisHandler implements RequestHandler { + @Override + public Void handleRequest(KinesisEvent event, Context context) { + return null; + } + } + + public static class TestKinesisFirehoseHandler implements RequestHandler { + @Override + public KinesisFirehoseEvent handleRequest(KinesisFirehoseEvent event, Context context) { + return event; + } + } + + public static class TestScheduledHandler implements RequestHandler { + @Override + public Void handleRequest(ScheduledEvent event, Context context) { + return null; + } + } + + public static class TestAPIGatewayHandler implements RequestHandler { + @Override + public String handleRequest(APIGatewayProxyRequestEvent event, Context context) { + return "ok"; + } + } + + public static class TestAPIGatewayV2HTTPHandler implements RequestHandler { + @Override + public String handleRequest(APIGatewayV2HTTPEvent event, Context context) { + return "ok"; + } + } +}