了解 APM:如何向 OpenTelemetry Java 代理添加扩展

作者:David Hope

如果没有代码访问权限,SRE 和 IT 运营人员无法始终获得所需的可见性

作为 SRE,你是否曾经遇到过这样的情况:你正在开发使用非标准框架编写的应用程序,或者你想从应用程序中获取一些有趣的业务数据(例如处理的订单数量),但你没有这样做?你有权访问源代码吗?

我们都知道,这可能是一个具有挑战性的场景,导致可见性差距、无法端到端地完全跟踪代码,以及丢失有助于了解问题的真正影响的关键业务监控数据。

我们该如何解决这个问题呢? 我们在以下三个博客中讨论了一种方法:

我们在这里为 Elastic® APM 代理开发一个插件,以帮助访问关键业务数据以进行监控,并在什么都不存在的情况下添加跟踪。

我们将在本博客中讨论如何使用扩展框架对 OpenTelemetry Java 代理执行相同的操作。

基本概念:APM 的工作原理

在继续之前,我们首先了解一些基本概念和术语。

  • Java Agent:这是一个可用于检测(或修改)Java 虚拟机 (JVM) 中类文件字节码的工具。 Java 代理可用于多种用途,例如性能监控、日志记录、安全性等。
  • Bytecode:这是 Java 编译器根据 Java 源代码生成的中间代码。 该代码由 JVM 即时解释或编译,以生成可以执行的机器代码。
  • Byte Buddy :Byte Buddy 是一个 Java 代码生成和操作库。 它用于在运行时创建、修改或调整 Java 类。 在 Java Agent 的上下文中,Byte Buddy 提供了一种强大且灵活的方式来修改字节码。 Elastic APM 代理和 OpenTelemetry 代理都在幕后使用 Byte Buddy

现在,我们来谈谈自动检测如何与 Byte Buddy 配合使用

自动检测是代理修改应用程序类的字节码的过程,通常是为了插入监视代码。 代理并不直接修改源代码,而是修改加载到 JVM 中的字节码。 这是在 JVM 加载类时完成的,因此修改在运行时有效。

以下是该过程的简化说明:

1)使用代理启动 JVM :启动 Java 应用程序时,可以使用 -javaagent 命令行选项指定 Java 代理。 这会指示 JVM 在调用应用程序的 main 方法之前加载代理。 此时,代理有机会设置类转换器(class transformer)。

2)向 Byte Buddy 注册类文件转换器(file transformer):你的代理将向 Byte Buddy 注册类文件转换器。 转换器是每次将类加载到 JVM 时都会调用的一段代码。 该转换器接收类的字节码,并且可以在实际使用该类之前修改该字节码。

3)转换字节码:当调用转换器时,它将使用 Byte Buddy 的 API 来修改字节码。 Byte Buddy 允许你以高级、富有表现力的方式指定转换,而不是手动编写复杂的字节码。 例如,你可以指定要检测的某个类和该类中的方法,并提供一个 "拦截器" 来向该方法添加新行为。

  • 例如,假设你想要测量方法的执行时间。 你将指示 Byte Buddy 定位特定的类和方法,然后提供一个拦截器,用计时代码包装方法调用。 每次调用此方法时,都会首先调用你的拦截器,测量开始时间,然后调用原始方法,最后测量结束时间并打印持续时间。

4)使用转换后的类:一旦代理设置了其转换器,JVM 就会像往常一样继续加载类。 每次加载类时,都会调用转换器,允许它们修改字节码。 然后,你的应用程序将使用这些转换后的类,就像它们是原始类一样,但它们现在具有你通过拦截器注入的额外行为。

本质上,Byte Buddy 的自动检测就是在运行时修改 Java 类的行为,而不需要直接更改源代码。 这对于日志记录、监视或安全性等横切关注点特别有用,因为它允许你将此代码集中在 Java 代理中,而不是将其分散在整个应用程序中。

应用程序、先决条件和配置

