OpenTelemetry

OpenTelemetry 是一个开源项目,旨在通过统一的工具集、API和SDK,简化在多样化的技术栈中集成可观测性功能,确保一致地收集、处理及导出应用性能数据。

opentelemetry-java-instrumentation项目介绍

陈陈

该项目是一个基于Java的agent与字节码增强技术构建的JAR包,可以attach到任何 Java 8 及以上版本的应用程序,并动态注入字节码,以实现用户不用改动代码就可以捕获不同框架的运行时信息以生成符合OpenTelemetry语意规范的Trace、Metrics、Logs数据。同时你也可以以多种格式导出这些数据,并发送到不同的后端,比如Opentelemetry Collector、Zipkin、Jaeger等等。

JavaAgent技术介绍

Java Agent技术是Java生态系统中一项强大的技术,它允许我们在JVM启动时加载自定义的代码,并在运行时修改字节码,从而实现对Java应用程序的监控、调试、性能分析等功能,而无需修改应用程序本身的代码。Java Agent技术起源于Java Platform Debugger Architecture (JPDA) 和 Java Virtual Machine Tool Interface (JVMTI) 规范。JPDA 提供了一套用于调试 Java 应用程序的标准 API,而 JVMTI 则允许开发者使用 C/C++ 编写与 JVM 交互的代理程序。

Java Agent 技术本质上是对 JVMTI 的封装,它提供了一种更加便捷的方式来编写和部署 Java 代理程序,开发者可以使用 Java 语言编写 Agent 代码,并将其打包成 JAR 文件,方便地加载到 JVM 中。

Java Agent 的工作原理可以概括为以下几个步骤:

  1. 启动阶段加载: 在 JVM 启动时,通过命令行参数 -javaagent 指定 Agent JAR 文件的位置。

  2. Agent 初始化: JVM 会加载 Agent JAR 文件,并调用其中的 premain 方法。开发者可以在 premain 方法中进行 Agent 的初始化工作,例如:注册 Instrumentation API、启动监控线程等。

  3. 字节码修改: Java Agent 利用 Instrumentation API 提供的 retransformClasses 等方法,在运行时修改类的字节码。开发者可以定义自己的字节码转换器,实现对特定方法的拦截、注入代码等操作。鉴于直接修改字节码成本较高,一般会用到一些流行的字节码修改库来进行字节码修改,常见的比如ASM、Javassist、bytebuddy等等。

  4. 运行时监控: Agent 代码在 JVM 中与应用程序一起运行,可以实时监控应用程序的运行状态,例如:方法调用次数、执行时间、线程状态等。

opentelemetry-java-instrumentation项目介绍

opentelemetry-java-instrumentation项目整体包含的内容较多,除了核心的可观测能力部分代码,还包含测试、自定义gradle插件等代码,下面仅介绍项目核心部分代码,其余代码在后续文章中介绍

opentelemetry-java-instrumentation各module作用分析

  • instrumentation-api: 主要包含一些公共的工具类、接口以及OpenTelemetry的一些语意规范的实现,该module下的类会被BootstapClassLoader加载
  • instrumentation-api-incubator:主要包含一些在孵化中的语意规范,稳定后会挪到instrumentation-api这个module下,该module下的类也会被BootstapClassLoader加载
  • javaagent-extension-api:javaagent使用的一些公共的工具类、接口,该module下的类也会被BootstapClassLoader加载
  • javaagent:该module不包含实际代码,仅用于打包
  • javaagent-bootstrap:Agent的实际启动类,该module下的类会被BootstapClassLoader加载,但是类io.opentelemetry.javaagent.OpenTelemetryAgent会由SystemClassLoader加载
  • javaagent-tooling:探针核心实现部分,在该module可以引入一些三方类,且该部分类会单独由AgentClassLoader加载
  • javaagent-internal-logging-application:用于将探针日志重定向到用户应用的日志框架中
  • javaagent-internal-logging-simple:用于将探针日志打印到标准输出或者标准错误输出
  • instrumentation:这个module下的子module分别包含对不同框架的增强实现
  • opentelemetry-api-shaded-for-instrmenting:对opentelemetry-api相关类的shade以避免和用户依赖的opentelemetry-api出现类冲突
  • opentelemetry-ext-annotation-shaded-for-instrmenting:对opentelemetry-extension-annotation相关类的shade以避免和用户依赖的opentelemetry-extension-annotation出现类冲突
  • opentelemetry-instrumentation-api-shaded-for-instrmenting:对opentelemetry-instrumentation-api相关类的shade以避免和用户依赖的opentelemetry-instrumentation-api出现类冲突
  • opentelemetry-instrumentation-annotation-shaded-for-instrmenting: 对opentelemetry-instrumentation-annotation相关类的shade以避免和用户依赖的opentelemetry-instrumentation-annotation出现类冲突

