コンテンツにスキップ

DDtrace カスタムインストルメンテーション


著者: 劉鋭

Java インストルメンテーションの紹介

インストルメンテーション:コードに計測機能を追加する仕組み(「プローブ」や「埋め込みポイント」と呼ばれることもあります。翻訳に誤りはなく、理解できれば問題ありません)。

Java インストルメンテーションは、JavaSE 6 で導入された新機能です。Java コード、すなわち java.lang.instrument を使用することで、インストルメンテーションを用いて Java コードで問題を解決する機能を実現できます。

インストルメンテーションを使用すると、開発者はアプリケーションとは独立したエージェントプログラム(Agent)を構築でき、JVM 上で実行されるプログラムを監視・支援したり、特定のクラスの定義を置き換えたり変更したりすることも可能です。この機能により、開発者はより柔軟なランタイム仮想マシンの監視や Java クラスの操作を実現できます。この特性は、仮想マシンレベルでサポートされた AOP の実装方法を提供し、開発者は JDK をアップグレードしたり変更したりすることなく、特定の AOP 機能を実現できます。

JavaSE 6 では、instrumentation パッケージにさらに強力な機能が追加されました。起動後のインストルメンテーション、ネイティブコード(native code)のインストルメンテーション、classpath の動的な変更などです。これらの変更は、Java がより強力な動的制御と解釈能力を持つことを意味し、Java 言語をより柔軟で多様なものにしています。

DDtrace カスタムインストルメンテーションの構造分析

image.png

  1. Decorator:デコレータ。インストルメンテーションを装飾するために使用されます。BaseDecorator は基本デコレータであり、カスタムデコレータはすべて BaseDecorator またはそのサブクラスを継承する必要があります。スパンに対する操作やカスタムタグなどの動作は、BaseDecorator を介して実装されます。
  2. Instrumentation:インストルメンテーションプログラム。@AutoService(Instrumenter.class) アノテーションを使用して、現在のクラスをインストルメンテーションアプリケーションとして登録します。エージェント起動時に、@AutoService(Instrumenter.class) アノテーションが付与されたクラスがロードされます。
  3. Advice:インストルメンテーションでインストルメントするメソッドの拡張処理を行います。主に 2 つのメソッドレベルのアノテーション @Advice.OnMethodEnter@Advice.OnMethodExit を提供し、それぞれメソッド呼び出し時とメソッド終了時に呼び出されます。
  4. Inject/Extract:注入/取出しを表します。必須ではありません。主な機能は、トレース情報の注入と抽出です。これを使用して、traceId、spanId、および関連する伝搬パラメータなどのトレース情報を透過的に伝送します。

Decorator クラス図

ここでは一部のクラス図を示します。

image.png

Instrumentation クラス図

ここでは一部のクラス図を示します。

image.png

Instrumenter は interface であり、豊富なインターフェースを提供し、定義に応じて実装します。

image.png

HasAdvice:メソッドのアラウンド処理、つまり通常言われる埋め込みポイント操作を行います。主に 1 つのインターフェースメソッドを提供し、埋め込みポイントが必要なメソッドを登録するために使用します。複数のメソッドに対して埋め込みポイント操作を実行できます。

/**
 * Instrumenters should register each advice transformation by calling {@link
 * AdviceTransformation#applyAdvice(ElementMatcher, String)} one or more times.
 */
void adviceTransformations(AdviceTransformation transformation);

Tracing:トレース用です。

/** Parent class for all tracing related instrumentations */
abstract class Tracing extends Default{...}

Profiling:現在のインストルメンテーションがプロファイリング用であることを示します。

/** Parent class for all profiling related instrumentations */
abstract class Profiling extends Default{...}

CiVisibility:CI インストルメンテーションタイプです。

/** Parent class for all CI related instrumentations */
abstract class CiVisibility extends Default {...}

Default:デフォルト実装です。

@SuppressFBWarnings("IS2_INCONSISTENT_SYNC")
abstract class Default implements Instrumenter, HasAdvice{...}

Inject クラス図

Setter の正式名称は AgentPropagation.Setter です。ここでは一部のクラス図を示します。

image.png

Extract クラス図

ContextVisitor の正式名称は AgentPropagation.ContextVisitor です。ここでは一部のクラス図を示します。

image.png

Inject と Extract のどちらも、Propagation(伝搬器)が関係します。伝搬器の使用と紹介については、ドキュメント extract + TextMapAdapter を使用したカスタム traceId を参照してください。

実践:DDtrace カスタム Dubbo インストルメンテーション

統合の考え方

