在之前的一篇博文中,Adam Quan介绍了如何为Spring Boot应用程序设置可观察性的精彩内容。对于度量,Adam使用了Prometheus Java Client库,并展示了如何使用范例来链接度量和跟踪。
然而,Prometheus Java Client库并不是从Spring Boot应用中获得度量的唯一方法。一个替代方法是使用OpenTelemetry Java仪表代理,以OpenTelemetry格式直接暴露Spring的度量。
这篇博文展示了如何用OpenTelemetry Java工具代理捕获Spring Boot指标。
在这篇博文中,我们将使用一个简单的Hello World REST服务作为应用实例。源代码来自Spring的Building a RESTful Web Service指南的示例代码的./complete/
目录:
git clone https://github.com/spring-guides/gs-rest-service.git
cd gs-rest-service/complete/
./mvnw clean package
java -jar target/rest-service-complete-0.0.1-SNAPSHOT.jar
该应用在8080端口暴露了一个REST服务,在那里你可以问候不同的名字,如http://localhost:8080/greeting?name=Grafana。它还没有暴露任何度量。
暴露一个普罗米修斯度量的端点
作为第一步,我们在我们的示例应用程序中启用度量,并直接以Prometheus格式公开这些度量。我们还不会使用OpenTelemetry的Java工具代理。
我们需要在pom.xml
中增加两个依赖项:
org.springframework.boot
spring-boot-starter-actuator
io.micrometer
micrometer-registry-prometheus
runtime
spring-boot-starter-actuator
提供指标API和一些开箱即用的指标。在引擎盖下,它使用Micrometer度量库。micrometer-registry-prometheus
是为了以Prometheus格式暴露Micrometer度量。
接下来,我们需要启用Prometheus端点。用以下一行创建一个文件./src/main/resources/application.properties
:
management.endpoints.web.exposure.include=prometheus
重新编译并重启应用程序后,你会在http://localhost:8080/actuator/prometheus 上看到指标。开箱即用的指标包括一些JVM指标,如jvm_gc_pause_seconds
,一些来自日志框架的指标,如logback_events_total
,以及一些来自REST端点的指标,如http_server_requests
。
最后,我们希望有一个自定义指标来玩。自定义度量必须在Spring Boot提供的MeterRegistry
。所以第一步是把MeterRegistry
注入到GreetingController
,例如通过构造函数注入:
// ...
import io.micrometer.core.instrument.MeterRegistry;
@RestController
public class GreetingController {
// ...
private final MeterRegistry registry;
// Use constructor injection to get the MeterRegistry
public GreetingController(MeterRegistry registry) {
this.registry = registry;
}
// ...
}
现在,我们可以添加我们的自定义度量。我们将创建一个Counter
,按名称追踪问候语的调用。我们将计数器添加到greeting()
REST端点的现有实现中:
@GetMapping("/greeting")
public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name){
// Add a counter tracking the greeting calls by name
registry.counter("greetings.total", "name", name).increment();
// ...
}
让我们试试吧。重新编译并重启应用程序,用不同的名字调用问候语端点,比如http://localhost:8080/greeting?name=Grafana。在http://localhost:8080/actuator/prometheus,你会看到指标greetings_total
,计算每个名字的调用数量:
# HELP greetings_total··
# TYPE greetings_total counter
greetings_total{name="Grafana",} 2.0
greetings_total{name="Prometheus",} 1.0
注意,使用用户输入作为标签值通常是个坏主意,因为这很容易导致cardinality爆炸(即为每个名字创建一个新的度量)。然而,在我们的例子中,这很方便,因为它给了我们一个简单的方法来尝试不同的标签值。
将OpenTelemetry收集器放在中间位置
OpenTelemetry收集器是一个用于接收、处理和输出遥测数据的组件。它通常位于中间,在要监测的应用程序和监测后端之间。
下一步,我们将配置一个OpenTelemetry收集器,从Prometheus端点抓取指标,并以Prometheus格式公开它们。
到目前为止,这不会增加任何功能,只是我们会得到OpenTelemetry收集器作为一个新的基础设施组件。收集器在8889端口暴露的指标应该与应用程序在8080端口暴露的指标相同。
从github.com/open-teleme… 下载最新的otelcol_*.tar.gz
版本,并将其解压。它应该包含一个名为otelcol
的可执行文件。在撰写本文时,最新的版本是otelcol_0.47.0_linux_amd64.tar.gz。
创建一个名为config.yaml
的配置文件,内容如下:
receivers:
prometheus:
config:
scrape_configs:
- job_name: "example"
scrape_interval: 5s
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ["localhost:8080"]
processors:
batch:
exporters:
prometheus:
endpoint: "localhost:8889"
service:
pipelines:
metrics:
receivers: [prometheus]
processors: [batch]
exporters: [prometheus]
现在运行采集器,用
./otelcol --config=config.yaml
你可以在http://localhost:8889/metrics 上访问度量。
附加OpenTelemetry Java仪器代理
我们现在准备把我们的应用程序从暴露Prometheus度量标准转换为直接提供OpenTelemetry度量标准。我们将摆脱应用程序中的Prometheus端点,使用OpenTelemetry的Java仪表代理来暴露指标。
首先,我们必须重新配置OpenTelemetry收集器的接收端,以使用OpenTelemetry线路协议(otlp),而不是从Prometheus端点刮取指标:
receivers:
otlp:
protocols:
grpc:
http:
processors:
batch:
exporters:
prometheus:
endpoint: "localhost:8889"
service:
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [prometheus]
现在,从github.com/open-teleme…,下载最新版本的OpenTelemetry Java仪器代理。代理中的度量值默认是禁用的,所以你需要通过设置环境变量OTEL_METRICS_EXPORTER=otlp
来启用它们。然后,在连接了代理的情况下重新启动示例程序:
export OTEL_METRICS_EXPORTER=otlp
java -javaagent:./opentelemetry-javaagent.jar -jar ./target/rest-service-complete-0.0.1-SNAPSHOT.jar
一分钟左右后,你会看到收集器再次在http://localhost:8889/metrics上暴露指标。然而,这一次它们被直接使用OpenTelemetry线路协议运送到收集器。应用程序的Prometheus端点不再参与。现在你可以删除application.properties
中的普罗米修斯端点配置,并从pom.xml
中删除micrometer-registry-prometheus
的依赖关系。
然而,如果你仔细观察,你会发现指标与之前暴露的不同。
衔接OpenTelemetry和Micrometer
我们现在在收集器中看到的指标是来自OpenTelemetry Java仪器代理本身。它们不是由Spring Boot应用程序维护的原始指标。代理在给我们提供一些关于REST端点调用的开箱即用的指标方面做得很好,比如http_server_duration
。但是,有些指标明显缺失,比如Spring框架最初提供的logback_events_total
。而我们的自定义指标greetings_total
,也不再可用了。
为了了解原因,我们需要看看Spring Boot度量的内部工作方式。Spring使用Micrometer作为其度量标准库。Micrometer为应用开发者提供了一个通用的API,并为供应商提供了一个灵活的仪表注册表,以便为他们特定的监控后端暴露指标。
在上面的第一步中,我们使用了Prometheus的仪表注册表,这是Micrometer的注册表,用于为Prometheus暴露度量。
用OpenTelemetry Java仪器代理捕获Micrometer指标几乎是开箱即用。该代理检测到Micrometer,并在飞行中注册了一个OpenTelemetryMeterRegistry
。
不幸的是,代理注册的是Micrometer的Metrics.globalRegistry
,而Spring通过依赖注入使用自己的注册实例。如果OpenTelemetryMeterRegistry
最终出现在错误的MeterRegistry
实例中,它就不会被Spring使用。
为了解决这个问题,我们需要将OpenTelemetry的OpenTelemetryMeterRegistry
作为一个Spring Bean来使用,这样Spring在设置依赖注入时就可以正确注册它。这可以通过在你的Spring boot应用程序中添加以下代码来实现:
@SpringBootApplication
public class RestServiceApplication {
// Unregister the OpenTelemetryMeterRegistry from Metrics.globalRegistry and make it available
// as a Spring bean instead.
@Bean
@ConditionalOnClass(name = "io.opentelemetry.javaagent.OpenTelemetryAgent")
public MeterRegistry otelRegistry() {
Optional otelRegistry = Metrics.globalRegistry.getRegistries().stream()
.filter(r -> r.getClass().getName().contains("OpenTelemetryMeterRegistry"))
.findAny();
otelRegistry.ifPresent(Metrics.globalRegistry::remove);
return otelRegistry.orElse(null);
}
// ...
}
上面的代码段从Micrometer的Metrics.globalRegistry
中取消了对OpenTelemetryMeterRegistry
的注册,而是将其作为Spring Bean公开。它只有在代理被连接时才会运行,这是通过@ConditionalOnClass
注解实现的。
在重新编译和重启应用程序后,所有的指标都将提供给OpenTelemetry收集器,包括所有原始的Spring Boot指标和我们自定义的greetings_total
。
有些信息是多余的,所以你甚至可以将Spring Boot提供的http_server_requests
中的信息与OpenTelemetry的Java仪器代理添加的http_server_duration
中的信息进行比较。
总结
在这篇博文中,我们向你展示了如何用OpenTelemetry Java仪器代理捕获Spring Boot指标。我们首先以Prometheus格式暴露Spring Boot指标,然后把OpenTelemetry收集器放在中间,然后把示例程序从暴露Prometheus切换到直接暴露OpenTelemetry线路协议。
最后,我们强调了你需要添加到你的Java应用中的几行代码,以便将Spring Boot的Micrometer指标与OpenTelemetry Java仪器代理提供的OpenTelemetryMeterRegistry
。