使用 Micrometer 在 Spring Boot 应用程序中定义自定义指标

Spring Boot 2.0 为我们喜爱的 Java 框架带来了大量新功能,其中之一便是将 Micrometer 集成到 Spring Boot Actuator 中。

Micrometer 是一个维度指标(metrics)监控门面库,它帮助开发人员将应用程序指标集成到各种监控系统中,同时使应用程序独立于具体的监控实现。正如项目首页所述,它类似于 SLF4J,但是用于指标(metrics)收集。

Micrometer 概述

在深入探讨如何使用 Micrometer 定义自定义指标之前,我们先来明确一下它的定义。

首先,Micrometer 是一个检测适配工具(Instrumentation Facade)。这意味着使用该库,开发人员可以使用统一的方式将指标数据发送到各种监控系统中。您可能会认为这没什么大不了的,确实如此。然而,市面上有许多用于监控应用程序的解决方案,每种方案满足监控需求的方法各不相同。这些差异可能细微如命名约定,也可能根本性地体现在数据收集方法上。

例如,在我们公司(AutSoft),我们使用 Prometheus 轮询应用程序中的新数据,而不是依赖像 DataDog 那样的推送模型。Micrometer 可以为您屏蔽所有这些差异,让您为所有解决方案使用统一的接口。

其次,Micrometer 遵循维度方法(Dimensional Approach),这意味着您可以使用任意数量的标签(Tags)来标记指标。例如,如果您有一个统计应用程序中 HTTP 请求的指标,可以使用请求命中的 URI 对其进行标记。一旦 Prometheus 收集了这些指标,您不仅可以查看请求总数,还可以向下钻取,检查特定 URI 的请求数。

Micrometer 与 Spring

借助新版本的 Spring Boot Actuator,Spring 团队决定使用 Micrometer 来报告内置指标(这并不奇怪,因为他们本身就是该库的开发维护者)。

要在现有的 Spring Boot 2 应用程序中检查这些指标,您实际上不需要做很多工作。只需导入 Spring Boot Actuator 和 Micrometer 依赖项并进行少量配置即可。

首先,将以下行添加到您 pom.xmldependencies 部分:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-core</artifactId>
</dependency>

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

然后,将以下配置粘贴到 application.properties(或 application.yml)文件中以暴露 Prometheus 抓取端点:

management.endpoints.web.exposure.include=prometheus

现在,如果启动应用程序并打开 http://localhost:8080/actuator/prometheus 端点,您将看到 Actuator 已经导出的大量指标。如果您查看前几行,就已经可以看到 Micrometer 和 Prometheus 的维度方法在起作用:

# HELP logback_events_total Number of error level events that made it to the logs
# TYPE logback_events_total counter
logback_events_total{level="warn",} 0.0
logback_events_total{level="debug",} 0.0
logback_events_total{level="error",} 0.0
logback_events_total{level="trace",} 0.0
logback_events_total{level="info",} 7.0

指标的名称是 logback_events_total,但有一个标签(或维度)叫做 level,这帮助您进行深入分析,准确检查每个日志记录级别发生了多少事件。

定义您的自定义指标

现在,让我们使用 Micrometer 和 Spring Boot 定义并导出自己的自定义指标。为此,我们将使用最喜欢的示例 BeerService,并将对其进行彻底监控。(您应该始终注意啤酒,不是吗?)

首先,我们需要保留 ApplicationContext 的 MeterRegistry 实例:

import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;

@Component
public class BeerService {

    private MeterRegistry meterRegistry;

    public BeerService(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }
}

MeterRegistry 负责收集和管理应用程序的 meters(计量器)。

定义计数器(Counter)

指标的最基本类型是计数器。计数器用于报告代表计数的单个数字,且通常只增不减。

在我们的示例中,我们将报告进入 BeerService 的订单数量:

private void initOrderCounters() {
    lightOrderCounter = this.meterRegistry.counter("beer.orders", "type", "light"); // 1 - 创建计数器
    aleOrderCounter = Counter.builder("beer.orders")    // 2 - 使用流式 API 创建计数器
            .tag("type", "ale")
            .description("The number of orders ever placed for Ale beers")
            .register(meterRegistry);
}

void orderBeer(Order order) {
    orders.add(order);

    if ("light".equals(order.type)) {
        lightOrderCounter.increment(1.0);  // 3 - 增加计数器
    } else if ("ale".equals(order.type)) {
        aleOrderCounter.increment();
    }
}

让我们看看这里发生了什么:

  1. 我们可以使用 meterRegistry 创建计数器。创建的指标将自动注册到注册表。
  2. 创建指标的更简洁方法是使用流式 Builder API。
  3. 计数器可以增加 1 或任何正数。

剩下要做的唯一件事就是实际订购一些啤酒。将以下行粘贴到 Application 类中,然后启动应用程序:

@SpringBootApplication
public class MicrometerApplication {

    public static void main(String[] args) {
        SpringApplication.run(MicrometerApplication.class, args);
    }

    private BeerService beerService;

    public MicrometerApplication(BeerService beerService) {
        this.beerService = beerService;
    }

    @EventListener(ApplicationReadyEvent.class)
    public void orderBeers() {
        Flux.interval(Duration.ofSeconds(2))
                .map(MicrometerApplication::toOrder)
                .doOnEach(o -> beerService.orderBeer(o.get()))
                .subscribe();
    }

