Перейти к основному содержимому

Метрики для вашего Spring REST API

· 11 мин. чтения

1. Обзор

В этом руководстве мы интегрируем базовые метрики в Spring REST API .

Мы создадим метрическую функциональность сначала с помощью простых фильтров сервлетов, а затем с помощью модуля Spring Boot Actuator.

2. Веб- файл.xml

Начнем с регистрации фильтра — « MetricFilter » — в файле web.xml нашего приложения:

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

Обратите внимание, как мы сопоставляем фильтр с охватом всех входящих запросов — «/*» — который, конечно же, полностью настраивается.

3. Фильтр сервлетов

Теперь давайте создадим наш собственный фильтр:

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.

Теперь, чтобы отобразить эту базовую метрику, мы собираемся сопоставить ее с методом контроллера :

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

А вот и образец ответа:

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

5. Метрика — коды состояния по запросу

Далее — записываем метрики для 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;
}
}

Мы будем отображать результаты метрик через 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» и только один — к «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);
}
}

И 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++) { результат[0][k] = результат[0][k].toString(); }

return result; 
}

Теперь мы собираемся сопоставить это с API:

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

И, наконец, мы собираемся отобразить это с помощью 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. Использование Spring Boot 1.x Actuator

В следующих нескольких разделах мы собираемся подключиться к функциям Actuator в Spring Boot, чтобы представить наши показатели.

Во-первых, нам нужно добавить зависимость привода в наш pom.xml :

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

7.1. МетрикаФильтр _ ``

Далее — мы можем превратить MetricFilter — в реальный bean-компонент Spring:

@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 для подсчета вхождений для каждого кода состояния:

@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 начинается с « counter.status », например « counter.status.200.root » .
  • Мы ведем учет количества статусов в минуту в нашем списке statusMetricsByMinute.

Мы можем экспортировать собранные данные, чтобы нарисовать их на графике , следующим образом:

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;
}

А вот наш метод контроллера 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 API-интерфейсы Spring Actuator претерпели серьезные изменения. Собственные метрики Spring заменены на Micrometer . Итак, давайте напишем тот же пример метрик выше с Micrometer .

8.1. Замена CounterService на MeterRegistry ``

Поскольку наше приложение Spring Boot уже зависит от стартера Actuator, Micrometer уже настроен автоматически. Мы можем внедрить MeterRegistry вместо CounterService . Мы можем использовать различные типы счетчиков для сбора показателей. Счетчик является одним из счетчиков :

@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:

@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. Публикация показателей с использованием счетчиков

Теперь мы также можем публиковать метрики, используя счетчики MeterRegistry:

@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.

Обратите внимание, что счетчики не потокобезопасны, поэтому они могут быть неточными без использования чего-то вроде атомных чисел. Это было преднамеренно только потому, что дельта должна быть небольшой, а 100% точность не является целью — скорее, раннее выявление трендов.

Конечно, есть более зрелые способы записи метрик HTTP в приложении, но это простой, легкий и очень полезный способ сделать это без дополнительной сложности полноценного инструмента.

Полную реализацию этой статьи можно найти в проекте GitHub .