トレースの計装をしている時に頻出の話題として、ヘルスチェックのエンドポイントなど不要なスパンをいかに落とすかという問題がある。今回は Java Agent を使用してゼロコード計装をしている場合に、Java 標準の SPI (Service Provider Interface)を使って特定のスパンを除外する方法を試してみた。
Java 17、OpenTelemetry Java Agent 2.25.0 で動作確認しています。
アプリケーションの構成
Spring Boot アプリケーションに加えて、OpenTelemetry Java Agent に独自の処理を注入するためのプロジェクトを otel-extension というディレクトリに作成する構成。
demo/
├── build.gradle
├── settings.gradle # include 'otel-extension' を追加
├── gradle.properties
│
├── src/main/
│ ├── java/com/example/demo/
│ │ ├── controller/
│ │ │ ├── HealthController.java # 除外したい /health エンドポイント
│ │ │ └── ...
│ │ ├── DemoApplication.java
│ │ └── ...
│ └── resources/
│ └── application.properties
│
├── otel-extension/ # 新規作成するサブプロジェクト
│ ├── build.gradle
│ └── src/main/
│ ├── java/com/example/extension/
│ │ ├── FilteringSampler.java # 方法1: Sampler
│ │ ├── FilteringSamplerProvider.java
│ │ ├── FilteringSpanExporter.java # 方法2: SpanExporter
│ │ └── FilteringSpanExporterProvider.java
│ └── resources/
│ └── META-INF/
│ └── services/
│ └── io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider
│ # SPI ファイル(中身は Provider の FQCN)
│
├── opentelemetry-javaagent.jar # OpenTelemetry Java Agent
│
└── build/libs/
└── demo-0.0.1-SNAPSHOT.jar # bootJar で生成される実行可能 JAR
otel-extension/build.gradle
plugins {
id 'java'
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
compileOnly 'io.opentelemetry:opentelemetry-sdk:1.54.1'
compileOnly 'io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:1.54.1'
compileOnly 'io.opentelemetry.semconv:opentelemetry-semconv:1.39.0'
}
SPI ファイル: otel-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider
SPI ファイルは、Agent が起動時に自動で発見・読み込みするための仕組みで、ファイル名がインターフェースの完全修飾名、中身が実装クラスの完全修飾名になる。今回は、io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider インターフェースの実装クラスの完全修飾名を指定する。
方法1: Sampler による除外
スパンの生成時にサンプリング判定を行い、不要なスパンをそもそも作らない方法。
FilteringSampler.java
package com.example.extension; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.context.Context; import io.opentelemetry.sdk.trace.data.LinkData; import io.opentelemetry.sdk.trace.samplers.Sampler; import io.opentelemetry.sdk.trace.samplers.SamplingResult; import io.opentelemetry.semconv.UrlAttributes; import java.util.List; import java.util.Set; class FilteringSampler implements Sampler { // 除外対象の URL パスを定義 // https://opentelemetry.io/docs/specs/semconv/registry/attributes/url/#url-path private static final Set<String> EXCLUDED_PATHS = Set.of("/health"); private final Sampler delegate; FilteringSampler(Sampler delegate) { this.delegate = delegate; } @Override public SamplingResult shouldSample( Context parentContext, String traceId, String name, SpanKind spanKind, Attributes attributes, List<LinkData> parentLinks) { String urlPath = attributes.get(UrlAttributes.URL_PATH); if (urlPath != null && EXCLUDED_PATHS.contains(urlPath)) { return SamplingResult.drop(); } return delegate.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); } @Override public String getDescription() { return "FilteringSampler{" + delegate.getDescription() + "}"; } }
FilteringSamplerProvider.java
package com.example.extension; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; import io.opentelemetry.sdk.trace.samplers.Sampler; public class FilteringSamplerProvider implements AutoConfigurationCustomizerProvider { @Override public void customize(AutoConfigurationCustomizer autoConfiguration) { autoConfiguration.addTracerProviderCustomizer( (tracerProviderBuilder, config) -> tracerProviderBuilder.setSampler( new FilteringSampler(Sampler.parentBased(Sampler.alwaysOn())))); } }
SPI ファイル
com.example.extension.FilteringSamplerProvider
注意点
Sampler ではスパンが作成される段階(startSpan)で設定された属性(attributes)のみ判定(shouldSample)に使用できる。
例えば、url.path は以下のようにスパン作成時に属性が設定されるため、Sampler での判定に使用できる。
- Instrumenter#doStartImpl が extractor の onStart を呼ぶ
- HttpServerAttributesExtractor#onStart が url attributes の extractor を呼ぶ
- 最終的に InternalUrlAttributesExtractor#onStart が呼ばれ、
url.pathが設定される
方法2: SpanExporter による除外
エクスポート時に不要なスパンを除外する方法。スパンの終了後に判定するため、全ての属性が揃った状態でフィルタリングできる。
FilteringSpanExporter.java
package com.example.extension; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.semconv.UrlAttributes; import java.util.Collection; import java.util.Set; class FilteringSpanExporter implements SpanExporter { // 除外対象の URL パスを定義 // https://opentelemetry.io/docs/specs/semconv/registry/attributes/url/#url-path private static final Set<String> EXCLUDED_PATHS = Set.of("/health"); private final SpanExporter delegate; FilteringSpanExporter(SpanExporter delegate) { this.delegate = delegate; } @Override public CompletableResultCode export(Collection<SpanData> spans) { var filtered = spans.stream() .filter( span -> { String path = span.getAttributes().get(UrlAttributes.URL_PATH); return path == null || !EXCLUDED_PATHS.contains(path); }) .toList(); if (filtered.isEmpty()) { return CompletableResultCode.ofSuccess(); } return delegate.export(filtered); } @Override public CompletableResultCode flush() { return delegate.flush(); } @Override public CompletableResultCode shutdown() { return delegate.shutdown(); } }
FilteringSpanExporterProvider.java
package com.example.extension; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; public class FilteringSpanExporterProvider implements AutoConfigurationCustomizerProvider { @Override public void customize(AutoConfigurationCustomizer autoConfiguration) { autoConfiguration.addSpanExporterCustomizer( (exporter, config) -> new FilteringSpanExporter(exporter)); } }
SPI ファイル
com.example.extension.FilteringSpanExporterProvider
ビルドと実行
ビルドと Java Agent の準備。
# otel-extension をビルド ./gradlew :otel-extension:build # Spring Boot アプリをビルド ./gradlew bootJar # OpenTelemetry Java Agent をダウンロード curl -L -O https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar
logging exporter を使用してコンソールにスパンを出力するようにして実行する。実行時に otel-extension の JAR を指定して起動しているところがポイント。SPI ファイルの中身を書き換えると 2 つの方法を切り替えられる(otel-extension はビルドし直す必要がある点に注意)。
java \ -javaagent:opentelemetry-javaagent.jar \ -Dotel.javaagent.extensions=otel-extension/build/libs/otel-extension.jar \ -Dotel.traces.exporter=logging \ -Dotel.metrics.exporter=none \ -Dotel.logs.exporter=none \ -jar build/libs/demo-0.0.1-SNAPSHOT.jar
この状態でアクセスすると、/health へのリクエストはフィルタリングされてスパンが出ないが、それ以外のエンドポイントは通常通りスパンが出力される。
その他の方法
OpenTelemetry Collector の Filter Processor
Filter Processor なら Collector の設定だけで属性を指定してスパンを除外できる。
Declarative Configuration
環境変数による設定よりも表現力豊かで、言語によらない宣言的な設定方法として Declarative Configuration の仕様策定が進んでいる。ヘルスチェックのエンドポイントを除外する設定は以下のようにシンプルに書ける。
file_format: "1.0-rc.3" resource: attributes: - name: service.name value: demo tracer_provider: processors: - batch: exporter: otlp_http: endpoint: https://otlp-vaxila.mackerelio.com/v1/traces headers: - name: Accept value: "*/*" - name: Mackerel-Api-Key value: ${MACKEREL_APIKEY} sampler: rule_based_routing: fallback_sampler: always_on: span_kind: SERVER rules: - action: DROP attribute: url.path pattern: /health
java -javaagent:opentelemetry-javaagent.jar \ -Dotel.experimental.config.file=otel-config.yaml \ -jar build/libs/demo-0.0.1-SNAPSHOT.jar
現時点では Experimental だけど、将来的には設定方法の主流になっていきそう。


