Search code examples
javaaws-lambda

Straightforward java lambda with s3 event


Here my working lambda:

public class TrelereEventHandler
        implements RequestHandler<Map<String, Object>, String> {

    @Override
    public String handleRequest(Map<String, Object> event, Context arg1) {
        for (Map.Entry<String, Object> entry : event.entrySet()) {
            System.out.println(entry.getKey() + ":" + entry.getValue());
        }
    }
}

This lambda is invoked and prints event correctly:

$ aws logs filter-log-events --log-group-name "/aws/lambda/trelere-dev-trelere"
{
  "events": [
    {
      "logStreamName": "2024/02/28/[$LATEST]e2bc0cc11deededb29e070183861d71a",
      "timestamp": 1709118900917,
      "message": "START RequestId: 8c1b3b79-eed8-463e-9046-aed99e6c5867 Version: $LATEST",
      "ingestionTime": 1709118901011,
      "eventId": "6608"
    },
    {
      "logStreamName": "2024/02/28/[$LATEST]e2bc0cc11deededb29e070183861d71a",
      "timestamp": 1709118900934,
      "message": "Records:[{body={\"Records\": [{\"eventVersion\": \"2.1\", \"eventSource\": \"aws:s3\", \"awsRegion\": \"us-east-1\", \"eventTime\": \"2024-02-28T11:15:00.511Z\", \"eventName\": \"ObjectCreated:Put\", \"userIdentity\": {\"principalId\": \"AIDAJDPLRKLG7UEXAMPLE\"}, \"requestParameters\": {\"sourceIPAddress\": \"127.0.0.1\"}, \"responseElements\": {\"x-amz-request-id\": \"e3617358\", \"x-amz-id-2\": \"eftixk72aD6Ap51TnqcoF8eFidJG9Z/2\"}, \"s3\": {\"s3SchemaVersion\": \"1.0\", \"configurationId\": \"c6417fbb\", \"bucket\": {\"name\": \"espaidoc\", \"ownerIdentity\": {\"principalId\": \"A3NL1KOZZKExample\"}, \"arn\": \"arn:aws:s3:::espaidoc\"}, \"object\": {\"key\": \"references/reference1.readme\", \"sequencer\": \"0055AED6DCD90281E5\", \"size\": 1917, \"eTag\": \"e8b5cd0ccb83955551ec584503cc4bd4\"}}}]}, receiptHandle=OTdhZjRhYzMtMGQ4MC00YzdmLWFiNDAtMDZkM2ZkZmFmY2JiIGFybjphd3M6c3FzOnVzLWVhc3QtMTowMDAwMDAwMDAwMDA6U3FzUXVldWVDcmVhdGUgOTlkZDkxZGYtOWU3Ni00NjA4LTk5NDEtYmM1NzMxMTEzNjNmIDE3MDkxMTg5MDAuODU5MjM5Mw==, md5OfBody=98ddb983c245cf3ed206d6d787412bd9, eventSourceARN=arn:aws:sqs:us-east-1:000000000000:SqsQueueCreate, eventSource=aws:sqs, awsRegion=us-east-1, messageId=99dd91df-9e76-4608-9941-bc573111363f, attributes={SenderId=000000000000, SentTimestamp=1709118900522, ApproximateReceiveCount=1, ApproximateFirstReceiveTimestamp=1709118900859}, messageAttributes={}}]",
      "ingestionTime": 1709118901011,
      "eventId": "6609"
    },
    {
      "logStreamName": "2024/02/28/[$LATEST]e2bc0cc11deededb29e070183861d71a",
      "timestamp": 1709118900951,
      "message": "END RequestId: 8c1b3b79-eed8-463e-9046-aed99e6c5867",
      "ingestionTime": 1709118901011,
      "eventId": "6610"
    },
    {
      "logStreamName": "2024/02/28/[$LATEST]e2bc0cc11deededb29e070183861d71a",
      "timestamp": 1709118900968,
      "message": "REPORT RequestId: 8c1b3b79-eed8-463e-9046-aed99e6c5867\tDuration: 4.58 ms\tBilled Duration: 5 ms\tMemory Size: 1024 MB\tMax Memory Used: 1024 MB\t",
      "ingestionTime": 1709118901011,
      "eventId": "6611"
    }
  ],
  "searchedLogStreams": [
    {
      "logStreamName": "2024/02/28/[$LATEST]e2bc0cc11deededb29e070183861d71a",
      "searchedCompletely": true
    }
  ]
}

As you can see, I'm sending an s3-put notification. Here formatted json:

