作者:来自 Elastic Sylvain Juge
一个 JAR 文件,在 OpenTelemetry Java agent 启动时加载,就可以过滤健康检查、重命名 span、添加资源属性以及控制采样,而无需修改应用程序代码。
你刚刚为一个 Java 应用设置了自动插桩。无需修改任何代码,追踪数据就开始流向你的可观测性平台。几分钟后,你发现健康检查端点正在大量充斥你的追踪视图,而且事务名称反映的是通用的框架模式,而不是你的业务领域操作。
分叉 agent 可以解决这个问题,但这样一来,你就需要负责处理每一次上游合并。你也可以使用手动插桩来获得完全的控制权,但这需要修改代码,并且需要持续维护。OpenTelemetry Java 扩展提供了一条更简洁的路径:使用一个独立的 JAR 文件,由 agent 在启动时加载,让你能够精确控制哪些内容被捕获和导出,而无需修改 agent 或应用程序代码。
例如,以下挑战非常常见:
- 健康检查探针正在大量充斥你的追踪视图。
- span 名称反映的是通用的框架模式,而不是你的业务领域操作。
- 某些 span 名称或属性具有高基数,在追踪数据中造成噪声。
- span 缺少与你的业务逻辑相关的属性。
- Baggage 标头正在传播到不应该接收它们的下游服务。
- 描述你的部署环境的资源属性无法被自动捕获,因为它们依赖于自定义环境变量。
其中一些问题可以通过配置来解决,或者使用中间的 OpenTelemetry Collector 进行处理。不过,这也可能增加遥测数据管道的复杂性,而你可能更希望在数据被捕获的源头解决这些问题。
什么是 OpenTelemetry Java 扩展
扩展是一个 JAR 文件,由 agent 在启动时加载。它通过 Java 的服务提供者接口(SPI)机制接入 agent 的扩展点,这也是 agent 内部使用的相同机制。
该扩展机制在上游 OpenTelemetry Java agent 和Elastic 的 OpenTelemetry 发行版中运行方式完全相同。你只需编写一次扩展,它就可以与两者配合使用。
作为参考,上游扩展文档提供了扩展点的完整概览以及一些示例。
本文并不旨在提供完整的参考资料,而是重点介绍一些你在生产环境中可能经常需要使用的简单场景:重命名 span、过滤嘈杂的追踪数据,或者传播 agent 在你的环境中未覆盖的上下文。
扩展还允许你修改和扩展 agent 自身的插桩功能。这超出了本文的讨论范围。以下是两个入门方向:
设置 OpenTelemetry Java 扩展项目
扩展是一个标准的 Java Gradle 项目,但有两个要求:输出必须是 shadow JAR(一个包含所有扩展依赖的 fat JAR),并且 OpenTelemetry 依赖必须声明为 compileOnly,这样就不会将 SDK 本身打包进去。
之所以要求使用 shadow JAR,是因为 agent 会通过自己的类加载器加载扩展。如果你将某个依赖声明为 implementation,它就会被打包进去,并可能与 agent 中已经存在的版本发生冲突。使用 compileOnly 可以让这些 JAR 完全不会被包含在扩展 JAR 中。
下面是一个最小的 build.gradle.kts 示例,用于创建一个不自定义插桩、因此仅依赖 OpenTelemetry SDK/API 的简单扩展。
1`1. plugins { 22. id("java") 33. id("com.gradleup.shadow") 44. } 5 66. repositories { 77. mavenCentral() 88. } 9 1010. java { 1111. toolchain { 1212. languageVersion.set(JavaLanguageVersion.of(8)) 1313. } 1414. } 15 1616. dependencies { 1717. // Use BOM to manage OpenTelemetry dependency versions 1818. compileOnly(platform("io.opentelemetry:opentelemetry-bom:1.64.0")) 1919. // OpenTelemetry SDK autoconfiguration SPI (provided by agent) 2020. compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") 2121. // OpenTelemetry SDK 2222. compileOnly("io.opentelemetry:opentelemetry-sdk") 2323. // Annotation processor for automatic SPI registration 2424. compileOnly("com.google.auto.service:auto-service:1.1.1") 2525. annotationProcessor("com.google.auto.service:auto-service:1.1.1") 2626. } 27 2828. tasks.assemble { 2929. dependsOn(tasks.shadowJar) 3030. }` AI写代码 31
开始之前,请先在 Maven Central 查看最新版本的 BOM。
扩展只在编译时依赖 OpenTelemetry SDK 和自动配置 SPI。agent 会在运行时提供其余的 SDK 和插桩实现。
在运行时加载 OpenTelemetry Java 扩展
要在运行时加载 OpenTelemetry Java 扩展,可以使用 otel.javaagent.extensions 系统属性或 OTEL_JAVAAGENT_EXTENSIONS 环境变量。该值是一个由逗号分隔的扩展 JAR 路径列表:
1`java -Dotel.javaagent.extensions=/path/to/my-extension.jar -javaagent:/path/to/opentelemetry-javaagent.jar -jar myapp.jar`AI写代码 2
上游 OpenTelemetry Java agent 还允许你直接将扩展嵌入 agent JAR,从而简化部署。
使用 OpenTelemetry Java 扩展过滤和重命名 span
你可以通过两种方式修改 span:
- 使用
SpanProcessor,它会在 span 开始或结束时同步调用。 - 使用
SpanExporter,它会在 span 导出时异步调用。
使用 SpanProcessor 重命名 span
SpanProcessor.onStart 会接收一个 ReadWriteSpan,这意味着你可以在 span 被导出之前调用 span.updateName()。如果需要根据 span 开始时就可用的属性进行重命名,这是正确的扩展点。
1` 2 31. public class OperationRenamingSpanProcessor implements SpanProcessor { 4 53. @Override 64. public void onStart(Context parentContext, ReadWriteSpan span) { 75. String operation = span.getAttribute(AttributeKey.stringKey("app.operation")); 86. if (operation != null) { 97. span.updateName(operation); 108. } 119. } 12 1311. @Override 1412. public boolean isStartRequired() { return true; } 15 1614. @Override 1715. public void onEnd(ReadableSpan span) {} 18 1917. @Override 2018. public boolean isEndRequired() { return false; } 21 2220. @Override 2321. public CompletableResultCode shutdown() { return CompletableResultCode.ofSuccess(); } 24 2523. @Override 2624. public CompletableResultCode forceFlush() { return CompletableResultCode.ofSuccess(); } 2725. } 28 29`AI写代码 30
通过 AutoConfigurationCustomizerProvider 注册 SpanProcessor,并将其与已经配置的 processor 组合:
svg
1` 2 31. @AutoService(AutoConfigurationCustomizerProvider.class) 42. public class RenamingCustomizerProvider implements AutoConfigurationCustomizerProvider { 5 64. @Override 75. public void customize(AutoConfigurationCustomizer customizer) { 86. customizer.addTracerProviderCustomizer(this::configureSdkTracerProvider); 97. } 10 119. private SdkTracerProviderBuilder configureSdkTracerProvider( 1210. SdkTracerProviderBuilder tracerProvider, ConfigProperties config) { 1311. return tracerProvider.addSpanProcessor(new OperationRenamingSpanProcessor()); 1412. } 15 1614. } 17 18`AI写代码 19
modify-span EDOT Java 扩展示例提供了完整的实现。
使用 SpanExporter 过滤 span
SpanExporter 包装器允许你在 span 离开进程之前修改或丢弃它们。这对于健康检查等已知的高噪声端点非常有效。
svg
1` 2 31. public class FilteringSpanExporter implements SpanExporter { 4 53. private final SpanExporter delegate; 6 75. public FilteringSpanExporter(SpanExporter delegate) { 86. this.delegate = delegate; 97. } 10 119. @Override 1210. public CompletableResultCode export(Collection<SpanData> spans) { 1311. List<SpanData> filtered = new ArrayList<>(); 1412. for (SpanData span : spans) { 1513. if (!"GET /health".equals(span.getName())) { 1614. filtered.add(span); 1715. } 1816. } 1917. return delegate.export(filtered); 2018. } 21 2220. @Override 2321. public CompletableResultCode flush() { return delegate.flush(); } 24 2523. @Override 2624. public CompletableResultCode shutdown() { return delegate.shutdown(); } 2725. } 28 29`AI写代码 30
通过 addSpanExporterCustomizer 注册 FilteringSpanExporter:
svg
1`customizer.addSpanExporterCustomizer((existing, config) -> new FilteringSpanExporter(existing));`AI写代码 2
modify-span EDOT Java 扩展示例提供了完整的实现。
这种方式有两个限制:
- 它不会丢弃可能已经创建的任何子 span,例如健康检查调用数据库时产生的子 span。
- 在 exporter 中过滤的 span 已经经过完整的 processor 管道,并占用了 batch processor 中的缓冲区空间。
如果你在这一阶段丢弃大量流量,自定义 Sampler(如下所示)会更加高效,因为它会在任何处理发生之前丢弃 span,同时也会过滤掉子 span。此外,在使用声明式配置时,基于规则的 sampler 允许你仅使用配置,就可以通过规则实现过滤。
使用 ResourceProvider 添加自定义资源属性
资源属性描述正在运行的内容:服务名称、版本和主机。ResourceProvider 允许你添加 agent 不知道的其他属性,例如你的平台通过环境变量注入的部署元数据。
下面的示例使用环境变量,但也可以使用配置文件、云元数据服务或 agent 在启动时能够访问的任何其他来源。
由于 SDK 初始化是同步的,因此在查询元数据端点等外部服务时,可能会导致 agent(以及应用程序)的启动速度变慢。如果可能,建议先检查环境变量和本地配置,然后再调用外部服务。
1` 2 31. @AutoService(ResourceProvider.class) 42. public class DeploymentResourceProvider implements ResourceProvider { 5 64. @Override 75. public Resource createResource(ConfigProperties config) { 86. AttributesBuilder attributes = Attributes.builder(); 9 108. String region = System.getenv("DEPLOY_REGION"); 119. if (region != null) { 1210. attributes.put(AttributeKey.stringKey("deployment.region"), region); 1311. } 14 1513. String buildVersion = System.getenv("BUILD_VERSION"); 1614. if (buildVersion != null) { 1715. attributes.put(AttributeKey.stringKey("build.version"), buildVersion); 1816. } 19 2018. return Resource.create(attributes.build()); 2119. } 2220. } 23 24`AI写代码 25
ResourceProvider 中的属性会与 agent 自身的资源合并。当两个 provider 提供相同的键时,具有更高 order() 值的 provider 获胜。agent 内置的 provider 使用 order 0,因此将 order() 重写为返回正整数,可以让你的 provider 获得更高优先级。
resource-attribute EDOT Java 扩展示例提供了完整的实现。
OpenTelemetry Java 中的自定义采样
当在 exporter 中进行过滤已经太晚或成本太高时,可以直接实现 Sampler。sampler 会在任何 span 处理之前运行,因此被丢弃的 span 不会接触 batch 缓冲区。
不过,采样决策只能依赖 span 开始时提供的属性。例如,HTTP 响应的状态码不能用于采样决策,因为它只有在 span 结束时才可用。
关键细节是:包装现有的 sampler,而不是替换它。这样,你的逻辑就可以与已有配置组合,同时仍然遵循上游服务传递的基于父级的决策。
1` 2 31. public class HealthCheckSampler implements Sampler { 4 53. private final Sampler delegate; 6 75. public HealthCheckSampler(Sampler delegate) { 86. this.delegate = delegate; 97. } 10 119. @Override 1210. public SamplingResult shouldSample( 1311. Context parentContext, 1412. String traceId, 1513. String name, 1614. SpanKind spanKind, 1715. Attributes attributes, 1816. List<LinkData> parentLinks) { 1917. if (spanKind == SpanKind.SERVER && name.contains("health")) { 2018. return SamplingResult.create(SamplingDecision.DROP); 2119. } 2220. return delegate.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); 2321. } 24 2523. @Override 2624. public String getDescription() { 2725. return "HealthCheckSampler{" + delegate.getDescription() + "}"; 2826. } 2927. } 30 31`AI写代码 32
通过 addSamplerCustomizer 注册 HealthCheckSampler,它会同时提供现有的 sampler 和解析后的配置:
svg
1`customizer.addSamplerCustomizer((existing, config) -> new HealthCheckSampler(existing));`AI写代码 2
opentelemetry-java-contrib 中的社区扩展
opentelemetry-java-contrib代码仓库包含多个由社区维护的扩展。
其中一些已经包含在 OpenTelemetry Java agent 中(并继承到 Elastic 发行版中),但默认选择不启用:
Elastic 发行版的大多数功能都以扩展的形式存在于 contrib 代码仓库中,因此你可以以与供应商无关的方式,将它们与上游 agent 一起使用。
进一步阅读和扩展示例
上游扩展示例涵盖了本文未展示的其他扩展点,包括自定义 propagator、ID 生成器和被忽略类型的配置器。
Elastic baggage 示例展示了 baggage 的过滤 propagator 如何在一个包含两个服务的应用中端到端运行,同时还展示了如何在不修改应用程序代码的情况下,通过自定义插桩来添加 baggage。
本文介绍了项目设置以及生产环境中最可能遇到的模式。上面的两个链接可以帮助你进一步深入了解:上游示例增加了本文未涵盖的扩展点,而 baggage 示例则展示了一个完整的双服务实现,你可以在本地运行它。
原文:OpenTelemetry Java extensions: skip the agent fork — Elastic Observability Labs