🏝️

いろんな言語で実装されたアプリケーションに計装を行う - Java(kotlin)編

2023/10/16に公開

はじめに

こんにちは、Splunk Observability の導入支援を行っている、kntr_nkgm です。
今回は Java(kotlin) で実装されているアプリケーションに対して、OpenTelemetry による計装を行っていきます。
シリーズ的に書いていく予定ですので、この一連の記事の位置づけについては、以下を参照してください。技術的な内容のみ参照したい場合は、読み飛ばしてもらって構いません。

この記事の位置づけ

これは何?

共通の仕様に基づいて、異なる言語・フレームワークを用いて実装されているアプリケーションに対して、OpenTelemetry によって計装を行っていきます。
「これぐらいのことをやれば、APMを活用できるんだね」という理解を深めていくことを目的としています

どうやるの?

アプリケーション

RealWorld (Github) からサンプルアプリケーションを拝借します。
これは、

  • マイクロブログサービスのサンプルコード集
  • 共通仕様に基づいて、異なる言語・ライブラリ・フレームワークを用いて実装されている
  • フロントエンドのみ、バックエンドのみのコードもあり、組み合わせ可能
    というものなので、学習用途にはちょうど良さそうです

APM/オブザーバビリティツール

Opentelemetry ベースで実装

  • オープンスタンダードなオブザーバビリティフレームワーク

言語・フレームワークによっては、Auto Instrumentation によりコードに手を加えることなく一定のデータ取得が可能になるので、基本的にはまずはそこから始めます。
加えて、いくつかのシナリオを想定して、Manual Instrumentation を行っていきます。
トラブル時の調査で「以下のような情報が確認できたらなー」というようなデータを想像しながら、より活用しやすくなるように、という観点で、トレースに含める情報を増やしたり変更したりする予定です。
バックエンドとしては、Splunk Observability Cloud を利用します。
が、OpenTelemetryに準拠していれば、どのバックエンドでも基本的に利用可能です。

シリーズものの投稿

環境準備

EC2 の環境で作業を実施していきます。
環境としては以下のようなものを用意しています。

  • Ubuntu 22.04 LTS, t2.large

事前に OS のアップデートまでは完了させておきます。

$ sudo apt update && sudo apt upgrade -y

Java環境の準備

今回のサンプルアプリケーションは、以下のレポジトリから拝借してきます。
kvision-realworld-example-app-fullstack

このアプリケーションは、JDK 8 以上が指定されているので、さらっとインストールできる最新バージョンの OpenJDK を導入します。

$ sudo apt install openjdk-17-jdk

アプリケーションのダウンロードと起動

READMEを参考にしながら、とりあえず起動できるまでやっていきます。

$ git clone https://github.com/rjaros/kvision-realworld-example-app-fullstack.git
$ cd kvision-realworld-example-app-fullstack/

# バックエンドアプリケーションを起動
$ ./gradlew backendRun

以下のような表示が出たら Ctrl + Z でバックエンド処理へ。

> Task :bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.7.0)

2023-09-29 09:31:11,802 [restartedMain] INFO  o.s.b.d.e.DevToolsPropertyDefaultsPostProcessor - Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2023-09-29 09:31:11,804 [restartedMain] INFO  o.s.b.d.e.DevToolsPropertyDefaultsPostProcessor - For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2023-09-29 09:31:12,749 [restartedMain] INFO  o.s.d.r.c.RepositoryConfigurationDelegate - Bootstrapping Spring Data R2DBC repositories in DEFAULT mode.
2023-09-29 09:31:12,774 [restartedMain] INFO  o.s.d.r.c.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 21 ms. Found 0 R2DBC repository interfaces.
2023-09-29 09:31:14,118 [restartedMain] INFO  o.s.b.d.a.OptionalLiveReloadServer - LiveReload server is running on port 35729
2023-09-29 09:31:14,222 [restartedMain] INFO  o.s.b.w.e.netty.NettyWebServer - Netty started on port 8080
<==========---> 81% EXECUTING [1m 43s]

続いて、フロントエンドを起動

$ ./gradlew -t frontendRun

こちらもこんな感じの表示が出たらOK

BUILD SUCCESSFUL in 1m 8s
18 actionable tasks: 16 executed, 2 up-to-date

Waiting for changes to input files... (ctrl-d to exit)
<i> [webpack-dev-server] [HPM] Proxy created: /kv  -> http://localhost:8080
<i> [webpack-dev-server] [HPM] Proxy created: /kvws  -> ws://localhost:8080
<i> [webpack-dev-server] Project is running at:
<i> [webpack-dev-server] Loopback: http://localhost:3000/
<i> [webpack-dev-server] On Your Network (IPv4): http://<ip_address>:3000/
<i> [webpack-dev-server] Content not from webpack is served from '/home/ubuntu/kvision-realworld-example-app-fullstack/build/processedResources/frontend/main' directory
<-------------> 0% WAITING

ブラウザで http://<EC2のIPアドレス>:3000 にアクセスすると、トップページが表示されますね。

とりあえず準備OKです。
アプリケーションは停止しておきます。

OpenTelemetry による計装

OpenTelemetry Collector の導入

今回も Splunk distro の Collector を使います。
前回の Node.js編 でも同様のアプローチを行いましたので、ここでは割愛します。
いわゆるクラウドサービスでは、このあたりは大体いい感じに自動化できるガイドが出ていることが一般的化と思います。

Auto Instrumentation

次に APM のセットアップです。
こちらも同様に、メニュー画面から実施していきますが、実際に実行するコマンドは以下です。

# Javaエージェントを取得
$ curl -L https://github.com/signalfx/splunk-otel-java/releases/latest/download/splunk-otel-javaagent.jar -o splunk-otel-javaagent.jar
# 環境変数設定
## アプリケーション名を指定
$ export OTEL_SERVICE_NAME='knrw-java'
## 取得するデータに付与するAttribute情報を指定
$ export OTEL_RESOURCE_ATTRIBUTES='deployment.environment=knrw,service.version=0.1.0'
## OpenTelemetry Collector のポート
$ export OTEL_EXPORTER_OTLP_ENDPOINT='http://localhost:4317'

Gradleが管理するパッケージをJarファイルにパッケージングします

$ ./gradlew jar

このJarファイルを OpenTelemetry Java エージェントとともに起動します。

$ java "-Xmx512m" "-Xms512m" \
-javaagent:/home/ubuntu/kvision-realworld-example-app-fullstack/splunk-otel-javaagent.jar \
-Dsplunk.profiler.enabled=true \
-Dsplunk.profiler.memory.enabled=true \
-Dsplunk.metrics.enabled=true \
-jar build/libs/*.jar

先ほど同様、バックグラウンド処理させておいて、さらにフロントエンド側を起動してあげます。

$ ./gradlew -t frontendRun

またブラウザでアクセスして、Sign Up, Sign Inなどの処理を実施してみます。

しばらくすると、Splunk Observability Cloud 側でサービスマップ画面が描写されました。

一応トレースも見れるようにはなりましたね。ちょっとこれだけだと理解しづらいですが。

何はともあれ、もう少し細かい粒度で見れるようにしたいですね。
ちょっと頑張ってみましょうか。

Manual Instrumentation

さて、前回同様、こちらでも手動での計装を追加してみましょう。

なにはともあれ、まずは必要な依存パッケージを追加します。

$ vi build.gradle.kts

val backendMain by getting という句があるので、その下部に以下を追加

build.gradle.kts
implementation("io.opentelemetry:opentelemetry-api:1.30.0")
implementation("io.opentelemetry:opentelemetry-sdk:1.30.0")
implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.30.0")
implementation("io.opentelemetry:opentelemetry-semconv:1.30.0-alpha")
implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:1.29.0")

一度この時点でパッケージングして、エラーが出ないことを確認しておきます。

$ ./gradlew jar

まずは Span を増やす

ログイン操作などに関連する処理は、以下のファイルに記述されているようです。
ここに手を加えてみましょう。

$ vi src/backendMain/kotlin/io/realworld/UserService.kt

ファイル冒頭部分で、モジュールを import している箇所があるの、その末尾ぐらいに、OpenTelemetry 用のモジュールを追加します。

src/backendMain/kotlin/io/realworld/UserService.kt
import io.opentelemetry.api.trace.Span
import io.opentelemetry.instrumentation.annotations.WithSpan

override suspend fun から始まる function が複数あるので、その上に以下のアノテーションを追加します

src/backendMain/kotlin/io/realworld/UserService.kt
@WithSpan

改めてパッケージングして、アプリケーションを起動してみます

$ ./gradlew jar
$ java "-Xmx512m" "-Xms512m" \
-javaagent:/home/ubuntu/kvision-realworld-example-app-fullstack/splunk-otel-javaagent.jar \
-Dsplunk.profiler.enabled=true \
-Dsplunk.profiler.memory.enabled=true \
-Dsplunk.metrics.enabled=true \
-jar build/libs/*.jar
# 起動したらバックグラウンド化
# フロントエンド起動
$ ./gradlew -t frontendRun

ブラウザで先ほどと同様、長いユーザー名で Sign Up 処理を実施してみましょう。
ほどなくして、今度はその処理を実施したスパンが表示されるようになります。

エラーとして表示されているので、このスパンをのぞいてみると、exception message やstacktrace が出力されていますね。

UserService.kt の 72行目でエラーが起きたという点も分かりました。助かりますねー。

Sign Up 時に指定したユーザ名などの情報を取得する

さて、では、エラーが起きた時にどんなユーザー名を指定されたかを確認できるようにしていきましょう。
先ほどエラーと表示されたスパンの情報から、72行目付近でユーザー名に関するエラーハンドリングの処理が行われているであろうことが想像されるので、それを開いてみましょう。

$ vi src/backendMain/kotlin/io/realworld/UserService.kt

throw ServiceException(errorMessages.joinToString("\n") という箇所があるので、その上に以下のコードを挿入します。

src/backendMain/kotlin/io/realworld/UserService.kt
            val span = Span.current()
            span.setAttribute("user", username.toString())
            span.setAttribute("email", email.toString())
            span.setAttribute("password", password.toString())

再パッケージング・アプリケーション起動・フロントエンド起動を行って、ブラウザで Sign Up 処理を実施してみます。

$ ./gradlew jar
$ java "-Xmx512m" "-Xms512m" \
-javaagent:/home/ubuntu/kvision-realworld-example-app-fullstack/splunk-otel-javaagent.jar \
-Dsplunk.profiler.enabled=true \
-Dsplunk.profiler.memory.enabled=true \
-Dsplunk.metrics.enabled=true \
-jar build/libs/*.jar
$ ./gradlew -t frontendRun

スパン属性として、ユーザー名などの情報が取れるようになりましたね。

おわりに

APM を使い始める場合には、簡単に始められる Java や Node.js (Javascript) からやってみるという勧めを聞いたことのある人もいるかと思いますが、まさに、それほど難しいものではなかったのではないかなと思います。
Manual Instrumentation をやりだすと、ちょっと手間がかかってきますが、それでも、これぐらいの感じでできちゃいますので、とりあえず試してみるというのもアリなのではないでしょうか。

たぶん次回は、Python での実装に対する計装を扱うことになると思いますので、いい感じにまとまったら公開していこうと思います。

Discussion