OpenTelemetryのTracingについてまとめる JavaでのSpanとContext管理編
これは何
OpenTelemetryのTraceシグナルについて理解するため,JavaでのOTel APIの実装について以下を調査する記事です.
- 1アプリケーション内でContextをどのように管理しているのか
- どのように現在の処理に対応するSpanを生成,取得するのか
この記事の内容はGitHub - open-telemetry/opentelemetry-java at release/v1.47.xに基づきます.
まとめ
- わかったこと
- わかっていないこと
LazyStorageClassの役割
Spanの生成~Closeまでの流れ
JavaではSpanの生成,activeなContextへの追加,SpanのCloseをtry-with-resource構文で行います. 以下はRecord Telemetry with API | OpenTelemetryからの引用です.
package otel; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; public class SpanAndContextUsage { private final Tracer tracer; SpanAndContextUsage(Tracer tracer) { this.tracer = tracer; } public void nestedSpanUsage() { // Start a span. Since we don't call makeCurrent(), we must explicitly call setParent on // children. Wrap code in try / finally to ensure we end the span. Span span = tracer.spanBuilder("span").startSpan(); try { // Start a child span, explicitly setting the parent. Span childSpan = tracer .spanBuilder("span child") // Explicitly set parent. .setParent(span.storeInContext(Context.current())) .startSpan(); // Call makeCurrent(), adding childSpan to Context.current(). Spans created inside the scope // will have their parent set to childSpan. try (Scope childSpanScope = childSpan.makeCurrent()) { // Call another method which creates a span. The span's parent will be childSpan since it is // started in the childSpan scope. doWork(); } finally { childSpan.end(); } } finally { span.end(); } } private int doWork() { Span doWorkSpan = tracer.spanBuilder("doWork").startSpan(); try (Scope scope = doWorkSpan.makeCurrent()) { int result = 0; for (int i = 0; i < 10; i++) { result += i; } return result; } finally { doWorkSpan.end(); } } }
SpanクラスのインスタンスのmakeCurrentメソッドを呼び出すことで,メソッドを実行されたSpanクラスインスタンスがActiceなSpan(現在の処理に対応したSpan)となります.
これにより,try-with-resource文内で新たに生成されたSpanではParent Spanとしてtry-with-resource文で指定したSpanが設定されます.
また,ScopeクラスのインスタンスはAutoClosableインターフェースを実装していることとtry-with-resource文内の外では以前のSpanがActiveなSpanに設定されるため,このクラスでSpanのActive化の解除を実装していると考えられます.
ここからはこのSpan.makeCurrentメソッド,Scopeクラスのインスタンスの実装を追い,ActiveなSpanに設定~設定の解除までがどのように実装されているか確認します.
Span.makeCurrentメソッドの実装を見る
実装を見てみると,ImplicitContextKeyedインターフェースでdefaultの実装が定義されていることがわかります.
このクラスはContextに保存されている値を表すinterfaceです.
コメントにもある通りmakeCurrentメソッドの実装はContext.current().with(value).makeCurrent()の呼び出しと等価とのことなので,この処理を追っていきます.
Context側の処理
上記で実行されている Context.current, with, makeCurentメソッドについて確認していきます.
Context.current
ContextStorageintefaceのgetメソッドを呼び出し,その返り値のcurrentメソッドを呼び出しています.
ContextStorage.getメソッドではLazyStorageClassのgetメソッドが呼び出されています.
github.com
このクラスの役割がよくわかっていないのですが,storage変数の値の初期化時の処理を見ると最終的にThreadLocalContextStorageClassの値が返されるケースがあるので,このクラスの値が返される前提で進めていきます.
ThreadLocalContextStorageクラスのcurrentの実装をみると,ThreadLocal<Context>クラスの変数のgetメソッドを呼び出しています.
このメソッドは変数に対応する値を取得する処理なので,最終的にContextインターフェースの値が返されていることがわかります.
ThreadLocalクラスはThread毎に異なる値を保持する機能をもったクラスなので,OTelのJava向けの実装ではThreadLocalを用いてContextの管理を行うケースがあることがわかります.
Context.with
Context.withでは,引数に渡された値のstoreInContextメソッドをContextクラスのインスタンス自身を引数として呼び出しています.
今回のケースの場合,引数のvalueがActiveにしようとしているSpan,thisがcurrentのContextクラスのインスタンスを表しています.
Spanインターフェースでの実装をみると, with(ContextKey<V> k1, V v1)で定義されたwithメソッドを呼び出していることがわかります.
github.com
適当なContextインターフェース実装クラスの実装を見てみると,このwithメソッドでは引数のKey, Valueを追加して新しいContextInterface実装クラスのインスタンスを返していることがわかります.(Contextのインスタンスはimmutableでなければならないのでそれはそう)
ここまでで,ActiveにしようとしているSpanが追加されたContextクラスのインスタンスが生成されています.
Context.makeCurent
最終的に,ここまでで生成されたContextインスタンスについてmakeCurrentメソッドが呼び出されます.
このメソッドではContextStorageインターフェースのattachメソッドを実行しています(先と同様にThreadLocalContextStorageクラスが利用される前提で進めます)
github.com
処理を見てみると,
- ActiveにしたいSpanが追加されたContextを
toAttach引数で受け取り ThreadLocal<Context>クラスの変数にActiveにしたいSpanが追加されたContextをセット- 現在のContextを代入した
beforeAttach変数と,toAttach引数を渡し.ScopeImpl`クラスのインスタンスを返す- これが最終的にユーザーのコードに現れる
Scopeインターフェースのインスタンスになる
- これが最終的にユーザーのコードに現れる
という処理を行っています.
ScopeImplクラスでは,closeメソッド内でbeforeAttachフィールドの値をThreadLocal<Context>クラスの変数に再度Setしています.
このメソッドはtry-with-resouce文の終了時に呼び出されるため,これにより一つ前のSpanが再度Activeになります.