コドモン Product Team Blog

株式会社コドモンの開発チームで運営しているブログです。エンジニアやPdMメンバーが、プロダクトや技術やチームについて発信します!

ECSで動作するSpringBoot (Kotlin)アプリケーションにDatadog APMを導入した

こんにちは。 コドモンの決済推進チームの杉山です。

この記事では私が所属している決済推進チームでどのようにDatadog APMを入れたかを紹介します。 Datadog APMを入れた経緯などはこの記事では触れません。

導入時にこちらのガイドを参考にしました。 このガイドにはSpringBootで動かす場合の説明などがありませんでした。また各データの紐付けなど少し調べる必要があったので、誰かのためになるかと思いで記事にしています。

この記事では以下の説明をします。

  • Datadog Agentを用いて、アプリケーションに関する情報をDatadogのAPMで見れるようにする
    • SpringBoot(Kotlin)で動いているアプリケーションのそれぞれのリクエストトレースやログ・スパンデータを関連付ける
    • ECSで動いているコンテナのモニタリング情報
  • fluentbitを使いlog_router経由でdatadogにログを送信する

イメージを掴むために先に今回の記事で導入したDatadog Agentを入れた場合の構成図を貼っておきます。ECS上にサイドカーとしてDatadog AgentとLog Routerのコンテナを作成し、Datadogにデータを送信しています。

DatadogAgentの構成図

構成

  • アプリケーション: Kotlin + SpringBoot
  • インフラ: AWS ECS
  • デプロイツール: ecspresso

アプリケーションに関する情報をDatadogのAPMで見れるようにする

APMで見れるようにするためにはメトリクスをDatadogに送信してくれるAgentをアプリケーションに入れる必要があります。

公式ドキュメントではインスツルメントするために、以下の対応が必要とあります。

IDE、Maven または Gradle アプリケーションスクリプト、java -jar コマンドから、Continuous Profiler、デプロイ追跡、ログ挿入(Datadog へログを送信する場合)を使用してアプリケーションを実行するには、-javaagent JVM 引数と、該当する以下のコンフィギュレーションオプションを追加します。 java -javaagent:/path/to/dd-java-agent.jar -Ddd.profiling.enabled=true -XX:FlightRecorderOptions=stackdepth=256 -Ddd.logs.injection=true -Ddd.service=my-app -Ddd.env=staging -Ddd.version=1.0 -jar path/to/your/app.jar

以下のアプリケーションコンテナの設定でも書きますが、起動時にJAVA_TOOL_OPTIONSを使うことで説明にある-javaagentを指定することができ、アプリケーション起動時にDatadog Agentも一緒に起動することができます。

私のチームではecspressoでデプロイしておりecspresso実行前にアプリケーションのDocker Imageを作成する必要があります。JAVA_TOOL_OPTIONSに渡しているdd-java-agent.jarを実行するには、Docker Imageにdd-java-agent.jar を一緒にビルドする必要があります。

ワークフローで起動時に必要なDatadog AgentをImageに入れる

ビルドはgithub actionsで行っており、以下のようなワークフローでImageを作成しています。

# .github/workflows/build-and-push-gradle-with-datadog.yml
name: __ build and push gradle app with Datadog agent

# このワークフローは、他のワークフローから実行されます
# 説明に必要な部分のみ記載しています

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
      commit-hash:
        description: Commit hash
        required: true
        type: string
      image-tag:
        description: Docker Image Tag
        required: true
        type: string
      source-directory:
        description: Source code directory relative to project root
        required: true
        type: string
      gradlew-bootbuildimage-options:
        description: gradlew bootBuildImage Options
        required: false
        type: string
        default: ''
      datadog-dir:
        description: Datadog Directory
        required: true
        type: string

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          ref: ${{ inputs.commit-hash }}

      # APM用のJavaエージェントをcurlで取得し、対象のディレクトリ(src/main/resources)に保存。
      - name: Configure Datadog
        run: |
          [ ! -d ${{ inputs.datadog-dir }} ] && mkdir -p ${{ inputs.datadog-dir }}
          curl -L 'https://dtdg.co/latest-java-tracer' -o ${{ inputs.datadog-dir }}/dd-java-agent.jar

      # ...Gradleのセットアップのstep

      # src/main/resourcesに配置されたdatadogのJavaエージェントを一緒にビルドすることで起動時にJAVA_TOOL_OPTIONSで指定できるようになる
      - name: Build and Push Image
        working-directory: ./${{ inputs.source-directory }}
        env:
          GRADLEW_OPTIONS: ${{ inputs.gradlew-bootbuildimage-options }}
        run: |
          ./gradlew bootBuildImage --imageName {repository_uri}:${{ inputs.image-tag }} $GRADLEW_OPTIONS
          docker push {repository_uri}:${{ inputs.image-tag }}