opentelemetry-java-instrumentation 启动流程一览

注意:由于社区活跃,代码变动较大,下述分析基于 v1.28.0 版本的 TAG 进行。

探针的启动类是io.opentelemetry.javaagent.OpenTelemetryAgent,其核心逻辑较为简单,如下图所示

不管是agentmain还是premain进来,最终都会调用starAgent方法。在starAgent方法中

  • 14行:获取当前探针jar包,并将探针jar包append到BootstrapClassLoader中。这一步也就完成了上一章节介绍的探针项目中部分类会由BootstapClassLoader加载
  • 15行:保存一下Instrumentation�的实例,方便其他地方使用
  • 16行:保存一下探针JAR文件实例,方便其他地方使用
  • 17行:执行探针初始化逻辑
public final class OpenTelemetryAgent {
public static void premain(String agentArgs, Instrumentation inst) {
startAgent(inst, true);
}
public static void agentmain(String agentArgs, Instrumentation inst) {
startAgent(inst, false);
}
private static void startAgent(Instrumentation inst, boolean fromPremain) {
try {
File javaagentFile = installBootstrapJar(inst);
InstrumentationHolder.setInstrumentation(inst);
JavaagentFileHolder.setJavaagentFile(javaagentFile);
AgentInitializer.initialize(inst, javaagentFile, fromPremain);
} catch (Throwable ex) {
// Don't rethrow. We don't have a log manager here, so just print.
System.err.println("ERROR " + OpenTelemetryAgent.class.getName());
ex.printStackTrace();
}
}
...
}

探针后续的初始化进入到 io.opentelemetry.javaagent.bootstrap.AgentInitializer#initialize方法中,该方法也较为简单,如下图所示,一些校验判断代码忽略,下面分析一下核心代码

  • 27行:创建探针自己的类加载器,其有两个入参数,第一个入参是”inst”,这里打个哑谜,先不解释。第二个入参则是探针jar包,这个很好理解,里面包含了探针类加载器需要加载的探针实现类。
  • 28行:使用上一步创建的探针类加载器,加载io.opentelemetry.javaagent.tooling.AgentStarterImpl�这个类,传入的后两个参数是io.opentelemetry.javaagent.tooling.AgentStarterImpl�的构造方法所需要,用这两个参数构建AgentStarterImpl实例
  • 30行:调用51行创建的AgentStarterImpl实例的start方法,执行探针启动逻辑
public final class AgentInitializer {
@Nullable private static ClassLoader agentClassLoader = null;
@Nullable private static AgentStarter agentStarter = null;
private static boolean isSecurityManagerSupportEnabled = false;
public static void initialize(Instrumentation inst, File javaagentFile, boolean fromPremain)
throws Exception {
if (agentClassLoader != null) {
return;
}
// we expect that at this point agent jar has been appended to boot class path and all agent
// classes are loaded in boot loader
if (AgentInitializer.class.getClassLoader() != null) {
throw new IllegalStateException("agent initializer should be loaded in boot loader");
}
isSecurityManagerSupportEnabled = isSecurityManagerSupportEnabled();
// this call deliberately uses anonymous class instead of lambda because using lambdas too
// early on early jdk8 (see isEarlyOracle18 method) causes jvm to crash. See CrashEarlyJdk8Test.
execute(
new PrivilegedExceptionAction<Void>() {
@Override
public Void run() throws Exception {
agentClassLoader = createAgentClassLoader("inst", javaagentFile);
agentStarter = createAgentStarter(agentClassLoader, inst, javaagentFile);
if (!fromPremain || !delayAgentStart()) {
agentStarter.start();
}
return null;
}
});
}
...
}

探针后续启动进入到io.opentelemetry.javaagent.tooling.AgentStarterImpl#start方法中,该方法也较为清晰,如下图所示:

  • 3行: 这个是探针的扩展机制,可以加载一些外部的探针扩展,这个时候,会创建一个组合ClassLoader,即能加载探针类,也能加载这些外部拓展类,但是一般不会用到。
  • 11-28行:主要执行探针日志的一些初始化工作,确定探针日志是输出到标准输出还是应用的logger中
  • 30行:探针核心埋点安装逻辑,会加载探针的所有InstrumentationModule并初始化