image.png

  1. DubboInstrumentation クラスを作成し、インストルメンテーション関連情報を設定します。
  2. adviceTransformations を使用して関連メソッドを拡張します。拡張ビジネスロジックは RequestAdvice クラスで実装し、主に 2 つのメソッド @Advice.OnMethodEnter@Advice.OnMethodExit を実装します。それぞれメソッド呼び出し時とメソッド終了時に呼び出されます。
  3. DubboDecorator はデコレータの役割を果たし、スパンに対する関連操作(タグの設定やスパンのクローズなど)を行います。
  4. Inject/Extract は注入/取出しを表します。主な機能は、トレース情報の注入と抽出です。これを使用して、traceId、spanId、および関連する伝搬パラメータなどのトレース情報を透過的に伝送します。DubboHeadersInjectAdapter は主に consumer が traceId、spanId などを伝搬するために使用され、provider は DubboHeadersExtractAdapter を使用して関連パラメータを抽出し、スパンを構築します。

統合手順

1 dd-java-agent\instrumentation ディレクトリに、モジュールを作成し、gradle 方式で作成します。

dubbo はメジャーバージョンが異なると、パッケージ名、クラス名、メソッド名が異なるため、モジュール作成時に対応するメジャーバージョン番号を付けると保守が容易になります。例:dubbo-2.7 は dubbo 2.7 以上のバージョンをサポートすることを示します。具体的なバージョンサポートは現在のモジュールの build.gradle で変更します。build.gradle という名前は保守に適さないため、ここでは dubbo-2.7.gradle に調整します。

muzzle {
  pass {
    group = "org.apache.dubbo"
    module = "dubbo"
    versions = "[2.7.0,)"
//    assertInverse = true
  }
}

apply from: "$rootDir/gradle/java.gradle"

apply plugin: 'org.unbroken-dome.test-sets'

dependencies {
  compileOnly(group: 'org.apache.dubbo', name: 'dubbo', version: '2.7.0')
}

testSets {
  latestDepTest {
    dirName = 'test'
  }
}

tasks.withType(Test).configureEach {
  usesService(testcontainersLimit)
}

同時に settings.gradle ファイルに dubbo-2.7.gradle を追加します。

...
include ':dd-java-agent:instrumentation:dropwizard'
include ':dd-java-agent:instrumentation:dropwizard:dropwizard-views'
include ':dd-java-agent:instrumentation:dubbo-2.7'
include ':dd-java-agent:instrumentation:elasticsearch'
include ':dd-java-agent:instrumentation:elasticsearch:rest-5'
include ':dd-java-agent:instrumentation:elasticsearch:rest-6.4'
include ':dd-java-agent:instrumentation:elasticsearch:rest-7'
...

2 パッケージ名 datadog.trace.instrumentation.dubbo_2_7x を作成します。

3 インストルメンテーションクラス DubboInstrumentation.java を作成します。

package datadog.trace.instrumentation.dubbo_2_7x;

import com.google.auto.service.AutoService;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;

import java.util.Map;

import static datadog.trace.agent.tooling.bytebuddy.matcher.ClassLoaderMatchers.hasClassesNamed;
import static datadog.trace.agent.tooling.bytebuddy.matcher.HierarchyMatchers.implementsInterface;
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.nameStartsWith;
import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named;
import static java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.*;

@AutoService(Instrumenter.class)
public class DubboInstrumentation extends Instrumenter.Tracing
    implements Instrumenter.ForTypeHierarchy {

  public DubboInstrumentation() {
    super("apache-dubbo");
  }

//  public static final String CLASS_NAME = "org.apache.dubbo.rpc.Filter";
  public static final String CLASS_NAME = "org.apache.dubbo.monitor.support.MonitorFilter";

  @Override
  public ElementMatcher<ClassLoader> classLoaderMatcher() {
    return  hasClassesNamed(CLASS_NAME);
  }

  @Override
  public ElementMatcher<TypeDescription> hierarchyMatcher() {
    return extendsClass(named(CLASS_NAME));
  }

  @Override
  public void adviceTransformations(AdviceTransformation transformation) {
    transformation.applyAdvice(
        isMethod()
            .and(isPublic())
            .and(nameStartsWith("invoke"))
            .and(takesArguments(2))
            .and(takesArgument(0, named("org.apache.dubbo.rpc.Invoker")))
            .and(takesArgument(1, named("org.apache.dubbo.rpc.Invocation"))),
        packageName + ".RequestAdvice");
  }

  @Override
  public String[] helperClassNames() {
    return new String[]{
        packageName + ".DubboDecorator",
        packageName + ".RequestAdvice",
        packageName + ".DubboHeadersExtractAdapter",
        packageName + ".DubboHeadersInjectAdapter"
    };
  }

  @Override
  public Map<String, String> contextStore() {
    return singletonMap("org.apache.dubbo.rpc.RpcContext", AgentSpan.class.getName());
  }
}

