Thundra

Thundra: Serverless Observability for AWS Lambda

The black box nature of AWS Lambda and other serverless environments means that identifying and fixing performance issues is difficult and time-consuming. Built for straightforward debugging, monitoring, and observability, Thundra provides deep insight into your entire serverless environment. Thundra collects and correlates all your metrics, logs, and traces, allowing you to quickly identify problematic invocations and also analyzes external services associated with that function. With Thundra’s zero overhead and automated instrumentation capabilities, your developers are free to write code without worrying about bulking up their Lambdas or wasting time on chasing black box problems.

Get Started    Discussions

Installation and Configuration

Installation and Migration

Request Handler

Let’s assume that you have the following RequestHandler based Lambda handler and want to migrate to Thundra:


...

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

...

public class UserGetHandler 
        implements RequestHandler<UserGetRequest, UserGetResponse> {

    private UserService userService;

    ...

    @Override
    public UserGetResponse handleRequest(UserGetRequest request, 
                                         Context context) {
        // Get the user through user service
        ...
    }

    ...

}

You need to make the following changes:

  • Implement from Thundra’s io.thundra.agent.lambda.core.handler.request.LambdaRequestHandler instead of AWS Lambda API’s com.amazonaws.services.lambda.runtime.RequestHandler
  • Update method name handleRequest, which is inherited from AWS Lambda API’s com.amazonaws.services.lambda.runtime.RequestHandler, as doHandleRequest which is inherited from Thundra’s io.thundra.agent.lambda.core.handler.request.LambdaRequestHandler
...

import com.amazonaws.services.lambda.runtime.Context;
import io.thundra.agent.lambda.core.handler.request.LambdaRequestHandler;

...

public class UserGetHandler 
        implements LambdaRequestHandler<UserGetRequest, UserGetResponse> {

    private UserService userService;

    ...

    @Override
    public UserGetResponse doHandleRequest(UserGetRequest request, 
                                           Context context) {
        // Get the user through user service
        ...
    }

    ...

}

Do not Specify Method Name

For your handler config, you SHOULDN'T specify method name (handleRequest) but just handler class full name. Because if method name is specified, AWS Lambda tries to resolve the request type from method arguments by Java Reflection API. But since generic types are not available at method level, requests data is deserialized into java.util.Map instance and this leads to ClassCastExceptions while passing request to your handler. Therefore, at your handler config, just specify handler full class name (io.thundra.lambda.demo.handler.user.UserGetHandler in this example) without method name (which it is not needed).

Request Stream Handler

Let’s assume that you have the following RequestStreamHandler based Lambda handler and want to migrate to Thundra:

...

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

...

public class UserGetHandler implements RequestStreamHandler {

    private UserService userService;

    ...

    @Override
    public void handleRequest(InputStream input, 
                              OutputStream output, 
                              Context context) throws IOException {
        // Deserialize the request from input stream
        // Get the user through user service
        // Serialize the response into output stream
        ...
    }

    ...

}

You need to make the following changes:

  • Implement Thundra’s io.thundra.agent.lambda.core.handler.requeststream.LambdaRequestStreamHandler instead of AWS Lambda API’s com.amazonaws.services.lambda.runtime.RequestStreamHandler
  • Remove handleRequest method which is inherited from AWS Lambda API’s com.amazonaws.services.lambda.runtime.RequestStreamHandler
  • Implement doHandleRequest method as shown below. Notice that in here you don’t get java.io.InputStream for request stream and java.io.OutputStream for response stream as inherited from AWS Lambda API’s com.amazonaws.services.lambda.runtime.RequestStreamHandler anymore. Instead, you get the request object and return the response object.
...

import com.amazonaws.services.lambda.runtime.Context;
import io.thundra.agent.lambda.core.handler.requeststream.LambdaRequestStreamHandler;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

...

public class UserGetHandler 
        implements LambdaRequestStreamHandler<UserGetRequest, UserGetResponse> {

    private UserService userService;

    ...

    @Override
    public UserGetResponse doHandleRequest(UserGetRequest request, 
                                           Context context) {
        // Get the user through user service
        ...
    }

    ...

}

Optionally, you can override getSerDe method and provide your custom io.thundra.agent.lambda.core.handler.requeststream.SerDe implementation if you need (this is optional) to customize deserialization of request and serialization of response on your own. By default JSON based serialization/deserialization is applied.

...

import com.amazonaws.services.lambda.runtime.Context;
import io.thundra.agent.lambda.core.handler.requeststream.LambdaRequestStreamHandler;
import io.thundra.agent.lambda.core.handler.requeststream.SerDe;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

...