public void start() {
EarlyInitAgentConfig earlyConfig = EarlyInitAgentConfig.create();
extensionClassLoader = createExtensionClassLoader(getClass().getClassLoader(), earlyConfig);
String loggerImplementationName = earlyConfig.getString("otel.javaagent.logging");
// default to the built-in stderr slf4j-simple logger
if (loggerImplementationName == null) {
loggerImplementationName = "simple";
}
LoggingCustomizer loggingCustomizer = null;
for (LoggingCustomizer customizer :
ServiceLoader.load(LoggingCustomizer.class, extensionClassLoader)) {
if (customizer.name().equalsIgnoreCase(loggerImplementationName)) {
loggingCustomizer = customizer;
break;
}
}
// unsupported logger implementation; defaulting to noop
if (loggingCustomizer == null) {
logUnrecognizedLoggerImplWarning(loggerImplementationName);
loggingCustomizer = new NoopLoggingCustomizer();
}
Throwable startupError = null;
try {
loggingCustomizer.init(earlyConfig);
earlyConfig.logEarlyConfigErrorsIfAny();
AgentInstaller.installBytebuddyAgent(instrumentation, extensionClassLoader, earlyConfig);
WeakConcurrentMapCleaner.start();
// LazyStorage reads system properties. Initialize it here where we have permissions to avoid
// failing permission checks when it is initialized from user code.
if (System.getSecurityManager() != null) {
Context.current();
}
} catch (Throwable t) {
// this is logged below and not rethrown to avoid logging it twice
startupError = t;
}
if (startupError == null) {
loggingCustomizer.onStartupSuccess();
} else {
loggingCustomizer.onStartupFailure(startupError);
}
}

探针后续进入到埋点安装代码io.opentelemetry.javaagent.tooling.AgentInstaller#installBytebuddyAgent(java.lang.instrument.Instrumentation, java.lang.ClassLoader, io.opentelemetry.javaagent.tooling.config.EarlyInitAgentConfig)中,其主要逻辑如下图所示,除去一些判断代码,主要是在

  • 14行:SPI方式加载一些io.opentelemetry.javaagent.extension.AgentListener,会在探针安装的不同阶段执行这些hook函数
  • 15行:具体执行埋点安装逻辑
public static void installBytebuddyAgent(
Instrumentation inst, ClassLoader extensionClassLoader, EarlyInitAgentConfig earlyConfig) {
addByteBuddyRawSetting();
Integer strictContextStressorMillis = Integer.getInteger(STRICT_CONTEXT_STRESSOR_MILLIS);
if (strictContextStressorMillis != null) {
io.opentelemetry.context.ContextStorage.addWrapper(
storage -> new StrictContextStressor(storage, strictContextStressorMillis));
}
logVersionInfo();
if (earlyConfig.getBoolean(JAVAAGENT_ENABLED_CONFIG, true)) {
setupUnsafe(inst);
List<AgentListener> agentListeners = loadOrdered(AgentListener.class, extensionClassLoader);
installBytebuddyAgent(inst, extensionClassLoader, agentListeners);
} else {
logger.fine("Tracing is disabled, not installing instrumentations.");
}
}

探针后续进入到埋点具体安装代码,代码如下图所示

  • 6-15: 初始化参数并构建OpenTelemetrySDK
  • 23-26:触发hook函数执行
  • 28-44: 初始化BytebudyAgentBuilder实例
  • 61-82: 加载所有 InstrumentationModule,并根据每个module中定义的逻辑来动态配置BytebudyAgentBuilder实例,指定要针对哪些类执行哪些增强逻辑。
  • 85: 安装bytebuddy agent