    private static Order toOrder(Long l) {
        double amount = l % 5;
        String type = l % 2 == 0 ? "ale" : "light";
        return new Order(amount, type);
    }

}

现在,如果您在浏览器中打开 http://localhost:8080/actuator/prometheus 该 URL,则可以在几行中找到我们的新指标:

# HELP beer_orders_total  
# TYPE beer_orders_total counter
beer_orders_total{type="light",} 5.0
beer_orders_total{type="ale",} 6.0

恭喜,我们已经使用 Micrometer 定义了第一个计数器指标!

定义仪表(Gauge)

接下来,让我们看一下 Gauges。仪表也代表单个数值,但有一些显著差异。首先,计数器存储单调递增的值,而 Gauge 的值也可以递减。其次,Gauge 的值仅在观察时才会更改,我们不会像在前面的示例中那样手动对其进行递增。相反,如果需要,我们提供了一个获取 Gauge 当前值的函数。此行为还意味着两次观察之间发生的任何事件都将丢失。

通常,建议对具有上限的值使用仪表,不建议对计数器可表示的量度使用仪表。

在我们的示例中,我们将使用仪表监视 order 列表的大小。(即使我们当前的“业务逻辑”并未对此列表实施上限,但我们假设我们可以处理的最大订单数量,除此以外的数量也将更多。)

扩展您 BeerService 的构造函数,如下所示:

public BeerService(MeterRegistry meterRegistry) {
    this.meterRegistry = meterRegistry;
    initOrderCounters();
    Gauge.builder("beer.ordersInQueue", orders, Collection::size)
            .description("Number of unserved orders")
            .register(meterRegistry);
}

同样,如果重新启动应用程序并在浏览器中检查 http://localhost:8080/actuator/prometheus URL,则应找到 beer.ordersInQueue 指标:

# HELP beer_ordersInQueue Number of unserved orders
# TYPE beer_ordersInQueue gauge
beer_ordersInQueue 9.0

定义计时器(Timer)

计时器具有两个功能:它测量某些事件(通常是方法执行)的时间,并同时对这些事件进行计数。如果您曾经维护或开发过 Web 应用程序,则很可能希望检查服务器的响应时间。这种用例是计时器最典型的用例。

计时器具有许多有用的功能,但我们只专注于测量给定方法的执行时间。在 Spring 中,配置 Micrometer 提供的 Aspect 后可以使用 micrometer-core@Timed 注解 TimedAspect。将以下代码行放入 Application 类(或任何 @Configuration 类)中:

@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
    return new TimedAspect(registry);
}

(确保导入 spring-boot-starter-aop Maven 依赖关系。)

然后,在 BeerService 中创建一个方法以服务 orders 列表中的第一顺序:

@Scheduled(fixedRate = 5000)
@Timed(description = "Time spent serving orders")
public void serveFirstOrder() throws InterruptedException {
    if (!orders.isEmpty()) {
        Order order = orders.remove(0);
        Thread.sleep(1000L * order.amount);
    }
}

(要使 @Scheduled 注解生效,您需要使用 @EnableScheduling 注解应用程序或任何其他 Configuration 类。)

现在,如果启动该应用程序,您将找到以下导出到 Prometheus 的指标:

# HELP method_timed_seconds Time spent serving orders
# TYPE method_timed_seconds summary
method_timed_seconds_count{class="com.demo.micrometer.BeerService",exception="none",method="serveFirstOrder",} 8.0
method_timed_seconds_sum{class="com.demo.micrometer.BeerService",exception="none",method="serveFirstOrder",} 11.041940917
# HELP method_timed_seconds_max Time spent serving orders
# TYPE method_timed_seconds_max gauge
method_timed_seconds_max{class="com.demo.micrometer.BeerService",exception="none",method="serveFirstOrder",} 4.003256321

您可以看到我们的应用程序在 11.04 秒内处理了 8 个订单,每个订单最多处理 4 秒。

在事件执行尚未完成之前,计时器通常不会报告测量的时间。如果您有很长的任务要在它们仍在运行时进行测量,请使用如下 @Timed 注解:

@Timed(description = "Time spent serving orders", longTask = true)

总结和下一步

在本文中,我们了解了 Micrometer 的基础知识,它与 Spring Boot Actuator 集成在一起,为了使事情变得更加令人兴奋,我们在 Spring Boot 应用程序中定义了自己的指标。

现在,您已经知道了 Micrometer 的基础知识,但仍有很多需要探索的地方。我建议您查看 Micrometer 文档概念页面 和/或在 SpringOne Platform 上观看 Jon Schneider 关于 Micrometer 的 演示

在本文中,我们没有介绍所定义指标的可视化。为此,我们将 PrometheusGrafana 结合 使用,因为它们非常适合我们的用例。

可以在我们的 GitHub 页面 上找到本指南的源代码。

如果您喜欢这篇文章或有任何疑问,请不要在下面发表评论。

参考资料

说明:本文基于 Spring Boot 2.x 版本编写。Spring Boot 3.x 在依赖包命名空间(如 Jakarta EE)上有所变化,但 Micrometer 的核心用法与本文描述基本一致。端点路径及配置属性在不同版本间可能存在细微差异,请以官方文档为准。