Blog Post

Apps on Azure Blog
8 MIN READ

Send metrics from Micronaut native image applications to Azure Monitor

Logico_jp's avatar
Logico_jp
Icon for Microsoft rankMicrosoft
Aug 15, 2025

The original post (Japanese) was written on 20 July 2025.
MicronautからAzure Monitorにmetricを送信したい – Logico Inside

This entry is related to the following one. Please take a look for background information.
Send signals from Micronaut native image applications to Azure Monitor | Microsoft Community Hub

Prerequisites

  • Maven: 3.9.10
  • JDK version 21
  • Micronaut: 4.9.0 or later

The following tutorials were used as a reference.

Create a Micronaut Application to Collect Metrics and Monitor Them on Azure Monitor Metrics

Collect Metrics with Micronaut

Create an archetype

We can create an archetype using Micronaut’s CLI (mn) or Micronaut Launch. In this entry, use application.yml instead of application.properties for application configuration. So, we need to specify the feature “yaml” so that we can include dependencies for using yaml.

Micronaut Launch
https://micronaut.io/launch/

$ mn create-app \
--build=maven \
--jdk=21 \
--lang=java \
--test=junit \
--features=validation,graalvm,micrometer-azure-monitor,http-client,micrometer-annotation,yaml \
dev.logicojp.micronaut.azuremonitor-metric

When using Micronaut Launch, click [FEATURES] and select the following features.

  • validation
  • graalvm
  • micrometer-azure-monitor
  • http-client
  • micrometer-annotation
  • yaml

After all features are selected, click [GENERATE PROJECT] and choose [Download Zip] to download an archetype in Zip file.

Implementation

In this section, we’re going to use the GDK sample code that we can find in the tutorial. The code is from the Micronaut Guides, but the database access and other parts have been taken out. We have made the following changes to the code to make it fit our needs.

a) Structure of the directory

In the GDK tutorial, folders called azure and lib are created, but this structure isn’t used in the standard Micronaut archetype. So, codes in both directories has now been combined.

b) Instrumentation Key

As the tutorial above and the Micronaut Micrometer documentation explain, we need to specify the Instrumentation Key. When we create an archetype using Micronaut CLI or Micronaut Launch, the configuration assuming the use of the Instrumentation Key is included in application.properties / application.yml.

6.3 Azure Monitor Registry
Micronaut Micrometer

This configuration will work, but currently, Application Insights does not recommend accessing it using only the Instrumentation Key. So, it is better to modify the connection string to include the Instrumentation Key. To set it up, open the file application.properties and enter the following information:

micronaut.metrics.export.azuremonitor.connectionString="InstrumentationKey=...."

In the case of application.yml, we need to specify the connection string in YAML format.

micronaut:
  metrics:
    enabled: true
    export:
      azuremonitor:
        enabled: true
        connectionString: InstrumentationKey=....

We can also specify the environment variable MICRONAUT_METRICS_EXPORT_AZUREMONITOR_CONNECTIONSTRING, but since this environment variable name is too long, it is better to use a shorter one. Here’s an example using AZURE_MONITOR_CONNECTION_STRING (which is also long, if you think about it).

micronaut.metrics.export.azuremonitor.connectionString=${AZURE_MONITOR_CONNECTION_STRING}
micronaut:
  metrics:
    enabled: true
    export:
      azuremonitor:
        enabled: true
        connectionString: ${AZURE_MONITOR_CONNECTION_STRING}

The connection string can be specified because Micrometer, which is used internally, already supports it. We can find the AzurMonitorConfig.java file here.

AzureMonitorConfig.java
micrometer/implementations/micrometer-registry-azure-monitor/src/main/java/io/micrometer/azuremonitor/AzureMonitorConfig.java at main · micrometer-metrics/micrometer

The settings in application.properties/application.yml are as follows. For more information about the specified meter binders, please look at the following documents.

Meter Binder
Micronaut Micrometer

micronaut:
  application:
    name: azuremonitor-metric
  metrics:
    enabled: true
    binders:
      files:
        enabled: true
      jdbc:
        enabled: true
      jvm:
        enabled: true
      logback:
        enabled: true
      processor:
        enabled: true
      uptime:
        enabled: true
      web:
        enabled: true
    export:
      azuremonitor:
        enabled: true
        step: PT1M
        connectionString: ${AZURE_MONITOR_CONNECTION_STRING}

