対数犬度関数

140字以上のことを書きます

OpenTelemetryのTracingについてまとめる JavaでのSpanとContext管理編

これは何

OpenTelemetryのTraceシグナルについて理解するため,JavaでのOTel APIの実装について以下を調査する記事です.

  • 1アプリケーション内でContextをどのように管理しているのか
  • どのように現在の処理に対応するSpanを生成,取得するのか

この記事の内容はGitHub - open-telemetry/opentelemetry-java at release/v1.47.xに基づきます.

まとめ

  • わかったこと
    • threadLocalValueを用いてContextの管理を行っている
      • Thread毎に異なる値を保持できる変数
    • Span.makeCurrent実行時に現在のContextを変数に退避 + Spanインスタンス自身を保存したContextをCurrentに設定している
    • ScopeインスタンスのClose時の処理として退避していたContextを復元し,一つ前のSpanがActiveとなる
  • わかっていないこと
    • 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の実装が定義されていることがわかります.

github.com

github.com

このクラスはContextに保存されている値を表すinterfaceです. コメントにもある通りmakeCurrentメソッドの実装はContext.current().with(value).makeCurrent()の呼び出しと等価とのことなので,この処理を追っていきます.

Context側の処理

上記で実行されている Context.current, with, makeCurentメソッドについて確認していきます.

www.javadoc.io

Context.current

ContextStorageintefaceのgetメソッドを呼び出し,その返り値のcurrentメソッドを呼び出しています.

github.com

ContextStorage.getメソッドではLazyStorageClassのgetメソッドが呼び出されています. github.com

このクラスの役割がよくわかっていないのですが,storage変数の値の初期化時の処理を見ると最終的にThreadLocalContextStorageClassの値が返されるケースがあるので,このクラスの値が返される前提で進めていきます.

ThreadLocalContextStorageクラスのcurrentの実装をみると,ThreadLocal<Context>クラスの変数のgetメソッドを呼び出しています. このメソッドは変数に対応する値を取得する処理なので,最終的にContextインターフェースの値が返されていることがわかります.

github.com

ThreadLocalクラスはThread毎に異なる値を保持する機能をもったクラスなので,OTelのJava向けの実装ではThreadLocalを用いてContextの管理を行うケースがあることがわかります.

Context.with

Context.withでは,引数に渡された値のstoreInContextメソッドをContextクラスのインスタンス自身を引数として呼び出しています.

github.com

今回のケースの場合,引数のvalueがActiveにしようとしているSpan,thisがcurrentのContextクラスのインスタンスを表しています.

Spanインターフェースでの実装をみると, with(ContextKey<V> k1, V v1)で定義されたwithメソッドを呼び出していることがわかります. github.com

適当なContextインターフェース実装クラスの実装を見てみると,このwithメソッドでは引数のKey, Valueを追加して新しいContextInterface実装クラスのインスタンスを返していることがわかります.(Contextのインスタンスはimmutableでなければならないのでそれはそう)

github.com

ここまでで,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インターフェースのインスタンスになる

という処理を行っています.

github.com

ScopeImplクラスでは,closeメソッド内でbeforeAttachフィールドの値をThreadLocal<Context>クラスの変数に再度Setしています. このメソッドはtry-with-resouce文の終了時に呼び出されるため,これにより一つ前のSpanが再度Activeになります.

github.com