Blog Post

Apps on Azure Blog
9 MIN READ

Send logs 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 29 July 2025.

MicronautからAzure Monitorにlogを送信したい – 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

Where can we post logs?

Log destination differs depending upon managed services such as App Service, Container Apps, etc. We can also send logs to specified destination which is different destination from the default one. In the case of Azure Container Apps, for instance, we have several options to send logs.

TypeDestinationHow to
Write console output to a log

ContainerAppConsoleLogs_CL

If diagnostic settings are configured, destination table may differ from the above.

The output destination can be changed in the diagnostic settings. This is handled by Container Apps, so no user action is required.
Use DCE (Data Collection Endpoint) to write logs to custom table in Log Analytics WorkspaceCustom tables in Log Analytics Workspace.

Follow these tutorials listed below.


Publish Application Logs to Azure Monitor Logs


Publish Micronaut application logs to Microsoft Azure Monitor Logs

Using the Log Appendertraces table in Application InsightsWhen writing logs to thetraces table in Application Insights, Log Appender configuration is required.

Log storage and monitoring options in Azure Container Apps

From now on, we elaborate the 3rd way — write logs to the traces table in Application Insights.

Prerequisites

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

Regarding logs, the logs posted with the following 4 log libraries are automatically collected. In this entry, we use Logback.

  • Log4j2
  • Logback
  • JBoss Logging
  • java.util.logging

Create Azure resource (Application Insights)

Create a resource group and configure Application Insights. Refer to the following documentation for details.

Create and configure Application Insights resources - Azure Monitor

That’s it for the Azure setup.

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

mn create-app \
--build=maven \
--jdk=21 \
--lang=java \
--test=junit \
--features=graalvm,azure-tracing,yaml \
dev.logicojp.micronaut.azuremonitor-log

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

  • graalvm
  • azure-tracing
  • yaml

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

Add dependencies and plugins to pom.xml

In order to output logs to Application Insights, the following dependencies must be added.

<dependency>
    <groupid>io.opentelemetry.instrumentation</groupid>
    <artifactid>opentelemetry-logback-appender-1.0</artifactid>
</dependency>
<dependency>
    <groupid>com.microsoft.azure</groupid>
    <artifactid>applicationinsights-logging-logback</artifactid>
</dependency>
<dependency>
  <groupid>io.micronaut.tracing</groupid>
  <artifactid>micronaut-tracing-opentelemetry-http</artifactid>
</dependency>

In this entry, we are using Logback for log output, so we are using opentelemetry-logback-appender-1.0. However, should you be using a different library, it will be necessary to specify the appropriate an appender for that library.

The dependency com.azure:azure-monitor-opentelemetry-autoconfigure is being included transitively since io.micronaut.tracing:azure-tracing depends upon the dependency. If Azure tracing has not yet been added, the following dependencies must be added explicitly.

<dependency>
    <groupid>com.azure</groupid>
    <artifactid>azure-monitor-opentelemetry-autoconfigure</artifactid>
</dependency>

Additionally, we need to add this dependency to use the GraalVM Reachability Metadata Repository. The latest version is 0.11.0 as of 29 July, 2025.

<dependency>
    <groupid>org.graalvm.buildtools</groupid>
    <artifactid>graalvm-reachability-metadata</artifactid>
    <version>0.11.0</version>
    <classifier>repository</classifier>
    <type>zip</type>
</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>

Application configuration

In order to proceed, it is necessary to include both Application Insights-specific settings and Azure-tracing settings. To ensure optimal performance when using Azure tracing, please refer to the settings outlined below.

Send traces from Micronaut native image applications to Azure Monitor | Microsoft Community Hub

For Application Insights-specific settings, please refer to the documentation provided.

Configuration options - Azure Monitor Application Insights for Java - Azure Monitor

According to the documentation, when specifying a connection string, the configuration should be as follows.

You can also set the connection string by using the environment variable APPLICATIONINSIGHTS_CONNECTION_STRING. It then takes precedence over the connection string specified in the JSON configuration.

Or you can set the connection string by using the Java system property applicationinsights.connection.string. It also takes precedence over the connection string specified in the JSON configuration.

Initially, it may appear that there is no alternative but to use environment variables or Java system properties. However, in the case of Micronaut (and similarly for Spring Boot and Quarkus), the connection string can be configured using the relationship between application settings and environment variables. This allows for defining it in application.properties or application.yml.

For instance, in the case of the connection string mentioned above, if we specify it using an environment variable, we would use APPLICATIONINSIGHTS_CONNECTION_STRING. In Micronaut, we can specify it as shown in lines 5–7 of the following application.yml example (the key matches the one used when setting it as a system property).

The configuration of application.yml, including Application Insights-specific settings, is as follows:

applicationinsights:
  connection:
    string: ${AZURE_MONITOR_CONNECTION_STRING}
  sampling:
    percentage: 100
  instrumentation:
    logging:
      level: "INFO"
  preview:
    captureLogbackMarker: true
    captureControllerSpans: true
