Module 4: AWS Lambda

Module Overview

Master working with AWS Lambda functions, cloud logging, and how to effectively test serverless applications.

What is Logging?

If we think about computer applications, we typically picture the way we, as users, interact with those applications - the user interface. For a desktop application, like Outlook, we imagine the application we open on our operating system. For web applications, like Code Browser, the user interface is presented via the browser. Let's imagine we own Code Browser. There may be a lot of information we want to keep track of that we don't want to expose to users. For example, if someone types in the name of a Gradle package that doesn't exist, we may show them a friendly error message just saying this package doesn't exist. But we may want to track who made the request and what they attempted to search for. Or if we aren't able to connect to another service that we need to get information about Gradle packages, we may show a user an error message just letting them know to try again later. But we probably want to keep track of what failed and why. We will use this information to help us answer customer questions about what they experienced or identify and fix issues our customers are experiencing.

The application requires a secondary interface for communication. Logging is the process of recording application actions and state to a secondary interface. Note: We do not mean a Java interface when we use the term "interface" here. We mean a shared boundary that is used for communication. This can be a user interface - a point and click visual. Or, as we are about to discuss, we can use something as simple as a file to share communication between an application and a developer.

We are recording application actions and state. Which actions and what state should we record? We are recording these to a secondary interface, which could be a file, another service, or a web application like CloudWatch in AWS. The nature of the interface is not important; what is important is that this is a secondary interface. The user who sees this interface is the developer supporting the application. They will be looking at this interface when they are trying to diagnose an issue with the application. This means that what is detailed in this log is very important; it is often the main source of information when diagnosing a problem.

As a developer, you will choose to record certain actions and values in your logs. We will show you how to do that below. In addition to the log statements you create, the code you depend on will also write information to the logs. So you'll find lots of additional information there. One thing you'll likely find most helpful as you develop and debug code is the exceptions that will be written there when your code breaks!

Log Levels

Some state changes or actions performed by an application are more important than others. To differentiate between importance, logging frameworks provide different classifications or levels associated with the information being sent to the log. These log levels gradually increase in severity, DEBUG, INFO, WARN, ERROR, and FATAL. WARN, ERROR, and FATAL communicate errors, while INFO is used for general information purposes, and DEBUG is used for debugging.

Logging frameworks write to the log in a structured manner, making it easier to search for entries relating to a certain level. Sometimes, certain log information is only useful in a beta (team testing) environment, though. DEBUG and INFO logs can help a lot when new code is being developed. We can log lots of information to help diagnose issues or see the flow of information through different code paths. However, our production systems receive a lot more traffic, so including this can cause a lot of noise in our logs when trying to solve actual customers' issues. To allow for this, logging frameworks allow you to configure the lowest level to log at, depending on your stage. You will configure your projects to log at the DEBUG level.

INFO

As the name suggests, these are purely informational messages; you should not use them to indicate an issue or error state in the application. To use this log level effectively, try to think about what general information would be useful for diagnosing an application error. This level is often used for usage information. Some examples are who is using the application, what API was requested, the inputs to an API or method, or when an external service is called.

DEBUG

This log level is used to indicate that the logged message is to be used for debugging purposes - in other words, these messages are aimed squarely at the developer. What you use this for really depends on the application you are developing. Many problems can be resolved via the debugger, making the use of DEBUG messages redundant; however, there are situations where you will not be able to attach a debugger.

In this situation, it can be useful to print out the value of variables or indications of which code path a request follows through branching logic or various methods.

WARN

This is the first level that indicates some form of application failure. WARN level messages should indicate that the application faced a potential problem; however, the user experience has not been affected in any way, and the user can continue. The user may not have even realized anything went wrong in this case. For example, a WARN message might be appropriate if we could not use an external service; however, a secondary service that performs the same functions was available. Also, a WARN message is appropriate if repeated attempts were required to access an external service, but the external service was eventually accessed successfully.

