领先一步
VMware 提供培训和认证,助您加速进步。
了解更多这是 Road to GA 系列中的一篇新博客文章,这次我们将探讨 Spring Boot 中的 OpenTelemetry。
在现代云原生架构中,可观测性不再是可选项;它是一项基本要求。你希望通过指标了解应用程序正在做什么,通过跟踪了解请求如何流经应用程序,以及通过日志了解应用程序正在说什么。
OpenTelemetry 项目(有时缩写为 OTel)提供了一个供应商中立的开源框架,用于收集、处理和导出遥测数据。它由 云原生计算基金会 提供支持,提供 API、SDK、用于导出数据的标准网络协议 OTLP,以及用于处理数据摄取、处理和导出到后端的可插拔架构(包括 OpenTelemetry Collector)。
经过检测的项目和库使用 API 发送可观测性数据。实现该 API 的 SDK 用于配置数据如何收集和导出。Collector 是一个可选组件,可以帮助聚合和筛选数据,但您也可以将数据直接发送到任何兼容的后端。
Spring 生态系统通过 Micrometer 提供了强大的可观测性支持,将 Spring Boot 与 OpenTelemetry 结合使用是涵盖所有可观测性信号(指标、追踪和日志)的强大方式。这里的关键支持因素是 OTLP 协议,而不是特定的库。
在这篇文章中,我们将详细介绍 OpenTelemetry 的含义,并了解如何将其与 Spring Boot 集成,以完美覆盖可观测性需求。
当您选择将 OpenTelemetry 与 Spring Boot 集成时,有几种替代路径,从“直接引入运行时代理”到“使用内置 Spring 支持”。您有三种选择:
入门很简单:您在启动时通过 -javaagent 标志附加 opentelemetry-javaagent.jar。此代理对库(HTTP、JDBC、Spring 等)进行字节码检测。这是最简单的“零代码更改”路径。代理会识别追踪、跨度(跨度是追踪的原子部分)、指标等。
这种方法的主要问题是 Java 代理必须与您的库版本紧密匹配,因为代理会修改它们的字节码。如果代理经过测试的版本与您使用的版本不匹配,问题可能很难诊断。此外,如果您正在使用 GraalVM 的 native-image 或想使用 Java 的 AOT 缓存,您必须经历额外的步骤。另外,如果您已经在使用代理,它们可能会发生冲突。
OpenTelemetry 有自己的 Spring Boot starter,它检测某些技术。然而,他们指出 OpenTelemetry Java 代理是他们默认的检测选择,并且只有在代理无法使用时才应使用 starter。此外,尽管starter 本身被标记为稳定,但它引入的依赖项带有 alpha 后缀。
随着 Spring Boot 4.0 的发布,我们正在引入一个新的 OpenTelemetry Spring Boot Starter。它被称为 spring-boot-starter-opentelemetry(我们对这些名称确实很聪明,不是吗),并且可以通过 start.spring.io 上的“OpenTelemetry”依赖项进行选择。
我们可能有些偏见,但这是我们在 Spring Boot 应用程序中实现可观测性最喜欢的选项。
该 starter 包含 OpenTelemetry API 和通过 OTLP 导出 Micrometer 信号的组件。Micrometer Tracing 与 OpenTelemetry 桥接一起使用 OpenTelemetry API,以 OTLP 格式导出追踪。Micrometer OtlpMeterRegistry 用于通过 OTLP 协议将通过 Micrometer API 收集的指标发送到支持 OpenTelemetry 的指标后端。
重申一下:重要的是协议,而不是所使用的库。尽管 Spring 组合项目使用 Micrometer 作为其可观测性 API,但完全可以通过 OTLP 将信号导出到任何支持 OpenTelemetry 的后端,您很快就会看到。
Spring Boot 还支持通过 OTLP 将日志发送到支持 OpenTelemetry 的后端,但它不会在 Logback 和 Log4j2 中开箱即用安装日志附加器。这将来可能会改变,但即使现在,也很容易做到,我们也会在这篇博客文章中介绍设置。
这篇博客文章的其余部分将重点介绍如何使用 Spring 团队的新 OpenTelemetry Spring Boot Starter 在您的 Spring Boot 应用程序中获得无缝的 OpenTelemetry 体验。
如前所述,Spring Boot 使用 Micrometer OTLP 注册表通过 OTLP 将 Micrometer 指标导出到任何支持 OpenTelemetry 的后端。所需的依赖项 io.micrometer:micrometer-registry-otlp 已包含在 spring-boot-starter-opentelemetry 中。有了这个依赖项,Micrometer 将以 OTLP 格式将指标导出到后端 https://:4318/v1/metrics。要自定义指标导出位置,请设置 management.otlp.metrics.export.url 属性,例如:
management.otlp.metrics.export.url=http://otlp.example.com:4318/v1/metrics
Micrometer 团队还为 OpenTelemetry 添加了所谓的观察约定。OpenTelemetry 中的信号遵循 OpenTelemetry 语义约定,Micrometer 中的观察约定实现了 OpenTelemetry 语义约定的稳定部分。要在 Spring Boot 中使用它们,您必须定义一些配置(这将来可能会改进)
@Configuration(proxyBeanMethods = false)
public class OpenTelemetryConfiguration {
@Bean
OpenTelemetryServerRequestObservationConvention openTelemetryServerRequestObservationConvention() {
return new OpenTelemetryServerRequestObservationConvention();
}
@Bean
OpenTelemetryJvmCpuMeterConventions openTelemetryJvmCpuMeterConventions() {
return new OpenTelemetryJvmCpuMeterConventions(Tags.empty());
}
@Bean
ProcessorMetrics processorMetrics() {
return new ProcessorMetrics(List.of(), new OpenTelemetryJvmCpuMeterConventions(Tags.empty()));
}
@Bean
JvmMemoryMetrics jvmMemoryMetrics() {
return new JvmMemoryMetrics(List.of(), new OpenTelemetryJvmMemoryMeterConventions(Tags.empty()));
}
@Bean
JvmThreadMetrics jvmThreadMetrics() {
return new JvmThreadMetrics(List.of(), new OpenTelemetryJvmThreadMeterConventions(Tags.empty()));
}
@Bean
ClassLoaderMetrics classLoaderMetrics() {
return new ClassLoaderMetrics(new OpenTelemetryJvmClassLoadingMeterConventions());
}
}
Spring Boot 不会自动配置用于指标的 OpenTelemetry API。如果您真的想使用 OpenTelemetry 指标 API(我们不推荐)而不是 Micrometer API,或者如果您有一个使用 OpenTelemetry 指标 API 的库,请查看 Spring Boot 文档,了解如何使其工作。
Spring 项目使用 Micrometer Observation API 创建观测。观测是 Micrometer 中一个有趣的概念,因为它可以转换为指标**和**追踪。
然后,通过 io.micrometer:micrometer-tracing-bridge-otel 依赖项(也包含在 spring-boot-starter-opentelemetry 中),将由观测生成的追踪适配到 OpenTelemetry API。
Spring Boot 包含 OpenTelemetry SDK 的自动配置。对于追踪,它安装了一个 OtlpHttpSpanExporter(如果您更喜欢 gRPC 而不是 HTTP,则为 OtlpGrpcSpanExporter)。有了这个导出器,OpenTelemetry SDK 现在开始以 OTLP 格式将追踪(源自 Micrometer Observation)发送到您的后端。
要在您的应用程序中启用它,您必须设置 management.opentelemetry.tracing.export.otlp.endpoint 属性,例如:
management.opentelemetry.tracing.export.otlp.endpoint=https://:4318/v1/traces
如前所述,Spring Boot 包含自动配置,可配置 OpenTelemetry SDK 以便能够以 OTLP 格式导出日志。然而,它不会在 Logback 或 Log4j2 中安装附加器,因此尽管底层基础设施存在,但实际上并没有导出任何日志。要以 OTLP 格式导出日志,您需要做两件事:
首先,设置属性 management.opentelemetry.logging.export.otlp.endpoint,例如:
management.opentelemetry.logging.export.otlp.endpoint=https://:4318/v1/logs
其次,在 Logback 或 Log4j2 中安装一个附加器,将日志条目发送到 OpenTelemetry API。
对于 Logback,我们首先需要包含 io.opentelemetry.instrumentation:opentelemetry-logback-appender-1.0:2.21.0-alpha 依赖项(版本号中的 -alpha 表示它被标记为不稳定;不幸的是,附加器没有非 alpha 版本。您可以在此处阅读更多信息)。
然后,我们必须创建一个自定义的 Logback 配置,该配置位于 src/main/resources/logback-spring.xml 文件中:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<appender name="OTEL" class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="OTEL"/>
</root>
</configuration>
此配置导入 Spring Boot 的 Logback 基本配置,然后定义一个名为 OTEL 的附加程序,该附加程序将所有日志事件发送到 OpenTelemetry API。然后将此附加程序添加到根记录器,从而除了控制台之外,还将所有日志条目发送到 OpenTelemetry API。
最后要做的是让 OpenTelemetryAppender 知道要使用哪个 OpenTelemetry API 实例。为此,我们可以创建一个小型 bean,将 OpenTelemetry 实例注入:
@Component
class InstallOpenTelemetryAppender implements InitializingBean {
private final OpenTelemetry openTelemetry;
InstallOpenTelemetryAppender(OpenTelemetry openTelemetry) {
this.openTelemetry = openTelemetry;
}
@Override
public void afterPropertiesSet() {
OpenTelemetryAppender.install(this.openTelemetry);
}
}
日志、指标和追踪使用上下文信息,例如当前的追踪 ID。默认情况下,当 Micrometer Tracing 在类路径上时,Spring Boot 会调整日志模式,以便在日志消息中也包含追踪 ID 和跨度 ID。如果您正在尝试查找属于某个追踪的日志,这会非常有用。
一个有用的模式是在服务器的响应中(例如,通过 HTTP 标头)包含请求的追踪 ID。这样,如果用户从您的 HTTP 端点收到错误响应,他们可以将追踪 ID 包含在工单中,您可以使用此追踪 ID 来获取属于该错误请求的所有日志。
使用此 Servlet 过滤器可以将追踪 ID 放入标头中:
@Component
class TraceIdFilter extends OncePerRequestFilter {
private final Tracer tracer;
TraceIdFilter(Tracer tracer) {
this.tracer = tracer;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String traceId = getTraceId();
if (traceId != null) {
response.setHeader("X-Trace-Id", traceId);
}
filterChain.doFilter(request, response);
}
private @Nullable String getTraceId() {
TraceContext context = this.tracer.currentTraceContext().context();
return context != null ? context.traceId() : null;
}
}
如果您正在处理切换线程的方法,例如 @Async 注解的方法或使用 Spring 的 AsyncTaskExecutor,您会注意到在新线程中上下文丢失了。丢失的上下文会影响日志(不再包含追踪 ID)和追踪(丢失了跨度)。
上下文丢失是因为它存储在 ThreadLocal 中,而 ThreadLocal 不会传输到新线程。然而,解决方案非常简单:在 AsyncTaskExecutor(也用于 @Async 注解的方法)中使用 ContextPropagatingTaskDecorator。ContextPropagatingTaskDecorator 使用 Micrometer 的上下文传播 API 来确保追踪上下文传输到新线程。安装 ContextPropagatingTaskDecorator 很简单:只需定义一个 @Bean 方法,如以下代码片段所示:
@Configuration(proxyBeanMethods = false)
public class ContextPropagationConfiguration {
@Bean
ContextPropagatingTaskDecorator contextPropagatingTaskDecorator() {
return new ContextPropagatingTaskDecorator();
}
}
Spring Boot 的自动配置会查找 TaskDecorator bean 并将其安装到 AsyncTaskExecutor 中。有了 ContextPropagatingTaskDecorator,上下文现在被传输到新线程,从而修复了日志中丢失的追踪 ID 和丢失的跨度。ContextPropagatingTaskDecorator 的整个设置将来可能会得到改进,以提供更无缝的体验。
如果您正在处理多个服务,如果所有服务的追踪 ID 都相同,这样您就可以查看一个追踪并看到所有相关服务,或者找到参与该追踪的所有服务的日志,那不是很好吗?这就是分布式追踪中“分布式”的来源。
现在,您是否曾想过被调用的服务如何知道它是正在进行的追踪的一部分?这又是上下文传播,但这次上下文不是跨线程传播,而是跨进程边界传播。
有一个关于通过 HTTP 进行上下文传播的 W3C 建议,Spring Boot 开箱即用地配置了所有相关组件以使用它。发送方必须将当前追踪 ID 添加到标头中,接收方必须从标头中提取追踪 ID 并恢复上下文。
这一切都无缝运行,只要您遵循一个简单规则:不要谈论搏击俱乐部。哦,抱歉,剧本错了。您必须遵循的唯一规则是:不要自己 new 一个 RestTemplate / RestClient / WebClient。相反,注入一个 RestTemplateBuilder / RestClient.Builder / WebClient.Builder 并使用它来创建客户端。
Spring Boot 会自动配置这些构建器,并提供所有必要的基础设施,以自动在标头中发送追踪上下文。如果您自己用 new 创建客户端,那么此基础设施将不存在,上下文也不会传播,导致值班团队成员感到悲伤,并且冲刺回顾中出现红色方块。
我们准备了一个示例项目,您可以使用它来体验 OpenTelemetry 可观测性。它包含三个服务:
用户服务使用内存中的 H2 数据库和 Spring Data JDBC 根据用户 ID 查找用户。它公开了一个 HTTP API,用于根据给定的用户 ID 查找用户。
问候语服务有一个 HTTP API,它根据 Accept-Language 标头中指定的语言返回问候语。它知道英语、德语和西班牙语的问候语。
Hello 服务是入口点。它有一个 HTTP API,接收用户 ID 并返回该用户的问候语。为此,它会使用用户 ID 调用用户服务以获取用户的姓名。它还会调用问候语服务以获取问候语。然后它将用户的姓名与问候语结合起来并返回。
首先,让我们启动所有三个服务。此设置还包括 spring-boot-docker-compose 模块,该模块会自动检测名为 compose.yaml 的 Docker Compose 配置文件并调用 docker compose up。compose.yaml 文件包含一个使用 grafana/otel-lgtm 镜像的服务。Grafana LGTM 堆栈包含支持 OTLP 的日志、指标和追踪后端,所有这些都打包在一个 UI 中,我们可以使用它来查看可观测性信号。
Docker 容器启动并运行后,Spring Boot 会自动配置日志、指标和追踪的导出器,使其指向 Docker 容器。这就是为什么我们在 application.properties 中找不到前面提到的 OTLP 导出属性;这一切都在幕后进行。当您将其部署到生产环境时,您必须自己设置这些属性。如果您想了解更多关于这种开发者体验功能的信息,请阅读这篇博客文章。
现在让我们执行第一个请求:
> curl -i https://:8080/api/1
HTTP/1.1 200
X-Trace-Id: 0dbe0809731e35081d6db16c2ca0ef91
Content-Type: application/json
Content-Length: 12
Hello Moritz
太棒了,成功了。现在让我们用德语试试:
> curl -i https://:8080/api/1 --header "Accept-Language: de"
HTTP/1.1 200
X-Trace-Id: 6c0753c7ec390fff15fcf05f536e21cd
Content-Type: application/json
Content-Length: 12
Hallo Moritz
很好,我们现在有两个追踪 ID 可以玩了。请注意,请求的追踪 ID 如何包含在 X-Trace-Id 标头中,使用了上面的 Servlet 过滤器。
让我们看看 Grafana UI 中是否有日志(您可以单击图像放大)
在这里,我们可以看到日志已通过 OTLP 发送到 Grafana,我们现在可以在一个 UI 中查看来自三个服务的所有日志。也可以找到所有服务的日志,并根据追踪 ID 进行查找。
现在让我们看看是否可以找到追踪 ID 对应的追踪。
找到了!在这里,我们可以看到 hello 服务以漂亮的瀑布图调用了 greeting 服务和 user 服务。您还可以展开一个跨度以查看跨度属性。
在这种情况下,我们使用了 Micrometer 的 @SpanTag 将问候语区域设置 (en_US) 和用户 ID (1) 附加到跨度。让我们看看第二个追踪,其中应该有德语区域设置:
非常好,按预期工作。
最后一站,让我们看看服务生成的指标
在这里,您可以看到一个自定义指标,名为 say-hello(通过使用 @Observed(name = "say-hello") 注解方法创建),它计算 hello 消息生成的次数。
您还可以开箱即用地获得大量关于应用程序的指标,例如执行器中的线程数、HTTP 服务器等。
或者许多 JVM 指标
我们希望您在 Spring Boot 与 OpenTelemetry 的这次旋风之旅中玩得开心。正如您所看到的,Spring Boot 与 OpenTelemetry 进行了很好的集成。无论您是否使用 Micrometer 的 Observation API,从 OTel 集成角度来看,这并不重要。重要的是协议 OTLP,它抽象了用于检测应用程序的 API。
带有新 OpenTelemetry starter 的 Spring Boot 4.0 将于 11 月 20 日发布。Micrometer 1.16 已发布,其中包含 OpenTelemetry 增强功能和许多其他新功能。如果您发现任何问题或对如何改进整个 OpenTelemetry 故事有好的想法,请在我们的问题跟踪器中联系我们!