Create and Trace a Micronaut Application Using Oracle Cloud Infrastructure Application Performance Monitoring

This guide describes how to to create and trace a Micronaut application using Oracle Cloud Infrastructure Application Performance Monitoring (APM).

Oracle Cloud Infrastructure Application Performance Monitoring provides a comprehensive set of features to monitor applications and diagnose performance issues.

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 #

Follow the steps below to create the application from scratch. However, you can also download the completed example in Java:

A note regarding your development environment

Consider using Visual Studio Code that provides native support for developing applications with the Graal Cloud Native Tools.

Note: If you use IntelliJ IDEA, enable annotation processing.

Windows platform: The GCN guides are compatible with Gradle only. Maven support is coming soon.

1. Create the Application #

Create an application using the GCN Launcher.

  1. Open the GCN Launcher in advanced mode.

  2. Create a new project using the following selections.
    • Project Type: Application (Default)
    • Project Name: oci-tracing-demo
    • Base Package: com.example (Default)
    • Clouds: OCI
    • Language: Java (Default)
    • Build Tool: Gradle (Groovy) or Maven
    • Test Framework: JUnit (Default)
    • Java Version: 17 (Default)
    • Micronaut Version: (Default)
    • Cloud Services: Tracing
    • Features: GraalVM Native Image (Default)
    • Sample Code: No
  3. Click Generate Project, then click Download Zip. The GCN Launcher creates an application with the default package com.example in a directory named oci-tracing-demo. The application ZIP file will be downloaded in your default downloads directory. Unzip it, open in your code editor, and proceed to the next steps.

Alternatively, use the GCN CLI as follows:

gcn create-app com.example.oci-tracing-demo \
    --clouds=oci \
    --services=tracing \
    --features=graalvm \
    --example-code=false \
    --build=gradle \
    --lang=java
gcn create-app com.example.oci-tracing-demo \
    --clouds=oci \
    --services=tracing \
    --features=graalvm \
    --example-code=false \
    --build=maven \
    --lang=java

For more information, see Using the GCN 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 from io.micronaut.tracing.annotation and OpenTelemetry.

The GCN 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.4. Store Controller #

The GCN 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 GCN 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 GCN 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 Oracle Cloud Infrastructure Application Performance #

To run tracing on Oracle Cloud Infrastructure, create an Application Performance Monitoring domain and configure the application’s tracer.

2.1. Create an Application Performance Monitoring Domain #

  1. Follow the steps in Create an APM Domain, using the following properties:

    • Name: Enter a name, for example, “gcn-apm-demo1”.
    • Create in compartment: Select the compartment from the compartment drop-down list, or use the default.
    • Description: Enter a description, such as “A domain to demo APM with GCN”.
  2. When the APM domain is created, click its name to view its details.

    • Take note of the Data Upload Endpoint (click Copy). (This guide refers to it as <DataUploadEndpoint>, it should resemble aaaaaaaaannaaannaa.apm-agt.us-phoenix-1.oci.oraclecloud.com.)

    • Click Data Keys in the list of resources and then take note of the public key (click Copy). (This guide refers to it as [public key], it should resemble AAANAANAANNNAAAAANNN.)

2.2. Configure the Tracer #

APM requires you to construct a URL by using the data upload endpoint as your base URL, and generating the path based on some choices, including values from your private or public key.

When you created your application, the GCN Launcher automatically added a default configuration to the file named oci/src/main/resources/application-oraclecloud.properties. Replace <DataUploadEndpoint> and [public key] with the values from the previous step.

GCN will use these values to construct the APM Collector URL which should resemble https://aaaaaaaaannaaannaa.apm-agt.us-phoenix-1.oci.oraclecloud.com/20200101/observations/public-span?dataFormat=zipkin&dataFormatVersion=2&dataKey=AAANAANAANNNAAAAANNN.

For more information about the endpoint format, see APM Collector URL Format.

3. Run the Application #

An application can send traces to APM from outside Oracle Cloud Infrastructure. This enables you to run the application locally and then view the traces in the APM Trace Explorer.

To run the application locally, use the following command, which starts the application on port 8080:

./gradlew :oci:run
./mvnw install -pl lib -am && ./mvnw mn:run -pl oci

4. Test the Application #

Test the application by accessing its REST endpoints, then review the trace output.

Open the Oracle Cloud Infrastructure APM Trace Explorer. Select the appropriate compartment and the APM domain that you created, then click Run to run the query.

It may take a few seconds for the traces to appear in the explorer. If necessary, run the query again by clicking Run, or by refreshing the page.

4.1. Get Item Count #

  1. 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
    
  2. Review the output in the Trace Explorer: you should see a row in the list of Traces.

    Trace Explorer Get Count

  3. Click the trace name to view its details. Each span is represented by a blue bar.

    Trace Details Get Count

4.2. Order an Item #

  1. 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}'
    
  2. Review the output again in the Trace Explorer: you should see a new row that represents the trace of the order.

    Trace Explorer Post Order

4.3. Get Inventory #

  1. Send an HTTP GET request to the /store/inventory endpoint to get the inventory:

     curl http://localhost:8080/store/inventory
    
  2. 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 name to view its details. You should see something similar to the following screenshot.

    inventory all

    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 #

GCN supports compiling a Java application ahead-of-time into a native executable 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.

To generate a native executable, run the following command:

./gradlew :oci:nativeCompile
./mvnw install -pl lib -am && ./mvnw package -pl oci -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 “oci-native”, as follows:

./oci/build/native/nativeCompile/oci -Dmicronaut.application.name=oci-native
./oci/target/oci -Dmicronaut.application.name=oci-native

The native executable starts instantaneously.

6.1. Get Item Count #

  1. 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.

  2. 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:

    Trace Explorer Get Count Natively

    Notice that the native executable requests are processed much faster than the Java application.

  3. Click the trace name to view its details. Each span is represented by a blue bar.

    Trace Details Get Count Natively

6.2. Order an Item #

  1. Repeat the test from step 4.2 by sending an HTTP POST request to the /store/order endpoint to order an item.

    You should see a new row in the Trace Explorer that represents the POST request.

    Note: If you click a span, you should see its details. However, for auto-activation of custom span attributes, APM requires the attributes to be seen at least five times. Spans created prior to auto-activation will not display these attributes. This means that the custom span attributes (count and order.item) you added to the source code will not be displayed in the Span Details. To work around this constraint, run the command above a further ten times.

    You should see ten new rows in the list of Traces.

    Trace Explorer Ten Orders

  2. Click the most recent trace name to view its details.

    Trace Details Tenth Request

  3. Click the span named oci-native: storecontroller.order#store.order to view its details showing the custom span attributes - count and order.item.

    Span Details

Summary #

This guide demonstrated how to create and trace a Micronaut application using Oracle Cloud Infrastructure Application Performance Monitoring.