这个 GitHub 存储库中有一个非常简单的应用程序,在整个博客中都会使用它。 它的作用只是要求你输入一些文本,然后计算单词数。

下面还列出了它:

java 复制代码
1.  package org.davidgeorgehope;
2.  import java.util.Scanner;
3.  import java.util.logging.Logger;

5.  public class Main {
6.      private static Logger logger = Logger.getLogger(Main.class.getName());

8.      public static void main(String[] args) {
9.          Scanner scanner = new Scanner(System.in);
10.          while (true) {
11.              System.out.println("Please enter your sentence:");
12.              String input = scanner.nextLine();
13.              Main main = new Main();
14.              int wordCount = main.countWords(input);
15.              System.out.println("The input contains " + wordCount + " word(s).");
16.          }
17.      }
18.      public int countWords(String input) {

20.          try {
21.              Thread.sleep(10000);
22.          } catch (InterruptedException e) {
23.              throw new RuntimeException(e);
24.          }

26.          if (input == null || input.isEmpty()) {
27.              return 0;
28.          }

30.          String[] words = input.split("\\s+");
31.          return words.length;
32.      }
33.  }

就本博客而言,我们将使用 Elastic Cloud 捕获 OpenTelemetry 生成的数据 --- 请按照此处的说明开始使用 Elastic Cloud

开始使用 Elastic Cloud 后,请从 APM 页面获取 OpenTelemetry 配置:

稍后你将需要这个。如果你想在自己的电脑里进行部署,那么你可以参考文章 "Elastic:开发者上手指南" 来部署 Elasticsearch 及 Kibana。

最后,下载 OpenTelemetry Agent

启动应用程序和 OpenTelemetry

如果你从这个简单的应用程序开始,请像使用 OpenTelemetry Agent 一样构建并运行它,并使用之前获得的变量填充适当的值。

ini 复制代码
java -javaagent:opentelemetry-javaagent.jar -Dotel.exporter.otlp.endpoint=XX -Dotel.exporter.otlp.headers=XX -Dotel.metrics.exporter=otlp -Dotel.logs.exporter=otlp -Dotel.resource.attributes=XX -Dotel.service.name=your-service-name -jar simple-java-1.0-SNAPSHOT.jar

你会发现什么也没有发生。 原因是 OpenTelemetry Agent 无法知道要监视什么。 具有自动检测功能的 APM 的工作方式是,它 "了解 " 标准框架(例如 Spring 或 HTTPClient),并且能够通过自动将跟踪代码 "注入" 到这些标准框架中来获得可见性。

它不知道我们简单的 Java 应用程序中的 org.davidgeorgehope.Main。

幸运的是,我们可以使用 OpenTelemetry Extensions 框架添加此功能。

OpenTelemetry 扩展

在上面的存储库中,除了 simple-java 应用程序之外,还有一个 Elastic APM 插件和 OpenTelemetry 扩展。 OpenTelemetry Extension 的相关文件位于此处 --- WordCountInstrumentation.java 和 WordCountInstrumentationModule.java 。

你会注意到,OpenTelemetry Extensions 和 Elastic APM Plugins 都使用 Byte Buddy,这是代码检测的通用库。 不过,代码引导方式存在一些关键差异。

WordCountInstrumentationModule 类扩展了 OpenTeletry 特定类 InstrumentationModule,其目的是描述一组需要一起应用以正确检测特定库的 TypeInstrumentation。 WordCountInstrumentation 类就是 TypeInstrumentation 的此类实例之一。

Type 检测被集中于一个模块中共享辅助类、入口运行时检查和适用的类加载器标准,并且只能作为一组启用或禁用。

这与 Elastic APM 插件的工作方式有点不同,因为使用 OpenTelemetry 注入代码的默认方法是使用 OpenTelemetry 内联(inline)的(这是默认方法),并且你可以使用 InstrumentationModule 配置将依赖项注入到核心应用程序类加载器中(如如下所示)。 Elastic APM 方法更安全,因为它允许隔离帮助程序类,并且更容易使用普通 IDE 进行调试,我们将此方法贡献给 OpenTelemetry。 这里我们将 TypeInstrumentation 类和 WordCountInstrumentation 类注入到类加载器中。

