【Java(Kotlin)編】OTel のドキュメントを読む
概要
OTel のドキュメントを読むことで、実装と用語の対応づけの理解を深めていきます。
今回は Java 編。
今回のために作成したサンプルコード。
動作確認は otel-tui で行う。
OpenTelemetry Java 入門
特に実装することがないのと、現時点で読んでも理解できない部分が多いのでスキップ。
サンプルによる入門
Java エージェントによる計装。
OTel のサンプルコードによくある Dice を使ったサンプルアプリケーション。
Spring Boot starter
API&SDKを順番に読み進めてもわかりづらいので、Spring Boot のゼロコード計装をさきにやる。
Getting started
以下を追加することで、自動的に OTel の計装がされていた
dependencies {
// otel まわりで必要なパッケージ
implementation(platform(SpringBootPlugin.BOM_COORDINATES))
implementation(platform("io.opentelemetry:opentelemetry-bom:1.44.0"))
implementation(platform("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:2.10.0"))
implementation("io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter")
}
@RestController
class Controller {
@GetMapping("/ping")
fun ping(): String {
return "pong"
}
}




Extending instrumentations with the API
Span
span の切り方。
package com.example.opentelemetry_zero_code_instrumentation_practice
import io.opentelemetry.api.OpenTelemetry
import io.opentelemetry.api.trace.Tracer
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class Controller(openTelemetry: OpenTelemetry) {
private val tracer: Tracer = openTelemetry.getTracer("application")
@GetMapping("/ping")
fun ping(): String {
this.pingSpan()
return "pong"
}
fun pingSpan() {
val span = tracer.spanBuilder("pingSpan").startSpan()
span.end()
return
}
}
Meter
Java API & SDK で実装するためスキップ
Meter
Java API & SDK で実装するためスキップ
SDK configuration
General configuration
dependencies {
// 中略
// 用途
// Propagator Distribution(tracecontext、b3)に必要
// - https://opentelemetry.io/docs/specs/otel/context/api-propagators/#propagators-distribution
// 変更点
// - application.yaml(application.properties)に otel.propagators=tracecontext,b3 を追加するとき
implementation("io.opentelemetry:opentelemetry-extension-trace-propagators")
}
spring:
application:
name: opentelemetry-zero-code-instrumentation-practice
otel:
propagators:
- tracecontext
- b3
Overriding Resource Attributes
Resource Attribute の上書き。スキップ。
Disable the OpenTelemetry Starter
OTEL の SDK を無効化する。これを設定するとシグナルが送信されなくなる。
Programmatic configuration
Exclude actuator endpoints from tracing
Sampler の設定。これを設定すると、トレースがサンプリングされなくなる。
ドキュメントのサンプルでは、actuator を drop する。
dependencies {
// 中略
// 名前
// - opentelemetry-samplers
// Maven Repository の URL
// - https://mvnrepository.com/artifact/io.opentelemetry.contrib/opentelemetry-samplers
// 用途
// - サンプリングのカスタマイズに必要
// - このリポジトリでは、actuator の drop に使用する
implementation("io.opentelemetry.contrib:opentelemetry-samplers:1.33.0-alpha")
implementation("org.springframework.boot:spring-boot-starter-actuator")
}
package com.example.opentelemetry_zero_code_instrumentation_practice
import io.opentelemetry.api.trace.SpanKind
import io.opentelemetry.contrib.sampler.RuleBasedRoutingSampler
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider
import io.opentelemetry.semconv.UrlAttributes
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class FilterPaths {
// actuator パスをフィルタリングするカスタマイズ
@Bean
fun otelCustomizer(): AutoConfigurationCustomizerProvider {
return AutoConfigurationCustomizerProvider { p ->
p.addSamplerCustomizer { fallback, config ->
RuleBasedRoutingSampler.builder(SpanKind.SERVER, fallback)
.drop(UrlAttributes.URL_PATH, "^/actuator")
.build()
}
}
}
}
たとえば、↓のような actuator が表示されなくなる。

Configure the exporter programmatically
OTLP exporter に設定を追加する。
今回の例だと、認証用の設定を追加する。
戦術の otelCustomizer を使い回すと起動しなくなるので注意。
package com.example.opentelemetry_zero_code_instrumentation_practice
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class CustomAuth {
@Bean
fun otelCustomizer(): AutoConfigurationCustomizerProvider {
return AutoConfigurationCustomizerProvider { p ->
p.addSpanExporterCustomizer { exporter, _ ->
if (exporter is OtlpHttpSpanExporter) {
exporter.toBuilder()
.setHeaders { headers() }
.build()
} else {
exporter
}
}
}
}
private fun headers(): Map<String, String> {
return mapOf("Authorization" to "Bearer ${refreshToken()}")
}
private fun refreshToken(): String {
// e.g. read the token from a kubernetes secret
return "token"
}
}
Resource Providers
OpenTelemetry Starter を利用すると、以下の Resource が自動的に付与される。
- Distribution Resource Provider
- telemetry.distro.name
- telemetry.distro.version
下記の項目は、所定の箇所に設定(application.yaml、build.gradle.kts、etc...)することで、反映される。
- Spring Resource Provider
- service.name
- service.version
application.yaml の設定例
// 略
otel:
// 略
resource:
attributes:
deployment.environment: dev
service:
name: zero-code-instrumentation-practice
version: 0.0.1
画像は反映後の例。

