MicronautからAzure Monitorにlogを送信したい

このエントリは2025/07/29現在の情報に基づいています。将来の機能追加や変更に伴い、記載事項からの乖離が発生する可能性があります。

このエントリは、以下のエントリに関連するもので、背景などはこちらを参照のこと。

ログの出力先

Azureでは、アプリケーションのデプロイ先によってログ出力先が異なる。また、明示的に異なる宛先に出力することもできる。例えばAzure Container Appsにデプロイしている場合、以下の方法が考えられる(App Serviceでは異なることに注意)。

種類宛先構成
コンソールへの出力をログとして書き出すContainerAppConsoleLogs_CLに出力される(診断設定で出力先が変更されている場合もある)。Container Appsがよろしくやってくれるので、特に利用者側で何かする必要はない。
DCE (Data Collection Endpoint)を利用してLog Analytics Workspaceに書き出すLog Analytics Workspaceのカスタムテーブルチュートリアルに従えばよい。
Publish Application Logs to Azure Monitor Logs
https://graal.cloud/gdk/gdk-modules/logging/micronaut-azure-logging/
Publish Micronaut application logs to Microsoft Azure Monitor Logs
https://guides.micronaut.io/latest/micronaut-azure-logging-maven-java.html
Log Appenderを使うApplication InsightsのtracesテーブルLog Appenderの構成が必要

Azure Container Apps のログ ストレージと監視オプション / Log storage and monitoring options in Azure Container Apps
https://learn.microsoft.com/azure/container-apps/log-options

このエントリでは、第3の方法について説明する。

準備するもの

利用したツールは以下。

  • Maven: 3.9.10
  • JDK: 21
  • Micronaut: 4.9.1

以下の4種類のLogライブラリを使っていると、ログは自動収集される。今回はLogbackを使う。

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

Azureリソースの準備

リソースグループを作成し、Application Insightsを構成しておく。詳細は以下のドキュメントを参照。

Application Insights リソースの作成と構成 / Create and configure Application Insights resources
https://learn.microsoft.com/azure/azure-monitor/app/create-workspace-resource

これでAzure側の準備はおしまい。

Archetypeの作成

MicronautのCLI (mn) もしくはMicronaut Launch(Webサイト)でarchetypeを作成する。今回はアプリケーション設定でapplication.propertiesではなく、application.ymlを使う。そのため、yamlを使うための依存関係を取り込めるよう、featureとしてyamlを明示的に指定する必要がある。

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

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

Micronaut Launchを使う場合、【FEATURES】をクリックして、

  • azure-tracing
  • graalvm
  • yaml

を選択した後、【GENERATE PROJECT】をクリックして【Download Zip】を選択すると、Zip形式でarchetypeを取得できる。

依存関係の追加

Application InsightsへのLog出力のためには、以下の依存関係を追加する必要がある。

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

今回はLogbackを使ってログ出力するので、opentelemetry-logback-appender-1.0を使っているが、他のライブラリを使う場合は、各ライブラリに対応したAppenderを指定する必要がある。以下はその例。

ライブラリOTelApplication Insights
Log4j2io.opentelemetry.instrumentation:opentelemetry-log4j-appender-2.17com.microsoft.azure:applicationinsights-logging-log4j2
Logbackio.opentelemetry.instrumentation:opentelemetry-logback-appender-1.0com.microsoft.azure:applicationinsights-logging-logback

今回、io.micronaut.tracing:azure-tracingを依存関係に含めているために、com.azure:azure-monitor-opentelemetry-autoconfigureが推移的に取り込まれている。もしazure-tracingを追加していない場合、以下の依存関係を明示的に追加する必要がある。

<dependency>
    <groupId>com.azure</groupId>
    <artifactId>azure-monitor-opentelemetry-autoconfigure</artifactId>
</dependency>

あとはGraalVM用のお約束の依存関係とPlugin設定を構成する。

GraalVM Reachability Metadata Repositoryを利用できるよう、以下の依存関係を追加する。2025/07/29現在の最新は0.11.0。

<dependency>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>graalvm-reachability-metadata</artifactId>
    <version>0.11.0</version>
    <classifier>repository</classifier>
    <type>zip</type>
</dependency>