ERROR

This is the second level of failure, and by its very name, it should indicate that something more critical has occurred. ERROR messages should indicate that the application faced a significant problem and that, as a result, the user experience was somehow affected. The user will be affected by this error. For example, a database connection could have failed, resulting in parts of the application being rendered unusable.

FATAL

This third level of failure should be used to indicate a fatal error. The user experience was not just affected; it has entirely ceased! For example, a component that is central to the operation of the application has failed in a way that leaves it in an unstable state, with the only possible course of action being to terminate the application altogether.

What Should Be Logged?

We now have a definition of logging and some guidelines for how to use each log level. Now, it is time for the big question: "What should I be logging?" This is not an easy question to answer, and it depends very much on the nature of the application being developed.

Is logging important? Anyone who has been in the position of debugging a live application armed with just a log file will tell you, "yes!" There is nothing more frustrating than finding a log file swamped with noisy, extra detail, or worse still, lacking that vital piece of information you require to diagnose the fault.

If the application log is the only detailed source of information available when diagnosing a failure, it probably needs to be quite detailed. However, if other tools are available for monitoring user activities like metrics, the logged information could be more sparse. Either way, regardless of the required level of detail, each individual logged message's quality is important, which is what this section will primarily focus on.

The Importance of Context

Whether an application executes a task successfully or not is often highly dependent on the input from the user. As a result, this contextual information may be vital when trying to diagnose a problem. Unfortunately, these vital last bits of information are often missing. Take the example of an Automated Teller Machine (ATM - or cashpoint). An application log might look like this:

2009-06-05 08:15:23 [INFO] User credentials entered
2009-06-05 08:15:23 [WARN] Incorrect PIN
2009-06-05 08:15:57 [INFO] User credentials entered
2009-06-05 08:15:57 [INFO] User credentials validated
2009-06-05 08:16:33 [INFO] Cash withdrawal requested
2009-06-05 08:16:34 [ERROR] Communication failure
Acme.ATM.Integration.ServerException: Unable to connect to bank server at Acme.ATM.Integration.WithdrawalService.Connect(): line 23 ...

If we were performing a daily audit of the ATM logs in identifying issues, what does the above log file excerpt tell us? It could be that some user logged on successfully after two attempts and requested a cash withdrawal. However, this request could not be serviced due to a communication error. Or did one user fail to log in and stop trying, and then a second user logged in successfully but failed to be serviced. The logged stack trace gives us the line of code where the exception was thrown; however, this same code may have been executed a few thousand times successfully in the same day. Basically, the above tells us very little; it informs us that a problem occurred but gives us little information to diagnose and hopefully fix it. The problem could relate to the specific user, a specific bank server, or something else.

The main piece of information missing in the above log is context. If we added the user's credentials, this would probably lead us to a wealth of information, such as their account details or the bank with which they have an account (could it be that bank's server that is down?). A few simple additional details relating to the context make a world of difference:

2009-06-05 08:15:23 [INFO] User credentials entered, Acc=123765987
2009-06-05 08:15:23 [WARN] Incorrect PIN
2009-06-05 08:15:57 [INFO] User credentials entered, Acc=123765987
2009-06-05 08:15:57 [INFO] User credentials validated
2009-06-05 08:16:33 [INFO] Cash withdrawal requested, Amount=450.00
2009-06-05 08:16:34 [ERROR] Communication failure, unable to connect to acmebank
Acme.ATM.Integration.ServerException: Unable to connect to bank server at Acme.ATM.Integration.WithdrawalService.Connect(): line 23 ...

This time, we know which user, how much was requested, and the bank contacted for the withdrawal. With this additional information, we now stand a fighting chance of tracking the bug down. Or, even if we are unable to from this log message, a pattern may emerge from future failures, leading us to the issue.