まず、org.apache.dubbo.rpc.Filter のソースコードを見てみます。

@SPI
public interface Filter {
    /**
     * Make sure call invoker.invoke() in your implementation.
     */
    Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException;

    interface Listener {

        void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation);

        void onError(Throwable t, Invoker<?> invoker, Invocation invocation);
    }

}

Filter は interface なので、implementsInterface 方式を採用する必要があります。これにより、Filter インターフェースのすべての実装をインターセプトできます。org.apache.dubbo.rpc.Filterinvoke メソッドを提供し、2 つのパラメータ InvokerInvocation を持っています。これらは後で使用します。

void adviceTransformations(AdviceTransformation transformation) をオーバーライドして、org.apache.dubbo.rpc.Filter のインターセプトを実装します。

applyAdvice パラメータの説明:

  • isMethod():メソッドをインターセプトすることを示します。
  • isPublic():アクセス修飾子が public であることを示します。
  • nameStartsWith("invoke"):メソッド名。
  • takesArguments:nameStartsWith("invoke") に必要なパラメータ数。
  • takesArgument:nameStartsWith("invoke") の関連パラメータ。必要に応じて記述します。パラメータタイプとパラメータ順序を誤ると、現在のインストルメンテーションが無効になります。
  • takesArgument(0, named("org.apache.dubbo.rpc.Invoker")):最初のパラメータのタイプを示します。
  • takesArgument(1, named("org.apache.dubbo.rpc.Invocation")):2 番目のパラメータのタイプ。

helperClassNames():ヘルパークラスです。追加でカスタム定義したクラスは、すべてここで宣言する必要があります。

Map contextStore():コンテキスト情報を保存するために使用します。主に AgentSpan または AgentScope 関連情報(traceId、spanId など)を保存します。ここでは singletonMap("org.apache.dubbo.rpc.Invocation", AgentSpan.class.getName()) と設定し、org.apache.dubbo.rpc.Invocation を拡張することを示します。

@AutoService は Google が提供する SPI インターフェース仕様で、コンパイル時に処理されます。

インストルメンテーションクラスは核心です。クラス名にアノテーション @AutoService(Instrumenter.class) を追加する必要があります。これはインストルメンテーションアプリケーションであることを示します。アプリケーションをコンパイルしてパッケージ化する際に、@AutoService(Instrumenter.class) 関連のクラスが反復処理され、関連するクラス名が META-INF/services/datadog.trace.agent.tooling.Instrumenter というファイルに格納され、クラスローダー起動時にロードされます。META-INF/services/datadog.trace.agent.tooling.Instrumenter ファイルは自動生成され、一部のコードは以下の通りです。

...
datadog.trace.instrumentation.datastax.cassandra.CassandraClientInstrumentation
datadog.trace.instrumentation.datastax.cassandra4.CassandraClientInstrumentation
datadog.trace.instrumentation.dubbo.DubboInstrumentation
datadog.trace.instrumentation.dubbo_2_7x.DubboInstrumentation
datadog.trace.instrumentation.elasticsearch5.Elasticsearch5RestClientInstrumentation
datadog.trace.instrumentation.elasticsearch6_4.Elasticsearch6RestClientInstrumentation
datadog.trace.instrumentation.elasticsearch7.Elasticsearch7RestClientInstrumentation
datadog.trace.instrumentation.elasticsearch2.Elasticsearch2TransportClientInstrumentation
datadog.trace.instrumentation.elasticsearch5.Elasticsearch5TransportClientInstrumentation
datadog.trace.instrumentation.elasticsearch5_3.Elasticsearch53TransportClientInstrumentation
datadog.trace.instrumentation.elasticsearch6.Elasticsearch6TransportClientInstrumentation
datadog.trace.instrumentation.elasticsearch7_3.Elasticsearch73TransportClientInstrumentation
...

4 DubboDecorator を作成します。

一部のコードは以下の通りです。

...
public class DubboDecorator extends BaseDecorator {
  private static final Logger log = LoggerFactory.getLogger(DubboDecorator.class);
  public static final CharSequence DUBBO_REQUEST = UTF8BytesString.create("dubbo");

  public static final CharSequence DUBBO_SERVER = UTF8BytesString.create("apache-dubbo");

  public static final DubboDecorator DECORATE = new DubboDecorator();

