SPECIALIST

多様な専門性を持つNRIデジタル社員のコラム、インタビューやインサイトをご紹介します。

BACK

Google Cloudで実行するSpringアプリケーションのトレーサビリティ向上に向けて

こんにちは、NRIデジタルの島です。
NRIグループでは、Javaのメジャーなフレームワークの一つであるSpringを使用したアプリケーション開発の案件が多く、昨今では新規開発、移行問わずパブリッククラウドをSpringアプリケーションの実行環境とすることが増えてきています。AWSとともに選択肢として多いのがGoogle Cloudです。

今回の記事では、Google CloudでSpringアプリケーションを稼働させる上で、運用上のトレーサビリティを高めるために必要なロギングトレーシングについて、実機で検証しながら確認していきたいと思います。

なお、本記事では、以下のような単純なWeb APIのユースケース(各APIのサーバ名を連結して返却)のアプリケーションを「Spring Boot(versionは3系を利用)」で実装し、「Cloud Run」にデプロイして動作検証に使用します。


ロギング

まず、ロギングです。Google Cloudでログ監視する場合、「Cloud Logging」を使用することが多いでしょう。
Cloud Logging | Google Cloud | Google Cloud

Cloud Loggingは、ログストア、監視サービスとして「Google Kubernetes Engine(GKE)」「App Engine」「Cloud Run」など多くのサービスと統合されており、簡単に各種ログを連携させ、参照することができます。今回はSpringアプリケーションをCloud Runにデプロイしますが、ログを標準出力さえすればCloud Loggingですぐに参照可能になります。

ただ、Cloud Loggingでの検索性を向上させるには一工夫が必要です。
以下は、何も考えず「Logback」のConsole Appenderにて標準出力した際のCloud Logging上のログです。

ログ出力(Logback)

・・・
Logger logger = LoggerFactory.getLogger(Application.class);
logger.info("this is server" + this.funcConfig.getServerNo() + " log message.");
・・・

Cloud Loggingコンソール


出力したログが「textPayload」に一行で出力されます。このままだと、ログ調査に必要な項目(リクエストIDやトレースIDなど)を出力したとしても、項目値でフィルタ(「requestId=”12345”」など)することが難しいです。検索性を高めるには、以下のページに案内のある通り、jsonPayloadに構造化ログとして出力することが必要になってきます。
構造化ロギング | Cloud Logging | Google Cloud

今回は上記ページで提案されているクライアントライブラリを使用したパターンで試したいと思います。
※以下Spring Cloudの「Cloud Loggingクライアントライブラリ」を使用します。

Cloud Logging

使用するには、モジュールの依存関係に以下を追加します。

本記事ではGradleを使用します。他のビルドツール(Mavenやsbtなど)をご使用の場合は読み替えてください。

build.gradle

・・・
dependencies {
・・・
    implementation 'com.google.cloud:spring-cloud-gcp-starter-logging'
・・・
}

dependencyManagement {
    imports {
       mavenBom "com.google.cloud:spring-cloud-gcp-dependencies:${springCloudGcpVersion}"
    }
}

また、Logbackを「JSON Appender」を使用するように構成します。

logback.xml

<configuration>
  <include resource="com/google/cloud/spring/logging/logback-json-appender.xml" />
  
  <root level="INFO">
    <appender-ref ref="CONSOLE_JSON" />
  </root>
</configuration>

各種パラメータを個別に設定することも可能です。
設定可能なパラメータなどはドキュメントをご確認ください。
(再掲)Cloud Logging

準備は以上です。
なお、本ライブラリはログ情報追加にLogbackの「Mapped Diagnostic Context(以下MDC)」を使用しています。そのため、カスタムフィールドを「jsonPayload」に追加する場合はMDCにセットする必要があります。以下のようにMDCにリクエストID(requestId)をセットし、メッセージを出力させてみます。

APIコード