c) pom.xml

To use the GraalVM Reachability Metadata Repository, you need to add this dependency. The latest version is 0.11.0 as of 20 July, 2025.

<dependency>
    <groupid>org.graalvm.buildtools</groupid>
    <artifactid>graalvm-reachability-metadata</artifactid>
    <version>0.11.0</version>
</dependency>

Add the GraalVM Maven plugin and enable the use of GraalVM Reachability Metadata obtained from the above dependency. This plugin lets us set optimization levels using buildArg (in this example, the optimisation level is specified). We can also add it to native-image.properties, the native-image tool (and the Maven/Gradle plugin) will read it.

<plugin>
    <groupid>org.graalvm.buildtools</groupid>
    <artifactid>native-maven-plugin</artifactid>
    <configuration>
        <metadatarepository>
            <enabled>true</enabled>
        </metadatarepository>
        <buildargs combine.children="append">
            <buildarg>-Ob</buildarg>
        </buildargs>
        <quickbuild>true</quickbuild>
    </configuration>
</plugin>

For now, let’s build it as a Java application.

$ mvn clean package

Check if it works as a Java application

At first, verify that the application is running without any problems and that metrics are being sent to Application Insights. Then, run the application using the Tracing Agent to generate the necessary configuration files.

# (1) Collect configuration files such as reflect-config.json
$JAVA_HOME/bin/java \
  -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/{groupId}/{artifactId}/ \
  -jar ./target/{artifactId}-{version}.jar
# (2)-a Generate a trace file
$JAVA_HOME/bin/java \
  -agentlib:native-image-agent=trace-output=/path/to/trace-file.json \
  -jar ./target/{artifactId}-{version}.jar
# (2)-b Generate a reachability metadata file from the collected trace file
native-image-configure generate \
  --trace-input=/path/to/trace-file.json \
  --output-dir=/path/to/config-dir/

Configure Native Image with the Tracing Agent

Collect Metadata with the Tracing Agent

The following files are stored in the specific directory.

  • jni-config.json
  • reflect-config.json
  • proxy-config.json
  • resource-config.json
  • reachability-metadata.json

These files can be located at src/main/resources/META-INF/native-image. The native-image tool picks up configuration files located in the directory src/main/resources/META-INF/native-image. However, it is recommended that we place the files in subdirectories divided by groupId and artifactId, as shown below.

src/main/resources/META-INF/native-image/{groupId}/{artifactId}

native-image.properties

When creating a native image, we call the following command.

mvn package -Dpackaging=native-image

We should specify the timing of class initialization (build time or runtime), the command line options for the native-image tool (the same command line options work in Maven/Gradle plugin), and the JVM arguments in the native-image.properties file. Indeed, these settings can be specified in pom.xml, but it is recommended that they be externalized.

a) Location of configuration files:

As described in the documentation, we can specify the location of configuration property files. If we build using the recommended method (placing the files in the directory src/main/resources/META-INF/native-image/{groupId}/{artifactId}), we can specify the directory location using ${.}.

  • -H:DynamicProxyConfigurationResources
  • -H:JNIConfigurationResources
  • -H:ReflectionConfigurationResources
  • -H:ResourceConfigurationResources
  • -H:SerializationConfigurationResources

Native Image Build Configuration

b) HTTP/HTTPS protocols support:

We need to use --enable-https/--enable-http when using the HTTP(S) protocol in your application.

URL Protocols in Native Image

c) When classes are loaded and initialized:

In the case of AOT compilation, classes are usually loaded at compile time and stored in the image heap (at build time). However, some classes might be specified to be loaded when the program is running. In these cases, it is necessary to explicitly specify initialization at runtime (and vice versa, of course). There are two types of build arguments.

# Explicitly specify initialisation at runtime
--initialize-at-run-time=...
# Explicitly specify initialisation at build time
--initialize-at-build-time=...

To enable tracing of class initialization, use the following arguments.

# Enable tracing of class initialization
--trace-class-initialization=... # Deprecated in GraalVM 21.3
--trace-object-instantiation=... # Current option

Specify Class Initialization Explicitly

Class Initialization in Native Image

d) Prevent fallback builds:

If the application cannot be optimized during the Native Image build, the native-image tool will create a fallback file, which needs JVM. To prevent fallback builds, we need to specify the option --no-fallback. For other build options, please look at the following document.

Command-line Options

Build a Native Image application

Building a native image application takes a long time (though it has got quicker over time). If building it for testing purpose, we strongly recommend enabling Quick Build and setting the optimization level to -Ob option (although this will still take time). See below for more information.

Maven plugin for GraalVM Native Image
Gradle plugin for GraalVM Native Image

Optimizations and Performance

Test as a native image application

When you start the native image application, we might see the following message: This message means that GC notifications are not available because GarbageCollectorMXBean of JVM does not provide any notifications.

GC notifications will not be available because no GarbageCollectorMXBean of the JVM provides any. GCs=[young generation scavenger, complete scavenger]

Let’s check if the application works.

1) GET /books and GET /books/{isbn}

This is a normal REST API. Call both of them a few times.

2) GET /metrics

We can check the list of available metrics.

{
  "names": [
    "books.find",
    "books.index",
    "executor",
    "executor.active",
    "executor.completed",
    "executor.pool.core",
    "executor.pool.max",
    "executor.pool.size",
    "executor.queue.remaining",
    "executor.queued",
    "http.server.requests",
    "jvm.classes.loaded",
    "jvm.classes.unloaded",
    "jvm.memory.committed",
    "jvm.memory.max",
    "jvm.memory.used",
    "jvm.threads.daemon",
    "jvm.threads.live",
    "jvm.threads.peak",
    "jvm.threads.started",
    "jvm.threads.states",
    "logback.events",
    "microserviceBooksNumber.checks",
    "microserviceBooksNumber.latest",
    "microserviceBooksNumber.time",
    "process.cpu.usage",
    "process.files.max",
    "process.files.open",
    "process.start.time",
    "process.uptime",
    "system.cpu.count",
    "system.cpu.usage",
    "system.load.average.1m"
  ]
}

At first, the following three metrics are custom ones added in the class MicroserviceBooksNumberService.

  • microserviceBooksNumber.checks
  • microserviceBooksNumber.time
  • microserviceBooksNumber.latest

And, the following two metrics are custom ones collected in the class BooksController, which collect information such as the time taken and the number of calls. Each metric can be viewed at GET /metrics/{metric name}.

  • books.find
  • books.index

The following is an example of microserviceBooksNumber.*.

// miroserviceBooksNumber.checks
{
  "name": "microserviceBooksNumber.checks",
  "measurements": [
    {
      "statistic": "COUNT",
      "value": 12
    }
  ]
}
// microserviceBooksNumber.time
{
  "name": "microserviceBooksNumber.time",
  "measurements": [
    {
      "statistic": "COUNT",
      "value": 12
    },
    {
      "statistic": "TOTAL_TIME",
      "value": 0.212468
    },
    {
      "statistic": "MAX",
      "value": 0.032744
    }
  ],
  "baseUnit": "seconds"
}
//microserviceBooksNumber.latest
{
  "name": "microserviceBooksNumber.latest",
  "measurements": [
    {
      "statistic": "VALUE",
      "value": 2
    }
  ]
}

Here is an example of the metric books.*.

// books.index
{
  "name": "books.index",
  "measurements": [
    {
      "statistic": "COUNT",
      "value": 6
    },
    {
      "statistic": "TOTAL_TIME",
      "value": 3.08425
    },
    {
      "statistic": "MAX",
      "value": 3.02097
    }
  ],
  "availableTags": [
    {
      "tag": "exception",
      "values": [
        "none"
      ]
    }
  ],
  "baseUnit": "seconds"
}
// books.find
{
  "name": "books.find",
  "measurements": [
    {
      "statistic": "COUNT",
      "value": 7
    }
  ],
  "availableTags": [
    {
      "tag": "result",
      "values": [
        "success"
      ]
    },
    {
      "tag": "exception",
      "values": [
        "none"
      ]
    }
  ]
}

Metrics from Azure Monitor (application insights)

Here is the grid view of custom metrics in Application Insights (microserviceBooks.time is the average value).

To confirm that the values match those in Application Insights, check the metric http.server.requests, for example. We should see three items on the graph and the value is equal to the number of API responses (3).

 

 

Updated Aug 15, 2025
Version 3.0
No CommentsBe the first to comment