SpringBootアプリケーションでは、デフォルトでsrc/main/resourcesディレクトリ配下のファイルはビルド時にJAR/WARファイルにバンドルされます。

そのため、このワークフローのようにcurlでDatadog Agentを取得し指定の場所に置くことでImageに入れることができます。

このワークフローを以下のように各サービスが利用します。

  build-and-push-public-api:
    uses: ./.github/workflows/build-and-push-gradle-with-datadog.yml
    with:
        // ...必要な変数
      datadog-dir: app/src/main/resources/datadog

これでアプリケーションの起動までに必要な準備は整いました。

次にAPMの設定を入れていくためにタスク定義の設定ファイルを修正します

アプリケーションコンテナのecspressoのタスク定義

{
  "containerDefinitions": [
    {
      "name": "{{ must_env `ECS_SERVICE_NAME` }}",
      "image": "{{ must_env `IMAGE` }}",
      "environment": [
        {
          "name": "ENV",
          "value": "{{ must_env `ENV` }}"
        },
        {
          "name": "JAVA_TOOL_OPTIONS",
          "value": "-javaagent:/workspace/BOOT-INF/classes/datadog/dd-java-agent.jar ...その他の設定"
        },
        {
          "name": "DD_ENV",
          "value": "{{ must_env `ENV` }}"
        },
        {
          "name": "DD_LOGS_INJECTION",
          "value": "true"
        },
        {
          "name": "DD_SERVICE",
          "value": "ここにサービス名"
        },
        {
          "name": "DD_TRACE_SAMPLE_RATE",
          "value": "1.0"
        },
        {
          "name": "DD_VERSION",
          "value": "{{ must_env `IMAGE_TAG` }}"
        },
        {
          "name": "DD_GIT_REPOSITORY_URL",
          "value": "ここにリポジトリURL"
        },
        {
          "name": "DD_GIT_COMMIT_SHA",
          "value": "{{ must_env `DD_GIT_COMMIT_SHA` }}"
        }
      ]
    }
  ]
}

設定はこのようになっています。must_envはecspressoのテンプレート機能で、環境変数から値を取得することができます。各環境によって変わる情報にはこれを使用します。

それぞれのDatadogのenvironment(DD_から始まる環境変数)に関する説明します。公式ドキュメントが複数にまたがっていたので、それぞれの説明にはドキュメントから引用をします。

DD_ENV

アプリケーション環境をトレースとログに設定するために使用されます。prd・stgなどの値を取ります。

DD_VERSION

アプリケーションのバージョンを指定します。私のチームではIMAGE_TAGを指定しています。アプリケーションではバージョンではなくCommit hashを使うことが多く、IMAGE_TAGはCommit hashを使ったタグ名になっているのでIMAGE_TAGを指定することで検索時にCommit hashからログなどを検索することができます。

DD_SERVICE

同一のジョブを実行するプロセスセットの名前。アプリケーションの統計のグループ化に使われます。

画面上にServiceとして表示されます。

DD_LOGS_INJECTION

Datadog のトレース ID とスパン ID に対する MDC キーの自動挿入を有効にします。

ログとトレースの相関付けを有効化します。trueに設定するとログにトレースIDとスパンIDが自動挿入され、Datadog上でログとAPMトレースを連携させることが可能になります。

DD_TRACE_SAMPLE_RATE

サンプリングレートを設定します。