typescript 复制代码
 1.   @Override
2.      public List<String> getAdditionalHelperClassNames() {
3.          return List.of(WordCountInstrumentation.class.getName(),"io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation");
4.      }

TypeInstrumentation 类的另一个有趣的部分是设置。

在这里,我们为我们的检测 "group" 命名。 一个 InstrumentationModule 至少需要有一个名称。 javaagent 的用户可以通过引用其名称之一来抑制所选的检测。 检测模块名称使用短横线命名。

csharp 复制代码
 1.      public WordCountInstrumentationModule() {
2.          super("wordcount-demo", "wordcount");
3.      }

除此之外,我们还看到此类中的方法可以根据需要指定相对于其他检测的加载顺序,并且我们指定扩展 TypeInstrumention 并负责大部分检测工作的类。

让我们看一下 WordCountInstrumention 类,它现在扩展了 TypeInstrumention:

less 复制代码
1.  // The WordCountInstrumentation class implements the TypeInstrumentation interface.
2.  // This allows us to specify which types of classes (based on some matching criteria) will have their methods instrumented.

4.  public class WordCountInstrumentation implements TypeInstrumentation {

6.      // The typeMatcher method is used to define which classes the instrumentation should apply to.
7.      // In this case, it's the "org.davidgeorgehope.Main" class.
8.      @Override
9.      public ElementMatcher<TypeDescription> typeMatcher() {
10.          logger.info("TEST typeMatcher");
11.          return ElementMatchers.named("org.davidgeorgehope.Main");
12.      }

14.      // In the transform method, we specify which methods of the classes matched above will be instrumented, 
15.      // and also the advice (a piece of code) that will be added to these methods.
16.      @Override
17.      public void transform(TypeTransformer typeTransformer) {
18.          logger.info("TEST transform");
19.          typeTransformer.applyAdviceToMethod(namedOneOf("countWords"),this.getClass().getName() + "$WordCountAdvice");
20.      }

22.      // The WordCountAdvice class contains the actual pieces of code (advices) that will be added to the instrumented methods.
23.      @SuppressWarnings("unused")
24.      public static class WordCountAdvice {
25.          // This advice is added at the beginning of the instrumented method (OnMethodEnter).
26.          // It creates and starts a new span, and makes it active.
27.          @Advice.OnMethodEnter(suppress = Throwable.class)
28.          public static Scope onEnter(@Advice.Argument(value = 0) String input, @Advice.Local("otelSpan") Span span) {
29.              // Get a Tracer instance from OpenTelemetry.
30.              Tracer tracer = GlobalOpenTelemetry.getTracer("instrumentation-library-name","semver:1.0.0");
31.              System.out.print("Entering method");

33.              // Start a new span with the name "mySpan".
34.              span = tracer.spanBuilder("mySpan").startSpan();

36.              // Make this new span the current active span.
37.              Scope scope = span.makeCurrent();

39.              // Return the Scope instance. This will be used in the exit advice to end the span's scope.
40.              return scope; 
41.          }

43.          // This advice is added at the end of the instrumented method (OnMethodExit).
44.          // It first closes the span's scope, then checks if any exception was thrown during the method's execution.
45.          // If an exception was thrown, it sets the span's status to ERROR and ends the span.
46.          // If no exception was thrown, it sets a custom attribute "wordCount" on the span, and ends the span.
47.          @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
48.          public static void onExit(@Advice.Return(readOnly = false) int wordCount,
49.                                    @Advice.Thrown Throwable throwable,
50.                                    @Advice.Local("otelSpan") Span span,
51.                                    @Advice.Enter Scope scope) {
52.              // Close the scope to end it.
53.              scope.close();

55.              // If an exception was thrown during the method's execution, set the span's status to ERROR.
56.              if (throwable != null) {
57.                  span.setStatus(StatusCode.ERROR, "Exception thrown in method");
58.              } else {
59.                  // If no exception was thrown, set a custom attribute "wordCount" on the span.
60.                  span.setAttribute("wordCount", wordCount);
61.              }

63.              // End the span. This makes it ready to be exported to the configured exporter (e.g. Elastic).
64.              span.end();
65.          }
66.      }
67.  }

