Create and Trace a Micronaut Application Using AWS X-Ray
This guide describes how to use the Graal Development Kit for Micronaut (GDK) to create and trace a Micronaut® application using AWS X-Ray.
AWS X-Ray is a service to monitor application traces, including the performance of calls to downstream components or services, in either cloud-hosted applications or locally deployments.
Tracing enables you to track service requests in a single or distributed application. Trace data shows the path, time spent in each section (called a span), and other information collected along during the trace. Tracing gives you observability into what is causing bottlenecks and failures.
Prerequisites #
- JDK 17 or higher. See Setting up Your Desktop.
- An Amazon Web Services (AWS) account. See Setting up Your Cloud Accounts.
- The AWS CLI.
- A Docker-API compatible container runtime such as Rancher Desktop or Docker.
- The GDK CLI. See Setting up Your Desktop. (Optional.)
Follow the steps below to create the application from scratch. However, you can also download the completed example:
A note regarding your development environment
Consider using Visual Studio Code, which provides native support for developing applications with the Graal Development Kit extension.
Note: If you use IntelliJ IDEA, enable annotation processing.
Windows platform: The GDK guides are compatible with Gradle only. Maven support is coming soon.
1. Create the Application #
Create an application using the GDK Launcher.
-
Open the GDK Launcher in advanced mode.
- Create a new project using the following selections.
- Project Type: Application (default)
- Project Name: aws-tracing-demo
- Base Package: com.example (Default)
- Clouds: AWS
- Build Tool: Gradle (Groovy) or Maven
- Language: Java (Default)
- Test Framework: JUnit (default)
- Java Version: 17 (default)
- Micronaut Version: (default)
- Cloud Services: Tracing
- Features: GraalVM Native Image (Default)
- Sample Code: No
- Click Generate Project, then click Download Zip. The GDK Launcher creates an application with the default package
com.example
in a directory named aws-tracing-demo. The application ZIP file will be downloaded to your default downloads directory. Unzip it, open it in your code editor, and proceed to the next steps.
Alternatively, use the GDK CLI as follows:
gdk create-app com.example.aws-tracing-demo \
--clouds=aws \
--services=tracing \
--features=graalvm \
--example-code=false \
--build=gradle \
--jdk=17 \
--lang=java
gdk create-app com.example.aws-tracing-demo \
--clouds=aws \
--services=tracing \
--features=graalvm \
--example-code=false \
--build=maven \
--jdk=17 \
--lang=java
For more information, see Using the GDK CLI.
1.1. Tracing Annotations #
The Micronaut framework uses the io.micronaut.tracing.annotation
package and OpenTelemetry to generate and export tracing data.
The io.micronaut.tracing.annotation
package provides the following three annotations:
-
@NewSpan
: Used on methods to create a new span; defaults to the method name, but you can assign a unique name instead. -
@ContinueSpan
: Used on methods to continue an existing span; primarily used in conjunction with@SpanTag
(below). -
@SpanTag
: Used on method parameters to assign a value to a span; defaults to the parameter name, but you can assign a unique name instead. To use the@SpanTag
on a method parameter, the method must be annotated with either@NewSpan
or@ContinueSpan
.
Your build configuration file contains the io.micronaut.tracing
dependency which means all HTTP server methods (those annotated with @Get
, @Post
, and so on) create spans automatically.
1.2. InventoryService #
Note: The
InventoryService
class demonstrates how to create and use spans fromio.micronaut.tracing.annotation
and OpenTelemetry.
The GDK Launcher created the InventoryService
class in a file named lib/src/main/java/com/example/InventoryService.java, as follows:
package com.example;
import io.micronaut.tracing.annotation.NewSpan;
import io.micronaut.tracing.annotation.SpanTag;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import jakarta.inject.Singleton;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Singleton
public class InventoryService {
private static final String storeName = "my_store";
private final Tracer tracer;
private final WarehouseClient warehouse;
private final Map<String, Integer> inventory = new ConcurrentHashMap<>();
InventoryService(Tracer tracer, WarehouseClient warehouse) { // <1>
this.tracer = tracer;
this.warehouse = warehouse;
inventory.put("laptop", 4);
inventory.put("desktop", 2);
inventory.put("monitor", 11);
}
public Collection<String> getProductNames() {
return inventory.keySet();
}
@NewSpan("stock-counts") // <2>
public Map<String, Integer> getStockCounts(@SpanTag("inventory.item") String item) { // <3>
Map<String, Integer> counts = new HashMap<>();
if (inventory.containsKey(item)) {
int count = inventory.get(item);
counts.put("store", count);
if (count < 10) {
counts.put("warehouse", inWarehouse(storeName, item));
}
}
return counts;
}
private int inWarehouse(String store, String item) {
Span.current().setAttribute("inventory.store-name", store); // <4>
return warehouse.getItemCount(store, getUPC(item));
}
public void order(String item, int count) {
orderFromWarehouse(item, count);
if (inventory.containsKey(item)) {
count += inventory.get(item);
}
inventory.put(item, count);
}
private void orderFromWarehouse(String item, int count) {
Span span = tracer.spanBuilder("warehouse-order") // <5>
.setAttribute("item", item)
.setAttribute("count", count)
.startSpan();
warehouse.order(Map.of(
"store", storeName,
"product", item,
"amount", count,
"upc", getUPC(item)));
span.end(); // <6>
}
private int getUPC(String item) {
return Math.abs(item.hashCode());
}
}
1 Inject an OpenTelemetry Tracing bean into the class.
2 Create a new io.micronaut.tracing.annotation
span called “stock-counts”.
3 Add a io.micronaut.tracing.annotation
tag called “inventory.item” that will contain the value contained in the parameter item
.
4 Get the current OpenTelemetry span and set the value of its attribute named “inventory.store-name” to the store
parameter.
5 Create an OpenTelemetry span named “warehouse-order”, set its attributes and start the span.
6 End the span started in 5.
1.3. Store Controller #
The GDK Launcher created the StoreController
class in a file named lib/src/main/java/com/example/StoreController.java, as follows:
package com.example;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.annotation.Status;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;
import io.micronaut.tracing.annotation.ContinueSpan;
import io.micronaut.tracing.annotation.NewSpan;
import io.micronaut.tracing.annotation.SpanTag;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static io.micronaut.http.HttpStatus.CREATED;
@ExecuteOn(TaskExecutors.IO)
@Controller("/store")
class StoreController {
private final InventoryService inventory;
StoreController(InventoryService inventory) {
this.inventory = inventory;
}
@Post("/order")
@Status(CREATED)
@NewSpan("store.order") // <1>
void order(@SpanTag("order.item") String item, @SpanTag int count) { // <2>
inventory.order(item, count);
}
@Get("/inventory") // <3>
List<Map<String, Object>> getInventory() {
return inventory.getProductNames().stream()
.map(this::getInventory)
.collect(Collectors.toList());
}
@Get("/inventory/{item}")
@ContinueSpan // <4>
Map<String, Object> getInventory(@SpanTag("item") String item) { // <5>
Map<String, Object> counts = new HashMap<>(inventory.getStockCounts(item));
if (counts.isEmpty()) {
counts.put("note", "Not available at store");
}
counts.put("item", item);
return counts;
}
}
1 Create a new span called “store.order”.
2 Add tags for the method parameters. Name the first parameter “order.item”, and use the default name for the second parameter.
3 A span is created automatically if your build configuration file contains the io.micronaut.tracing
dependency.
4 Required for @SpanTag
(see 5).
5 Add a tag for the method parameter.
1.4. Warehouse Client #
The GDK Launcher created the WarehouseClient
class in a file named lib/src/main/java/com/example/WarehouseClient.java, as follows:
package com.example;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.annotation.QueryValue;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.tracing.annotation.ContinueSpan;
import io.micronaut.tracing.annotation.NewSpan;
import io.micronaut.tracing.annotation.SpanTag;
import java.util.Map;
@Client("/warehouse") // <1>
public interface WarehouseClient {
@Post("/order")
@NewSpan
void order(@SpanTag("warehouse.order") Map<String, ?> json);
@Get("/count")
@ContinueSpan
int getItemCount(@QueryValue String store,
@SpanTag @QueryValue int upc);
}
1 Some external service without tracing.
1.5. Warehouse Controller #
The GDK Launcher created a WarehouseController
class to represent an external service that will be called by WarehouseClient
in a file named lib/src/main/java/com/example/WarehouseController.java, as follows:
package com.example;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;
import java.util.Random;
@ExecuteOn(TaskExecutors.IO) //<1>
@Controller("/warehouse") // <2>
class WarehouseController {
@Get("/count") // <3>
HttpResponse<?> getItemCount() {
return HttpResponse.ok(new Random().nextInt(11));
}
@Post("/order") // <4>
HttpResponse<?> order() {
try {
//To simulate an external process taking time
Thread.sleep(500);
} catch (InterruptedException ignored) {
}
return HttpResponse.accepted();
}
}
1 It is critical that any blocking I/O operations (such as fetching the data from the database) are offloaded to a separate thread pool that does not block the event loop.
2 The class is defined as a controller with the @Controller
annotation mapped to the path /warehouse
.
3 The @Get
annotation maps the getItemCount
method to an HTTP GET request on /warehouse/count
.
4 The @Get
annotation maps the order
method to an HTTP GET request on /warehouse/order
.
2. Setup AWS Tracing Export #
To run tracing on AWS, run the AWS OTel Collector locally and set up the application to export traces to the collector.
2.1. Configure AWS Authentication #
To test exporting traces to AWS X-Ray from a local machine, you may use an AWS Access key configured in the AWS CLI. If you do not have AWS CLI, follow the AWS documentation for installing or updating the latest version of the AWS CLI.
If you do not have an access key, complete the steps in AWS Secure Credential Types Documentation to create a long-term access key for your account. Run aws configure
and provide access key ID and secret access key to authenticate as your user.
Instead of using your AWS root account, use an administrator account. If you do not have one already, see Setting up Your Cloud Accounts.
2.2. Run the AWS OTel Collector #
AWS Open Telemetry Collector will use the configured AWS profile to export traces to AWS X-Ray. To run the AWS Collector, you will need a configuration file.
- Save the example configuration file from the AWS OTel repository as otel-local-config.yaml:
curl https://raw.githubusercontent.com/aws-observability/aws-otel-collector/main/examples/docker/config-test.yaml --output otel-local-config.yaml
-
Open the file and change the value of
exporters.awsxray.region
to your AWS region. -
Run the AWS Collector container image using the command similar to the following. Edit the command to match the AWS region from previous step and change the AWS profile if needed.
docker run --name awscollector --rm -p 4317:4317 -p 55680:55680 -p 8889:8888 \ -e AWS_REGION=us-east-1 -e AWS_PROFILE=default \ -v ~/.aws:/home/aoc/.aws -v "$(pwd)/otel-local-config.yaml:/otel-local-config.yaml" \ public.ecr.aws/aws-observability/aws-otel-collector:latest \ --config otel-local-config.yaml
Note: For Windows, install Docker Desktop. Start the WSL terminal, and use the following command (replace
<Username>
with your Windows user name):docker run --name awscollector --rm -p 4317:4317 -p 55680:55680 -p 8889:8888 \ -e AWS_REGION=us-east-2 -e AWS_PROFILE=default \ -v "/mnt/c/Users/<Username>/.aws:/home/aoc/.aws" -v "$(pwd)/otel-local-config.yaml:/otel-local-config.yaml" \ public.ecr.aws/aws-observability/aws-otel-collector:latest \ --config otel-local-config.yaml
If you are using other container manager Desktop applications, configure WSL integration in their settings (for example, Rancher Desktop automatically installs WSL and configures a Windows terminal profile that you can use to run the command).
Visit the AWS OTel Docker demo documentation page for more information on starting the AWS Open Telemetry Docker Collector.
2.3. Configure Tracer #
Change the application configuration to export traces to the AWS OTel Collector that you just started. Add the following to the aws/src/main/resources/application.properties configuration file:
otel.traces.exporter=otlp
otel.traces.propagator=tracecontext, baggage, xray
3. Run the Application #
Application traces can be sent to AWS X-Ray from outside Amazon Web Services. This allows you to run the application locally and view the traces in the AWS X-Ray Trace Explorer.
To run the application locally, use the following command, which starts the application on port 8080:
./gradlew :aws:run
./mvnw install -pl lib -am
./mvnw mn:run -pl aws -Dmicronaut.application.name=aws
4. Test the Application #
Test the application by accessing REST endpoints. Then review the trace output.
Open AWS X-Ray:
It may take a few seconds for the traces to be displayed.
4.1. Get Item Counts #
- Send an HTTP GET request to the
/store/inventory/{item}
endpoint to get the count of an item, for example:curl http://localhost:8080/store/inventory/laptop
-
Open the Trace Explorer and refresh it. You should see a row in the list of traces that corresponds to the endpoint to which you sent the request:
-
Click the trace link to view its details. Each span is represented by a green bar. There is a node graph representing a request that would contain more nodes if multiple applications were used in the request or resources, such as a database.
4.2. Order Items #
-
Send an HTTP POST request to the
/store/order
endpoint to order an item, as follows:curl -X "POST" "http://localhost:8080/store/order" \ -H 'Content-Type: application/json; charset=utf-8' \ -d $'{"item":"laptop", "count":5}'
-
Review the output again in the Trace Explorer: you should see a new row that represents the trace of the order. You can find it by looking at the time stamps of the traces and picking the most recent.
-
Review the details.
4.3. Get Inventory #
-
Send an HTTP GET request to the
/store/inventory
endpoint to get the inventory:curl http://localhost:8080/store/inventory
-
Review the output again in the Trace Explorer: you should see a new row that represents the trace of the request to retrieve the inventory. Click the trace link to view its details. You should see something similar to the following screenshot.
Looking at the trace, you may conclude that retrieving the items sequentially might not be the best design choice.
5. Generate a Native Executable Using GraalVM #
The GDK supports compiling Java applications ahead-of-time into native executables using GraalVM Native Image. You can use the Gradle plugin for GraalVM Native Image building/Maven plugin for GraalVM Native Image building. Packaged as a native executable, it significantly reduces application startup time and memory footprint.
Prerequisites: Make sure you have installed a GraalVM JDK. The easiest way to get started is with SDKMAN!. For other installation options, visit the Downloads section.
To generate a native executable, run the following command:
./gradlew :aws:nativeCompile
./mvnw install -pl lib -am
./mvnw package -pl aws -Dpackaging=native-image
6. Run and Test the Native Executable #
Run the native executable, using the option -Dmicronaut.application.name
to set the name of the application as “aws-native-demo”, as follows:
aws/build/native/nativeCompile/aws-tracing-demo-aws -Dmicronaut.application.name=aws-native-demo
aws/target/aws-tracing-demo-aws -Dmicronaut.application.name=aws-native-demo
The native executable starts instantaneously.
6.1. Get Item Counts #
-
Repeat the test from step 4.1 by sending an HTTP GET request to the
/store/inventory/{item}
endpoint to get the count of an item:curl http://localhost:8080/store/inventory/laptop
-
Review the trace output in Trace Explorer: you should see a new row in the list of Traces that represents the request from the native executable:
Notice that the native executable requests are processed a lot faster than its original.
-
Click the trace name to view its details. Each span is represented by a green bar.
6.2. Order Items #
-
Repeat the test from step 4.2 by sending an HTTP POST request to the
/store/order
endpoint to order an item:curl -X "POST" "http://localhost:8080/store/order" \ -H 'Content-Type: application/json; charset=utf-8' \ -d $'{"item":"laptop", "count":5}'
You should see a new row in the Trace Explorer that represents the POST request.
-
Click the most recent trace name to view its details.
-
Click the span named
aws-native-demo: StoreController.order#store.order
to view its details showing the custom span attributes:count
andorder.item
.
Summary #
This guide demonstrated how to create and trace a Micronaut application using AWS X-Ray.