public class UserGetHandler 
        implements LambdaRequestStreamHandler<UserGetRequest, UserGetResponse> {

    private UserService userService;

    ...

    @Override
    public UserGetResponse doHandleRequest(UserGetRequest request, 
                                           Context context) {
        // Get the user through user service        
        ...
    }

    @Override
    public SerDe<UserGetRequest, UserGetResponse> getSerDe() {
        return new UserGetSerDe();
    }

    private static class UserGetSerDe 
                implements SerDe<UserGetRequest, UserGetResponse> {

        @Override
        public UserGetRequest readRequest(InputStream in) 
                throws IOException {
            // Deserialize the request from input stream
            ...
        }

        @Override
        public void writeResponse(UserGetResponse response, OutputStream out) 
                throws IOException {
        	// Serialize the response into output stream
        	...
        }
        
    }

    ...

}

Configuration

We are hosting Thundra artifacts over our own Nexus repository, so to get the Thundra artifacts, you need to add the Thundra repository definition:


<repositories> 
    <repository> 
        <id>thundra-releases</id> 
        <name>Thundra Releases</name>
        <url>http://repo.thundra.io/content/repositories/thundra-releases</url> 
    </repository> 
</repositories>
repositories { 
    maven { 
        url "http://repo.thundra.io/content/repositories/thundra-releases" 
    } 
}

Then add the required dependencies to your application. Latest available Thundra version (to be used as ${thundra.version}) is 2.0.0:

<dependencies> 

    ...

    <!-- Trace support if you need -->
    <dependency> 
        <groupId>io.thundra.agent</groupId> 
        <artifactId>thundra-agent-lambda-trace</artifactId> 
        <version>${thundra.version}</version>
    </dependency> 

    <!-- Metric support if you need -->
    <dependency> 
        <groupId>io.thundra.agent</groupId> 
        <artifactId>thundra-agent-lambda-metric</artifactId> 
        <version>${thundra.version}</version>
    </dependency>

    <!-- Log support if you need -->
    <dependency> 
        <groupId>io.thundra.agent</groupId> 
        <artifactId>thundra-agent-lambda-log</artifactId> 
        <version>${thundra.version}</version>
    </dependency>

    ...  

</dependencies>
dependencies { 
    ...

    // Audit (trace) support if you need 
    compile group: 'com.opsgenie.thundra, 
            name: 'thundra-lambda-audit', 
            version: '${thundra.version}' 

    // Stat (metric) support if you need 
    compile group: 'com.opsgenie.thundra, 
            name: 'thundra-lambda-stat', 
            version: '${thundra.version}' 

    // Log support if you need 
    compile group: 'com.opsgenie.thundra, 
            name: 'thundra-lambda-log', 
            version: '${thundra.version}' 

    ...
}

Thundra has a plugin-based architecture and discovers its plugins based on Java Service API (https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) protocol. So the plugins are defined under META-INF/services folder and discovered by Thundra’s core engine from there. Since there might be multiple plugin (or service) definitions of the same plugin interface (so has same plugin definition file name), the plugin definitions must be merged. For this reason, it is also advised to add the following configurations to your build process.

Maven:
If you are using shade plugin to create single (uber) artifact, you should add ServicesResourceTransformer transformer to merge Thundra’s plugin (service) definition files.

<plugins> 
    
    ...  

    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        ...
        <configuration>
            ...
            <transformers> 
                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> 
            </transformers> 
            ...
        </configuration>
        ... 
    </plugin>
    
    ...

</plugins>

Gradle:
If you are using shadow plugin to create single (uber) artifact, you should add mergeServiceFiles merge Thundra’s plugin (service) definition files.

...

shadowJar { 
    ...

    mergeServiceFiles() 

    ...
}

...

Additional Configurations

Thundra's Java agent is extremely rich in its monitoring abilities. Therefore, you can configure additional parameters in your configurations and to do so, you can see a list of configurable parameters here.

Deploying on AWS Lambda

Deployment after configuration your Lambda functions allows you decide how to get your configured Java Lambda functions up and running on AWS Lambda. Thundra for Java functions can be deployed in two ways, providing flexibility in how you prefer to monitor your applications. These include:

  • Serverless Framework - Setting environment variables in .yml file
  • Configuration via AWS console - Setting environment variables in AWS console

Serverless

Serverless framework deployment is facilitated by using Thundra’s environment variables and setting them in your serverless.yml file. Deploying your serverless application will set these variables and allow Thundra to monitor your functions. A list of environment variables that you may configure in your serverless.yml file can be found here.

Configuration via AWS Console

This allows you to set Thundra’s environment variables in your AWS lambda function console itself. Hence allowing you to set them through AWS.

To do so, ensure that you have all the required Java files zipped in a single file. By choosing the ‘Upload a zip file’ option in the ‘Code entry type’ drop-down field of the console, you can upload your code with a few clicks, and begin to configure Thundra as you prefer.

Environment variables go in the ‘Tags’ field of the console and clicking 'Save' on the top right-hand side will set your configurations.