{
   "Records":[
      {
         "body":"{\"Records\": [{\"eventVersion\": \"2.1\", \"eventSource\": \"aws:s3\", \"awsRegion\": \"us-east-1\", \"eventTime\": \"2024-02-28T05:30:34.882Z\", \"eventName\": \"ObjectCreated:Put\", \"userIdentity\": {\"principalId\": \"AIDAJDPLRKLG7UEXAMPLE\"}, \"requestParameters\": {\"sourceIPAddress\": \"127.0.0.1\"}, \"responseElements\": {\"x-amz-request-id\": \"b38d4b33\", \"x-amz-id-2\": \"eftixk72aD6Ap51TnqcoF8eFidJG9Z/2\"}, \"s3\": {\"s3SchemaVersion\": \"1.0\", \"configurationId\": \"058322eb\", \"bucket\": {\"name\": \"espaidoc\", \"ownerIdentity\": {\"principalId\": \"A3NL1KOZZKExample\"}, \"arn\": \"arn:aws:s3:::espaidoc\"}, \"object\": {\"key\": \"references/reference1.readme\", \"sequencer\": \"0055AED6DCD90281E5\", \"size\": 1917, \"eTag\": \"e8b5cd0ccb83955551ec584503cc4bd4\"}}}]}",
         "receiptHandle":"ZWQ1YmRjYjctODU0OC00NzMyLWIzNWEtOWMyZjRjMjk4MzdjIGFybjphd3M6c3FzOnVzLWVhc3QtMTowMDAwMDAwMDAwMDA6U3FzUXVldWVDcmVhdGUgMDdlYWZlMTQtOTRjMi00YjA5LTgwNjMtNTU2M2MyOTViMmFjIDE3MDkwOTgyMzUuODg5MDgwOA==",
         "md5OfBody":"dff5137ad0700f8f44763d5eed490032",
         "eventSourceARN":"arn:aws:sqs:us-east-1:000000000000:SqsQueueCreate",
         "eventSource":"aws:sqs",
         "awsRegion":"us-east-1",
         "messageId":"07eafe14-94c2-4b09-8063-5563c295b2ac",
         "attributes":{
            "SenderId":"000000000000",
            "SentTimestamp":"1709098234899",
            "ApproximateReceiveCount":"1",
            "ApproximateFirstReceiveTimestamp":"1709098235889"
         },
         "messageAttributes":{

         }
      }
   ]
}

However, when I'm trying to receive an S3EventNotification:

public class TrelereEventHandler
        implements RequestHandler<S3EventNotification, String> {

    @Override
    public String handleRequest(S3EventNotification input, Context context) {
        System.out.println(input.toString());
        return "Ok";
    }
}

Then I'm getting:

java.lang.InstantiationException: com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification

        at java.base/java.lang.Class.newInstance(Class.java:639)
        at com.serverless.mapper.DefaultMapper.read(DefaultMapper.java:25)
        at com.serverless.InvokeBridge.invoke(InvokeBridge.java:78)
        at com.serverless.InvokeBridge.<init>(InvokeBridge.java:38)
        at com.serverless.InvokeBridge.main(InvokeBridge.java:137)
Caused by: java.lang.NoSuchMethodException: com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification.<init>()
        at java.base/java.lang.Class.getConstructor0(Class.java:3585)
        at java.base/java.lang.Class.newInstance(Class.java:626)
        ... 4 more

However, it's present as dependency into my pom.xml:

        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-events</artifactId>
            <version>${aws-lambda-events.version}</version>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-core</artifactId>
            <version>1.2.3</version>
            <scope>provided</scope>  <!-- also I've tried without this line -->
        </dependency>

Any ideas?


Solution

  • You're not getting the event directly from S3 - you're getting it through SQS. When sending through SQS (and SNS) the event is "wrapped" and so you're getting an SQSEvent with a body field that is a S3Event. One possibility is to send the S3 event directly to Lambda:

    enter image description here

    and change your function to be:

    public class S3EventHandler implements RequestHandler<S3Event, String> {
        private static final Gson gson = new Gson();
    
        @Override
        public String handleRequest(S3Event s3Event, Context context) {
            context.getLogger().log(gson.toJson(s3Event));
            return "ok";
        }
    }
    

    (using Gson to help log out the JSON). The other option is to use the SQS setup you have:

    public class SQSEventHandlerLambda implements RequestHandler<SQSEvent, String> {
        private static final Gson gson = new Gson();
    
        @Override
        public String handleRequest(SQSEvent sqsEvent, Context context) {
            context.getLogger().log(gson.toJson(sqsEvent));
            return "ok";
        }
    }
    

    and read out of sqsEvent.getRecords(). From each of those records you can then call getBody() to get what is really a S3Event. To convert this from a String to the S3Event, do something similar to:

    for(SQSEvent.SQSMessage nextMessage: messages) {
        S3Event s3Event = gson.fromJson(nextMessage.getBody(), S3Event.class);
    }
    

    A confusing thing is that an S3Event is a S3EventNotification. I would advise you to always take events from the com.amazonaws.services.lambda.runtime.events package. The com.amazonaws.services.lambda.runtime.events.models.s3 package is a set of helper classes for S3.

    Ultimately I'd like to see AWS have this setup better. Getting from an SQSMessage should allow you to get one of the wrapped classes through templating. But it doesn't feel like the Lambda event library has had too much attention paid to it.