Each time we write a log, there are a few things we should check:

  • Security. We want to think about the security of the data we log. We should never log any data that has been classified as highly confidential or more secure. You can see examples of data classifications in the Data Classification Catalog. For example, in the logs above, we would never want to log a customer's PIN.
  • Exceptions. If we are writing a log because an exception has been thrown. We should always include the exception in the log. You want to be sure to include this full context. If you have a catch block similar to:
    catch(Exception e) {
        log.error("Communication failure, unable to connect to " + bankName, e);
    }
    you would pass e to the log statement as one of the parameters.
  • Overriding toString(). When we add context to a log, we will often want to include an object to see its value. In that case, we need to make sure that toString() is defined for that class. Without toString() the memory location of the object will be printed in the log. We need to make sure that toString() is overridden to see the values of the variables in that object. Customer@458ad742 is not very helpful when what we would like is the id of the customer!

Why Not Log Everything?

The more information we have at our disposal, the easier it is to diagnose an issue, so the question "why not log everything?" does sound like quite a reasonable one. What might logging "everything" look like? This might mean that you log the start and end of each method, the start/end of code blocks within the method, the method arguments, etc.

Long and detailed log files full of unnecessary information can take a long time to analyze and make finding the critical information challenging and time-consuming. Code files full of lines that write information to logs can make our code less readable and crowded.

How to Log

We will use a framework called Log4j for logging. When a new Gradle package or service is first being set up, some configuration needs to be done to set up logging. As discussed earlier, this may include specifying the log level to write (including different log levels depending on the stage, beta vs. prod). Your unit project is configured to log all levels, including INFO and DEBUG. This overall set-up of a logging framework is beyond the scope of our discussion for this lesson, but we will discuss how to write logs from a class once logging is set up in your service. To log information from your service, you need to do two things in each class you want to log from.

Create a Logger

To write logs, we need to create a Logger. We can create a Logger instance using the LogManager. We want to create a Logger whose name is the class emitting the logs. We can do this by using the getLogger(...) method and passing it the current class. This will include the name of the class in the line that gets emitted to the log file.

import org.apache.logging.log4j.LogManager; 
import org.apache.logging.log4j.Logger;

public class AuthenticationService {
    private Logger log = LogManager.getLogger(AuthenticationService.class);
}

By calling AuthenticationService.class we actually get an object of type Class<AuthenticationService>. Class<T> is a generic type that represents classes or interfaces in a running Java application. So our Class<AuthenticationService> doesn't represent a particular instance of the AuthenticationService, but the overall concept of the class.

Write Log Messages

The Logger class has methods mapping to each of our log levels - .info(...), .debug(...), .warn(...), .error(...), and .fatal(...) among others. Let's take a look at how this works in the AuthenticationService class:

public class AuthenticationService {
    private Logger log = LogManager.getLogger(AuthenticationService.class);

    public boolean authenticate(int attempt) {
        String account = getAccount();
        int pin = getPin();
        boolean authenticated = checkPin(account, pin);
        if (authenticated) {
            log.info("Account=" + account + ", credentials validated");
            return true;
        } else {
            log.error("Incorrect PIN used for Account=" + account);
            return false;
        }
    }

    private boolean checkPin(String account, int pin) {...}
}

Our log statements above provide context, which is great! However, instead of using string concatenation to add context, a preferred way is to do this with log4j. You can add "{}" to a string as a placeholder. The Logger methods then accept a comma-separated list of variables to substitute into the string.

Let's add a log statement that uses this type of substitution:

public class AuthenticationService {
    private Logger log = LogManager.getLogger(AuthenticationService.class);

    public boolean authenticate(int attempt) {
        String account = getAccount();
        int pin = getPin();
        boolean authenticated = checkPin(account, pin);
        if (authenticated) {
            log.info("Account={}, credentials validated", account);
            return true;
        } else {
            log.error("Incorrect PIN used for Account={}", account);
            return false;
        }
    }

