1. 概述
本教程將整合基本指標到 Spring REST API 中。
我們將首先使用簡單的 Servlet 過濾器構建指標功能,然後使用 Spring Boot Actuator 模塊。
2. Web.xml
讓我們首先在我們的應用程序的 web.xml 中註冊一個過濾器 – "MetricFilter":
<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>請注意我們如何將過濾器映射到所有傳入的請求上——,當然,它完全可配置。
3. 自定義 Servlet 過濾器
現在,讓我們創建一個自定義過濾器:
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);
}
}由於過濾器不是標準 Bean,因此我們不會注入 metricService,而是通過 ServletContext 檢索它。
此外,我們通過調用 doFilter API 繼續執行過濾器鏈。
4. 指標 – 狀態碼計數
接下來,讓我們來查看我們的簡單 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;
}
}我們使用內存中的 ConcurrentMap 來存儲每個 HTTP 狀態碼的計數。
現在 – 為了顯示此基本指標 – 我們將其映射到一個 Controller 方法上:
@GetMapping(value = "/status-metric")
@ResponseBody
public Map getStatusMetric() {
return metricService.getStatusMetric();
}以下是一個示例響應:
{
"404":1,
"200":6,
"409":1
}5. 按請求 – 請求計數指標
接下來,讓我們記錄按請求的計數指標:
@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;
}
}我們將通過 API 顯示指標結果:
@GetMapping(value = "/metric")
@ResponseBody
public Map getMetric() {
return metricService.getFullMetric();
}以下是這些指標的顯示效果:
{
"GET /users":
{
"200":6,
"409":1
},
"GET /users/1":
{
"404":1
}
}根據以上示例,API 的活動如下:
- “7” 個請求發送到 “GET /users”
- 其中“6” 個請求返回了 “200” 狀態碼響應,只有“1” 個返回了 “409” 狀態碼
6. 指標 – 時間序列數據
總體計數在應用程序中具有一定用處,但如果系統運行時間較長——很難判斷這些指標的實際含義
需要時間上下文,以便數據具有意義並易於解釋。
現在讓我們構建一個簡單的基於時間的指標,我們將記錄每分鐘的狀態碼計數,如下所示:
@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);
}
}以及 :
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;
}我們現在將此映射到 API:
@GetMapping(value = "/metric-graph-data")
@ResponseBody
public Object[][] getMetricData() {
return metricService.getGraphData();
}最後,我們將使用 Google 圖表將其渲染出來:
<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. 使用 Spring Boot 1.x Actuator
在接下來的幾個部分中,我們將利用 Spring Boot 1.x Actuator 的功能來展示我們的指標。
首先,我們需要將 actuator 依賴添加到我們的 pom.xml 中:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>7.1. 指標過濾器
接下來,我們可以將
@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);
}
}當然,這只是一個輕微的簡化——但為了擺脱之前手動配置依賴關係,這樣做仍然值得一做。
7.2. 使用 CounterService
現在,讓我們使用 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. 使用 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);
}
}請注意,我們存儲的是每分鐘的狀態碼數量。
7.4. Spring Boot PublicMetrics
我們可以使用 Spring Boot PublicMetrics 來導出指標,而不是使用我們自己的過濾器——如下所示:
首先,我們有用於每分鐘導出指標的計劃任務:
@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);
}我們當然需要初始化 HTTP 狀態碼列表:
private List<Integer> initializeStatuses(int size) {
List<Integer> counterList = new ArrayList<>();
for (int i = 0; i < size; i++) {
counterList.add(0);
}
return counterList;
}然後,我們將實際更新指標,包括狀態碼計數:
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);
}
}請注意:
- PublicMetics 狀態計數器名稱以“”開頭,例如“”
- 我們記錄每分鐘的狀態計數數據,存儲在列表 中
我們可以將我們收集的數據導出,以便繪製成圖表 – 如下所示:
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. 使用指標繪製圖表
最後,讓我們通過二維數組來表示這些指標,以便我們可以繪製圖表:
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;
}以下是我們的 Controller 方法 getMetricData():
@GetMapping(value = "/metric-graph-data")
@ResponseBody
public Object[][] getMetricData() {
return metricService.getGraphData();
}以下是一個示例響應:
[
["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. 使用 Spring Boot 2.x Actuator
在 Spring Boot 2 中,Spring Actuator 的 API 經歷了重大變更。 Spring 本身的指標已被 Micrometer 替換。 因此,讓我們用 Micrometer 編寫上述相同的指標示例。
8.1. 替換 CounterService 為 MeterRegistry
由於我們的 Spring Boot 應用程序已經依賴於 Actuator starter,因此 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. 查看自定義指標
由於我們的指標已與 Micrometer 註冊,首先,請在應用程序配置中啓用它們。 這樣,您可以通過導航到 Actuator 端點 /actuator/metrics 來查看它們。
{
"names": [
"application.ready.time",
"application.started.time",
"counter.status.200",
"disk.free",
"disk.total",
.....
]
}我們可以在這裏看到我們的 counter.status.200 指標列出了標準 Actuator 指標中的一項。此外,通過在 URI 中提供選擇器,還可以獲取該指標的最新值,例如:/actuator/metrics/counter.status.200:
{
"name": "counter.status.200",
"description": null,
"baseUnit": null,
"measurements": [
{
"statistic": "COUNT",
"value": 2
}
],
"availableTags": []
}8.3. 使用 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. 使用 Meters 的發佈指標
現在,我們還可以使用 MeterRegistry 的 Meters 發佈指標。
@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. 結論
在本文中,我們探討了如何在 Spring Web 應用程序中構建一些基本指標能力的方法。
請注意,這些計數器不可保證線程安全——因此,在不使用諸如原子數字之類的東西時,它們可能不準確。這主要是因為增量應該很小,100% 的準確性並不是目標——而是要儘早發現趨勢。
當然,還有更成熟的方法可以在應用程序中記錄 HTTP 指標,但這種方法簡單、輕量級且非常實用,無需使用完整的工具帶來的額外複雜性。