我们检测的目标类是在 typeMatch 方法中定义的,而我们想要检测的方法是在 Transform 方法中定义的。 我们的目标是 Main 类和 countWords 方法。

正如你所看到的,我们这里有一个内部类,它完成了定义 onEnter 和 onExit 方法的大部分工作,它告诉我们当进入 countWords 方法和退出 countWords 方法时要做什么。

在 onEnter 方法中,我们设置了一个新的 OpenTelemetry span,在 onExit 方法中,我们结束了该 span。 如果该方法成功结束,我们还会获取字数并将其添加到属性中。

现在让我们看看运行它时会发生什么。 好消息是,我们通过提供一个 dockerfile 供你完成所有工作,使这变得非常简单。

把这一切放在一起

如果你还没有克隆 GitHub 存储库,请克隆它,在继续之前,让我们快速浏览一下我们正在使用的 dockerfile。

markdown 复制代码
1.  # Build stage
2.  FROM maven:3.8.7-openjdk-18 as build

4.  COPY simple-java /home/app/simple-java
5.  COPY opentelemetry-custom-instrumentation /home/app/opentelemetry-custom-instrumentation

7.  WORKDIR /home/app/simple-java
8.  RUN mvn install

10.  WORKDIR /home/app/opentelemetry-custom-instrumentation
11.  RUN mvn install

13.  # Package stage
14.  FROM maven:3.8.7-openjdk-18
15.  COPY --from=build /home/app/simple-java/target/simple-java-1.0-SNAPSHOT.jar /usr/local/lib/simple-java-1.0-SNAPSHOT.jar
16.  COPY --from=build /home/app/opentelemetry-custom-instrumentation/target/opentelemetry-custom-instrumentation-1.0-SNAPSHOT.jar /usr/local/lib/opentelemetry-custom-instrumentation-1.0-SNAPSHOT.jar

18.  WORKDIR /

20.  RUN curl -L -o opentelemetry-javaagent.jar https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar

22.  COPY start.sh /start.sh
23.  RUN chmod +x /start.sh

25.  ENTRYPOINT ["/start.sh"]

该 dockerfile 分为两部分:在 docker 构建过程中,我们从源代码构建 simple-java 应用程序,然后构建自定义检测。 之后,我们下载最新的 OpenTelemetry Java Agent。 在运行时,我们只需执行如下所述的 start.sh 文件:

ini 复制代码
1.  #!/bin/sh
2.  java \
3.  -javaagent:/opentelemetry-javaagent.jar \
4.  -Dotel.exporter.otlp.endpoint=${SERVER_URL} \
5.  -Dotel.exporter.otlp.headers="Authorization=Bearer ${SECRET_KEY}" \
6.  -Dotel.metrics.exporter=otlp \
7.  -Dotel.logs.exporter=otlp \
8.  -Dotel.resource.attributes=service.name=simple-java,service.version=1.0,deployment.environment=production \
9.  -Dotel.service.name=your-service-name \
10.  -Dotel.javaagent.extensions=/usr/local/lib/opentelemetry-custom-instrumentation-1.0-SNAPSHOT.jar \
11.  -Dotel.javaagent.debug=true \
12.  -jar /usr/local/lib/simple-java-1.0-SNAPSHOT.jar

此脚本有两件重要的事情需要注意:第一是我们启动设置为 opentelemetry-javaagent.jar 的 javaagent 参数 --- 这将启动 OpenTelemetry javaagent 运行,它在执行任何代码之前启动。