private static void installBytebuddyAgent(
Instrumentation inst,
ClassLoader extensionClassLoader,
Iterable<AgentListener> agentListeners) {
WeakRefAsyncOperationEndStrategies.initialize();
EmbeddedInstrumentationProperties.setPropertiesLoader(extensionClassLoader);
setDefineClassHandler();
// If noop OpenTelemetry is enabled, autoConfiguredSdk will be null and AgentListeners are not
// called
AutoConfiguredOpenTelemetrySdk autoConfiguredSdk =
installOpenTelemetrySdk(extensionClassLoader);
ConfigProperties sdkConfig = AutoConfigureUtil.getConfig(autoConfiguredSdk);
InstrumentationConfig.internalInitializeConfig(new ConfigPropertiesBridge(sdkConfig));
copyNecessaryConfigToSystemProperties(sdkConfig);
setBootstrapPackages(sdkConfig, extensionClassLoader);
for (BeforeAgentListener agentListener :
loadOrdered(BeforeAgentListener.class, extensionClassLoader)) {
agentListener.beforeAgent(autoConfiguredSdk);
}
AgentBuilder agentBuilder =
new AgentBuilder.Default(
// default method graph compiler inspects the class hierarchy, we don't need it, so
// we use a simpler and faster strategy instead
new ByteBuddy()
.with(MethodGraph.Compiler.ForDeclaredMethods.INSTANCE)
.with(VisibilityBridgeStrategy.Default.NEVER)
.with(InstrumentedType.Factory.Default.FROZEN))
.with(AgentBuilder.TypeStrategy.Default.DECORATE)
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.with(new RedefinitionDiscoveryStrategy())
.with(AgentBuilder.DescriptionStrategy.Default.POOL_ONLY)
.with(AgentTooling.poolStrategy())
.with(new ClassLoadListener())
.with(AgentTooling.transformListener())
.with(AgentTooling.locationStrategy());
if (JavaModule.isSupported()) {
agentBuilder = agentBuilder.with(new ExposeAgentBootstrapListener(inst));
}
agentBuilder = configureIgnoredTypes(sdkConfig, extensionClassLoader, agentBuilder);
if (AgentConfig.isDebugModeEnabled(sdkConfig)) {
agentBuilder =
agentBuilder
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.with(new RedefinitionDiscoveryStrategy())
.with(new RedefinitionLoggingListener())
.with(new TransformLoggingListener());
}
int numberOfLoadedExtensions = 0;
for (AgentExtension agentExtension : loadOrdered(AgentExtension.class, extensionClassLoader)) {
if (logger.isLoggable(FINE)) {
logger.log(
FINE,
"Loading extension {0} [class {1}]",
new Object[] {agentExtension.extensionName(), agentExtension.getClass().getName()});
}
try {
agentBuilder = agentExtension.extend(agentBuilder, sdkConfig);
numberOfLoadedExtensions++;
} catch (Exception | LinkageError e) {
logger.log(
SEVERE,
"Unable to load extension "
+ agentExtension.extensionName()
+ " [class "
+ agentExtension.getClass().getName()
+ "]",
e);
}
}
logger.log(FINE, "Installed {0} extension(s)", numberOfLoadedExtensions);
agentBuilder = AgentBuilderUtil.optimize(agentBuilder);
ResettableClassFileTransformer resettableClassFileTransformer = agentBuilder.installOn(inst);
ClassFileTransformerHolder.setClassFileTransformer(resettableClassFileTransformer);
addHttpServerResponseCustomizers(extensionClassLoader);
runAfterAgentListeners(agentListeners, autoConfiguredSdk);
}

自此整个探针的启动就完成了,后续就会执行用户主程序,并且当用户的一些特定框架类加载时,就会被拦截并增强,插入埋点逻辑生成span和指标数据。

opentelemetry-java-instrumentation jar包结构分析

对opentelemetry最终打出来的jar包做解压后分析其目录结构对于了解该agent的启动以及类加载十分有用。下面是opentelemetry-java-instrumentation解压后的目录分析,第一级目录包含三个文件夹

inst: 里面包含了探针javaagent-tooling、三方依赖以及所有的instrumentation类,这里面的类有两个特点

  • 类路径不是标准路径,即都在inst目录下,比如io.opentelemetry.OpentelemetrySdk的类文件的标准相对路径应该是./io/opentelemetry/OpentelemetrySdk.class。但是在这里是.inst/io/opentelemetry/OpentelemetrySdk.class
  • 其次正常的类文件是以.class结尾,但是这里.classdata结尾

基于这两个特点,要加载inst中的类,类加载器要做两个事情,

  • 加载一个类的时候,找类文件的路径要加一个统一的前缀inst,这也就是前面启动分析过程中说到的创建探针类加载器时,为什么要传入一个”inst”字符串。
  • 不应该需要.class结尾的文件,而要寻找.classdata结尾的文件。

io: 里面包含了探针shade了之后的opentelemetry-api、opentelemetry-instrumentation-api等类以及javaagent-bootstap中的类,这些类由于类路径和实际路径是一样的,所以会直接由BootstapClassLoader加载

META-INF: 一些元信息,其中比较重要的是在MANIFEST.MF文件中说明了探针的启动类。

总结

上面也就完成了opentelemetry-java-instrumentation项目的一个简单介绍,opentelemetry-java-instrumentation项目还应用了很多有趣的机制来解决字节码增强过程中的一些通用问题,比如muzzle check、VirtualFied等等,后续也会有专门的文档对这些技术进行详细的解读


observability.cn Authors 2024 | Documentation Distributed under CC-BY-4.0
Copyright © 2017-2024, Alibaba. All rights reserved. Alibaba has registered trademarks and uses trademarks.
浙ICP备2021005855号-32