GraalVM maven pluginを追加し、上記依存関係で取得したGraalVM Reachability Metadataを利用できるようにしておく。このpluginでは、buildArgを使って最適化レベルなども設定できる(この例では最適化レベルを指定)。もちろん、native-image.propertiesに追加すれば、native-imageツール(Maven/Gradle pluginも同じ)が読み取ってくれる。

<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 Insights固有の設定だけでなく、azure-tracingの設定も入れる必要がある。azure-tracingについては、以下に記載した設定を使う。

Application Insights固有の設定は以下のドキュメントを参照。

構成オプション: Azure Monitor Application Insights for Java / Configuration options: Azure Monitor Application Insights for Java
https://learn.microsoft.com/azure/azure-monitor/app/java-standalone-config

このドキュメントによると、例えば接続文字列を指定する場合、以下のような方法で設定するように、との指示がある。

環境変数 APPLICATIONINSIGHTS_CONNECTION_STRING を使用して、接続文字列を設定することもできます。 その場合、JSON 構成で指定された接続文字列より優先されます。

または、Java システム プロパティ applicationinsights.connection.string を使用して接続文字列を設定することもできます。 また、これは JSON 構成で指定された接続文字列より優先されます。
—-
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.

これだけを読むと、環境変数もしくはJavaシステムプロパティを使わざるを得ないように見えるが、Micronautの場合(Spring Boot、Quarkusでも同じだが)、アプリケーション設定と環境変数の関係を使えば、application.properties / application.yml で構成することもできる。

例えば上記の接続文字列の場合、環境変数で指定する場合には APPLICATIONINSIGHTS_CONNECTION_STRING を使うので、Micronautの場合、以下のapplication.ymlの例の5~7行目のように指定できる(システムプロパティで設定する場合のKeyと一致する)。

Application Insights固有の設定を含めたapplication.ymlの構成は以下の通り。

micronaut:
  application:
    name: azuremonitor-log

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}

コード実装

a) Application Insightsを利用可能にするために

Log送信のためにOpenTelemetryオブジェクトを明示的に作成しなければならない(azure-tracingであればApplication Insightsを利用可能にする操作をよろしくやってくれるが、その際に生成されるOpenTelemetryオブジェクトはpublicではないため、外部から取得できない)。

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

b) Log Appenderの構成

Archetypeを作成した時点で、src/main/resources/logback.xmlが生成されているはず。この中で、io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppenderクラスのオブジェクトと関連付けるためのAppenderを追加する。

<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" />
    </root>
</configuration>

そして、先ほど作成したOpenTelemetryオブジェクトを、Log Appenderに関連付けることで、LogをOpenTelemetryで送信できるようにしておく。

OpenTelemetryAppender.install(openTelemetry);

c) その他の実装

Hello Worldのような小さなREST APIを作成するが、今回はTraceとTrace logを確認することが目的なので、その中でloggerを使っていろいろログを吐き出すようにしている(実際のアプリケーションではこんなに無駄にLogを吐き出させるようなことはしないはず、知らんけど)。

例えば、HelloController.javaはこんな感じ。

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);
    }
}

これでひとまずJavaアプリケーションとしてビルドしておく。

mvn clean package

Javaアプリケーションとしての動作確認

問題なくアプリケーションが動作し、Application Insightsにtrace、tracesテーブルにログが送信されていて、Traceの画面で確認できることを確認する。GET /api/hello?name=Logico_jpという呼び出しであれば、tracesテーブルでは以下のように、

そしてTraceでは、Requestと共に以下のように見えるはず。

動作確認できたら、Tracing Agentを使ってアプリケーションを実行し、必要な構成ファイルを生成しておく。

# (1) 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 trace fileを生成
$JAVA_HOME/bin/java -agentlib:native-image-agent=trace-output=/path/to/trace-file.json -jar ./target/{artifactId}-{version}.jar
# (2)-b 作成したtrace fileからartifact固有のreachability metadataを生成
native-image-configure generate --trace-input=/path/to/trace-file.json --output-dir=/path/to/config-dir/

Configure Native Image with the Tracing Agent
https://www.graalvm.org/latest/reference-manual/native-image/guides/configure-with-tracing-agent/
Collecting Metadata with the Tracing Agent
https://www.graalvm.org/latest/reference-manual/native-image/metadata/AutomaticMetadataCollection/

以下のファイルを所定のディレクトリに作成する。