在这个 jar 中,必须有一个带有 JVM 将查找的 premain 方法的类。 这将引导 java 代理。 如上所述,任何编译的字节码本质上都会通过 javaagent 代码进行过滤,因此它可以在执行之前修改类。

这里第二件重要的事情是 javaagent.extensions 的配置,它加载我们构建的扩展,以便为我们的 simple-java 应用程序添加工具。

现在运行以下命令:

ini 复制代码
1.  docker build -t djhope99/custom-otel-instrumentation:1 .
2.  docker run -it -e 'SERVER_URL=XXX' -e 'SECRET_KEY=XX djhope99/custom-otel-instrumentation:1

如果你使用之前在此处获得的 SERVER_URL 和 SECRET_KEY,你应该会看到此连接到 Elastic。

当它启动时,它会要求你输入一个句子,输入几个句子,然后按回车键。 这样做几次 ------ 这里有一个睡眠(sleep)来强制一个长时间运行的事务:

最终你将看到该服务显示在服务地图(service map)中:

会出现跟踪:

在该 span 内,你将看到我们收集的 wordcount 属性:

这可用于进一步的仪表板和 AI/ML,包括异常检测(如果需要),这很容易做到,如下所示。

首先点击左侧的 Elastic 图标,选择 Dashboard 创建一个新的仪表板:

从这里,单击创建可视化。

在 APM 索引中搜索 wordcount 标签,如下图:

正如你所看到的,因为我们在 Span 代码中创建了此属性,如下所示,并将 wordCount 作为 "Integer" 类型,所以我们能够自动将其分配为 Elastic 中的数字字段:

less 复制代码
span.setAttribute("wordCount", wordCount);

从这里我们可以将其拖放到可视化中以显示在我们的仪表板上! 超级简单。

综上所述

该博客阐明了 OpenTelemetry Java 代理在填补可见性差距和获取关键业务监控数据方面的宝贵作用,特别是在无法访问源代码的情况下。

该博客阐述了对 Java Agent、Bytecode 和 Byte Buddy 的基本了解,随后全面检查了 Byte Buddy 的自动检测过程。

借助一个简单的 Java 应用程序演示了使用扩展框架的 OpenTelemetry Java 代理的实现,这强调了代理将跟踪代码注入到应用程序中以方便监控的能力。

它详细介绍了如何配置代理和集成 OpenTelemetry Extension,并概述了示例应用程序的操作,以帮助用户了解所讨论信息的实际应用。 对于寻求使用 OpenTelemetry 的自动检测功能来优化应用程序工作的 SRE 和 IT 运营人员来说,这篇富有启发性的博客文章是一个极好的资源。

原文:Understanding APM: How to add extensions to the OpenTelemetry Java Agent | Elastic Blog

相关推荐
Mephisto.java3 小时前
【大数据学习 | Spark】Spark的改变分区的算子
大数据·elasticsearch·oracle·spark·kafka·memcache
mqiqe3 小时前
Elasticsearch 分词器
python·elasticsearch
小马爱打代码3 小时前
Elasticsearch简介与实操
大数据·elasticsearch·搜索引擎
java1234_小锋12 小时前
Elasticsearch是如何实现Master选举的?
大数据·elasticsearch·搜索引擎
梦幻通灵18 小时前
ES分词环境实战
大数据·elasticsearch·搜索引擎
Elastic 中国社区官方博客18 小时前
Elasticsearch 中的热点以及如何使用 AutoOps 解决它们
大数据·运维·elasticsearch·搜索引擎·全文检索
小黑屋说YYDS1 天前
ElasticSearch7.x入门教程之索引概念和基础操作(三)
elasticsearch
Java 第一深情1 天前
Linux上安装单机版ElasticSearch6.8.1
linux·elasticsearch·全文检索
KevinAha2 天前
Elasticsearch 6.8 分析器
elasticsearch
wuxingge2 天前
elasticsearch7.10.2集群部署带认证
运维·elasticsearch