Metrics for your Spring REST API – 为你的Spring REST API提供指标

最后修改: 2015年 3月 28日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll integrate basic Metrics into a Spring REST API.

在本教程中,我们将在Spring REST API中集成基本的Metrics

We’ll build out the metric functionality first using simple Servlet Filters, then using the Spring Boot Actuator module.

我们将首先使用简单的Servlet过滤器,然后使用Spring Boot Actuator模块来构建度量衡的功能。

2. The web.xml

2、web.xml

Let’s start by registering a filter – “MetricFilter” – into the web.xml of our app:

让我们开始注册一个过滤器–“MetricFilter“–到我们应用程序的web.xml

<filter>
    <filter-name>metricFilter</filter-name>
    <filter-class>org.baeldung.metrics.filter.MetricFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>metricFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Note how we’re mapping the filter to cover all requests coming in – “/*” – which is of course fully configurable.

请注意我们是如何将过滤器映射到覆盖所有进来的请求的–“/*”–这当然是完全可配置的。

3. The Servlet Filter

3.Servlet过滤器

Now – let’s create our custom filter:

现在–让我们来创建我们的自定义过滤器。

public class MetricFilter implements Filter {

    private MetricService metricService;

    @Override
    public void init(FilterConfig config) throws ServletException {
        metricService = (MetricService) WebApplicationContextUtils
         .getRequiredWebApplicationContext(config.getServletContext())
         .getBean("metricService");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
      throws java.io.IOException, ServletException {
        HttpServletRequest httpRequest = ((HttpServletRequest) request);
        String req = httpRequest.getMethod() + " " + httpRequest.getRequestURI();

        chain.doFilter(request, response);

        int status = ((HttpServletResponse) response).getStatus();
        metricService.increaseCount(req, status);
    }
}

Since the filter isn’t a standard bean, we’re not going to inject the metricService but instead retrieve it manually – via the ServletContext.

由于过滤器不是一个标准的Bean,我们不打算注入metricService,而是通过ServletContext手动获取它。

Also note that we’re continuing the execution of the filter chain by calling the doFilter API here.

还要注意的是,我们在这里通过调用doFilterAPI继续执行过滤器链。

4. Metric – Status Code Counts

4.指标–状态代码计数

Next – let’s take a look at our simple InMemoryMetricService:

接下来–让我们看看我们简单的InMemoryMetricService

@Service
public class MetricService {

    private Map<Integer, Integer> statusMetric;

    public MetricService() {
        statusMetric = new ConcurrentHashMap<>();
    }
    
    public void increaseCount(String request, int status) {
        Integer statusCount = statusMetric.get(status);
        if (statusCount == null) {
            statusMetric.put(status, 1);
        } else {
            statusMetric.put(status, statusCount + 1);
        }
    }

    public Map getStatusMetric() {
        return statusMetric;
    }
}

We’re using an in-memory ConcurrentMap to hold the counts for each type of HTTP status code.

我们使用一个内存中的ConcurrentMap来保存每种类型的HTTP状态代码的计数。

Now – to display this basic metric – we’re going to map it to a Controller method:

现在–为了显示这个基本指标–我们要把它映射到一个Controller方法。

@GetMapping(value = "/status-metric")
@ResponseBody
public Map getStatusMetric() {
    return metricService.getStatusMetric();
}

And here is a sample response:

而这里是一个答复样本。

{  
    "404":1,
    "200":6,
    "409":1
}

5. Metric – Status Codes by Request

5.衡量标准–按要求的状态代码

Next – let’s record metrics for Counts by Request:

下一步 – 让我们按要求记录计数的指标

@Service
public class MetricService {

    private Map<String, Map<Integer, Integer>> metricMap;

    public void increaseCount(String request, int status) {
        Map<Integer, Integer> statusMap = metricMap.get(request);
        if (statusMap == null) {
            statusMap = new ConcurrentHashMap<>();
        }

        Integer count = statusMap.get(status);
        if (count == null) {
            count = 1;
        } else {
            count++;
        }
        statusMap.put(status, count);
        metricMap.put(request, statusMap);
    }

    public Map getFullMetric() {
        return metricMap;
    }
}

We’ll display the metric results via the API:

我们将通过API显示度量结果。

@GetMapping(value = "/metric")
@ResponseBody
public Map getMetric() {
    return metricService.getFullMetric();
}

Here’s how these metrics look like:

以下是这些指标的情况。

{
    "GET /users":
    {
        "200":6,
        "409":1
    },
    "GET /users/1":
    {
        "404":1
    }
}

According to the above example, the API had the following activity:

根据上述例子,API有以下活动。

  • “7” requests to “GET /users
  • “6” of them resulted in “200” status code responses and only one in a “409”

6. Metric – Time Series Data

6.度量衡–时间序列数据

Overall counts are somewhat useful in an application, but if the system has been running for a significant amount of time – it’s hard to tell what these metrics actually mean.

总体计数在应用中有些用处,但如果系统已经运行了相当长的时间–很难说这些指标到底意味着什么

You need the context of the time in order for the data to make sense and be easily interpreted.

你需要当时的背景,以使数据有意义并易于解释。

Let’s now build a simple time-based metric; we’ll keep a record of status code counts per minute – as follows:

现在让我们建立一个简单的基于时间的指标;我们将记录每分钟的状态代码计数–如下所示。

@Service
public class MetricService {

    private static final SimpleDateFormat DATE_FORMAT = 
      new SimpleDateFormat("yyyy-MM-dd HH:mm");
    private Map<String, Map<Integer, Integer>> timeMap;

    public void increaseCount(String request, int status) {
        String time = DATE_FORMAT.format(new Date());
        Map<Integer, Integer> statusMap = timeMap.get(time);
        if (statusMap == null) {
            statusMap = new ConcurrentHashMap<>();
        }

        Integer count = statusMap.get(status);
        if (count == null) {
            count = 1;
        } else {
            count++;
        }
        statusMap.put(status, count);
        timeMap.put(time, statusMap);
    }
}

And the getGraphData():

还有getGraphData()

public Object[][] getGraphData() {
    int colCount = statusMetric.keySet().size() + 1;
    Set<Integer> allStatus = statusMetric.keySet();
    int rowCount = timeMap.keySet().size() + 1;
    
    Object[][] result = new Object[rowCount][colCount];
    result[0][0] = "Time";

    int j = 1;
    for (int status : allStatus) {
        result[0][j] = status;
        j++;
    }
    int i = 1;
    Map<Integer, Integer> tempMap;
    for (Entry<String, Map<Integer, Integer>> entry : timeMap.entrySet()) {
        result[i][0] = entry.getKey();
        tempMap = entry.getValue();
        for (j = 1; j < colCount; j++) {
            result[i][j] = tempMap.get(result[0][j]);
            if (result[i][j] == null) {
                result[i][j] = 0;
            }
        }
        i++;
    }

    for (int k = 1; k < result[0].length; k++) {
        result[0][k] = result[0][k].toString();
    }
   return result; 
}

We’re now going to map this to the API:

我们现在要把这个映射到API上。

@GetMapping(value = "/metric-graph-data")
@ResponseBody
public Object[][] getMetricData() {
    return metricService.getGraphData();
}

And finally – we’re going to render it out using Google Charts:

最后–我们要用谷歌图表把它渲染出来。

<html>
<head>
<title>Metric Graph</title>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load("visualization", "1", {packages : [ "corechart" ]});

function drawChart() {
$.get("/metric-graph-data",function(mydata) {
    var data = google.visualization.arrayToDataTable(mydata);
    var options = {title : 'Website Metric',
                   hAxis : {title : 'Time',titleTextStyle : {color : '#333'}},
                   vAxis : {minValue : 0}};

    var chart = new google.visualization.AreaChart(document.getElementById('chart_div'));
    chart.draw(data, options);

});

}
</script>
</head>
<body onload="drawChart()">
    <div id="chart_div" style="width: 900px; height: 500px;"></div>
</body>
</html>

7. Using Spring Boot 1.x Actuator

7.使用Spring Boot 1.x Actuator

In the next few sections, we’re going to hook into the Actuator functionality in Spring Boot to present our metrics.

在接下来的几节中,我们将与Spring Boot中的Actuator功能挂钩,以展示我们的指标。

First – we’ll need to add the actuator dependency to our pom.xml:

首先–我们需要在我们的pom.xml中添加执行器的依赖关系。

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

7.1. The MetricFilter

7.1.MetricFilter

Next – we can turn the MetricFilter – into an actual Spring bean:

接下来,我们可以把MetricFilter变成一个实际的Spring Bean。

@Component
public class MetricFilter implements Filter {

    @Autowired
    private MetricService metricService;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
      throws java.io.IOException, ServletException {
        chain.doFilter(request, response);

        int status = ((HttpServletResponse) response).getStatus();
        metricService.increaseCount(status);
    }
}

This is, of course, a minor simplification – but one that’s worth doing to get rid of the previously manual wiring of dependencies.

当然,这只是一个小的简化–但为了摆脱以前手动接线的依赖关系,这个简化是值得做的。

7.2. Using CounterService

7.2.使用CounterService

Let’s now use the CounterService to count occurrences for each Status Code:

现在让我们使用CounterService来计算每个状态代码的出现次数。

@Service
public class MetricService {

    @Autowired
    private CounterService counter;

    private List<String> statusList;

    public void increaseCount(int status) {
        counter.increment("status." + status);
        if (!statusList.contains("counter.status." + status)) {
            statusList.add("counter.status." + status);
        }
    }
}

7.3. Export Metrics Using MetricRepository

7.3.使用MetricRepository导出指标

Next – we need to export the metrics – using the MetricRepository:

接下来–我们需要导出指标–使用MetricRepository

@Service
public class MetricService {

    @Autowired
    private MetricRepository repo;

    private List<List<Integer>> statusMetric;
    private List<String> statusList;
    
    @Scheduled(fixedDelay = 60000)
    private void exportMetrics() {
        Metric<?> metric;
        List<Integer> statusCount = new ArrayList<>();
        for (String status : statusList) {
            metric = repo.findOne(status);
            if (metric != null) {
                statusCount.add(metric.getValue().intValue());
                repo.reset(status);
            } else {
                statusCount.add(0);
            }
        }
        statusMetric.add(statusCount);
    }
}

Note that we’re storing counts of status codes per minute.

请注意,我们存储的是每分钟状态代码的计数

7.4. Spring Boot PublicMetrics

7.4.Spring Boot PublicMetrics

We can also use Spring Boot PublicMetrics to export metrics instead of using our own filters – as follows:

我们还可以使用Spring Boot的PublicMetrics来导出指标,而不是使用我们自己的过滤器–如下所示。

First, we have our scheduled task to export metrics per minute:

首先,我们的计划任务是每分钟导出指标

@Autowired
private MetricReaderPublicMetrics publicMetrics;

private List<List<Integer>> statusMetricsByMinute;
private List<String> statusList;
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");

@Scheduled(fixedDelay = 60000)
private void exportMetrics() {
    List<Integer> lastMinuteStatuses = initializeStatuses(statusList.size());
    for (Metric<?> counterMetric : publicMetrics.metrics()) {
        updateMetrics(counterMetric, lastMinuteStatuses);
    }
    statusMetricsByMinute.add(lastMinuteStatuses);
}

We, of course, need to initialize the list of HTTP status codes:

当然,我们需要初始化HTTP状态代码的列表。

private List<Integer> initializeStatuses(int size) {
    List<Integer> counterList = new ArrayList<>();
    for (int i = 0; i < size; i++) {
        counterList.add(0);
    }
    return counterList;
}

And then we’re going to actually update the metrics with status code count:

然后我们要用状态代码计数实际更新指标。

private void updateMetrics(Metric<?> counterMetric, List<Integer> statusCount) {

    if (counterMetric.getName().contains("counter.status.")) {
        String status = counterMetric.getName().substring(15, 18); // example 404, 200
        appendStatusIfNotExist(status, statusCount);
        int index = statusList.indexOf(status);
        int oldCount = statusCount.get(index) == null ? 0 : statusCount.get(index);
        statusCount.set(index, counterMetric.getValue().intValue() + oldCount);
    }
}

private void appendStatusIfNotExist(String status, List<Integer> statusCount) {
    if (!statusList.contains(status)) {
        statusList.add(status);
        statusCount.add(0);
    }
}

Note that:

请注意,。

  • PublicMetics status counter name start with “counter.status” for example “counter.status.200.root
  • We keep a record of status count per minute in our list statusMetricsByMinute

We can export our collected data to draw it in a graph – as follows:

我们可以导出我们收集到的数据,将其绘制成图表–如下所示。

public Object[][] getGraphData() {
    Date current = new Date();
    int colCount = statusList.size() + 1;
    int rowCount = statusMetricsByMinute.size() + 1;
    Object[][] result = new Object[rowCount][colCount];
    result[0][0] = "Time";
    int j = 1;

    for (String status : statusList) {
        result[0][j] = status;
        j++;
    }

    for (int i = 1; i < rowCount; i++) {
        result[i][0] = dateFormat.format(
          new Date(current.getTime() - (60000L * (rowCount - i))));
    }

    List<Integer> minuteOfStatuses;
    List<Integer> last = new ArrayList<Integer>();

    for (int i = 1; i < rowCount; i++) {
        minuteOfStatuses = statusMetricsByMinute.get(i - 1);
        for (j = 1; j <= minuteOfStatuses.size(); j++) {
            result[i][j] = 
              minuteOfStatuses.get(j - 1) - (last.size() >= j ? last.get(j - 1) : 0);
        }
        while (j < colCount) {
            result[i][j] = 0;
            j++;
        }
        last = minuteOfStatuses;
    }
    return result;
}

7.5. Draw Graph Using Metrics

7.5.使用指标绘制图表

Finally – let’s represent these metrics via a 2 dimension array – so that we can then graph them:

最后–让我们通过一个二维数组来表示这些指标–这样我们就可以对它们进行绘图。

public Object[][] getGraphData() {
    Date current = new Date();
    int colCount = statusList.size() + 1;
    int rowCount = statusMetric.size() + 1;
    Object[][] result = new Object[rowCount][colCount];
    result[0][0] = "Time";

    int j = 1;
    for (String status : statusList) {
        result[0][j] = status;
        j++;
    }

    ArrayList<Integer> temp;
    for (int i = 1; i < rowCount; i++) {
        temp = statusMetric.get(i - 1);
        result[i][0] = dateFormat.format
          (new Date(current.getTime() - (60000L * (rowCount - i))));
        for (j = 1; j <= temp.size(); j++) {
            result[i][j] = temp.get(j - 1);
        }
        while (j < colCount) {
            result[i][j] = 0;
            j++;
        }
    }

    return result;
}

And here is our Controller method getMetricData():

这里是我们的控制器方法getMetricData()

@GetMapping(value = "/metric-graph-data")
@ResponseBody
public Object[][] getMetricData() {
    return metricService.getGraphData();
}

And here is a sample response:

而这里是一个答复样本。

[
    ["Time","counter.status.302","counter.status.200","counter.status.304"],
    ["2015-03-26 19:59",3,12,7],
    ["2015-03-26 20:00",0,4,1]
]

8. Using Spring Boot 2.x Actuator

8.使用Spring Boot 2.x Actuator

In Spring Boot 2, Spring Actuator’s APIs witnessed a major change. Spring’s own metrics have been replaced with Micrometer. So let’s write the same metrics example above with Micrometer.

在Spring Boot 2中,Spring Actuator的API见证了一个重大变化。Spring自己的指标已经被Micrometer所取代。因此,让我们用Micrometer编写上面同样的指标例子。

8.1. Replacing CounterService With MeterRegistry

8.1.用MeterRegistry替换CounterService

As our Spring Boot application already depends on the Actuator starter, Micrometer is already auto-configured. We can inject MeterRegistry instead of CounterService. We can use different types of Meter to capture metrics. The Counter is one of the Meters:

由于我们的Spring Boot应用程序已经依赖于Actuator启动器,Micrometer已经被自动配置了。我们可以注入MeterRegistry而不是CounterService。我们可以使用不同类型的Meter来捕获度量。Counter是计量器中的一种。

@Autowired
private MeterRegistry registry;

private List<String> statusList;

@Override
public void increaseCount(int status) {
    String counterName = "counter.status." + status;
    registry.counter(counterName).increment(1);
    if (!statusList.contains(counterName)) {
        statusList.add(counterName);
    }
}

8.2. Viewing Custom Metrics

8.2.查看自定义指标

As our metrics are now registered with Micrometer, first, let’s enable them in the application configuration. Now we can view them by navigating to the Actuator endpoint at /actuator/metrics:

由于我们的指标现在已经在Micrometer上注册,首先,让我们在应用程序配置中启用它们。现在我们可以通过导航到/actuator/metrics的执行器端点来查看它们。

{
  "names": [
    "application.ready.time",
    "application.started.time",
    "counter.status.200",
    "disk.free",
    "disk.total",
    .....
  ]
}

Here we can see our counter.status.200 metric is listed amongst the standard Actuator metrics. In addition, we can also get the latest value of this metric by providing the selector in the URI as /actuator/metrics/counter.status.200:

这里我们可以看到我们的counter.status.200指标被列在标准的执行器指标中。此外,我们还可以通过在URI中提供/actuator/metrics/counter.status.200的选择器来获得这个指标的最新值。

{
  "name": "counter.status.200",
  "description": null,
  "baseUnit": null,
  "measurements": [
    {
      "statistic": "COUNT",
      "value": 2
    }
  ],
  "availableTags": []
}

8.3. Exporting Counts Using MeterRegistry

8.3.使用MeterRegistry导出计数

In Micrometer, we can export the Counter values using MeterRegistry:

在Micrometer中,我们可以使用MeterRegistry:导出Counter值。

@Scheduled(fixedDelay = 60000)
private void exportMetrics() {
    List<Integer> statusCount = new ArrayList<>();
    for (String status : statusList) {
        Search search = registry.find(status);
        Counter counter = search.counter();
         if (counter == null) {
             statusCount.add(0);
         } else {
             statusCount.add(counter != null ? ((int) counter.count()) : 0);
             registry.remove(counter);
         }
    }
    statusMetricsByMinute.add(statusCount);
}

8.3. Publishing Metrics Using Meters

8.3.使用Meters发布指标

Now we can also publish Metrics using MeterRegistry’s Meters:

现在我们也可以使用MeterRegistry的Meters来发布Metrics:

@Scheduled(fixedDelay = 60000)
private void exportMetrics() {
    List<Integer> lastMinuteStatuses = initializeStatuses(statusList.size());

    for (Meter counterMetric : publicMetrics.getMeters()) {
        updateMetrics(counterMetric, lastMinuteStatuses);
    }
    statusMetricsByMinute.add(lastMinuteStatuses);
}

private void updateMetrics(Meter counterMetric, List<Integer> statusCount) {
    String metricName = counterMetric.getId().getName();
    if (metricName.contains("counter.status.")) {
        String status = metricName.substring(15, 18); // example 404, 200
        appendStatusIfNotExist(status, statusCount);
        int index = statusList.indexOf(status);
        int oldCount = statusCount.get(index) == null ? 0 : statusCount.get(index);
        statusCount.set(index, (int)((Counter) counterMetric).count() + oldCount);
    }
}

9. Conclusion

9.结论

In this article, we explored a few simple ways to build out some basic metrics capabilities into a Spring web application.

在这篇文章中,我们探讨了一些简单的方法,在Spring网络应用中建立一些基本的度量功能。

Note that the counters aren’t thread-safe – so they might not be exact without using something like atomic numbers. This was deliberate just because the delta should be small and 100% accuracy isn’t the goal – rather, spotting trends early is.

请注意,这些计数器不是线程安全的–所以如果不使用原子数之类的东西,它们可能并不精确。这是特意为之的,只是因为delta应该很小,而且100%的准确性不是我们的目标–相反,早期发现趋势才是。

There are of course more mature ways to record HTTP metrics in an application, but this is a simple, lightweight, and super-useful way to do it without the extra complexity of a full-fledged tool.

当然,还有一些更成熟的方法来记录应用程序中的HTTP指标,但这是一个简单、轻量级和超级有用的方法,不需要一个成熟的工具的额外复杂性。

The full implementation of this article can be found in the GitHub project.

本文的完整实现可以在GitHub项目中找到。