    private boolean checkPin(String account, int pin) {...}
}

A logline that you would see in the logs from the above code might look like:

09 Feb 2021 22:42:13,535 [INFO ]  (main) com.amazon.atacurriculumatm.service.AuthenticationService:67: Account=123765987, credentials validated

Intro to grep

grep is a tool we will often use to find information within log files. Log files can be giant, and we will often use grep with the context we talked about in the first reading to find the right lines in the log files. You may know the customer ID that is experiencing a problem. You can use grep to find log files with that customer ID.

You can find the complete files used in this example here.

The presenter has a grep option enabled by default (--color) that highlights the matching part of the line returned by grep. If you execute his commands exactly, you likely won't see the same highlighting. If you'd like to know more about the color option, you can read "Grep with color output".

They also show an example of a recursive search using the -r flag. Effectively, when this option is provided and the search targets include a directory, the grep command will be run on all files within that directory. If there are more directories inside that, then the process will continue until every file in the directory tree has been searched. We will cover recursion as a programming concept in detail in a later lesson.

less

This video opens with a quick mention of the cat command, which prints the contents of a file to the console. The log files we will be inspecting with less will often be large like the file the presenter uses in this video, which he demonstrates cat is not suitable for, so don't worry about learning cat at the moment.

Introduction to AWS Lambda

AWS Lambda is a serverless compute service that lets you run code without provisioning or managing servers. Lambda functions are event-driven and automatically scale based on incoming requests, making them ideal for many types of applications.

Key benefits of Lambda include:

  • No server management required
  • Automatic scaling based on workload
  • Pay only for compute time consumed
  • Integrated with many AWS services
  • Support for multiple programming languages including Java
// Example of a basic AWS Lambda function in Java
public class MyLambdaFunction implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
    @Override
    public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
        context.getLogger().log("Processing request: " + input.getBody());
        
        // Process the request
        String result = processRequest(input.getBody());
        
        // Create and return response
        APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
        response.setStatusCode(200);
        response.setBody(result);
        return response;
    }
    
    private String processRequest(String requestBody) {
        // Business logic here
        return "Processed: " + requestBody;
    }
}

Learning Objectives

  • Understand the serverless computing model and AWS Lambda's role
  • Learn how to create and deploy Lambda functions in Java
  • Master the Lambda function lifecycle and execution environment
  • Implement effective logging strategies for Lambda functions
  • Understand AWS Lambda's integration with other AWS services
  • Develop unit tests for Lambda functions
  • Create local testing environments for Lambda functions
  • Implement proper error handling in serverless applications
  • Understand best practices for Lambda function design
  • Learn strategies for monitoring and debugging Lambda functions

Testing AWS Lambda Functions

Testing Lambda functions presents unique challenges due to their cloud-based nature and integration with AWS services. Effective testing strategies include:

  1. Unit testing: Test the core business logic independently from AWS Lambda
  2. Local testing: Use AWS SAM or LocalStack to emulate the Lambda environment
  3. Integration testing: Test the function with actual AWS services
  4. Mock testing: Use mocks for AWS services and dependencies
// Example of unit testing a Lambda function
@Test
public void handleRequest_validInput_returnsExpectedResponse() {
    // GIVEN
    MyLambdaFunction lambdaFunction = new MyLambdaFunction();
    
    // Mock the AWS Context
    Context mockContext = mock(Context.class);
    LambdaLogger mockLogger = mock(LambdaLogger.class);
    when(mockContext.getLogger()).thenReturn(mockLogger);
    
    // Create test input
    APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent();
    request.setBody("{\"name\":\"John\",\"id\":123}");
    
    // WHEN
    APIGatewayProxyResponseEvent response = lambdaFunction.handleRequest(request, mockContext);
    
    // THEN
    assertEquals(200, response.getStatusCode().intValue());
    assertTrue(response.getBody().contains("John"));
    verify(mockLogger).log(contains("Processing request"));
}