・・・
@GetMapping("/servers")
public String servers(
    @RequestHeader(required = false) String requestId,
    @RequestParam(required = false) String servers
) {

  // カスタムフィールドをセット
  MDC.put("requestId", requestId);
  // ログを出力
  log.info("this is server" + this.funcConfig.getServerNo() + " log message.");
  
・・・

このコードをCloud Runへデプロイし、Curlを使ってリクエストしてみます。

curl -v https://******************.a.run.app/servers -H "requestId: 12345"

Cloud Loggingの管理コンソールで参照すると、期待通りログメッセージもカスタムフィールドもjsonPayload下に出力されていました。


構造化して出力できたことで、検索時にフィルタをかけることも容易になりました。


MDCはスレッドバウンドな変数のため、Spring Webfluxなどのリアクティブフレームワークを使用する場合はそのままでは正常に機能しませんので注意してください。
※WebFilterでログ出力したいリクエスト情報をコンテキストに詰めておくなどの対応が必要になります。

エラー発生時のログ出力についても確認するために、Server2でエラーを発生するようにしてみます。


APIコードを、エラーフラグ(IsError)が「True」の際にはエラーになるよう実装し、呼び出し先のServer2のエラーフラグを「True」に設定します。

APIコード

・・・
if (Boolean.parseBoolean(this.funcConfig.getIsError())) {
  throw new RuntimeException("Error");
}
・・・

エラーフラグ(Server2 環境変数)

・・・
IS_ERROR: 'true'
・・・

呼び出し元のServer1では「request」メソッドでServer2を呼び出した際にエラーを受信するようになります。エラーキャッチ後、エラーログを出力し、実行時エラーをスローするように実装してみます。

APIコード

・・・
try {
  names = !(this.funcConfig.getTargetHost().isEmpty()) ? this.request(names) : names;
} catch (Throwable t) {
  log.error("An error has occurred. Exception->" + t.getClass());
  throw new RuntimeException(t);
}
・・・
private String request(String names) {

  return this.webClient.get()
      .uri(this.funcConfig.getTargetProtocol() + "://" + this.funcConfig.getTargetHost()
          + "/servers?servers=" + names)
      .header("requestId",  MDC.get("requestId"))
      .retrieve()
      .bodyToMono(String.class)
      .block();

}
・・・

再度実行するとエラーが発生しますが、以下のようにエラーメッセージも期待通りjsonPayload下に出力され、後続のスタックトレースは1つのログエントリで出力されています。

エラーログ


スタックトレース


開発時の考慮事項

アプリケーションをローカルで開発する際、Cloud Logging用に整形されたログ出力では逆に見にくいと感じる開発者もいると思います。

標準出力されたログをCloud Loggingに送信するのは各サービスのエージェントが実施しているので、このままローカルで実行してもエラーにはなりません。

その場合は、以下のようにConsole Appenderも追加で登録することで、見慣れたフォーマットでも出力することが可能です。

logback.xml

<configuration>
  <include resource="com/google/cloud/spring/logging/logback-json-appender.xml" />
  <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
  <include resource="org/springframework/boot/logging/logback/console-appender.xml" />

  <root level="INFO">
    <appender-ref ref="CONSOLE_JSON" />
    <appender-ref ref="CONSOLE" />
  </root>
</configuration>

2パターンで出力される


ただし、ログが2重に出力されて煩わしく、また、Google Cloud上で実行する場合に不要な定義になることから、Springのプロファイル切り替えなどを使用し、以下のようにローカル用とGoogle Cloud用で分けるのも一つの方法です。

application-gcp.yml

・・・
logging:
  config: classpath:logback-gcp.xml
・・・

logback-gcp.xml

<configuration>
  <include resource="com/google/cloud/spring/logging/logback-json-appender.xml" />
  
  <root level="INFO">
    <appender-ref ref="CONSOLE_JSON" />
  </root>
</configuration>

logback-local.xml

<configuration>
  <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
  <include resource="org/springframework/boot/logging/logback/console-appender.xml" />

  <root level="INFO">
    <appender-ref ref="CONSOLE" />
  </root>
</configuration>

Console Appenderではなく、File Appenderを追加して、ローカル実行時はファイルの方を見るというやり方もできます。ただ、Google Cloud上での実行時に不要な定義が残ることに変わりはありません。

トレーシング

次にトレーシングです。Google Cloudでのトレーシングには「Cloud Trace」を使用します。
Cloud Trace | Google Cloud

Springを使用してトレースデータをCloud Traceと連携するためには、Spring Cloudの以下「Cloud Traceクライアントライブラリ」を使用します。
Cloud Trace

本ライブラリは内部でトレース情報をキャプチャするTracerとしてMicrometerを使用し、トレースされた情報をCloud Traceに送信・保存します。

MicrometerはJavaアプリケーションでメトリクスを収集・処理、及びエクスポートするためのメトリクス・ファサード・ライブラリで、PrometheusDatadogをはじめ、多くのメトリクス監視プロダクトへの連携が可能です。
もともとはメトリクスのみのサポートでしたが、Spring6(Spring Boot3)から本格的にトレーシングもサポートするようになりました(Micrometer Tracing)。
このMicrometer Tracingはファサードとして各トレースライブラリのトレース情報を変換及び伝搬する「TracerBridge」を提供します。執筆時点では以下のトレースライブラリをサポートします。

今回利用するCloud Traceクライアントライブラリは、Zipkin Braveのトレーシングライブラリを使用しますが、Zipkinを経由することなく、Cloud Traceに直接トレースデータを送信・保存することが可能です。

使用するには、モジュールの依存関係に「Cloud Traceクライアントライブラリ」を追加します。

build.gradle

・・・
dependencies {
・・・
    implementation 'com.google.cloud:spring-cloud-gcp-starter-logging'
    implementation 'com.google.cloud:spring-cloud-gcp-starter-tracing' ←追加
・・・
}

dependencyManagement {
    imports {
       mavenBom "com.google.cloud:spring-cloud-gcp-dependencies:${springCloudGcpVersion}"
    }
}
必要に応じてサンプリング間隔などを設定します。
※サンプリング間隔はデフォルト「0.1(10%)」ですが、今回は検証のため、「1(100%)」で設定します。

application.yml

・・・
management:
  tracing:
    sampling:
      probability: 1
・・・

準備はこれだけです。(コードの実装などは不要)

では、Cloud Runへのデプロイ後、再度Curlを使ってリクエストしてみます。
以下の通り、Cloud Traceの管理コンソールで、一意のトレースIDで各APIのトレース情報が参照できるようになりました。

Cloud Trace コンソール


一部のAPI(Server2)のみ5秒スリープさせ、再度実行してみます。遅延の状況が可視化され、Server2の処理が原因であることがわかります。


Cloud Loggingのログとも連携されており、トレース情報から該当のログを追跡することが可能です。


なお、デフォルトではトレースIDはjsonPayload下に出力されないようです。出力したい場合は、ロギングのところでご説明した通り、MDCにセットしてください。
ただし、ライブラリ側でデフォルトで「traceId」という名前を使用しているため、別名でMDCにセットするか、ロギングのところで掲載したLogbackの設定をカスタマイズして、ライブラリ側が使用する名前を変更してください。

①別名でMDCにセットする場合

APIコード

・・・
@GetMapping("/servers")
public String servers(
    @RequestHeader(required = false) String requestId,
    @RequestHeader(required = false, value = "x-b3-traceid") String xB3Traceid,
    @RequestParam(required = false) String servers

) throws Exception {

  // トレース情報を設定(別名)
  MDC.put("xB3Traceid", xB3Traceid == null ? "": xB3Traceid);
  
・・・

Cloud Logging


②ライブラリ側が使用する名前を変更する場合

logback.xml

<configuration>
  <property name="projectId" value="${projectId:-${GOOGLE_CLOUD_PROJECT}}"/>

  <appender name="CONSOLE_JSON" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
      <layout class="com.google.cloud.spring.logging.StackdriverJsonLayout">
        <projectId>${projectId}</projectId>
        <traceIdMdcField>xB3TraceId</traceIdMdcField> ←別名に変更
      </layout>
    </encoder>
  </appender>

  <root level="INFO">
    <appender-ref ref="CONSOLE_JSON" />
  </root>

</configuration>

APIコード

・・・
@GetMapping("/servers")
public String servers(
    @RequestHeader(required = false) String requestId,
    @RequestHeader(required = false, value = "x-b3-traceid") String traceId,
    @RequestParam(required = false) String servers

) throws Exception {

  // トレース情報を設定
  MDC.put("traceId", traceId== null ? "": traceId);
  
・・・

Cloud Logging


開発時の考慮事項

ロギングと異なり、トレース情報はライブラリが直接Cloud Traceへ送信します。そのため、ローカル開発時は、ローカルからCloud Traceへアクセス不可の場合エラーとなってしまいます。

(参考)エラーで起動不可


停止したい場合は、以下のように設定にてトレーシングを無効にすることが可能です。

application.yml

・・・
spring:
  cloud:
    gcp:
      trace:
        enabled: false  ←無効に設定
・・・

もちろん、ローカルでトレーシングも確認したい場合は、認証情報を設定することで動作可能です。
必要に応じて設定いただければと思います。
gcloud CLIを使用して認証する | Authentication | Google Cloud

検証を終えて

Google CloudにてSpringアプリケーションのトレーサビリティを高めたい際に必要と思われるロギング及びトレーシングについて、実機を触りながら試してみました。
本記事で紹介した事項はGoogle CloudやSpringの公式ページで案内はありますが、実際に動かしてみた事例はあまり多くないように思います。本記事の内容は最低限のものではありますが、トレーサビリティ向上の第一歩としての一助になれば幸いです。