Service name
Resource の Service の反映の優先順位について記載。
Out of the box instrumentation
Spring Boot starter によって自動計装されるライブラリ一覧。
ライブラリによっては追加の設定が必要。
Annotations
WithSpan アノテーションを利用すると、AOP で Span を生成できる。
package com.example.opentelemetry_zero_code_instrumentation_practice
import io.opentelemetry.api.trace.Span
import io.opentelemetry.api.trace.SpanKind
import io.opentelemetry.instrumentation.annotations.SpanAttribute
import io.opentelemetry.instrumentation.annotations.WithSpan
import org.springframework.stereotype.Component
@Component
class TracedClass {
@WithSpan
fun tracedMethod() {}
@WithSpan(value = "TracedClass span name")
fun tracedMethodWithSpanName() {
val currentSpan = Span.current()
currentSpan.addEvent("ADD EVENT TO tracedMethodWithName SPAN");
currentSpan.setAttribute("isTestAttribute", true);
}
@WithSpan(kind = SpanKind.CLIENT)
fun tracedMethodWithoutAnnotation() {}
@WithSpan
fun tracedMethodWithAttribute(@SpanAttribute("attributeName") parameter: String) {}
}
dependencies {
// 中略
// 名前
// - spring-boot-starter-aop
// Maven Repository の URL
// - https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop
// 用途
// - アノテーションを利用したトレースに必要
// - AOP であるため、アノテーションを付与しただけでは動作せず、Constructor Injection などでインスタンス化する必要がある
implementation("org.springframework.boot:spring-boot-starter-aop")
}
ただし、AOP で動作するため、DI しなければ利用できないことに注意。
package com.example.opentelemetry_zero_code_instrumentation_practice
import io.opentelemetry.api.OpenTelemetry
import io.opentelemetry.api.trace.Tracer
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class Controller(
openTelemetry: OpenTelemetry,
private val tracedClass: TracedClass
) {
private val tracer: Tracer = openTelemetry.getTracer("application")
@GetMapping("/ping")
fun ping(): String {
this.pingSpan()
return "pong"
}
fun pingSpan() {
val span = tracer.spanBuilder("pingSpan").startSpan()
span.end()
return
}
@GetMapping("/ping2")
fun ping2(): String {
tracedClass.tracedMethod()
tracedClass.tracedMethodWithSpanName()
tracedClass.tracedMethodWithoutAnnotation()
tracedClass.tracedMethodWithAttribute("attributeValue")
return "pong2"
}
}
Additional instrumentation
log4j2 の OTel instrumentation について。
多分、ログのブリッジ機能について言っているはず。
Other Spring autoconfiguration
Zipkin の話。よくわからないからスキップ。
APIによるテレメトリーの記録
Context API
Context
Context:アプリケーション全体で暗黙的または明示的に伝搬される不変のキー値ペアのバンドル
アプリケーション全体で、保持したいキーバリューで使う。
以下は使い方一覧。
package com.example.otel_kotlin_api_usage_with_spring_starter
import io.opentelemetry.context.Context
import io.opentelemetry.context.ContextKey
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
class ContextUsage {
companion object {
@JvmStatic
@Throws(Exception::class)
fun contextUsage() {
// 例のコンテキストキーを定義
val exampleContextKey: ContextKey<String> = ContextKey.named("example-context-key")
// コンテキストは追加するまでキーを含まない
// Context.current() は現在のコンテキストにアクセス
// 出力 => current context value: null
println("current context value: " + Context.current().get(exampleContextKey))
// コンテキストにエントリを追加
val context = Context.current().with(exampleContextKey, "value")
// ローカルコンテキスト変数には追加された値が含まれる
// 出力 => context value: value
println("context value: " + context.get(exampleContextKey))
// 現在のコンテキストはまだ値を含まない
// 出力 => current context value: null
println("current context value: " + Context.current().get(exampleContextKey))
// context.makeCurrent() を呼び出すと、スコープが閉じられるまで
// Context.current() がコンテキストに設定され、その後 Context.current() は
// context.makeCurrent() が呼び出される前の状態に復元される。
// 結果として得られる Scope は AutoCloseable を実装し、通常は
// try-with-resources ブロックで使用される。Scope.close() の呼び出しに失敗すると
// エラーとなり、メモリリークやその他の問題を引き起こす可能性がある。
context.makeCurrent().use { scope ->
// 現在のコンテキストに追加された値が含まれる
// 出力 => context value: value
println("context value: " + Context.current().get(exampleContextKey))
}
// ローカルコンテキスト変数には追加された値がまだ含まれる
// 出力 => context value: value
println("context value: " + context.get(exampleContextKey))
// 現在のコンテキストにはもう値が含まれない
// 出力 => current context value: null
println("current context value: " + Context.current().get(exampleContextKey))
val executorService = Executors.newSingleThreadExecutor()
val scheduledExecutorService = Executors.newScheduledThreadPool(1)
// コンテキストインスタンスはアプリケーションコード内で明示的に渡すことができるが、
// Context.makeCurrent() を呼び出し、Context.current() を介してアクセスする
// 暗黙のコンテキストを使用する方が便利である。
// コンテキストは暗黙のコンテキスト伝播のための多数のユーティリティを提供する。
// これらのユーティリティは Scheduler、ExecutorService、ScheduledExecutorService、
// Runnable、Callable、Consumer、Supplier、Function などのユーティリティクラスを
// ラップし、実行前に Context.makeCurrent() を呼び出すように動作を変更する。
context.wrap(::callable).call()
context.wrap(::runnable).run()
context.wrap(executorService).submit(::runnable)
context.wrap(scheduledExecutorService).schedule(::runnable, 1, TimeUnit.SECONDS)
context.wrapConsumer<Any>(::consumer).accept(Any())
context.wrapConsumer<Any, Any>(::biConsumer).accept(Any(), Any())
context.wrapFunction<Any, Any>(::function).apply(Any())
context.wrapSupplier<Any>(::supplier).get()
}
/** 例の [java.util.concurrent.Callable]。 */
private fun callable(): Any {
return Any()
}
/** 例の [Runnable]。 */
private fun runnable() {}
/** 例の [java.util.function.Consumer]。 */
private fun consumer(obj: Any) {}
/** 例の [java.util.function.BiConsumer]。 */
private fun biConsumer(object1: Any, object2: Any) {}
/** 例の [java.util.function.Function]。 */
private fun function(obj: Any): Any {
return obj
}
/** 例の [java.util.function.Supplier]。 */
private fun supplier(): Any {
return Any()
}
}
}
関数を区切った方がわかりやすいので、区切った時の実装は以下。
ポイントは Context.current().get(keyName) と context.get(keyName) で値を共有できるか。
前者は、context.makeCurrent() の内部ならどこでも呼べるようになる。後者は context を変数として引き回す必要がある。
package com.example.otel_kotlin_api_usage_with_spring_starter
import io.opentelemetry.api.GlobalOpenTelemetry
import io.opentelemetry.api.trace.Span
import io.opentelemetry.context.Context
import io.opentelemetry.context.ContextKey
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class Controller {
private val tracer = GlobalOpenTelemetry.getTracer("otel-kotlin-api-usage")
private val userIdKey: ContextKey<String> = ContextKey.named("user-id")
@GetMapping("/ping")
fun ping(): String {
return "pong"
}
@GetMapping("/context-demo")
fun contextDemo(): Map<String, String> {
val span = tracer.spanBuilder("context-demo").startSpan()
try {
span.makeCurrent().use { scope ->
val context = Context.current().with(userIdKey, "user-123")
// context を共有しないか、makeCurrent の外で呼び出すと null になる
contextCallInMakeCurrent(userIdKey)
// context を共有して、内部で makeCurrent を呼び出ときに値が取得できる
contextStartUsage(context, userIdKey)
context.makeCurrent().use { contextScope ->
// makeCurrent の中であれば current を引数に利用しなくても、Context.current() で値が取得できる
contextCallInMakeCurrent(userIdKey)
val currentUserId = Context.current().get(userIdKey)
Span.current().setAttribute("user.id", currentUserId ?: "unknown")
return mapOf(
"message" to "Context demonstration",
"userId" to (currentUserId ?: "not set"),
"traceId" to span.spanContext.traceId
)
}
}
} finally {
span.end()
}
}
/**
* contextCallInMakeCurrent
*
* makeCurrent の中で呼び出すと Context.current() で値が取得できる。
* makeCurrent の外で呼び出すと Context.current() でnullを取得する
*
* @param userIdKey
*/
fun contextCallInMakeCurrent(userIdKey: ContextKey<String>) {
val currentUserId = Context.current().get(userIdKey)
println("userIdKey in contextCallInMakeCurrent is $currentUserId")
}
/**
* contextStartUsage
*
* Context.current().get() は makeCurrent の中で呼び出すと値が取得できる。
* context.get() は makeCurrent の外で呼び出しても値が取得できる。ただし、context を共有(変数の引き回し)をした場合に限る。
*
* @param context
* @param userIdKey
*/
fun contextStartUsage(context: Context, userIdKey: ContextKey<String>) {
context.makeCurrent().use { contextScope ->
contextCallInMakeCurrent(userIdKey)
val currentUserId = Context.current().get(userIdKey)
println("currentUserId in contextStartUsage is $currentUserId")
}
val currentUserId = context.get(userIdKey)
println("userIdKey in contextStartUsage is $currentUserId")
}
@GetMapping("/context-service-demo")
fun contextServiceDemo(): String {
ContextUsage.contextUsage()
return "context-service-demo"
}
}
ContextStorage
Context.current() の内部で利用されている。
ContextStorageは現在のContextを保存および取得するメカニズムです。
デフォルトのContextStorage実装はContextをスレッドローカルに保存します。
public interface Context {
/** Return the context associated with the current {@link Scope}. */
static Context current() {
Context current = ContextStorage.get().current();
return current != null ? current : root();
}
// 略
}
ContextPropagators
アウトバンドHTTPリクエストにコンテキストを注入(コンテキストプロぱゲーション)したいときに利用する。
下記は http://localhost:8080/resource へのリクエストにコンテキストを注入するサンプルコード。
package com.example.otel_kotlin_api_usage_with_spring_starter
import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator
import io.opentelemetry.context.Context
import io.opentelemetry.context.propagation.ContextPropagators
import io.opentelemetry.context.propagation.TextMapPropagator
import io.opentelemetry.context.propagation.TextMapSetter
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
class InjectContextUsage {
companion object {
private val TEXT_MAP_SETTER: TextMapSetter<HttpRequest.Builder> = HttpRequestSetter()
fun injectContextUsage() {
// w3cトレースコンテキストとw3cバゲージを伝播するContextPropagatorsインスタンスを作成
val propagators = ContextPropagators.create(
TextMapPropagator.composite(
W3CTraceContextPropagator.getInstance(),
W3CBaggagePropagator.getInstance()
)
)
// HttpRequestビルダーを作成
val httpClient = HttpClient.newBuilder().build()
val requestBuilder = HttpRequest.newBuilder()
.uri(URI("http://127.0.0.1:8080/resource"))
.GET()
// ContextPropagatorsインスタンスがあるとき、現在のコンテキストをHTTPリクエストキャリアに注入
propagators.textMapPropagator.inject(Context.current(), requestBuilder, TEXT_MAP_SETTER)
// 注入されたコンテキストでリクエストを送信
httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.discarding())
}
}
/** [HttpRequest.Builder]キャリアを持つ[TextMapSetter]。 */
private class HttpRequestSetter : TextMapSetter<HttpRequest.Builder> {
override fun set(carrier: HttpRequest.Builder?, key: String, value: String) {
carrier?.setHeader(key, value)
}
}
}
@GetMapping("/inject-context-demo")
fun injectContextDemo(): Map<String, String> {
val span = tracer.spanBuilder("inject-context-demo").startSpan()
try {
span.makeCurrent().use {
// バゲージを設定
val baggage = io.opentelemetry.api.baggage.Baggage.builder()
.put("user.id", "user-456")
.put("request.id", "req-789")
.build()
// バゲージをコンテキストに追加
baggage.makeCurrent().use {
// InjectContextUsageを呼び出し
InjectContextUsage.injectContextUsage()
return mapOf(
"message" to "Context injection demonstration",
"traceId" to span.spanContext.traceId,
"userId" to (baggage.getEntryValue("user.id") ?: "not set")
)
}
}
} finally {
span.end()
}
}
@GetMapping("/resource")
fun resource(): Map<String, String> {
val currentSpan = Span.current()
val baggage = io.opentelemetry.api.baggage.Baggage.current()
// 受け取ったトレース情報とバゲージを表示
val userId = baggage.getEntryValue("user.id")
val requestId = baggage.getEntryValue("request.id")
println("=== /resource endpoint ===")
println("TraceId: ${currentSpan.spanContext.traceId}")
println("SpanId: ${currentSpan.spanContext.spanId}")
println("Baggage - user.id: $userId")
println("Baggage - request.id: $requestId")
return mapOf(
"message" to "Resource endpoint",
"traceId" to currentSpan.spanContext.traceId,
"spanId" to currentSpan.spanContext.spanId,
"userId" to (userId ?: "not received"),
"requestId" to (requestId ?: "not received")
)
}
以下は inject しなかった例。
Spring Boot へのアクセスが 2 つになっており、resource のトレースが区切れている。nginx は外部からリクエストされたときにしか、通らないため 1 つ。



以下は inject した例。
Spring Boot へのアクセスが 1 つになり、resource のトレースがつながっている。


extract パターンもある。
別リポジトリで実装。
長いので分割
OpenTelemetry API