デフォルトでも1.0です。1.0は全量を取得します。0.0〜1.0の範囲で指定します。例として設定していますが、ここの値はトラフィック量に応じて調整すると良いと思います。

DD_GIT_REPOSITORY_URL・DD_GIT_COMMIT_SHA

これらはテレメトリーを Git リポジトリと連携させ、関連するソースコード行にアクセスしてスタックトレースやスロープロファイルなどの問題をデバッグすることができます。 設定しておいて損はないと思います。

以下は参考にしたドキュメントです。

注意点

タスク定義のCPUとメモリの設定に関して注意していただきたい点があります。

私のチームではアプリケーションコンテナの設定を元々以下のようにしていました。

CPU:0.5vCPU

メモリ:2 GB

Datadog APMを導入した際に同じ状態の設定でリリースした際に、アプリケーションコンテナが起動しなかったり、起動しても落ちてしまうような事象がありました。そのため私のチームではDatadog Agentを入れる際は最低でも以下の設定にすることにしました。

CPU:1vCPU

メモリ:4 GB

テストではうまく動いたとしても、リリース時にAgentがCPUに影響を及ぼす可能性があるので導入時にはここの設定に気をつけながらリソースをちゃんと監視してリリースできると良いと思います。

fluentbitを使いlog_router経由でdatadogにログを送信する

公式ドキュメントとほとんど同じになりますが、設定は以下になります。

...
[FILTER]
    Name grep
    Match *
    Exclude health check
    Exclude user_agent Datadog Agent/*

[OUTPUT]
    Name        datadog
    Match       *
    Host        http-intake.logs.datadoghq.com
    TLS         on
    compress    gzip
    apikey      ${ここにAPI_KEY}
    dd_service  ${SERVICE_NAME}
    dd_source   java
    dd_tags     env:${ENVIRONMENT},version:${IMAGE_TAG}
...  

ここで設定されているSERVICE_NAMEは先ほどAPMで設定したものと同じものを設定する必要があります。そうすることでAPMのLogsのタブで見ることができるようになります。

また、必要に応じてFilterで不要なログを落としてください。私のチームの設定ではヘルスチェックとDatadogAgentに関するログは落とすようにしています。

これで、ひと通りの設定は完了です!

別途必要だった対応

ログとトレースの紐付け🔗

諸々の設定を入れた後APMやログが見れるようになりましたが、リクエストトレースとログが紐づいていないような状態でした。画面(APM > 対象のサービス > 画面上のTraces )から対象のトレースを選択し、Logsのタブを開いた際にNo logs foundと表示されてしまいます。

調べたところこちらの記事にあるようにログとtrace_idを手動で紐付ける作業が必要でした。

なので、画面(Logs > 右上のLog Configuration > Pipelines > pipelinesのPreprocessing for JSON) から設定を編集し、以下を追加の対応をしました。

  • Trace Id attributesにcontext.dd.trace_idを追加
  • Span Id attributesにcontext.dd.span_idを追加

このようにすることでリクエストトレースのLogsタブでそのリクエストに紐づいたログを見ることができるようになりました。

docs.datadoghq.com

ログのレベルが全てINFOになってしまうℹ️

{
  "container_id": "000000000",
  "container_name": "app-api",
  "timestamp": "1234567890",
  "Environment": "dev",
  "source": "stdout",
  "log": {
    "level": "ERROR"
  }
}

上記のログのように情報は正しく出力できていますが、画面上では全てのログがINFOログ(青色のログ)として表示されていました。

こちらにあるように、前提としてDatadogのログレベルはstatus属性として保持しています。

このデフォルトの status は、ログ自体に含まれる実際のステータスを必ずしも反映しているとは限りません。

このようにログの情報から自動で判定してくれる場合もありますが、今回はDatadogがログのレベルを判定するのに必要な情報が付与されておらず、log.levelをログのレベルとして認識できていないことが問題でした。

そのためlog.levelをログのレベルとして認識させる設定変更が必要でした。

Status Remapperの機能を使用してlog.levelをログレベルとして認識されるように調整しました。

Status RemapperはLogs > 右上のLog Configuration > Pipelines から作成することができます。