  public static final String SIDE_KEY = "side";

  public static final String PROVIDER_SIDE = "provider";

  public static final String CONSUMER_SIDE = "consumer";

  public static final String GROUP_KEY = "group";

  public static final String VERSION = "release";
  @Override
  protected String[] instrumentationNames() {
    return new String[]{"apache-dubbo"};
  }

  @Override
  protected CharSequence spanType() {
    return DUBBO_SERVER;
  }

  @Override
  protected CharSequence component() {
    return DUBBO_SERVER;
  }

  public AgentSpan startDubboSpan(Invoker invoker, Invocation invocation) {
    URL url = invoker.getUrl();
    boolean isConsumer = isConsumerSide(url);

    String methodName = invocation.getMethodName();
    String resourceName = generateOperationName(url,invocation);
    String shortUrl = generateRequestURL(url,invocation);
    System.out.println("isConsumer : "+isConsumer);
    if (log.isDebugEnabled()) {
      log.debug("isConsumer:{},method:{},resourceName:{},shortUrl:{},longUrl:{},version:{}",
          isConsumer,
          methodName,
          resourceName,
          shortUrl,
          url.toString(),
          getVersion(url)
          );
    }
    AgentSpan span;
    RpcContext rpcContext = RpcContext.getContext();
    if (isConsumer){
      // this is consumer
      span = startSpan(DUBBO_REQUEST);
    }else{
      // this is provider
      AgentSpan.Context parentContext = propagate().extract(rpcContext, GETTER);
      span = startSpan(DUBBO_REQUEST,parentContext);
    }
    span.setTag("url", url.toString());
    span.setTag("short_url", shortUrl);
    span.setTag("method", methodName);
    span.setTag("dubbo-version",getVersion(url));
    afterStart(span);

    withMethod(span, resourceName);
    if (isConsumer){
      propagate().inject(span, rpcContext, SETTER);
//      InstrumentationContext.get(Invocation.class, AgentSpan.class).put(invocation, span);
    }
    return span;
  }

  public void withMethod(final AgentSpan span, final String methodName) {
    span.setResourceName(methodName);
  }

  @Override
  public AgentSpan afterStart(AgentSpan span) {
    return super.afterStart(span);
  }

    ...
}


...

dubbo は RPC フレームワークとして、consumer と provider があります。isConsumer によって、現在のコードが consumer の実行か provider の実行かを判断します。consumer の場合は、直接スパンを作成します。その traceId と parentId は他のトレースからの伝搬によってもたらされ、propagate().inject(span, invocation, SETTER) を使用して provider にデータを伝搬します。provider の場合は、propagate().extract(invocation, GETTER) を使用して抽出し、parentContext を構築し、さらに parentContext を使用して現在のスパン情報を構築し、トレースの連鎖を完了します。

5 RequestAdvice を作成します。

public class RequestAdvice {

  @Advice.OnMethodEnter(suppress = Throwable.class)
  public static AgentScope beginRequest(@Advice.This Filter filter,@Advice.Argument(0) final Invoker invoker,
                                        @Advice.Argument(1) final Invocation invocation) {

    System.out.println(filter.getClass().getName());
    final int callDepth = CallDepthThreadLocalMap.incrementCallDepth(RpcContext.class);
    if (callDepth > 0) {
      return null;
    }

    AgentScope agentScope = DECORATE.buildSpan(invoker, invocation);
    return agentScope;
  }

  @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
  public static void stopSpan(
      @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) {
    if (scope == null) {
      return;
    }
    DECORATE.onError(scope.span(), throwable);
    DECORATE.beforeFinish(scope.span());

    scope.close();
    scope.span().finish();
    CallDepthThreadLocalMap.reset(RpcContext.class);
  }
}

RequestAdvice クラスは主に 2 つのメソッドを実装します。メソッド名はカスタマイズ可能で、2 つのメソッドはそれぞれ @Advice.OnMethodEnter@Advice.OnMethodExit アノテーションを使用し、メソッドの呼び出し時と終了時に実行する操作を表します。CallDepthThreadLocalMap.incrementCallDepth(RpcContext.class) を使用することで、メソッドの再入を防ぐことができます。OnMethodExit で終了する際には、CallDepthThreadLocalMap.reset(RpcContext.class) でリセットルールを適用する必要があります。

6 コンパイルとパッケージ化

gradle shadowJar を使用してパッケージ化します。パッケージ化後、ファイルは dd-java-agent\build\libs に格納されます。

ソースコードアドレス

<dubbo-instrumentation>

フィードバック

このページは役に立ちましたか?