没有代码访问权限,SRE 和 IT 运维人员无法始终获得所需的可视性
作为一名 SRE,您是否遇到过这种情况:您正在处理一个使用非标准框架编写的应用程序,或者您想从应用程序中获取一些有趣的业务数据(例如,处理的订单数量),但您无法访问源代码?
我们都知道这可能是一个具有挑战性的场景,会导致可见性差距、无法完全端到端跟踪代码以及缺少对理解问题真正影响有用的关键业务监控数据。
我们如何解决这个问题?我们在以下三篇博文中讨论了一种方法
在这里,我们为 Elastic® APM Agent 开发一个插件,以帮助访问关键业务数据进行监控,并在不存在跟踪的地方添加跟踪。
我们将在本博文中讨论的是,如何使用扩展框架通过 OpenTelemetry Java Agent 实现相同的目的。
基本概念:APM 的工作原理
在我们继续之前,让我们首先了解一些基本概念和术语。
- Java Agent:这是一种工具,可用于检测(或修改)Java 虚拟机 (JVM) 中类文件的字节码。Java Agent 用于多种目的,例如性能监控、日志记录、安全性等。
- 字节码:这是 Java 编译器从您的 Java 源代码生成的中间代码。此代码由 JVM 解释或动态编译,以生成可执行的机器代码。
- Byte Buddy:Byte Buddy 是一个用于 Java 的代码生成和操作库。它用于在运行时创建、修改或调整 Java 类。在 Java Agent 的上下文中,Byte Buddy 提供了一种强大而灵活的方式来修改字节码。Elastic APM Agent 和 OpenTelemetry Agent 都使用 Byte Buddy 作为底层。
现在,让我们讨论一下自动检测如何使用 Byte Buddy 工作
自动检测是指 Agent 修改应用程序类字节码的过程,通常是为了插入监控代码。Agent 不直接修改源代码,而是修改加载到 JVM 中的字节码。这在 JVM 加载类时完成,因此修改在运行时生效。
以下是该过程的简化说明
-
使用 Agent 启动 JVM:在启动 Java 应用程序时,您可以使用 -javaagent 命令行选项指定 Java Agent。这指示 JVM 在调用应用程序的 main 方法之前加载您的 Agent。此时,Agent 有机会设置类转换器。
-
使用 Byte Buddy 注册类文件转换器:您的 Agent 将使用 Byte Buddy 注册一个类文件转换器。转换器是一段代码,每次将类加载到 JVM 时都会调用该代码。此转换器接收类的字节码,并且可以在实际使用该类之前修改此字节码。
-
转换字节码:当调用您的转换器时,它将使用 Byte Buddy 的 API 来修改字节码。Byte Buddy 允许您以高级、表达性的方式指定转换,而不是手动编写复杂的字节码。例如,您可以指定要检测的类和该类中的方法,并提供一个“拦截器”,它将向该方法添加新的行为。
-
使用转换后的类:一旦 Agent 设置了其转换器,JVM 将像往常一样继续加载类。每次加载类时,都会调用您的转换器,允许它们修改字节码。然后,您的应用程序使用这些转换后的类,就好像它们是原始类一样,但它们现在具有您通过拦截器注入的额外行为。
本质上,使用 Byte Buddy 进行自动检测是在运行时修改 Java 类的行为,而无需直接更改源代码。这对于日志记录、监控或安全性等横切关注点尤其有用,因为它允许您将此代码集中在 Java Agent 中,而不是分散在您的应用程序中。
应用程序、先决条件和配置
此 GitHub 存储库中有一个非常简单的应用程序,在本博文中将始终使用它。它的作用是简单地要求您输入一些文本,然后它会计算单词数。
它还列在下面
package org.davidgeorgehope;
import java.util.Scanner;
import java.util.logging.Logger;
public class Main {
private static Logger logger = Logger.getLogger(Main.class.getName());
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("Please enter your sentence:");
String input = scanner.nextLine();
Main main = new Main();
int wordCount = main.countWords(input);
System.out.println("The input contains " + wordCount + " word(s).");
}
}
public int countWords(String input) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (input == null || input.isEmpty()) {
return 0;
}
String[] words = input.split("\s+");
return words.length;
}
}
在本博文中,我们将使用 Elastic Cloud 来捕获 OpenTelemetry 生成的数据 - 按照此处的说明 开始使用 Elastic Cloud。
开始使用 Elastic Cloud 后,从 APM 页面获取 OpenTelemetry 配置
您稍后会需要它。
启动应用程序和 OpenTelemetry
如果您从这个简单的应用程序开始,请构建它并使用 OpenTelemetry Agent 运行它,并使用您之前获得的适当变量填充它们。
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 扩展框架来添加此功能。
OpenTelemetry 扩展
在上面的存储库中,除了简单的 java 应用程序外,还有一个用于 Elastic APM 的插件和一个用于 OpenTelemetry 的扩展。OpenTelemetry 扩展的相关文件位于此处 — WordCountInstrumentation.java 和 WordCountInstrumentationModule.java 。
您会注意到 OpenTelemetry 扩展和 Elastic APM 插件都使用了 Byte Buddy,这是一个用于代码检测的通用库。但是,代码启动方式存在一些关键差异。
WordCountInstrumentationModule 类扩展了一个 OpenTelemetry 特定的类 InstrumentationModule,其目的是描述一组需要一起应用的 TypeInstrumentation,以正确检测特定的库。WordCountInstrumentation 类就是 TypeInstrumentation 的一个实例。
模块中分组的类型检测共享辅助类、muzzle 运行时检查和适用的类加载器标准,并且只能作为一组启用或禁用。
这与 Elastic APM 插件的工作方式略有不同,因为使用 OpenTelemetry 注入代码的默认方法是内联(这是 OpenTelemetry 的默认设置),并且您可以使用 InstrumentationModule 配置将依赖项注入到核心应用程序类加载器中(如下所示)。Elastic APM 方法更安全,因为它允许隔离辅助类,并且可以更轻松地使用我们正在为 OpenTelemetry 贡献的普通 IDE 进行调试。在这里,我们将 TypeInstrumentation 类和 WordCountInstrumentation 类注入到类加载器中。
@Override
public List<String> getAdditionalHelperClassNames() {
return List.of(WordCountInstrumentation.class.getName(),"io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation");
}
TypeInstrumentation 类的另一个有趣的部分是设置。
在这里,我们给我们的检测“组”一个名称。InstrumentationModule 至少需要有一个名称。javaagent 的用户可以通过引用其名称之一来抑制选定的检测。检测模块名称使用 kebab-case。
public WordCountInstrumentationModule() {
super("wordcount-demo", "wordcount");
}
除此之外,我们在此类中看到了一些方法,用于指定此加载相对于其他检测的顺序(如果需要),并且我们指定了扩展 TypeInstrumention 的类,该类负责检测工作的大部分。
现在让我们看一下那个扩展了 TypeInstrumention 的 WordCountInstrumention 类
// The WordCountInstrumentation class implements the TypeInstrumentation interface.
// This allows us to specify which types of classes (based on some matching criteria) will have their methods instrumented.
public class WordCountInstrumentation implements TypeInstrumentation {
// The typeMatcher method is used to define which classes the instrumentation should apply to.
// In this case, it's the "org.davidgeorgehope.Main" class.
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
logger.info("TEST typeMatcher");
return ElementMatchers.named("org.davidgeorgehope.Main");
}
// In the transform method, we specify which methods of the classes matched above will be instrumented,
// and also the advice (a piece of code) that will be added to these methods.
@Override
public void transform(TypeTransformer typeTransformer) {
logger.info("TEST transform");
typeTransformer.applyAdviceToMethod(namedOneOf("countWords"),this.getClass().getName() + "$WordCountAdvice");
}
// The WordCountAdvice class contains the actual pieces of code (advices) that will be added to the instrumented methods.
@SuppressWarnings("unused")
public static class WordCountAdvice {
// This advice is added at the beginning of the instrumented method (OnMethodEnter).
// It creates and starts a new span, and makes it active.
@Advice.OnMethodEnter(suppress = Throwable.class)
public static Scope onEnter(@Advice.Argument(value = 0) String input, @Advice.Local("otelSpan") Span span) {
// Get a Tracer instance from OpenTelemetry.
Tracer tracer = GlobalOpenTelemetry.getTracer("instrumentation-library-name","semver:1.0.0");
System.out.print("Entering method");
// Start a new span with the name "mySpan".
span = tracer.spanBuilder("mySpan").startSpan();
// Make this new span the current active span.
Scope scope = span.makeCurrent();
// Return the Scope instance. This will be used in the exit advice to end the span's scope.
return scope;
}
// This advice is added at the end of the instrumented method (OnMethodExit).
// It first closes the span's scope, then checks if any exception was thrown during the method's execution.
// If an exception was thrown, it sets the span's status to ERROR and ends the span.
// If no exception was thrown, it sets a custom attribute "wordCount" on the span, and ends the span.
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
public static void onExit(@Advice.Return(readOnly = false) int wordCount,
@Advice.Thrown Throwable throwable,
@Advice.Local("otelSpan") Span span,
@Advice.Enter Scope scope) {
// Close the scope to end it.
scope.close();
// If an exception was thrown during the method's execution, set the span's status to ERROR.
if (throwable != null) {
span.setStatus(StatusCode.ERROR, "Exception thrown in method");
} else {
// If no exception was thrown, set a custom attribute "wordCount" on the span.
span.setAttribute("wordCount", wordCount);
}
// End the span. This makes it ready to be exported to the configured exporter (e.g. Elastic).
span.end();
}
}
}
我们的检测的目标类在 typeMatch 方法中定义,而我们要检测的方法在 transform 方法中定义。我们的目标是 Main 类和 countWords 方法。
如您所见,我们这里有一个内部类,它完成了定义 onEnter 和 onExit 方法的大部分工作,它告诉我们在进入 countWords 方法时该做什么以及在退出 countWords 方法时该做什么。
在 onEnter 方法中,我们设置一个新的 OpenTelemetry span,在 onExit 方法中,我们结束 span。如果方法成功结束,我们还会获取 wordcount 并将其附加到属性。
现在让我们看一下运行此程序时会发生什么。好消息是,我们通过提供一个 dockerfile 供您使用来完成所有工作,从而使它变得非常简单。
将所有内容组合在一起
如果您尚未这样做,请克隆 GitHub 存储库,在继续之前,让我们快速看一下我们正在使用的 dockerfile。
# Build stage
FROM maven:3.8.7-openjdk-18 as build
COPY simple-java /home/app/simple-java
COPY opentelemetry-custom-instrumentation /home/app/opentelemetry-custom-instrumentation
WORKDIR /home/app/simple-java
RUN mvn install
WORKDIR /home/app/opentelemetry-custom-instrumentation
RUN mvn install
# Package stage
FROM maven:3.8.7-openjdk-18
COPY /home/app/simple-java/target/simple-java-1.0-SNAPSHOT.jar /usr/local/lib/simple-java-1.0-SNAPSHOT.jar
COPY /home/app/opentelemetry-custom-instrumentation/target/opentelemetry-custom-instrumentation-1.0-SNAPSHOT.jar /usr/local/lib/opentelemetry-custom-instrumentation-1.0-SNAPSHOT.jar
WORKDIR /
RUN curl -L -o opentelemetry-javaagent.jar https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar
COPY start.sh /start.sh
RUN chmod +x /start.sh
ENTRYPOINT ["/start.sh"]
此 dockerfile 分两个部分工作:在 docker 构建过程中,我们首先从源代码构建简单的 java 应用程序,然后构建自定义检测。之后,我们下载最新的 OpenTelemetry Java Agent。在运行时,我们只需执行下面描述的 start.sh 文件
#!/bin/sh
java \
-javaagent:/opentelemetry-javaagent.jar \
-Dotel.exporter.otlp.endpoint=${SERVER_URL} \
-Dotel.exporter.otlp.headers="Authorization=Bearer ${SECRET_KEY}" \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-Dotel.resource.attributes=service.name=simple-java,service.version=1.0,deployment.environment=production \
-Dotel.service.name=your-service-name \
-Dotel.javaagent.extensions=/usr/local/lib/opentelemetry-custom-instrumentation-1.0-SNAPSHOT.jar \
-Dotel.javaagent.debug=true \
-jar /usr/local/lib/simple-java-1.0-SNAPSHOT.jar
此脚本有两点需要注意:第一点是,我们将 javaagent 参数设置为 opentelemetry-javaagent.jar — 这将启动 OpenTelemetry javaagent 运行,它会在执行任何代码之前启动。
在此 jar 内部必须有一个带有 premain 方法的类,JVM 将查找该方法。这将引导 java 代理。如上所述,任何已编译的字节码本质上都通过 javaagent 代码进行过滤,因此它可以在执行之前修改该类。
这里的第二个重要事项是 javaagent.extensions 的配置,它会加载我们构建的扩展,以便为我们简单的 java 应用程序添加检测。
现在运行以下命令
docker build -t djhope99/custom-otel-instrumentation:1 .
docker run -it -e 'SERVER_URL=XXX' -e 'SECRET_KEY=XX djhope99/custom-otel-instrumentation:1
如果您在此处使用您之前获得的 SERVER_URL 和 SECRET_KEY,您应该会看到它连接到 Elastic。
启动时,它会要求您输入一个句子,输入几个句子,然后按 Enter。执行几次此操作 — 这里有一个睡眠,以便强制执行长时间运行的事务
最终您会在服务地图中看到该服务
跟踪将会出现
在 span 中,您会看到我们收集的 wordcount 属性
这可以用于进一步的仪表板和 AI/ML,包括异常检测(如果需要),这很容易做到,如下所示。
首先单击左侧的汉堡图标,然后选择 仪表板 以创建新仪表板
在这里,单击 创建可视化。
在 APM 索引中搜索 wordcount 标签,如下所示
如您所见,因为我们在 Span 代码中创建了此属性,如下所示,wordCount 的类型为“Integer”,所以我们能够自动将其指定为 Elastic 中的数字字段
span.setAttribute("wordCount", wordCount);
从这里,我们可以将其拖放到可视化中,以便在我们的仪表板上显示!非常容易。
总结
此博客阐明了 OpenTelemetry Java Agent 在填补可见性差距和获取关键业务监控数据方面的宝贵作用,尤其是在无法访问源代码的情况下。
该博客揭示了对 Java Agent、Bytecode 和 Byte Buddy 的基本理解,然后全面检查了使用 Byte Buddy 进行的自动检测过程。
通过一个简单的 Java 应用程序演示了使用扩展框架的 OpenTelemetry Java Agent 的实现,这突显了该代理将跟踪代码注入应用程序以促进监控的能力。
它详细介绍了如何配置代理并集成 OpenTelemetry 扩展,并概述了示例应用程序的操作,以帮助用户理解所讨论信息的实际应用。这篇有指导意义的博客文章是 SRE 和 IT 运营人员寻求使用 OpenTelemetry 的自动检测功能优化其应用程序工作的绝佳资源。
还没有 Elastic Cloud 帐户?注册 Elastic Cloud。
本文中描述的任何特性或功能的发布和时间安排均由 Elastic 自行决定。目前无法提供的任何特性或功能可能无法按时交付或根本不交付。