Best Practices for Lambda Functions

To create robust, efficient Lambda functions, follow these best practices:

  • Keep functions focused: Each function should do one thing well
  • Minimize cold starts: Keep deployment packages small
  • Handle exceptions gracefully: Catch and log all exceptions
  • Use environment variables: For configuration and sensitive information
  • Implement proper logging: Include transaction IDs and relevant context
  • Write stateless functions: Don't rely on local state between invocations
  • Set appropriate timeouts: Based on expected function execution time
  • Monitor performance: Use CloudWatch metrics and X-Ray tracing
// Example of improved Lambda function with best practices
public class ImprovedLambdaFunction implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private final UserService userService;
    
    // Use dependency injection for testability
    public ImprovedLambdaFunction() {
        this.userService = new UserService();
    }
    
    public ImprovedLambdaFunction(UserService userService) {
        this.userService = userService;
    }
    
    @Override
    public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
        String requestId = context.getAwsRequestId();
        LambdaLogger logger = context.getLogger();
        logger.log(String.format("Processing request %s: %s", requestId, input.getBody()));
        
        APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
        response.setHeaders(Map.of("Content-Type", "application/json"));
        
        try {
            UserRequest userRequest = OBJECT_MAPPER.readValue(input.getBody(), UserRequest.class);
            UserResponse result = userService.processUser(userRequest);
            
            response.setStatusCode(200);
            response.setBody(OBJECT_MAPPER.writeValueAsString(result));
        } catch (JsonProcessingException e) {
            logger.log(String.format("Error parsing request %s: %s", requestId, e.getMessage()));
            response.setStatusCode(400);
            response.setBody("{\"error\":\"Invalid request format\"}");
        } catch (Exception e) {
            logger.log(String.format("Error processing request %s: %s", requestId, e.getMessage()));
            response.setStatusCode(500);
            response.setBody("{\"error\":\"Internal server error\"}");
        }
        
        return response;
    }
}

Guided Project

Additional Resources:

AWS Lambda Functions Sample Project

Create and Test AWS Lambda Functions in the Cloud

Everything you will do here will be discussed by your instructor in the Guided Project session of class. Be sure to jot down any questions or problems you encounter for the Guided Project or Office Hours sessions.

This task is not included in the Sprint project and will not be evaluated in CodeGrade. Hence the"Pseudo" in the title. This is a standalone task whose skills you may use at your discretion and as necessary through your training here and your I.T. career.

Fork and clone this Git Repo: AWS Lambda Functions down to your machine.

Milestone 1 - Login to your Bloom Tech AWS Account

  1. Go to Okta.
  2. Choose the AWS Single Sign-on application.
  3. Click on the entry with your name appended to it.
  4. Choose the entry that says BD-your-name-Unit-3
  5. Click on the Management console link.
  6. You should now be logged into the AWS management console

Milestone 2 - Generate the zip file to be uploaded to AWS Lambda:

  1. Open the project bd_unit_3_preparedness_task in Intellij.
  2. Verify the Project SDK is set to corretto-11:
    • Go to the IntelliJ File menu and choose Project structure.
    • Be sure the Project SDK is set to corretto-11. Change it if it is something else.
    • Click OK to leave the dialog.
  3. Open the build.gradle file (double-click it in the file list).
  4. Click on the green arrow next to the task buildZip(type Zip) entry in the build.gradle file. Then click on the entry in the pop-up window that says Run bd_unit_3_preparedness. This will create a distribution zip file of the code.
  5. Look in the project file list for a folder name build. Click on the arrow next to build. Click on the arrow next to distributions.
  6. You should see a file called Unit_3_Preparedness_Task-1.0-SNAPSHOT.zip. This is the file containing the code you will upload to AWS Lambda.

