Рубрики
Без рубрики

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

Как реализовать и отобразить метрики HTTP в API Spring REST – подсчеты кода состояния, подсчеты по запросу, данные временных рядов и т. Д.

Автор оригинала: Eugen Paraschiv.

1. Обзор

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

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

2. web.xml

Давайте начнем с регистрации фильтра – ” MetricFilter ” – в web.xml нашего приложения:


    metricFilter
    org.baeldung.web.metric.MetricFilter


    metricFilter
    /*

Обратите внимание, как мы сопоставляем фильтр, чтобы охватить все поступающие запросы– “/*” – что, конечно, полностью настраивается.

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

Поскольку фильтр не является стандартным компонентом, мы не собираемся вводить metricService , а вместо этого извлекаем его вручную – через ServletContext .

Также обратите внимание, что мы продолжаем выполнение цепочки фильтров, вызывая API doFilter здесь.

4. Метрика – Количество Кодов Состояния

Далее – давайте взглянем на наш простой MetricService :

@Service
public class MetricService {

    private ConcurrentMap 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 :

@RequestMapping(value = "/status-metric", method = RequestMethod.GET)
@ResponseBody
public Map getStatusMetric() {
    return metricService.getStatusMetric();
}

А вот пример ответа:

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

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

Далее – давайте запишем метрики для подсчетов по запросу :

@Service
public class MetricService {

    private ConcurrentMap> metricMap;

    public void increaseCount(String request, int status) {
        ConcurrentHashMap 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:

@RequestMapping(value = "/metric", method = RequestMethod.GET)
@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 ConcurrentMap> timeMap;
    private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");

    public void increaseCount(String request, int status) {
        String time = dateFormat.format(new Date());
        ConcurrentHashMap 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 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;
    ConcurrentMap tempMap;
    for (Entry> 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++;
    }

    return result;
}

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

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

И, наконец, мы собираемся визуализировать его с помощью Google Charts:



Metric Graph





    

7. Использование Пружинного ботинка 1.x Привод

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

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


    org.springframework.boot
    spring-boot-starter-actuator

7.1. Метрический фильтр

Далее – мы можем превратить MetricFilter – в настоящий весенний боб:

@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. Использование Встречного Сервиса

Теперь давайте использовать службу Counter для подсчета вхождений для каждого кода состояния:

@Service
public class MetricService {

    @Autowired
    private CounterService counter;

    private List statusList;

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

7.3. Экспорт Метрик С Помощью Репозитория Метрик

Далее – нам нужно экспортировать метрики – используя MetricRepository :

@Service
public class MetricService {

    @Autowired
    private MetricRepository repo;

    private List> statusMetric;
    private List statusList;
    
    @Scheduled(fixedDelay = 60000)
    private void exportMetrics() {
        Metric metric;
        ArrayList 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

Мы также можем использовать Spring Boot Public Metrics для экспорта метрик вместо использования наших собственных фильтров – следующим образом:

Во-первых, у нас есть запланированная задача экспортировать показатели в минуту :

@Autowired
private MetricReaderPublicMetrics publicMetrics;

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

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

Нам, конечно, нужно инициализировать список кодов состояния HTTP:

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

А затем мы фактически обновим метрики с помощью status code count :

private void updateMetrics(Metric counterMetric, ArrayList statusCount) {
    String status = "";
    int index = -1;
    int oldCount = 0;

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

private void appendStatusIfNotExist(String status, ArrayList statusCount) {
    if (!statusList.contains(status)) {
        statusList.add(status);
        statusCount.add(0);
    }
}

Обратите внимание, что:

  • Общедоступные метрики имя счетчика состояния начинается с ” counter.status “например” counter.status.200.корень
  • Мы ведем учет количества статусов в минуту в нашем списке Показатели статуса по минутам

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

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() - (60000 * (rowCount - i))));
    }

    List minuteOfStatuses;
    List last = new ArrayList();

    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. Нарисуйте График С Использованием Метрик

Наконец, давайте представим эти метрики с помощью массива 2 измерений, чтобы затем мы могли их отобразить:

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 temp;
    for (int i = 1; i < rowCount; i++) {
        temp = statusMetric.get(i - 1);
        result[i][0] = dateFormat.format
          (new Date(current.getTime() - (60000 * (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() :

@RequestMapping(value = "/metric-graph-data", method = RequestMethod.GET)
@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. Использование Пружинного ботинка 2.Привод x

В Spring Boot 2 API приводов Spring претерпели серьезные изменения. Собственные метрики Spring были заменены на Микрометр . Итак, давайте напишем тот же пример метрики выше с Микрометром .

8.1. Замена Службы Счетчика Реестром Счетчиков

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

@Autowired
private MeterRegistry registry;

private List statusList;

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

8.2. Экспорт Счетчиков С Использованием Реестра Счетчиков

В микрометре мы можем экспортировать значения Счетчика с помощью реестра Meter:

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

8.3. Публикация Метрик С Использованием Счетчиков

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

@Scheduled(fixedDelay = 60000)
private void exportMetrics() {
    ArrayList lastMinuteStatuses = initializeStatuses(statusList.size());

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

private void updateMetrics(final Meter counterMetric, final ArrayList statusCount) {
    String status = "";
    int index = -1;
    int oldCount = 0;

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

9. Заключение

В этой статье мы рассмотрели несколько простых способов создания некоторых основных возможностей метрик в веб-приложении Spring.

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

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

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