AWS FargateでJavaアプリのログをX-Rayトレースと紐付ける方法

# 1. はじめに

前回の記事「AWS FargateでJavaアプリを自動計装してX-Rayでトレースする方法」では、アプリケーションコードを変更することなく、AWS Distro for OpenTelemetry (ADOT)を使用したJavaアプリケーションの自動計装方法を解説しました。

[AWS FargateでJavaアプリを自動計装してX-Rayでトレースする方法](https://qiita.com/noza_ysk/items/070aee0f24ecf9a7d11d)

これにより、X-Rayでのトレース収集は実現できましたが、より効果的なトラブルシューティングのためには「ログとトレースの紐付け」が重要です。

今回は、前回構築した環境をさらに発展させ、**アプリケーションログにX-Rayトレース情報を埋め込む方法**を解説します。これにより、CloudWatch Logsで見つけたエラーログからX-Rayトレースへの直接リンク、またはその逆の流れが可能になり、問題の根本原因分析が大幅に効率化されます。

# 2. ログとトレースの紐付けがもたらす価値

## 2.1 なぜログとトレースの紐付けが重要か？

分散システムでは、問題が発生したとき「何が」「どこで」「なぜ」起きたのかを特定するのが難しいことがあります。

- **ログだけでは**: 詳細な情報は得られるものの、リクエストの全体像が見えない
- **トレースだけでは**: リクエストの流れは把握できるが、詳細なアプリケーション状態が分からない

**両方を紐付ければ**:
1. エラーログを見つけたら、そのリクエストの全体的な処理フロー（トレース）を確認できる
2. トレースで異常を見つけたら、関連する詳細なアプリケーションログを参照できる

## 2.2 AWS環境での実現方法

AWS環境では、X-RayとCloudWatch Logsの連携により、この紐付けを実現できます。

1. アプリケーションログにX-Ray形式のトレースIDを埋め込む
2. CloudWatch LogsのログイベントからX-Rayトレースへリンクする機能を活用

# 3. 実装手順：ログとトレースの紐付け

前回の自動計装環境に、以下の3つのファイル修正/追加を行います。

[AWS FargateでJavaアプリを自動計装してX-Rayでトレースする方法](https://qiita.com/noza_ysk/items/070aee0f24ecf9a7d11d)

1. **build.gradle.kts** - OpenTelemetry Logbackの依存関係を追加
2. **logback.xml** - ログにトレースIDを埋め込む設定を追加
3. **Dockerfile** - 上記設定ファイルの配置と環境変数の設定

## 3.1 build.gradle.ktsの更新

まず、build.gradle.ktsファイルに、OpenTelemetryとLogbackの連携に必要な依存関係を追加します。

```kotlin
plugins {
  java
  application
  id("org.springframework.boot") version "3.1.3"
  id("io.spring.dependency-management") version "1.1.3"
}

sourceSets {
  main {
    java.setSrcDirs(setOf("."))
  }
}

repositories {
  mavenCentral()
}

dependencies {
  implementation("org.springframework.boot:spring-boot-starter-web")
  // OpenTelemetryとLogbackの連携のための依存関係を追加
  implementation("io.opentelemetry.instrumentation:opentelemetry-logback-mdc-1.0:1.33.0-alpha")
}
```

**変更ポイント**:
- `opentelemetry-logback-mdc-1.0` ライブラリを追加（バージョンは必ず使用する自動計装エージェントと揃える）
- このライブラリにより、Logbackでトレースコンテキストに含まれる情報にアクセスできるようになります

## 3.2 logback.xml の作成

次に、Logbackの設定ファイルを作成します。この設定により、ログメッセージにトレースIDを含めることができます。

```xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} trace_id=%X{AWS-XRAY-TRACE-ID} span_id=%X{span_id} trace_flags=%X{trace_flags} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- OpenTelemetryAppenderでラップして、トレースコンテキストの情報をMDCに注入 -->
  <appender name="OTEL" class="io.opentelemetry.instrumentation.logback.mdc.v1_0.OpenTelemetryAppender">
    <appender-ref ref="CONSOLE"/>
  </appender>

  <!-- OTELアペンダーを使用 -->
  <root level="DEBUG">
    <appender-ref ref="OTEL"/>
  </root>
</configuration>
```

**重要ポイント**:
- `trace_id=%X{AWS-XRAY-TRACE-ID}` でX-Ray形式のトレースIDをログに埋め込む
- `OpenTelemetryAppender` でLogbackアペンダーをラップし、トレースコンテキスト情報をMDC(Mapped Diagnostic Context)に自動注入
- MDCとは、スレッドローカルにキー/値ペアを保存するLogbackの機能で、トレース情報をログに埋め込むのに最適です

## 3.3 Dockerfileの更新

前回のDockerfileを以下のように更新します。

```dockerfile
FROM public.ecr.aws/docker/library/gradle:jdk17 AS builder

WORKDIR /adot-java-sample
COPY *.java .
COPY build.gradle.kts .
COPY logback.xml .

RUN ["gradle", "assemble"]

FROM gcr.io/distroless/java17-debian11:latest

WORKDIR /adot-java-sample
COPY --from=builder /adot-java-sample/build/libs/ ./build/libs/
COPY logback.xml .

ENV SERVER_PORT=80

# ADOTエージェントのダウンロード
ADD https://github.com/aws-observability/aws-otel-java-instrumentation/releases/download/v1.33.0/aws-opentelemetry-agent.jar ./aws-opentelemetry-agent.jar
ENV JAVA_TOOL_OPTIONS "-javaagent:/adot-java-sample/aws-opentelemetry-agent.jar"

# OpenTelemetry関連の環境変数設定
ENV OTEL_RESOURCE_ATTRIBUTES "service.name=xraytestapp"
ENV OTEL_IMR_EXPORT_INTERVAL "60000"
ENV OTEL_EXPORTER_OTLP_ENDPOINT "http://127.0.0.1:4317"
ENV OTEL_METRICS_EXPORTER "none"
ENV OTEL_TRACES_EXPORTER="otlp"

EXPOSE 80

CMD ["/adot-java-sample/build/libs/adot-java-sample.jar"]
```

**変更ポイント**:
- `logback.xml` をコピー
- `LOGGING_CONFIG` 環境変数でlogback.xmlの場所を指定

# 4. 実装の詳細解説

## 4.1 トレースコンテキストがログに伝播される仕組み

この実装で重要なのは、トレースコンテキスト情報がアプリケーションログに自動的に含まれる仕組みです。

1. **OpenTelemetry Java自動計装エージェント**:
   - サーバーリクエスト受信時に自動的にトレースを開始
   - 実行スレッドにトレースコンテキスト（トレースID、スパンIDなど）を関連付け
   
2. **OpenTelemetry Logback MDC統合**:
   - `opentelemetry-logback-mdc-1.0` ライブラリが現在のトレースコンテキスト情報をLogbackのMDCに注入
   - 主要なコンテキスト情報: `AWS-XRAY-TRACE-ID`, `span_id`, `trace_flags`

3. **logback.xml**:
   - MDCに注入された値をログパターンで参照（`%X{キー名}`記法で）

## 4.2 X-Ray形式のトレースID

AWS X-Rayでは、トレースIDは以下の形式で表現されます。

```
1-5759e988-bd862e3fe1be46a994272793
```

この形式は:
- `1-` で始まるプレフィックス（バージョン）
- タイムスタンプ（8桁の16進数）
- ランダムな24桁の16進数

CloudWatch Logsでは、この形式のトレースIDを含むログエントリが自動的にX-Rayトレースにリンクされます。

# 5. 実際にやってみた

3章でも記載の通り前回の自動計装環境に、以下の3つのファイル修正/追加を行います。
環境の準備ができていない方は以下記事をご参照ください。
[AWS FargateでJavaアプリを自動計装してX-Rayでトレースする方法](https://qiita.com/noza_ysk/items/070aee0f24ecf9a7d11d)

## 5.1 ファイル準備

1\. **すべてのファイルを用意**

前回の作業ディレクトリに、更新したbuild.gradle.ktsと新しいlogback.xmlを追加します。

<details><summary>
すべてのファイル作成コマンド（まとめて実行）
</summary>

```bash
# DiceApplication.javaの作成
cat > DiceApplication.java << 'EOL'
package otel;

import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DiceApplication {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(DiceApplication.class);
        app.setBannerMode(Banner.Mode.OFF);
        app.run(args);
    }
}
EOL

# RollController.javaの作成
cat > RollController.java << 'EOL'
package otel;

import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RollController {
    private static final Logger logger = LoggerFactory.getLogger(RollController.class);

    @GetMapping("/rolldice")
    public String index(@RequestParam("player") Optional<String> player) {
        int result = this.getRandomNumber(1, 6);
        if (player.isPresent()) {
            logger.info("{} is rolling the dice: {}", player.get(), result);
        } else {
            logger.info("Anonymous player is rolling the dice: {}", result);
        }
        return Integer.toString(result);
    }

    @GetMapping("/health")
    public String health() {
        return "OK";
    }

    private int getRandomNumber(int min, int max) {
        return ThreadLocalRandom.current().nextInt(min, max + 1);
    }
}
EOL

# 更新したbuild.gradle.ktsの作成
cat > build.gradle.kts << 'EOL'
plugins {
    java
    application
    id("org.springframework.boot") version "3.1.3"
    id("io.spring.dependency-management") version "1.1.3"
}

sourceSets {
  main {
    java.setSrcDirs(setOf("."))
  }
}

repositories {
  mavenCentral()
}

dependencies {
  implementation("org.springframework.boot:spring-boot-starter-web")
  // OpenTelemetryとLogbackの連携のための依存関係を追加
  implementation("io.opentelemetry.instrumentation:opentelemetry-logback-mdc-1.0:1.33.0-alpha")
}
EOL

# logback.xmlの作成
cat > logback.xml << 'EOL'
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} trace_id=%X{AWS-XRAY-TRACE-ID} span_id=%X{span_id} trace_flags=%X{trace_flags} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- OpenTelemetryAppenderでラップして、トレースコンテキストの情報をMDCに注入 -->
  <appender name="OTEL" class="io.opentelemetry.instrumentation.logback.mdc.v1_0.OpenTelemetryAppender">
    <appender-ref ref="CONSOLE"/>
  </appender>

  <!-- OTELアペンダーを使用 -->
  <root level="DEBUG">
    <appender-ref ref="OTEL"/>
  </root>
</configuration>
EOL

# 更新したDockerfileの作成
cat > Dockerfile << 'EOL'
FROM public.ecr.aws/docker/library/gradle:jdk17 AS builder

WORKDIR /adot-java-sample
COPY *.java .
COPY build.gradle.kts .
COPY logback.xml .

RUN ["gradle", "assemble"]

FROM gcr.io/distroless/java17-debian11:latest

WORKDIR /adot-java-sample
COPY --from=builder /adot-java-sample/build/libs/ ./build/libs/
COPY logback.xml .

ENV SERVER_PORT=80

# ADOTエージェントのダウンロード
ADD https://github.com/aws-observability/aws-otel-java-instrumentation/releases/download/v1.33.0/aws-opentelemetry-agent.jar ./aws-opentelemetry-agent.jar
ENV JAVA_TOOL_OPTIONS "-javaagent:/adot-java-sample/aws-opentelemetry-agent.jar"

# OpenTelemetry関連の環境変数設定
ENV OTEL_RESOURCE_ATTRIBUTES "service.name=xraytestapp"
ENV OTEL_IMR_EXPORT_INTERVAL "60000"
ENV OTEL_EXPORTER_OTLP_ENDPOINT "http://127.0.0.1:4317"
ENV OTEL_METRICS_EXPORTER "none"
ENV OTEL_TRACES_EXPORTER="otlp"

# logback.xmlの場所を指定
ENV LOGGING_CONFIG=/adot-java-sample/logback.xml

EXPOSE 80

CMD ["/adot-java-sample/build/libs/adot-java-sample.jar"]
EOL
```
</details>

## 5.2 イメージのビルドとデプロイ

1\. **Dockerイメージのビルド**
```bash
docker build -t adot-java-sample-with-logs .
```

2\. **ECRリポジトリへの認証**
```bash
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin $(aws sts get-caller-identity --query 'Account' --output text).dkr.ecr.ap-northeast-1.amazonaws.com
```

3\. **イメージのタグ付け**
```bash
docker tag adot-java-sample:latest $(aws sts get-caller-identity --query 'Account' --output text).dkr.ecr.ap-northeast-1.amazonaws.com/adot-java-sample:latest
```

ECR名は適宜更新してください。

4\. **イメージのプッシュ**
```bash
docker push $(aws sts get-caller-identity --query 'Account' --output text).dkr.ecr.ap-northeast-1.amazonaws.com/adot-java-sample:latest
```
ECR名は適宜更新してください。

5\. **ECSサービスの更新**

前回作成したECSサービスが新しいイメージを使用するように更新されます。
ECSサービスのデプロイが完了後、アプリケーションにアクセスしてみましょう。

## 5.3 結果の確認

1\. **アプリケーションへのアクセス**

ALB経由で`/rolldice`エンドポイントに複数回アクセスします。
```
https://your-alb-domain/rolldice
https://your-alb-domain/rolldice?player=Alice
```

2\. **CloudWatchログの確認**

CloudWatch Logsコンソールで、アプリケーションのログを確認します。ログには以下のようにトレースID情報が含まれています。

```
05:46:03.853 [http-nio-80-exec-7] INFO otel.RollController trace_id=1-67ee209b-047d7d7e766d57968527e70d@706540b73155c174 span_id=706540b73155c174 trace_flags=01 - Anonymous player is rolling the dice: 6
```

このログエントリには:
- `trace_id=1-67ee209b-047d7d7e766d57968527e70d` - X-Ray形式のトレースID
- `span_id=706540b73155c174` - 現在のスパンID
- `trace_flags=01` - トレースフラグ（サンプリングされたかどうかなど）

が含まれています。

3\. **X-Rayトレースの確認**

X-Rayコンソールでトレースを確認すると、トレース内の各セグメントに対応するログを見ることができます。
![Xray検証-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553930/20abfeca-2d19-4210-b051-52b553c5cdb7.jpeg)


# 6. まとめ

今回の実装により、以下のメリットが得られます。

1. **効率的なトラブルシューティング**:
   - トレースからログへのスムーズな移動
   - エラーの発生したコンテキストをより明確に把握可能

2. **観測可能性の向上**:
   - アプリケーション内部の状態（ログ）と分散システム全体の状態（トレース）の両方を一貫して監視可能

3. **低侵襲な実装**:
   - アプリケーションコードへの変更は不要
   - 設定ファイルの追加と依存関係の更新のみで実現

この方法は、特にマイクロサービスアーキテクチャを採用している環境や、多数のコンテナを運用している場合に、運用効率を大きく向上させます。自動計装とログ・トレース紐付けの組み合わせにより、AWSのマネージドサービスの利点を最大限に活かした可観測性基盤を構築できます。

# 参考資料

- [AWS X-Ray ドキュメント - CloudWatch Logsとの連携](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-java-configuration.html#xray-sdk-java-configuration-logging)
- [OpenTelemetry Logback インストゥルメンテーション](https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/logback/logback-mdc-1.0/library)
- [OpenTelemetry 分散トレースの概念](https://opentelemetry.io/docs/concepts/signals/traces/)
- [AWS Distro for OpenTelemetry のログとトレースの統合](https://aws-otel.github.io/docs/getting-started/java-sdk/trace-logs-correlation)