Milestone 3 - Create a serverless function in AWS Lambda

  1. Login to the AWS Management Console (if you are not already logged in).
  2. Click on the Lambda service. It will be listed on the left as recently visited or under All services, Compute. You should now be viewing the AWS Lambda console. It will say AWS Lambda in the upper left corner of the page.
  3. Click on Functions in the list of choices on the left side of the page. You should now be viewing the Functions page. Look for Lambda > Functions at the top of the page.
  4. Click on the Create function button in the upper right corner of the page.
  5. You should now be viewing the Create Function page. Look for Lambda > Functions > Create function at the top of the page.
  6. Click on the box that says Author from scratch.
  7. In the Basic Information section be sure choose the following:
    • In the Function name field enter the name: BD_Unit_3_Preparedness_Task_Function
    • In the Runtime field choose: Java 11 (Corretto)
    • In the Architecture field choose: x86_64
  8. Click the Create function button in the bottom right corner of the page.
  9. Be patient, it sometimes takes a few seconds to create the function.
  10. When the function has been created you see the function management page with a message near the top of the page that indicates the function has been successfully created.
  11. Click on the Upload from drop down on the right side of the Code source section of the page. Choose the zip or jar file option.
  12. Click the Upload button in the bottom left corner of the dialog.
  13. Navigate to the distributions folder for the project ( bd_unit_3_preparedness_task/build/distributions) and select the Unit_3_Preparedness_Task-1.0-SNAPSHOT.zip file you created in IntelliJ. Click Open in the lower right corner of the file list.
  14. Verify the SNAPSHOT file name is displayed next to the Upload button and click Save in the lower right corner.
  15. Scroll down to the Runtime settings section of the page and click Edit in the upper right corner of Runtime settings.
  16. Verify the Runtime value is Java 11 (Corretto). Change it, if it is not.
  17. Change the Handler value to lambdaapp.LambdaHandler::handleRequest. Be sure everything is spelled correctly and the punctuation is correct.
  18. Click Save in the lower right corner.
  19. Verify the runtime settings section of the page has the following values:
    • Runtime: Java 11 (Corretto)
    • Handler: lambdaapp.LambdaHandler::handleRequest
    • Architecture: x86_64

Milestone 4 - Test your serverless function in AWS Lambda

If you are not already there, go to your AWS Lambda Function page. You will see Lambda > Functions > BD_Unit_3_Preparedness_Task at the top of the page with the function name (BD_Unit_3_Preparedness_Task) underneath it in larger bold letters.

  1. Click on the Test option (next to Code) above the Code Source section.
  2. If the template shown is not hello-world, choose the hello-world template from the drop down list.
  3. Enter BD_Preparedness_Task_Test in the *Name field. Replace whatever is in the text box under the *Name field with the following JSON object:
{ 
"firstName" : "Java", 
"lastName"  : "Language", 
"dob"       : "01/23/1996" 
}

There is a file in the project named requestData.json containing this data. You may open that file in Intellij (double click it in the file list) then copy and paste it from there into the text box.

  1. Click the Save changes button in the upper right corner of the Test Event section of the page.
  2. Click the Test button in the upper right corner of the Test Event section to test the AWS Lambda function with the data in the test event.
  3. Above the Test Event section of the page you will see a message indicating whether the test succeeded or failed. You want to see Execution result: succeeded(logs).
  4. Click details drop down under the Execution result message for information related to the test. If your test was successful, in the The area below shows the result returned by your function execution. Learn more about returning results from your function you should see:
{ 
"creationTimeStamp": "2021-11-16 16:17:35.363", 
"fullName": "Java Language", 
"dob": "01/23/1996" 
}

Note: Your creationTimeStamp will have a different value than shown above.

Scroll down to the Log Output section. Under The section below shows the logging calls in your code. Click here to view the corresponding CloudWatch log group. you should see:

START RequestId: a0f99857-bc2c-4835-9402-e0906f3c4f95 Version: $LATEST 
Request Received: RequestData{firstName='Java', lastName='Language', dob='01/23/1996' 
------------------------------------------------------------------------------------- 
Hello there Java Language 
Welcome to BD Unit_3 Preparedness Task AWS Lambda App 
I see were born on 01/23/1996 
------------------------------------------------------------------------------------- 
Response was: ResponseData{creationTimeStamp='2021-11-16 16:17:35.363', fullName='Java Language', dob='01/23/1996' 
END RequestId: a0f99857-bc2c-4835-9402-e0906f3c4f95 REPORT RequestId: a0f99857-bc2c-4835-9402-e0906f3c4f95 Duration: 613.96 ms Billed Duration: 614 ms Memory Size: 512 MB Max Memory Used: 81 MB Init Duration: 346.35 ms

Congratulations! You have successfully uploaded and tested an AWS Lamdba function!

Optional: View the function log in CloudWatch

  1. Click on the Services dropdown in the upper left corner of the AWS menu at the top of the page.
  2. Find and click on the CloudWatch service to get to the CloudWatch page.
  3. Click on the Logs dropdown on the left side of the page and then the Log Groups choice.
  4. You should see a list of log groups for the account. Look for the /aws/lambda/BD_Unit_3_Preparedness_Task group and click on it.
  5. Choose the log with the Last event timestamp (right side of page) of the function execution you would like to view. The timestamp will most likely not be your local time as the time is the AWS server time that ran your function which is probably in a different time zone.
  6. You should see the same output you viewed in the Log output window when you tested your Lambda function.

Stretch Task: Review the code in the preparedness task project so you become familiar with what it does.

The code in the preparedness task project is an example of the fundamentals required for an AWS Lambda function. It will serve as a good model for any AWS Lambda function you may code in the future.

Pseudo Mastery Task 1: Logging to the Cloud

Mastery Task Guidelines

Mastery Tasks are opportunities to test your knowledge and understanding through code. When a mastery task is shown in a module, it means that we've covered all the concepts that you need to complete that task. You will want to make sure you finish them by the end of the week to stay on track and complete the unit.

This mastery task is a little different from others you have done in prior units:

  • This is a stand alone task. It is not part of the Sprint project nor is it included in the evaluation of successful completion of the Sprint project. Hence the "Pseudo" in it's name.
  • It does not have automated style checks nor coding tests.
  • You will demonstrate your completion of this task by copying output from the task into a text file and saving it with your code. (Instructions below)
  • Your instructor will evaluate your results individually

Due to some recent reorganization of this unit and time constraints to complete those changes, the number of this task (1) has not been updated. So although it is numbered Task 1, it is actually the last Task. Do not let this confuse you. Just refer to it as the "AWS Lambda Task".

The purpose of this task is to help you become familiar with creating and testing an AWS Lambda Function and logging using the log4j framework. You will be using AWS Lambda Functions often in the next Unit and will have opportunity to incorporate logging into subsequent Unit Mastery Tasks as you deem appropriate.

Click Here and Fork and Clone the Mastery Task Repository

For this task we will be adding simple log4j logging messages for errors to an existing application.

Milestone 0: Create an AWS Lambda Function for this task.

This task will use a new AWS Lambda Function for the code given to implement and test the functionality. The AWS Lambda function should be named: bd_unit_3_logging-to-the-cloud

Please refer to Milestone-1, Milestone-2 and Milestone-3 in the Preparedness Task: Setup to Use AWS Lambda Functions document to do so.

Note: There is a file in the project called: TestExamples.json with sample json objects you may use or model for your test executions.

Milestone 1: Add Logging to an Existing Application

  1. Open the task project in Intellij.
  2. Find the AppFunctions.java code in the lambdaapp package.
  3. Perform all tasks indicated by the // TODO comments in the code.
  4. Upload and test your code in AWS Lambda. Be sure to test each error condition:
    • Try to find a customer not in the datastore. Customer ID 98765 would work.
    • Try to add a new customer with no values in the payload.
    • Use an action not defined for the application. delete would work.

Be sure to include all the attributes expected in your test data:

  • actionRequested
  • customerID
  • payload

Note: You may have alter the logic of the existing code slightly to successfully complete this task.

Milestone 2: Review Log Entries in AWS Cloudwatch

Login to AWS Cloudwatch and find the log entries created by your code. It should look something like this:

2021-11-22T10:44:51.426-07:00 START RequestId: 2f8d7873-21f9-4fb2-821c-510620d3259b Version: $LATEST 
2021-11-22T10:44:51.439-07:00 Request Received: RequestData{actionRequested='find', customerId=98765, payload=''} 
2021-11-22T10:44:51.439-07:00 17:44:51.432 [main] ERROR lambdaapp.AppFunctions - Customer Id: 98765 not found! 
2021-11-22T10:44:51.439-07:00 Response was: ResponseData{returnCode=404, aCustomer=null, message='Customer Id: 98765 not found!', creationTimeStamp='
2021-11-22 17:44:51.432'} 
2021-11-22T10:44:51.495-07:00 END RequestId: 2f8d7873-21f9-4fb2-821c-510620d3259b 
2021-11-22T10:44:51.495-07:00 REPORT RequestId: 2f8d7873-21f9-4fb2-821c-510620d3259b Duration: 48.75 ms Billed Duration: 49 ms Memory Size: 128 MB Max Memory Used: 108 MB 
2021-11-22T10:46:39.335-07:00 START RequestId: f4e4f874-d9a7-4d34-a506-3c2bf96be5e8 Version: $LATEST 
2021-11-22T10:46:39.339-07:00 Request Received: RequestData{actionRequested='add', customerId=0, payload=''} 
2021-11-22T10:46:39.340-07:00 17:46:39.339 [main] ERROR lambdaapp.AppFunctions - Cannot add a new Customer without at least a name 
2021-11-22T10:46:39.360-07:00 Response was: ResponseData{returnCode=400, aCustomer=null, message='Cannot add a new Customer without at least a name', creationTimeStamp='
2021-11-22 17:46:39.34'} 
2021-11-22T10:46:39.381-07:00 END RequestId: f4e4f874-d9a7-4d34-a506-3c2bf96be5e8 
2021-11-22T10:46:39.381-07:00 REPORT RequestId: f4e4f874-d9a7-4d34-a506-3c2bf96be5e8 Duration: 41.55 ms Billed Duration: 42 ms Memory Size: 128 MB Max Memory Used: 108 MB 
2021-11-22T10:54:46.815-07:00 START RequestId: 16523f93-ba1a-4b67-91e8-cbc627bb88d5 Version: $LATEST 
2021-11-22T10:55:03.849-07:00 Request Received: RequestData{actionRequested='delete', customerId=11111, payload=''} 
2021-11-22T10:55:04.109-07:00 17:55:04.049 [main] ERROR lambdaapp.AppFunctions - Invalid action: delete 
2021-11-22T10:55:04.690-07:00 Response was: ResponseData{returnCode=400, aCustomer=null, message='Invalid action: delete', creationTimeStamp='
2021-11-22 17:55:04.128'} 2021-11-22T10:55:04.870-07:00 END RequestId: 16523f93-ba1a-4b67-91e8-cbc627bb88d5 
2021-11-22T10:55:04.870-07:00 REPORT RequestId: 16523f93-ba1a-4b67-91e8-cbc627bb88d5 Duration: 18052.70 ms Billed Duration: 18053 ms Memory Size: 128 MB Max Memory Used: 107 MB Init Duration: 352.15 ms

Once you have verified the log entries are what you expected:

  1. Create a new text file in your IntelliJ project.
  2. Copy and paste the log entries from Cloudwatch into the text file.
  3. (the text file will be used to grade your project)

Resources