はじめに
こちらの記事は、Oracle Cloud Infrastructure Advent Calender 2023 Day 4 の記事として書かれています。
2023/10 に Java のマイクロサービス開発用のフレームワークである Helidon は 4.0.0 がリリースされ、MicroProfile 6.0 に対応しました。それ以外にも Web サーバーの実装が Netty から Virtual Threads ベースの Níma に置き換わったりと大きく変化をしています。
MicroProfile 6.0 の文脈で言えば、近年の分散トレーシングの標準化に伴い、MicroProfile Telemetry Tracing という新しい仕様が追加されています。これは、OpenTelemetry 1.13 仕様に準拠するもので、MicroProfile アプリケーションが OpenTelemetry(以下、OTel) を介して分散トレーシングが有効になっている環境に簡単に参加できるようにするための振舞いが定義されています。ただし、仕様にも記載のある通り、OTel の Metrics, Logging の統合は、MicroProfile Telemetry Tracing の範囲外となっており、実装者は必要に応じて Metrics, Logging の両方のサポートを自由に実装を提供することができます。
そこで今回は、MicrpProfile 仕様を実装したフレームワークである Helidon を用いて MicroProfile Telemetry Tracing を試してみようと思います。OTel のバックエンドは、Oracle Cloud Infrastructure の 通称 APM(Application Performance Monitoring)というサービスを使用します。
全体の構成図
以下のように、Kubernetes(OKE)上で Helidon を用いたアプリケーションを稼働させ、OTLP(OpenTelemetry Protocol)を用いて、テレメトリーデータを APM へ送信します。
尚、使用するサンプルアプリケーションは作成済みの以下のアプリケーションを使用します。
エンドポイントは、2 つ存在し、それぞれ以下のように振舞います。
-
/api/cowsay/say
: MicroProfile REST Client を用いて、/remote/cowsay/say
にリクエストを送信 -
/api/cowsay/delay
: MicroProfile REST Client を用いて、/remote/cowsay/delay
にリクエストを送信
Automatic Instrumentation
MicroProfile に限らず、Java の自動計装は専用の Java Agent を実行中の JVM にアタッチするだけでテレメトリーデータの収集が可能です。簡単で良きですね!
コンテナで実行する場合は、Dockerfile を以下のように書いておけば環境によって変わり得る部分は、環境変数から読み込む等の工夫がしやすいと思います。( Kubernetes 上で稼働させる場合は、initContainers などで Agent の JAR ファイルを取得するのも良いかもしれません。 )
OpenTelemetry Operator の存在をこの記事を執筆した後に知ったので、合わせて記事としました。実施していることは、initContainers を用いた方法と同じですが、既に存在する Operator があるのでこちらを使うのがよろしいかと思います。
FROM maven:3.9.5 as build
WORKDIR /helidon
ADD pom.xml pom.xml
RUN mvn package -Dmaven.test.skip -Declipselink.weave.skip -Declipselink.weave.skip -DskipOpenApiGenerate
ADD src src
RUN mvn package -DskipTests
RUN echo "done!"
# Multistage build の 1 stage で使用する OpenTelemetry Java Agent をダウンロードする
FROM busybox as download
WORKDIR /var/cache
RUN wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v1.32.0/opentelemetry-javaagent.jar
RUN echo "done! - download stage"
FROM eclipse-temurin:21-jre
WORKDIR /helidon
COPY --from=build /helidon/target/otel-helidon.jar ./
COPY --from=build /helidon/target/libs ./libs
COPY --from=download /var/cache ./agent
ENV JAVA_TOOL_OPTIONS=-javaagent:/helidon/agent/opentelemetry-javaagent.jar
CMD ["java", "-jar", "otel-helidon.jar"]
EXPOSE 8080
上記のように JAVA_TOOL_OPTIONS
を設定しても良いですし、java -javaagent=<path-to-otel-javaagent>.oteltelemetry-javaagent.jar -jar xxx.jar
のように起動時にパラメータとして渡してあげても良いです。
これを Kubernetes 上で稼働させるための Deployment の定義は以下のようになっています。
kind: Deployment
apiVersion: apps/v1
metadata:
name: otel-helidon
namespace: examples
spec:
replicas: 1
selector:
matchLabels:
app: otel-helidon
template:
metadata:
labels:
app: otel-helidon
version: v1
spec:
containers:
- name: otel-helidon
image: shukawam/otel-helidon:1.0.4
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: OTEL_SERVICE_NAME
value: otel-helidon
- name: OTEL_TRACES_EXPORTER
value: otlp
- name: OTEL_METRICS_EXPORTER
value: none
- name: OTEL_LOGS_EXPORTER
value: none
- name: OTEL_EXPORTER_OTLP_TRACES_PROTOCOL
value: http/protobuf
- name: OTEL_EXPORTER_OTLP_METRICS_PROTOCOL
value: http/protobuf
- name: OTEL_EXPORTER_OTLP_LOGS_PROTOCOL
value: http/protobuf
- name: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
valueFrom:
secretKeyRef:
key: otlp-traces-endpoint
name: otel-helidon-secret
- name: OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
valueFrom:
secretKeyRef:
key: otlp-metrics-endpoint
name: otel-helidon-secret
# - name: OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
# valueFrom:
# secretKeyRef:
# key: otlp-logs-endpoint
# name: otel-helidon-secret
- name: OTEL_EXPORTER_OTLP_HEADERS
valueFrom:
secretKeyRef:
key: otlp-headers
name: otel-helidon-secret
- name: OTEL_EXPORTER_OTLP_TRACES_HEADERS
valueFrom:
secretKeyRef:
key: otlp-headers
name: otel-helidon-secret
- name: OTEL_EXPORTER_OTLP_METRICS_HEADERS
valueFrom:
secretKeyRef:
key: otlp-headers
name: otel-helidon-secret
# - name: OTEL_EXPORTER_OTLP_LOGS_HEADERS
# valueFrom:
# secretKeyRef:
# key: otlp-headers
# name: otel-helidon-secret
OTLP exporter の設定は、Java system properties or 環境変数から行うことができますが今回は環境変数から行っています。
env:
- name: OTEL_SERVICE_NAME
value: otel-helidon
- name: OTEL_TRACES_EXPORTER
value: otlp
- name: OTEL_METRICS_EXPORTER
value: none
- name: OTEL_LOGS_EXPORTER
value: none
- name: OTEL_EXPORTER_OTLP_TRACES_PROTOCOL
value: http/protobuf
- name: OTEL_EXPORTER_OTLP_METRICS_PROTOCOL
value: http/protobuf
- name: OTEL_EXPORTER_OTLP_LOGS_PROTOCOL
value: http/protobuf
- name: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
valueFrom:
secretKeyRef:
key: otlp-traces-endpoint
name: otel-helidon-secret
# - name: OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
# valueFrom:
# secretKeyRef:
# key: otlp-metrics-endpoint
# name: otel-helidon-secret
# - name: OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
# valueFrom:
# secretKeyRef:
# key: otlp-logs-endpoint
# name: otel-helidon-secret
- name: OTEL_EXPORTER_OTLP_HEADERS
valueFrom:
secretKeyRef:
key: otlp-headers
name: otel-helidon-secret
- name: OTEL_EXPORTER_OTLP_TRACES_HEADERS
valueFrom:
secretKeyRef:
key: otlp-headers
name: otel-helidon-secret
# - name: OTEL_EXPORTER_OTLP_METRICS_HEADERS
# valueFrom:
# secretKeyRef:
# key: otlp-headers
# name: otel-helidon-secret
# - name: OTEL_EXPORTER_OTLP_LOGS_HEADERS
# valueFrom:
# secretKeyRef:
# key: otlp-headers
# name: otel-helidon-secret
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
, OTEL_EXPORTER_OTLP_HEADERS
に何を設定すれば良いか?は、APM のドキュメントに記載があります。
-
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
:https://<dataUploadEndpoint>/20200101/opentelemetry/private/v1/traces
-
OTEL_EXPORTER_OTLP_HEADERS
:Authorization=dataKey <your-datakey>
コンソールだと以下部分から参照すれば良いです。
実際に動かして /api/cowsay/say
にリクエストを送ってみると、以下のように APM にテレメトリーデータが収集されていることがわかります。
Manual Instrumentation
自動計装で不十分な場合は、明示的にスパン情報を追加できます。スパンを追加するには、CDI ベースで測定したいメソッドに @WithSpan
というアノテーションをつけるか、io.opentelemetry.api.trace.Tracer.spanBuilder
を使って、スパンの開始と終了を宣言すれば良いです。
@WithSpan
を用いる例
@WithSpan(value = "cdi.span")
private void delayWithCDISpan() {
delayWithSpanBuilder();
try {
Thread.sleep(DELAY);
} catch (InterruptedException | IllegalArgumentException e) {
throw new RuntimeException(e);
}
}
io.opentelemetry.api.trace.Tracer.spanBuilder
を用いる例
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.github.ricksbrown.cowsay.Cowsay;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.annotations.WithSpan;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
@Path("/remote/cowsay")
public class CowsayRemoteExecutor {
private final Tracer tracer;
@Inject
public CowsayRemoteExecutor(Tracer tracer) {
this.tracer = tracer;
}
// ...
private void delayWithSpanBuilder() {
var span = tracer.spanBuilder("builder.span")
.setSpanKind(SpanKind.INTERNAL)
.setParent(Context.current())
.setAttribute("my.attribute","value")
.startSpan();
try {
Thread.sleep(DELAY);
} catch (InterruptedException | IllegalArgumentException e) {
throw new RuntimeException(e);
}
span.end();
}
}
io.opentelemetry.api.trace.Tracer.spanBuilder
を用いた場合には、独自の属性が追加できたり、スパンの開始時刻を設定できたりとスパンの生成をより柔軟に行うことができます。
使用したサンプルアプリケーションの /api/cowsay/delay
の方には、CDI ベースと spanBuilder を使うパターンでカスタムスパンを 2 つ仕込んでいます。実際に、エンドポイントを実行し APM を参照してみると以下のようになっています。
きちんとトレーシングの情報を参照することができました。
終わりに
今回は、Helidon 4.0.0 で使えるようになった MicroProfile 6.0 に含まれている MicroProfile Telemetry Tracing を試してみました。OpenTelemetry のような言語非依存な仕様を活用することで Cloud Native な環境下におけるテレメトリー収集をいい感じに効率よく(大事!)行えると思います。
参考