Create and Trace a Micronaut Application Using Azure Monitor

This guide describes how to use the Graal Development Kit for Micronaut (GDK) to create and trace a Micronaut® application using Azure Monitor.

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:

The application ZIP file will be downloaded in your default downloads directory. Unzip it and proceed to the next steps.

A note regarding your development environment

Consider using Visual Studio Code, which provides native support for developing applications with the Graal Development Kit for Micronaut Extension Pack.

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.

  1. Open the GDK Launcher in advanced mode.

  2. Create a new project using the following selections.
    • Project Type: Application (Default)
    • Project Name: azure-tracing-demo
    • Base Package: com.example (Default)
    • Clouds: Azure
    • 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: Yes (Default)
  3. Click Generate Project, then click Download Zip. The GDK Launcher creates an application with the package com.example in a directory named azure-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.azure-tracing-demo \
 --clouds=azure \
 --services=tracing \
 --features=graalvm \
 --build=gradle \
 --jdk=17  \
 --lang=java

Open the micronaut-cli.yml file, you can see what features are packaged with the application:

features: [app-name, azure-tracing, gdk-azure-cloud-app, gdk-azure-tracing, gdk-bom, gdk-license, graalvm, http-client, java, java-application, junit, logback, maven, maven-enforcer-plugin, micronaut-http-validation, netty-server, properties, readme, serialization-jackson, shade, static-resources]

The GDK Launcher creates a multi-module project with two subprojects: azure for Microsoft Azure, and lib for common code and configuration shared across cloud platforms. You develop the application logic in the lib subproject, and keep the Microsoft Azure-specific configurations in the azure subproject.

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 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. Create Azure Cloud Resources

You will create a resource group, a Log Analytics Workspace, and an Application Insights Resource.

First define the following environment variables. You can customize the values as needed:

export LOCATION=eastus
export RESOURCE_GROUP_NAME=gdkguides

2.1. Create a Resource Group

We recommend that you create a new resource group for this guide, but you can use an existing resource group instead.

Run the az group create command to create a resource group named gdkguides in the eastus region:

If you prefer using the region geographically closer to you, run az account list-locations to list all available regions.

2.2. Register Microsoft.OperationalInsights as a Resource Provider

Run the az provider register command to register Microsoft.OperationalInsights as a Resource Provider:

az provider register --namespace 'Microsoft.OperationalInsights'

This will take a while to execute. Check the status with:

az provider show --namespace Microsoft.OperationalInsights

Once the "registrationState": "Registering" changes to "registrationState": "Registered", the registration is complete.

2.3. Create a Log Analytics Workspace

Run the az monitor log-analytics workspace create command to create a log analytics workspace:

az monitor log-analytics workspace create \
   --workspace-name gdkworkspace \
   --location $LOCATION \
   --resource-group $RESOURCE_GROUP_NAME

2.4. Add the application-insights CLI Extension

Run the az extension add command to add the application-insights CLI Extension:

az extension add --name application-insights

2.5. Create an Application Insights Resource

Run the az monitor app-insights component create command to create an Application Insights resource:

az monitor app-insights component create \
   --app gdkapp \
   --location $LOCATION \
   --resource-group $RESOURCE_GROUP_NAME \
   --workspace gdkworkspace

The response should look like this:

{
  "appId": "e0cc1a...",
  "applicationId": "gdkapp",
  "applicationType": "web",
  "connectionString": "InstrumentationKey=2ad36...;IngestionEndpoint=https://eastus.../;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/;ApplicationId=09de44...",
  ...
  "ingestionMode": "LogAnalytics",
  "instrumentationKey": "46b7c...",
  "kind": "web",
  "laMigrationDate": null,
  "location": "eastus",
  "name": "gdkapp",
  "namePropertiesName": "gdkapp",
  "privateLinkScopedResources": null,
  "provisioningState": "Succeeded",
  "publicNetworkAccessForIngestion": "Enabled",
  "publicNetworkAccessForQuery": "Enabled",
  "requestSource": "rest",
  ...
}

Save the value of the connectionString attribute from the response, you’ll need it in the next step.

2.6. Configure the Connection String

To run the application, specify the azure.tracing.connection-string in azure/src/main/resources/application.properties with the value you saved from the previous step:

azure.tracing.connection-string=InstrumentationKey=2ad36...;IngestionEndpoint=https://eastus.../;LiveEndpoint=https://eastus.livediagnostics.monitor.azure.com/;ApplicationId=09de44...

3. Run the Application

An application can send traces to Azure Monitor from outside Azure Cloud. This enables you to run the application locally and then view the traces in Azure Monitor.

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

./gradlew :azure:run

4. Test the Application

You will test the application by accessing its REST endpoints, then review the trace output.

Navigate to Application Insights and open the gdkapp Application Insights Resource that you created earlier.

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. You should see data in the Server requests pane. It may take a few seconds for data to appear. If necessary, refresh the page.

    Application Insights Get Count
  3. Click the requests to view the details:

    Trace Details Get Count
  4. Click Drill into…​ 2 Samples to view the spans. 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. Return to the gdkapp Application Insight: you should see new request data that represents the trace of the order; click it to view the details:

    Application Insights 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. Navigate again to the gdkapp Application Insight and click the latest requests to see the data for the inventory request:

    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

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.

  1. To generate a native executable, use the following command:

    ./gradlew :azure:nativeCompile

    The native executable is created in the azure/build/native/nativeCompile/ directory

  2. You can then run the native executable with the following command:

    azure/build/native/nativeCompile/azure-tracing-demo-azure

    You can customize the name of the resulting binary by updating the Maven/Gradle plugin for GraalVM Native Image configuration.

6. Run and Test the Native Executable

Run the native executable, and then perform the same tests as in step 4.

7. Clean up Cloud Resources

Once you are done with this guide, you can delete the Azure resources created to avoid incurring unnecessary charges.

Delete the resource group and all of its resources with:

Alternatively, run these commands to delete resources individually:

Summary

This guide demonstrated how to create and trace a Micronaut application using Azure Monitor.