
Java 应用通过 OpenTelemetry API 实现手动埋点
我们知道对于 Java 应用可以通过 OpenTelemetry 提供的 Java agent 来实现自动埋点功能,在大多数场景下也完全足够了,但是有时候我们需要更加精细的控制,这时候我们就需要使用手动埋点的方式来实现了。
使用注解埋点
我们可以在 Java 应用通过手动埋点的方式来实现链路追踪,但如果我们不希望进行太多的代码更改,那么可以使用注解的方式来实现,OpenTelemetry 提供了一些注解来帮助我们实现手动埋点,比如 @WithSpan
、@SpanAttribute
。
首先我们需要添加依赖库 opentelemetry-instrumentation-annotations
。
开发人员可以使用 @WithSpan
注解来向 OpenTelemetry 自动检测发送信号,每当标记的方法被执行时都应创建一个新的 span。
比如我们在 Order Service 中的 IndexController
中添加一个 @WithSpan
注解,代码如下所示:
然后我们重建镜像,重新启动容器,当我们访问首页的时候就可以看到 Jaeger UI 中多了一个 IndexController.home
的 span 了。
每次应用程序调用有注解的方法时,它都会创建一个表示其持续时间并提供任何抛出异常的 span。默认情况下,span 名称是 <className>.<methodName>
,当然也可以在注解中提供了一个名称作为参数,比如可以使用 @WithSpan("indexSpan")
来指定 span 的名称,这样在 Jaeger UI 中就可以看到 indexSpan
的 span 了。
此外当为一个带注解的方法创建一个 span 时,可以通过使用 @SpanAttribute
注解来自动将方法调用的参数值添加为创建 span 的属性。
比如我们在 IndexController
中添加一个 fetchId
函数,并接收一个 id 参数,我们就可以使用 @SpanAttribute
注解来将接收的 id 参数添加为 indexSpanWithAttr
这个 span 的属性,代码如下所示:
然后我们重建镜像,重新启动容器,当我们访问 http://localhost:8081/123
的时候就可以看到 Jaeger UI 中多了一个 indexSpanWithAttr
的 span 了,并且该 span 的属性中包含了我们传递的 id 参数。
使用 API 手动埋点
除了使用注解的方式来实现埋点之外,我们还可以使用 OpenTelemetry 提供的 API 来实现手动埋点,这样我们就可以更加精细的控制我们的 span 了,当然这样也会增加我们的代码量,但就不需要使用 java agent 了。
在 Java 应用中,要实现手动埋点,首先第一步是获取 OpenTelemetry 接口的实例,我们需要尽早在应用程序中配置一个 OpenTelemetrySdk 的实例,我们可以使用 OpenTelemetrySdk.builder()
方法来完成这个操作。然后可以通过返回的 OpenTelemetrySdkBuilder
实例获取与信号、跟踪和指标相关的提供程序,以构建 OpenTelemetry 实例。我们可以使用 SdkTracerProvider.builder()
和 SdkMeterProvider.builder()
方法来构建 Provider
。此外还强烈建议将 Resource
实例定义为生成遥测数据的实体的表示;特别是 service.name
属性是最重要的遥测源标识信息的一部分。
当然我们需要先在应用中添加相关依赖库,代码如下所示:
在 pom.xml
文件中添加了 opentelemetry-api
、opentelemetry-sdk
、opentelemetry-exporter-otlp
、opentelemetry-semconv
这几个依赖库,其中 opentelemetry-semconv
是用来定义一些常用的属性的,比如 service.name
、http.method
、http.status_code
等,当然现在我们就不需要 opentelemetry-instrumentation-annotations
这个依赖库了。
在 Spring Boot 项目中,初始化 OpenTelemetry 的一种常见方法是使用 @Configuration
类。这样的类会在 Spring Boot 应用启动时自动运行,使得初始化工作更加集中和组织化。
我们这里创建一个如下所示的 OpenTelemetryConfig
类,代码如下所示:
在上述代码中,我们定义了一个 @Configuration
类,并使用 @Bean
注解为 OpenTelemetry 创建了一个 Bean,Spring 会管理这个 Bean 的生命周期,并在需要时自动注入。
这样,你的 Spring Boot 应用每次启动时,都会执行这些初始化代码,从而确保了 OpenTelemetry 的正确配置。
在真正初始化的代码中,我们首先从环境变量中获取 OTLP Exporter 的地址,然后初始化 OTLP Exporter,接着初始化 TracerProvider,最后初始化并返回 OpenTelemetry SDK。
比如现在我们在 OrderController
中的 getAllOrders
处理器中来手动埋点,代码如下所示:
上面代码中我们首先通过 openTelemetry.getTracer(OrderController.class.getName())
方法来初始化 Tracer,然后通过 tracer.spanBuilder("getAllOrders").startSpan()
方法来创建一个新的 Span,接着通过 span.makeCurrent()
方法将 Span 注入到上下文中,然后就可以在 try
代码块中执行我们的业务逻辑了,这里我们添加了两个属性,如果出现了异常则会记录异常信息,最后在 finally
代码块中结束 Span。
我们还需要修改 Dockerfile 中的启动命令,代码如下所示:
因为现在我们不需要使用 java agent 了,所以去掉 -Pdev
参数(该 profile 中定义了 java agent 启动参数),然后重新构建镜像,重新启动容器,当我们访问订单列表后就可以看到 Jaeger UI 中多了一个 getAllOrders
的 span 了。
很明显我们可以看到现在的 span 非常简单,没有和前端 frontend 服务的 span 关联起来。
由于前端 frontend 在请求后端接口的时候我们已经注入了 W3CTraceContext
,所以我们只需要在 Java 应用中通过 propagation api
来获取到 span context,然后将其作为父级 span,这样就可以将前端的 span 和后端的 span 关联起来了。
这里我们可以添加一个拦截器来使用 propagation
接口解析 span context,代码如下所示:
上面代码中我们首先通过 openTelemetry.getPropagators().getTextMapPropagator()
方法来获取到 TextMapPropagator
,然后通过 extract
方法来解析 span context,然后将解析出来的 span context 设置为子 span 的父级 span,最后将 span context 存储在请求属性中,以便后续使用。
这里的关键是在初始化 OpenTelemetry 的时候需要配置 ContextPropagators
,代码如下所示:
这样我们才能去解析 TraceContext 和 Baggage 两种上下文传播机制。而其中的 getter
就是用来从 HTTP 请求头中获取 span context 的方式。
当然最后我们还需要在 WebMvcConfig
中注册该拦截器,代码如下所示:
这样当我们在请求 /api/orders/**
下面的接口时,就可以从请求属性中获取父级的 span context 了。
现在我们重新修改 getAllOrders
处理器,代码如下所示:
这里我们首先通过请求属性获取到 span context,这里我们添加了两个属性,如果出现了异常则会记录异常信息,最后在 finally
代码块中结束 Span。
现在我们重新启动容器,当我们访问订单列表后就可以看到 Jaeger UI 中多了一个 GET /api/orders
的 span 了,并且该 span 和前端 frontend 服务的 span 关联起来了。
当然这还不够,因为我们的订单列表接口还会去请求 user-service
服务来获取用户信息,还会去请求 catalog-service
服务获取书籍信息,所以我们还需要在这两个请求中也注入我们这里的 span,这样就可以将整个链路串联起来了。
首先针对 TokenInterceptor
拦截器我们先创建一个子 span,代码如下所示:
在上面代码中我们首先获取当前上下文的 Span,然后创建一个名为 GET /api/userinfo
的 span,将其设置为当前上下文的子 span,并将上下文切换到当前子 span,然后执行我们的业务逻辑,最后结束子 span。
然后我们可以统一在 WebClient 中来注入 span context,这样当我们 Java 服务请求其他服务的时候就可以形成链路。
在上面代码中我们为 WebClient 添加了一个名为 traceExchangeFilterFunction
的过滤器函数,在该函数中我们首先获取当前上下文的 Span,然后创建一个新的请求头并添加跟踪信息,最后将当前上下文的 Span 通过 Propagator
接口注入到请求头中,这样当我们请求其他服务的时候就可以形成链路了。
现在我们重新启动容器,当我们访问订单列表后就可以看到 Jaeger UI 中多了一个 GET /api/userinfo
的 span 了,并且该 span 和还会和 user-service
服务的 span 关联起来。
同样的方式我们还可以在 getAllOrders
处理器中添加数据库查询的 span,代码如下所示:
将 Order 转换为 OrderDto 也可以添加一个 span,代码如下所示:
这里同样我们会为每一个转换创建一个子 span,然后将其设置为当前上下文的子 span,最后结束子 span,这样当我们通过 WebClient 去请求 catalog-service
服务的时候也就可以形成链路了。
最后我们再去查看下完整的链路,如下图所示:
完整代码请查看:https://github.com/cnych/podemo。
文章转载自公众号:k8s技术圈