azure:
  tracing:
    connection-string: ${AZURE_MONITOR_CONNECTION_STRING}

Codes

a) To enable Application Insights

We need to explicitly create an OpenTelemetry object to send logs. Please note that while Azure-tracing enables Application Insights, the OpenTelemetry object generated during this process is not publicly accessible and cannot be retrieved from outside.

AutoConfiguredOpenTelemetrySdkBuilder sdkBuilder = AutoConfiguredOpenTelemetrySdk.builder();
OpenTelemetry openTelemetry = sdkBuilder.build().getOpenTelemetrySdk();
AzureMonitorAutoConfigure.customize(sdkBuilder, "connectionString");

b) Log Appender

When we create the archetype, src/main/resources/logback.xml should be generated. In this file, add an Appender to associate with the io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender class object.

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- encoders are assigned the type
             ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
        <encoder>
            <pattern>%cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level)
                %magenta(%logger{36}) - %msg%n
            </pattern>
        </encoder>
    </appender>
    <appender name="OpenTelemetry" class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
        <captureexperimentalattributes>true</captureexperimentalattributes>
        <capturecodeattributes>true</capturecodeattributes>
        <capturemarkerattribute>true</capturemarkerattribute>
        <capturekeyvaluepairattributes>true</capturekeyvaluepairattributes>
        <capturemdcattributes>*</capturemdcattributes>
    </appender>
    <root level="info">
        <appender-ref ref="STDOUT">
        <appender-ref ref="OpenTelemetry">
    </appender-ref></appender-ref></root>
</configuration>

Then, associate the OpenTelemetry object we created earlier with Log Appender so that logs can be sent using OpenTelemetry.

OpenTelemetryAppender.install(openTelemetry);

c) Other implementation

The objective of this article is to verify the Trace and Trace log. To that end, we will develop a rudimentary REST API, akin to a “Hello World” application. However, we will utilize the logger feature to generate multiple logs. In a real-world application, we would likely refine this process to avoid generating excessive logs.

For example, HelloController.java is shown below.

package dev.logicojp.micronaut;

import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.*;
import io.micronaut.http.exceptions.HttpStatusException;
import io.micronaut.scheduling.TaskExecutors;
import io.micronaut.scheduling.annotation.ExecuteOn;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Controller("/api/hello")
@ExecuteOn(TaskExecutors.IO)
public class HelloController {

    private static final Logger logger = LoggerFactory.getLogger(HelloController.class);

    public HelloController(OpenTelemetry _openTelemetry){
        OpenTelemetryAppender.install(_openTelemetry);
        logger.info("OpenTelemetry is configured and ready to use.");
    }

    @Get
    @Produces(MediaType.APPLICATION_JSON)
    public GreetingResponse hello(@QueryValue(value = "name", defaultValue = "World") String name) {
        logger.info("Hello endpoint was called with query parameter: {}", name);
        // Simulate some processing
        HelloService helloService = new HelloService();
        GreetingResponse greetingResponse = helloService.greet(name);
        logger.info("Processing complete, returning response");
        return greetingResponse;
    }

    @Post
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Status(HttpStatus.ACCEPTED)
    public void setGreetingPrefix(@Body GreetingPrefix greetingPrefix) {
        String prefix = greetingPrefix.prefix();
        if (prefix == null || prefix.isBlank()) {
            logger.error("Received request to set an empty or null greeting prefix.");
            throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Prefix cannot be null or empty");
        }
        HelloService helloService = new HelloService();
        helloService.setGreetingPrefix(prefix);
        logger.info("Greeting prefix set to: {}", prefix);
    }
}

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

mvn clean package

Test as a Java application

Please verify that the application is running without any issues …

  • that traces are being sent to Application Insights
  • that logs are being sent to the traces table
  • that they can be confirmed on the Trace screen.

If the call is GET /api/hello?name=Logico_jp, the traces table will look like this:

In the Trace application, it should resemble this structure, in conjunction with the Request.

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

Make the following files in the specified folder.

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

This is also explained in the metric entry, so some details will be left out. If needed, please check the metric entry.

Send metrics from Micronaut native image applications to Azure Monitor | Microsoft Community Hub

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

Verify that this application works the same as a normal Java application. For example, call GET /api/hello?name=xxxx, GET /api/hello?name=, GET /api/hello , and POST /api/hello.

Check if traces and logs are visible in Azure Monitor (application insights)

When reviewing the traces table in Application Insights, it becomes evident that four records were added at 3:14 p.m.

When checking traces…

As can be seen in the traces table, the logs have indeed been added to the trace. Naturally, the occurrence times remain consistent.

Summary

I have outlined the process of writing to the traces table in Application Insights. However, it should be noted that some code is necessary to configure the Log Appender. Consequently, zero code instrumentation cannot be achieved strictly. However, the actual configuration is relatively minor, so implementation is not difficult.

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