乐趣区

关于java:OpenTelemetry系列-五|-OpenTelemetry-Java-Instrumentation二次开发指南

前言

咱们上一章介绍了 OpenTelemetry Java Instrumentation 的应用,然而那些都是一些根本的应用,如果有一些自定义的性能,还是须要一些开发工作。本章节将以保姆级教程来介绍一下如何在 OpenTelemetry Java Instrumentation 上进行二次开发(本文中会将 OpenTelemetry Java Instrumentation 简写成 OJI 以不便浏览)。

本文中的相干源码以及相干的实现均为目前的 OJI 的最新 main 分支版本目前为 1.22.0-SNAPSHOT

开发筹备

第一次编译

OJI应用 gradle 来进行依赖治理,外围的依赖和仓库等等信息都蕴含在根目录下的 settings.gradle.kts 中,后续的相干保护也要在其中。并且 OJI 的开发须要 jdk9+ 的版本,因而须要确认本人的 jdk 版本是否符合要求。

在确认好后期筹备后进入我的项目,执行 gradle assemble 来进行打包操作。首次打包可能须要破费超过 1 个小时,须要急躁期待。除此之外如果遇到依赖拉取失败问题,则能够在 build.gradle.kts 文件中增加如下配置以应用其余的仓库地址,此处应用的是阿里云仓库:

allprojects {
  repositories {
    maven {setUrl("https://maven.aliyun.com/nexus/content/groups/public/")
    }
    mavenCentral()
    mavenLocal()
    jcenter {setUrl("https://jcenter.bintray.com/")
    }
    google()}
}

通过一段时间期待,当控制台输入如下文字,即示意编译胜利。

BUILD SUCCESSFUL in 12m 25s
2464 actionable tasks: 2212 executed, 232 from cache, 20 up-to-date

A build scan was not published as you have not authenticated with server 'ge.opentelemetry.io'.
For more information, please see https://gradle.com/help/gradle-authenticating-with-gradle-enterprise.

之后咱们就能够在 javaagent/build/libs 目录下找到最终的 agent 包opentelemetry-javaagent-{version}.jar

新建组件

为了便于管理,咱们后续的所有性能示例都将蕴含在一个组件中,而不会进行多组件的宰割。

在进行所有的开发之前咱们须要新建一个组件:

  1. instrumentation 目录下新建一个目录作为组件的目录,我此处将其命名为bjwzds
  2. 在此目录中新建 javaagent 目录,并在此目录下创立 build.gradle.kts 文件,文件内容如下:

    plugins {id("otel.javaagent-instrumentation")
    }
    
    dependencies {}
  3. 在全局的 settings.gradle.kts 中增加 hideFromDependabot(":instrumentation:bjwzds:javaagent") 或者是 include(":instrumentation:bjwzds:javaagent") 来引入咱们新增的模块。
  4. 开始在 javaagent 目录下构建咱们的我的项目构造,大抵如下:

至此咱们开发的筹备工作曾经实现,接下来就是欢快的 coding 环节了!

自定义 Instrumentation

须要留神的是在晚期版本中能够本人齐全创立我的项目而后以内部插件的模式来注入,然而在后续版本中这种形式被废除,因而后续的开发都是在 clone 下来的 OJI 我的项目中进行开发。

尽管 OJI 曾经提供了十分多的 Instrumentation 类库,许许多多出名的开源我的项目都在其中,然而总是可能会有一些须要本人来解决的内容,比方一些商用库,比方一些公司自制的二方或者三方依赖。这些都不可能找到现成的实现,那么就只能本人来了!

OJI提供了欠缺的 Instrumentation 扩大能力,大家能够自行定义本人须要的Instrumentation

简略例子

要创立一个自定义的 Instrumentation,最根底的是要先创立一个继承InstrumentationModule 的类:

@AutoService(InstrumentationModule.class)
public class BjwzdsInstrumentationModule extends InstrumentationModule {public BjwzdsInstrumentationModule() {
    // 此处定义的是组件的名称,以及组件的别名,会在配置组件的开关时应用
    super("bjwzds", "bjwzds-1.0");
  }

  @Override
  public List<TypeInstrumentation> typeInstrumentations() {
    // 组件内蕴含的 TypeInstrumentation,是一个 list
    return Collections.singletonList(new BjwzdsInstrumentation());
  }
}

在有了 InstrumentationModule 的类后,只须要再创立一个 TypeInstrumentation 的实现类,咱们的一个最简略的 Instrumentation 就曾经实现了。如下是一个 TypeInstrumentation 的实现的例子:

public class BjwzdsInstrumentation implements TypeInstrumentation {
  @Override
  public ElementMatcher<TypeDescription> typeMatcher() {return named("org.example.bjwzds.AgentTest");
  }

  @Override
  public void transform(TypeTransformer transformer) {
    transformer.applyAdviceToMethod(isMethod()
            .and(isPublic())
            .and(named("test")),
        this.getClass().getName() + "$BjwzdsAdvice");
  }

  @SuppressWarnings("unused")
  public static class BjwzdsAdvice {@Advice.OnMethodEnter(suppress = Throwable.class)
    public static void methodEnter() {System.out.println("enter method");
    }

    @Advice.OnMethodExit(suppress = Throwable.class)
    public static void methodExit() {System.out.println("exit method");
    }
  }
}

这个例子很简略,作用是在 org.example.bjwzds.AgentTest.test 办法的执行前和执行后别离输入日志。

  1. typeMatcher 中定义的事须要进行切面的类,这个例子中指定了名称,全限定名叫做org.example.bjwzds.AgentTest
  2. transform 中定义切面的办法,以及办法绑定的执行类。在这个例子中切面的办法为是 public 办法且名字是 test 的办法,绑定的类是以后类的外部类BjwzdsAdvice
  3. 绑定的执行类 BjwzdsAdvice 定义了 @Advice.OnMethodEnter@Advice.OnMethodExit这两个注解别离用来定义进入办法和来到办法。因而这个类会在进入办法时执行 System.out.println("enter method"); 并在来到时执行System.out.println("exit method");

咱们将批改完的代码编译生成新的 agent,而后在咱们的 demo 我的项目中进行测试。

DEMO 我的项目很简略,只有一个类文件:

package org.example.bjwzds;

public class AgentTest {public void test() {System.out.println("This is a test function");
    }

    public static void main(String[] args) {AgentTest agentTest = new AgentTest();

        agentTest.test();}
}

咱们先不引入 agent 执行这个类,输入:

而后咱们引入 Agent,再执行,输入:

咱们显著能够看到咱们的 Agent 曾经失效了,并且如咱们设计的一样,在 test 执行前后别离输入了日志。

扩大能力

下面的例子是一个简略的 Instrumentation 例子,然而实际上在具体的应用中仅仅如此是不够的,接下来咱们来介绍一些 Instrumentation 更广大的能力。

接下来为了让大家更加理解 Instrumentation 的实现,咱们来一起实现一个真正的调用链的 Instrumentation 样例。

首先须要阐明的是调用链组件的实现模式和咱们上述的例子并无二致,两者间的差距是绑定的执行办法中逻辑的区别。在正式开始前咱们先来看一个 java-http-client 插件的例子:

@Advice.OnMethodEnter(suppress = Throwable.class)
    public static void methodEnter(@Advice.Argument(value = 0) HttpRequest httpRequest,
        @Advice.Local("otelContext") Context context,
        @Advice.Local("otelScope") Scope scope) {Context parentContext = currentContext();
      if (!instrumenter().shouldStart(parentContext, httpRequest)) {return;}

      context = instrumenter().start(parentContext, httpRequest);
      scope = context.makeCurrent();}

    @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
    public static void methodExit(@Advice.Argument(0) HttpRequest httpRequest,
        @Advice.Return HttpResponse<?> httpResponse,
        @Advice.Thrown Throwable throwable,
        @Advice.Local("otelContext") Context context,
        @Advice.Local("otelScope") Scope scope) {if (scope == null) {return;}

      scope.close();
      instrumenter().end(context, httpRequest, httpResponse, throwable);
    }

这是一个典型的调用链插件的实现逻辑,代码中的 shouldStartstartend 都是 OJI 提供的 API,用来帮忙生成 TraceSpan,以及解决一些必要的逻辑。上述代码中的instrumenter() 是本人实现的代码,然而其也不过是调用 API 来构建一个特定类型的Instrumenter

依照下面的说法,那咱们岂不是只有 CtrlC,CtrlV 就可能简略的创立一个调用链组件了呢?

答案是“是,也不齐全是”。大抵的逻辑上各个组件并无二致,甚至如果是同样类型的组件,如 apache-httpclientokhttp这种,那么绝大部分的代码都可能复用,然而这里有一个微小的区别。

在之前的文章中咱们已经提到过调用链想要串联在一起须要将 TraceId 一级一级的透传下去,然而不同的组件透传的形式是不一样的,是的,这个区别就是每个组件须要依据本人的特点来实现 TraceId 的透传。

听下来仿佛很简单,然而好在 OpenTelemetry 在这方面也做好了筹备,它将这个过程进行了形象并提供了 TextMapSetterTextMapGetter接口来让咱们本人实现。大抵流程如下:

也就是说实际上咱们须要解决的是上下游传递数据的形式,以及如何进行数据的解析组装。

上面是一个简略实现的 OpenFeign 的调用链组件(其实 feign 的组件在社区在就有人提出,然而历经多个版本 pr 仍未被合并,这里的组件是我本人实现的一个极其繁难版本,仅用于 demo 展现):

FeignInstrumentation:

public class FeignInstrumentation implements TypeInstrumentation {
  @Override
  public ElementMatcher<TypeDescription> typeMatcher() {return named("feign.SynchronousMethodHandler");
  }

  @Override
  public void transform(TypeTransformer transformer) {
    transformer.applyAdviceToMethod(named("executeAndDecode"), this.getClass().getName() + "$RequestAdvice");
  }

  @SuppressWarnings("unused")
  public static class RequestAdvice {@Advice.OnMethodEnter(suppress = Throwable.class)
    public static void addRequestEnter(@Advice.Argument(0) RequestTemplate template,
        @Advice.Local("otelContext") Context context,
        @Advice.Local("otelScope") Scope scope) {Context parentContext = Java8BytecodeBridge.currentContext();

      if (!instrumenter().shouldStart(parentContext, template)) {return;}

      context = instrumenter().start(parentContext, template);
      scope = context.makeCurrent();}

    @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
    public static void addRequestExit(@Advice.Argument(0) RequestTemplate template,
        @Advice.Thrown Throwable exception,
        @Advice.Local("otelContext") Context context,
        @Advice.Local("otelScope") Scope scope) {if (scope == null) {return;}

      scope.close();

      instrumenter().end(context, template, null, exception);
    }
  }
}

FeignSingleton外围实例构建类,将设定好的配置类组合在一起生成 Instrumenter 实例:

public class FeignSingleton {
  private static final String INSTRUMENTATION_NAME = "io.opentelemetry.feign-1.0";
  private static final Instrumenter<RequestTemplate, Void> INSTRUMENTER;

  static {
    HttpClientAttributesGetter<RequestTemplate, Void> httpAttributesGetter =
        new FeignHttpAttributesGetter();

    NetClientAttributesGetter<RequestTemplate, Void> netAttributesGetter =
        new FeignNetAttributesGetter();

    INSTRUMENTER =
        Instrumenter.<RequestTemplate, Void>builder(GlobalOpenTelemetry.get(),
                INSTRUMENTATION_NAME,
                HttpSpanNameExtractor.create(httpAttributesGetter))
            .addAttributesExtractor(HttpClientAttributesExtractor.builder(httpAttributesGetter, netAttributesGetter).build())
            .buildClientInstrumenter(HttpHeaderSetter.INSTANCE);
  }

  public static Instrumenter<RequestTemplate, Void> instrumenter() {return INSTRUMENTER;}

  private FeignSingleton() {}
}

HttpHeaderSetter,负责将内存中的数据转换并存储到发往上游申请的申请头:

enum HttpHeaderSetter implements TextMapSetter<RequestTemplate> {
  INSTANCE;

  @Override
  public void set(@Nullable RequestTemplate carrier, String key, String value) {if (carrier == null) {return;}
    carrier.header(key, value);
  }
}

FeignHttpAttributesGetter,span 中的 http 局部数据采集:

public class FeignHttpAttributesGetter implements HttpClientAttributesGetter<RequestTemplate, Void> {
  @Nullable
  @Override
  public String url(RequestTemplate requestTemplate) {return requestTemplate.url();
  }

  @Nullable
  @Override
  public String flavor(RequestTemplate requestTemplate, @Nullable Void unused) {return "feign";}

  @Nullable
  @Override
  public String method(RequestTemplate requestTemplate) {return requestTemplate.method();
  }

  @Override
  public List<String> requestHeader(RequestTemplate requestTemplate, String name) {return new ArrayList<>();
  }

  @Nullable
  @Override
  public Integer statusCode(RequestTemplate requestTemplate, Void unused,
      @Nullable Throwable error) {return 200;}

  @Override
  public List<String> responseHeader(RequestTemplate requestTemplate, Void unused, String name) {return new ArrayList<>();
  }
}

FeignNetAttributesGetter,span 中的 net 局部数据采集:

public class FeignNetAttributesGetter implements NetClientAttributesGetter<RequestTemplate, Void> {
  @Nullable
  @Override
  public String transport(RequestTemplate requestTemplate, @Nullable Void unused) {return "transport";}

  @Nullable
  @Override
  public String peerName(RequestTemplate requestTemplate) {return "peerName";}

  @Nullable
  @Override
  public Integer peerPort(RequestTemplate requestTemplate) {return 10000;}
}

成果展现

至此,咱们就简略的实现了一个可用的调用链插件,实际上纵观整体源码中自带的的插件也不过是这个例子的欠缺版本,基本原理都是万变不离其宗。

其余的自定义扩大能力

OJI 中除了用户能够自定义调用链的组件,同样的用户也能够自定义一些其余的扩大插件能力,在这个篇章会介绍一些比拟可能用到的扩大能力。

自定义配置

AutoConfigurationCustomizerProvider是用来自定义用户须要注入的配置的接口。

一个简略的例子:

@AutoService(AutoConfigurationCustomizerProvider.class)
public class MineAutoConfigurationCustomizerProvider implements
    AutoConfigurationCustomizerProvider {

  @Override
  public void customize(AutoConfigurationCustomizer autoConfigurationCustomizer) {
    autoConfigurationCustomizer
        .addPropertiesSupplier(this::getDefaultProperties);
  }

  private Map<String, String> getDefaultProperties() {Map<String, String> properties = new HashMap<>();
    properties.put("otel.exporter.otlp.endpoint", "http://backend:8080");
    properties.put("otel.exporter.otlp.insecure", "true");
    properties.put("otel.config.max.attrs", "16");
    properties.put("otel.traces.sampler", "demo");
    return properties;
  }
}

在下面的例子中在启动之时增加了自定义的一些配置。接口的裸露的办法是 customize,其参数是AutoConfigurationCustomizer,在AutoConfigurationCustomizer 中提供了一系列的办法来帮忙用户自定义想要的配置内容:

自定义调用链 ID 生成规定

在默认的 SDK 实现中 OpenTelemetry 应用的是默认的 RandomIdGenerator 来生成 traceIdspanId,然而在实在的应用场景中往往须要自定义本人的规定来制订独特的 traceIdspanId,这个时候就须要应用到 IdGenerator 了。

简略实现接口 IdGenerator 就可能自定义 Id 生成规定:

public class MineIdGenerator implements IdGenerator {
  @Override
  public String generateSpanId() {return String.valueOf(System.currentTimeMillis());
  }

  @Override
  public String generateTraceId() {return String.valueOf(System.currentTimeMillis());
  }
}

之后在 AutoConfigurationCustomizer 中以如下形式将本人定义的 IdGenerator 退出进去即可失效:

@Override
  public void customize(AutoConfigurationCustomizer autoConfigurationCustomizer) {
    autoConfigurationCustomizer
        .addTracerProviderCustomizer(this::configureSdkTracerProvider);
  }

  private SdkTracerProviderBuilder configureSdkTracerProvider(SdkTracerProviderBuilder tracerProvider, ConfigProperties configProperties) {
    return tracerProvider
        .setIdGenerator(new MineIdGenerator());
  }

自定义透传

在上文中咱们提到过调用链的一个外围是调用链数据的透传,透传就须要借助到 Propagator 机制,在 OJI 中默认应用的是两个 PropagatorTraceparentBaggage

Traceparent用于调用链的 traceId 等调用链根底数据的传递
Baggage 能够用于自定义的申请头的传递,以固定的申请头 ”baggage”,其中他的指格局为 K - V 构造

依照情理来说这两个 Propagator 根本也曾经够用了,然而在一些场景,如全链路灰度,全链路压测时,往往须要应用本人独有的标识,那么一个自定义的传递标识(申请头)就很有必要了。

想要创立本人的Propagator,那么就须要先实现接口ConfigurablePropagatorProvider

@AutoService(ConfigurablePropagatorProvider.class)
public class ColorConfigurablePropagatorProvider implements ConfigurablePropagatorProvider {
  @Override
  public TextMapPropagator getPropagator(ConfigProperties config) {return new ColorPropagator();
  }

  @Override
  public String getName() {return "color";}
}

在这个接口中定义了 Propagator 的名称与实现。

接下来就是实现类,实现类须要实现 TextMapPropagator 接口,如下:

public class ColorPropagator implements TextMapPropagator {
  private static final String FIELD = "color";
  private static final ContextKey<String> PROPAGATION_KEY =
      ContextKey.named("propagation.color");

  @Override
  public Collection<String> fields() {return Collections.singletonList(FIELD);
  }

  @Override
  public <C> void inject(Context context, @Nullable C carrier, TextMapSetter<C> setter) {String v = context.get(PROPAGATION_KEY);

    if (v != null) {setter.set(carrier, FIELD, v);
    }
  }

  @Override
  public <C> Context extract(Context context, @Nullable C carrier, TextMapGetter<C> getter) {String v = getter.get(carrier, FIELD);
    if (v != null) {return context.with(PROPAGATION_KEY, v);
    } else {return context;}
  }
}

下面实现了一个最简略的 Propagator,这个Propagator 的名称是 color,流传时的 header 关键字也是color,他只做了一件事,就是接收数据时将数据从 header 中取出寄存入内存中,ContextKey"propagation.color",而数据收回时就从内存中取出数据放入 header 之中。因而上述的实现流程以最简略的模式构建了一个反对透传的Propagator

其余

其实 OJI 还反对其余品种的扩大,然而因为篇幅无限,以及一些扩大形式并不是很罕用,因而略过。如果感兴趣能够在此处 extensions 找到官网的更多文档。

总结

这篇讲述 OJI 的二次开发的一些形式并列出了代码,我自认为全网应该不会有比这篇更加具体的文章来讲述这方面的内容。(这都是踩了有数的坑总结进去的!)

至此 OpenTelemetry 系列的文章临时就告一段落了,然而这不代表后续相干的内容就齐全完结了。其中蕴含整体观测体系的架构,调用链的存储形式,metrics 的解决都是能够开展细谈的。所以后续我可能会视状况更新一些相干的文章,然而不会再放在这个系列中了,敬请期待。

退出移动版