ファイルjni-config.json
reflect-config.json
proxy-config.json
resource-config.json
reachability-metadata.json
場所src/main/resources/META-INF/native-image
native-imageツールは上記ディレクトリ以下にある構成ファイルを拾い出すが、以下のようにgroupId、artifactIdでディレクトリを切ったサブディレクトリにファイルを配置してビルドすることが推奨されている。
src/main/resources/META-INF/native-image/{groupId}/{artifactId}

Native Image作成時の設定

mvn package -Dpackaging=native-image

でNative Imageを作成するが、クラスの初期化タイミング(ビルド時、実行時)やnative-imageツール(Maven plugin)へのコマンドラインオプション、JVM引数をnative-image.propertiesに設定する。pom.xml内にも記載できるが、こうした設定は外部化しておくことを推奨。

a) 各種構成ファイルの在処

ドキュメントに記載の通り、Dynamic Proxy、Reflectionなどの構成プロパティファイルの在処を指定できる。上記の推奨に従って同じディレクトリ(src/main/resources/META-INF/native-image/{groupId}/{artifactId})に入れてビルドする場合、ディレクトリの場所として${.}を使ってnative-image.properties内で指定できる。

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

Embed a Configuration File
https://www.graalvm.org/latest/reference-manual/native-image/overview/BuildConfiguration/#embed-a-configuration-file

b) HTTP/HTTPSプロトコルのサポート

--enable-https/--enable-httpは、アプリケーションでHTTP(S)プロトコルを使う場合には必須である。

URL Protocols in Native Image
https://www.graalvm.org/latest/reference-manual/native-image/dynamic-features/URLProtocols/

c) クラスロードのタイミング

AOTコンパイルでは、コンパイル時にクラスをロードし、イメージヒープに保持する(ビルド時)挙動が通常だが、クラスによっては実行時にロードすることを指定している場合がある。その場合は実行時の初期化を明示的に指定する必要がある(もちろんその逆も然り)。その際のビルド引数が以下の2種類。

# 実行時の初期化を明示的に指定
--initialize-at-run-time=...

# ビルド時の初期化を明示的に指定
--initialize-at-build-time=...

クラス初期化のトレースを有効化するには以下のビルド引数を使う。

# クラス初期化のトレース有効化
# GraalVM 21.3でdeprecated
--trace-class-initialization=...

# 現行のオプション
--trace-object-instantiation=... 

Specify Class Initialization Explicitly
https://www.graalvm.org/latest/reference-manual/native-image/guides/specify-class-initialization/
Class Initialization in Native Image
https://www.graalvm.org/latest/reference-manual/native-image/optimizations-and-performance/ClassInitialization/

d) fallbackビルドを抑止する

Native Imageのビルド時にアプリケーションを最適化できない場合、JVMが必要なfallbackファイルを生成してしまうが、これではただのJavaアプリケーションと何も変わらないので、可能な限り

--no-fallback

というオプションをビルド引数として指定しておく。

その他のビルドオプションは以下を参照。

Build Options
https://www.graalvm.org/latest/reference-manual/native-image/overview/Options/#build-options

Native Imageのビルド

ビルドには時間を要するので、開発環境かつテストであれば、Quick Buildを有効にして、最適化レベルを-Obにすることを推奨する(それでも時間はかかる)。

Native Image Options
https://graalvm.github.io/native-build-tools/latest/maven-plugin.html#native-image-options
https://graalvm.github.io/native-build-tools/latest/gradle-plugin.html#native-image-options
Optimization Levels
https://www.graalvm.org/latest/reference-manual/native-image/optimizations-and-performance/#optimization-levels

Native Imageとしての動作確認

GET /api/hello?name=xxxxGET /api/hello?name=GET /api/helloを呼び出したり、POST /api/helloで挨拶のフレーズを変更したり、通常のJavaアプリケーションと同じように動作確認する。

Azure Monitor (application insights) から確認

Application Insightsでtracesテーブルを確認してみると、4レコードが15時14分に追加されていることがわかる。

Traceを確認してみると、

tracesテーブルに記載のログがTraceに付加されていることがわかる(当然発生時間は同じ)。

まとめ

Application Insightsのtracesテーブルに書き出す方法をまとめたが、一部コードによるLog Appenderの設定が必要なので、厳密な意味でのZero code instrumentationは実現できない。ただ、実際の設定自体は軽微なものなので、実装自体は難しくない。

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください