首页 > 其他分享 >实战:如何编写一个 OpenTelemetry Extensions

实战:如何编写一个 OpenTelemetry Extensions

时间:2024-04-16 09:34:57浏览次数:29  
标签:实战 opentelemetry javaagent extension jar 插件 OpenTelemetry Extensions extensions

前言

前段时间我们从 SkyWalking 切换到了 OpenTelemetry ,与此同时之前使用 SkyWalking 编写的插件也得转移到 OpenTelemetry 体系下。

我也写了相关介绍文章:
实战:如何优雅的从 SkyWalking 切换到 OpenTelemetry

好在 OpenTelemetry 社区也提供了 Extensions 的扩展开发,我们可以不用去修改社区发行版:opentelemetry-javaagent.jar 的源码也可以扩展其中的能力。

比如可以:

  • 修改一些 trace,某些 span 不想记录等。
  • 新增 metrics

这次我准备编写的插件也是和 metrics 有关的,因为 pulsar 的 Java sdk 中并没有暴露客户端的一些监控指标,所以我需要在插件中拦截到一些关键函数,然后执行暴露出指标。

截止到本文编写的时候, Pulsar 社区也已经将 Java-client 集成了 OpenTelemetry,后续正式发版后我这个插件也可以光荣退休了。


由于 OpenTelemetry 社区还处于高速发展阶段,我在中文社区没有找到类似的参考文章(甚至英文社区也没有,只有一些 example 代码,或者是只有去社区成熟插件里去参考代码)

其中也踩了不少坑,所以觉得非常有必要分享出来帮助大家减少遇到同类问题的机会。

开发流程

OpenTelemetry extension 的写法其实和 skywalking 相似,都是用的 bytebuddy这个字节码增强库,只是在一些 API 上有一些区别。

创建项目

首先需要创建一个 Java 项目,这里我直接参考了官方的示例,使用了 gradle 进行管理(理论上 maven 也是可以的,只是要找到在 gradle 使用的 maven 插件)。

这里贴一下简化版的 build.gradle 文件:

plugins {
    id 'java'
    id "com.github.johnrengelman.shadow" version "8.1.1"
    id "com.diffplug.spotless" version "6.24.0"
}

group = 'com.xx.otel.extensions'
version = '1.0.0'

ext {
    versions = [
            // this line is managed by .github/scripts/update-sdk-version.sh
            opentelemetrySdk           : "1.34.1",

            // these lines are managed by .github/scripts/update-version.sh
            opentelemetryJavaagent     : "2.1.0-SNAPSHOT",
            opentelemetryJavaagentAlpha: "2.1.0-alpha-SNAPSHOT",

            junit                      : "5.10.1"
    ]

    deps = [
    // 自动生成服务发现 service 文件
            autoservice: dependencies.create(group: 'com.google.auto.service', name: 'auto-service', version: '1.1.1')
    ]
}

repositories {
    mavenLocal()
    maven { url "https://maven.aliyun.com/repository/public" }
    mavenCentral()
}

configurations {
    otel
}


dependencies {

    implementation(platform("io.opentelemetry:opentelemetry-bom:${versions.opentelemetrySdk}"))

    /*
    Interfaces and SPIs that we implement. We use `compileOnly` dependency because during
    runtime all necessary classes are provided by javaagent itself.
     */
    compileOnly 'io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:1.34.1'
    compileOnly 'io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:1.32.0'
    compileOnly 'io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api:1.32.0-alpha'

    //Provides @AutoService annotation that makes registration of our SPI implementations much easier
    compileOnly deps.autoservice
    annotationProcessor deps.autoservice

    // https://mvnrepository.com/artifact/org.apache.pulsar/pulsar-client
    compileOnly 'org.apache.pulsar:pulsar-client:2.8.0'

}

test {
    useJUnitPlatform()
}

然后便是要创建 javaagent 的一个核心类:

@AutoService(InstrumentationModule.class)  
public class PulsarInstrumentationModule extends InstrumentationModule {
    public PulsarInstrumentationModule() {
        super("pulsar-client-metrics", "pulsar-client-metrics-2.8.0");
    }	
}

在这个类中定义我们插件的名称,同时使用 @AutoService 注解可以在打包的时候帮我们在 META-INF/services/目录下生成 SPI 服务发现的文件:

这是一个 Google 的插件,本质是插件是使用 SPI 的方式进行开发的。

关于 SPI 以前也写过一篇文章,不熟的朋友可以用作参考:

创建 Instrumentation

之后就需要创建自己的 Instrumentation,这里可以把它理解为自己的拦截器,需要配置对哪个类的哪个函数进行拦截:

public class ProducerCreateImplInstrumentation implements TypeInstrumentation {

    @Override
    public ElementMatcher<TypeDescription> typeMatcher() {
        return named("org.apache.pulsar.client.impl.ProducerBuilderImpl");
    }
    @Override
    public void transform(TypeTransformer transformer) {
        transformer.applyAdviceToMethod(
                isMethod()
                        .and(named("createAsync")),
                ProducerCreateImplInstrumentation.class.getName() + "$ProducerCreateImplConstructorAdvice");
    }

比如这就是对 ProducerBuilderImpl 类的 createAsync 创建函数进行拦截,拦截之后的逻辑写在了 ProducerCreateImplConstructorAdvice 类中。

值得注意的是对一些继承和实现类的拦截方式是不相同的:

@Override  
public ElementMatcher<TypeDescription> typeMatcher() {  
    return extendsClass(named(ENHANCE_CLASS));  
    // return implementsInterface(named(ENHANCE_CLASS));
}

从这两个函数名称就能看出,分别是针对继承和实现类进行拦截的。

这里的 API 比 SkyWalking 的更易读一些。

之后需要把我们自定义的 Instrumentation 注册到刚才的 PulsarInstrumentationModule 类中:

    @Override
    public List<TypeInstrumentation> typeInstrumentations() {
        return Arrays.asList(
                new ProducerCreateImplInstrumentation(),
                new ProducerCloseImplInstrumentation(),
                );
    }

有多个的话也都得进行注册。

编写切面代码

之后便是编写我们自定义的切面逻辑了,也就是刚才自定义的 ProducerCreateImplConstructorAdvice 类:

    public static class ProducerCreateImplConstructorAdvice {

        @Advice.OnMethodEnter(suppress = Throwable.class)
        public static void onEnter() {
            // inert your code
            MetricsRegistration.registerProducer();
        }

        @Advice.OnMethodExit(suppress = Throwable.class)
        public static void after(
                @Advice.Return CompletableFuture<Producer> completableFuture) {
            try {
                Producer producer = completableFuture.get();
                CollectionHelper.PRODUCER_COLLECTION.addObject(producer);
            } catch (Throwable e) {
                System.err.println(e.getMessage());
            }
        }
    }

可以看得出来其实就是两个核心的注解:

  • @Advice.OnMethodEnter 切面函数调用之前
  • @Advice.OnMethodExit 切面函数调用之后

还可以在 @Advice.OnMethodExit的函数中使用 @Advice.Return获得函数调用的返回值。

当然也可以使用 @Advice.This 来获取切面的调用对象。

编写自定义 metrics

因为我这个插件的主要目的是暴露一些自定义的 metrics,所以需要使用到 io.opentelemetry.api.metrics 这个包:

这里以 Producer 生产者为例,整体流程如下:

  • 创建生产者的时候将生产者对象存储起来
  • OpenTelemetry 框架会每隔一段时间回调一个自定义的函数
  • 在这个函数中遍历所有的 producer 获取它的监控指标,然后暴露出去。

注册函数:

public static void registerObservers() {  
    Meter meter = MetricsRegistration.getMeter();  
  
    meter.gaugeBuilder("pulsar_producer_num_msg_send")  
            .setDescription("The number of messages published in the last interval")  
            .ofLongs()  
            .buildWithCallback(  
                    r -> recordProducerMetrics(r, ProducerStats::getNumMsgsSent));

private static void recordProducerMetrics(ObservableLongMeasurement observableLongMeasurement, Function<ProducerStats, Long> getter) {  
    for (Producer producer : CollectionHelper.PRODUCER_COLLECTION.list()) {  
        ProducerStats stats = producer.getStats();  
        String topic = producer.getTopic();  
        if (topic.endsWith(RetryMessageUtil.RETRY_GROUP_TOPIC_SUFFIX)) {  
            continue;  
        }        observableLongMeasurement.record(getter.apply(stats),  
                Attributes.of(PRODUCER_NAME, producer.getProducerName(), TOPIC, topic));  
    }}

回调函数,在这个函数中遍历所有的生产者,然后读取它的监控指标。

这样就完成了一个自定义指标的暴露,使用的时候只需要加载这个插件即可:

java -javaagent:opentelemetry-javaagent.jar \
     -Dotel.javaagent.extensions=ext.jar
     -jar myapp.jar

-Dotel.javaagent.extensions=/extensions
当然也可以指定一个目录,该目录下所有的 jar 都会被作为 extensions 被加入进来。

打包

使用 ./gradlew build 打包,之后可以在build/libs/目录下找到生成物。

当然也可以将 extension 直接打包到 opentelemetry-javaagent.jar中,这样就可以不用指定 -Dotel.javaagent.extensions参数了。

具体可以在 gradle 中加入以下 task:

task extendedAgent(type: Jar) {
  dependsOn(configurations.otel)
  archiveFileName = "opentelemetry-javaagent.jar"
  from zipTree(configurations.otel.singleFile)
  from(tasks.shadowJar.archiveFile) {
    into "extensions"
  }
  //Preserve MANIFEST.MF file from the upstream javaagent
  doFirst {
    manifest.from(
      zipTree(configurations.otel.singleFile).matching {
        include 'META-INF/MANIFEST.MF'
      }.singleFile
    )
  }
}

具体可以参考这里的配置:
https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/examples/extension/build.gradle#L125

踩坑

看起来这个开发过程挺简单的,但其中的坑还是不少。

NoClassDefFoundError

首先第一个就是我在调试过程中出现 NoClassDefFoundError 的异常。

但我把打包好的 extension 解压后明明是可以看到这个类的。

排查一段时间后没啥头绪,我就从头仔细阅读了开发文档:

发现我们需要重写 getAdditionalHelperClassNames函数,用于将我们外部的一些工具类加入到应用的 class loader 中,不然在应用在运行的时候就会报 NoClassDefFoundError 的错误。

因为是字节码增强的关系,所以很多日常开发觉得很常见的地方都不行了,比如:

  • 如果切面类是一个内部类的时候,必须使用静态函数
  • 只能包含静态函数
  • 不能包含任何字段,常量。
  • 不能使用任何外部类,如果要使用就得使用 getAdditionalHelperClassNames 额外加入到 class loader 中(这一条就是我遇到的问题)
  • 所有的函数必须使用 @Advice 注解

以上的内容其实在文档中都有写:

所以还是得仔细阅读文档。

缺少异常日志

其实上述的异常刚开始都没有打印出来,只有一个现象就是程序没有正常运行。

因为没有日志也不知道如何排查,也怀疑是不是运行过程中报错了,所以就尝试把@Advice 注解的函数全部 try catch ,果然打印了上述的异常日志。

之后我注意到了注解的这个参数,原来在默认情况下是不会打印任何日志的,需要手动打开。

比如这样:@Advice.OnMethodExit(suppress = Throwable.class)

调试日志

最后就是调试功能了,因为我这个插件的是把指标发送到 OpenTelemetry-collector ,再由它发往 VictoriaMetrics/Prometheus;由于整个链路比较长,我想看到最终生成的指标是否正常的干扰条件太多了。

好在 OpenTelemetry 提供了多种 metrics.exporter 的输出方式:

  • -Dotel.metrics.exporter=otlp (default),默认通过 otlp 协议输出到 collector 中。
  • -Dotel.metrics.exporter=logging,以 stdout 的方式输出到控制台,主要用于调试
  • -Dotel.metrics.exporter=logging-otlp
  • -Dotel.metrics.exporter=prometheus,以 Prometheus 的方式输出,还可以配置端口,这样也可以让 Prometheus 进行远程采集,同样的也可以在本地调试。

采用哪种方式可以根据环境情况自行选择。

Opentelemetry-operator 配置 extension

最近在使用 opentelemetry-operator注入 agent 的时候发现 operator 目前并不支持配置 extension,所以在社区也提交了一个草案,下周会尝试提交一个 PR 来新增这个特性。

这个需求我在 issue 列表中找到了好几个,时间也挺久远了,不太确定为什么社区还为实现。

目前 operator 只支持在自定义镜像中配置 javaagent.jar,无法配置 extension:

这个原理在之前的文章中有提到。

apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
  name: my-instrumentation
spec:
  java:
    image: your-customized-auto-instrumentation-image:java

我的目的是可以在自定义镜像中把 extension 也复制进去,类似于这样:

FROM busybox

ADD open-telemetry/opentelemetry-javaagent.jar /javaagent.jar

# Copy extensions to specify a path.
ADD open-telemetry/ext-1.0.0.jar /ext-1.0.0.jar

RUN chmod -R go+r /javaagent.jar
RUN chmod -R go+r /ext-1.0.0.jar

然后在 CRD 中配置这个 extension 的路径:

apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
  name: my-instrumentation
spec:
  java:
    image: custom-image:1.0.0
    extensions: /ext-1.0.0.jar
    env:
    # If extension.jar already exists in the container, you can only specify a specific path with this environment variable.
      - name: OTEL_EXTENSIONS_DIR
        value: /custom-dir

这样 operator 在拿到 extension 的路径时,就可以在环境变量中加入 -Dotel.javaagent.extensions=${java.extensions} 参数,从而实现自定义 extension 的目的。

总结

整个过程其实并不复杂,只是由于目前用的人还不算多,所以也很少有人写教程或者文章,相信用不了多久就会慢慢普及。

这里有一些官方的 example可以参考。

参考链接:

标签:实战,opentelemetry,javaagent,extension,jar,插件,OpenTelemetry,Extensions,extensions
From: https://www.cnblogs.com/crossoverJie/p/18137419

相关文章

  • Pytorch计算机视觉实战(更新中)
    第一章人工神经网络基础1.1人工智能与传统机器学习学习心得:传统机器学习(ML):需要专业的主题专家人工提取特征,并通过一个编写良好的算法来破译给定的特征,从而判断这幅图像中的内容。输入-->人工提取特征-->特征-->具有浅层结构的分类器-->输出当存在欺骗性的图片出现时可能会......
  • 通义灵码×西安交通大学携手打造“云工开物-高校训练营”,解锁 AI 时代编程学习与实战
    作为大学生如何利用AI“整活儿”?欢迎各位同学关注阿里云与西安交通大学计算机学院携手打造的“云工开物-高校训练营”,带你走近AI编程助手“通义灵码”。通义灵码是阿里推出的免费AI编程工具,拥有实时代码续写与优化、自然语言生成代码、单元测试生成、代码注释生成、代码解释......
  • Java并发编程实战读书笔记
    1.线程池模型    netty实战中讲到的线程池模型可以描述为:1.从线程池中选择一个空间的线程去执行任务,2.任务完成时,把线程归还给线程池。这个模型与连接池类似。    根据jdk源码的研究,具体的实现模型是,线程池ThreadPoolExecutor中有一个静态内部类Worker,使用装饰器模式扩......
  • 鸿蒙HarmonyOS实战-ArkUI组件(Tabs)
    ......
  • 高并发场景QPS等专业指标揭秘大全与调优实战
    高并发场景QPS等专业指标揭秘大全与调优实战最近经常有小伙伴问及高并发场景下QPS的一些问题,特意结合项目经验和网上技术贴做了一些整理和归纳,供大家参考交流。一、一直再说高并发,多少QPS才算高并发?高并发的四个角度只说并发不提高可用就是耍流氓。可以从四个角度讨论这个问......
  • 面试官:实战中用过CountDownLatch吗?详细说一说,我:啊这
    写在开头在很多的面经中都看到过提问CountDownLatch的问题,正好我们最近也在梳理学习AQS(抽象队列同步器),而CountDownLatch又是其中典型的代表,我们今天就继续来学一下这个同步工具类!CountDownLatch有何作用?我们知道AQS是专属于构造锁和同步器的一个抽象工具类,基于它Java构造出了......
  • 鸿蒙HarmonyOS实战-ArkUI组件(Navigation)
    ......
  • k8s 实战 Centos8.4 版本
    网络前置操作nmcliconmod"Wiredconnection1"ipv4.addresses192.168.0.201/24#设置IP地址nmcliconmod"Wiredconnection1"ipv4.gateway192.168.0.1#设置网关nmcliconmod"Wiredconnection1"ipv4.methodmanual#设置静态IPnmcliconm......
  • Python量化交易系统实战--计算交易指标
     作者:麦克煎蛋  出处:https://www.cnblogs.com/mazhiyong/转载请保留这段声明,谢谢! 本节主要包括以下内容:1、计算涨跌幅使用shift函数计算涨跌幅defcalculate_change_pct(data):"""涨跌幅=(当期收盘价-前期收盘价)/前期收盘价:paramdata:datafram......
  • Python+FastJson漏洞批量检测实战
    #-*-coding:utf-8-*-importosimportsubprocess#指定要读取文件的目录directory='D:/gongju02/anq/FastJson/JsonExp-1.4.0'defjson_exp(text_path):"""指定要检测的接口文件目录"""try:#改变当前工作目录os.chdir(di......