Métricas para sua API REST do Spring

Métricas para sua API REST do Spring

*1. Visão geral *

Neste tutorial, integraremos* Métricas básicas em uma API REST do Spring *.

Construiremos a funcionalidade da métrica primeiro usando filtros de servlet simples e, em seguida, usando um atuador de inicialização por mola.

*2. O web.xml *

Vamos começar registrando um filtro - "MetricFilter" - no web.xml do nosso aplicativo:

<filter>
    <filter-name>metricFilter</filter-name>
    <filter-class>org..web.metric.MetricFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>metricFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Observe como estamos mapeando o filtro para cobrir todas as solicitações recebidas - _ “/*” _ - que, é claro, são totalmente configuráveis.

*3. O filtro de servlet *

Agora - vamos criar nosso filtro personalizado:

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

Como o filtro não é um bean padrão, não injetaremos o metricService, mas o recuperaremos manualmente - por meio do ServletContext.

Observe também que estamos continuando a execução da cadeia de filtros chamando a API doFilter aqui.

===* 4. Métrica - Contagens de códigos de status *

A seguir - vamos dar uma olhada no nosso MetricService simples:

@Service
public class MetricService {

    private ConcurrentMap<Integer, Integer> statusMetric;

    public MetricService() {
        statusMetric = new ConcurrentHashMap<Integer, Integer>();
    }

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

Estamos usando um ConcurrentMap na memória para armazenar as contagens de cada tipo de código de status HTTP.

Agora - para exibir essa métrica básica - vamos mapeá-la para um método Controller:

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

E aqui está uma resposta de amostra:

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

===* 5. Métrica - códigos de status por solicitação *

Em seguida -* vamos registrar métricas para contagens por solicitação *:

@Service
public class MetricService {

    private ConcurrentMap<String, ConcurrentHashMap<Integer, Integer>> metricMap;

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

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

Exibiremos os resultados da métrica por meio da API:

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

Veja como são essas métricas:

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

De acordo com o exemplo acima, a API teve a seguinte atividade:

  • "7" solicita "GET /users" *"6" deles resultaram em "200" respostas de código de status e apenas uma em "409"

===* 6. Métrica - Dados de séries temporais *

As contagens gerais são úteis em um aplicativo, mas se o sistema estiver em execução por um período de tempo significativo -* é difícil dizer o que essas métricas realmente significam *.

Você precisa do contexto de tempo para que os dados façam sentido e sejam facilmente interpretados.

Vamos agora criar uma métrica simples baseada em tempo; manteremos um registro das contagens de códigos de status por minuto, da seguinte maneira:

@Service
public class MetricService{

    private ConcurrentMap<String, ConcurrentHashMap<Integer, Integer>> 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<Integer, Integer> statusMap = timeMap.get(time);
        if (statusMap == null) {
            statusMap = new ConcurrentHashMap<Integer, Integer>();
        }

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

E o _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;
    ConcurrentMap<Integer, Integer> tempMap;
    for (Entry<String, ConcurrentHashMap<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++;
    }

    return result;
}

Agora, vamos mapear isso para a API:

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

E, finalmente, vamos exibi-lo usando o 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. Usando o Atuador Spring Boot 1.x *

Nas próximas seções, abordaremos a funcionalidade do atuador no Spring Boot para apresentar nossas métricas.

Primeiro - precisamos adicionar a dependência do atuador ao nosso pom.xml:

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

====* 7.1 O MetricFilter *

Em seguida - podemos transformar o MetricFilter - em um Spring bean real:

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

Essa é, obviamente, uma pequena simplificação - mas vale a pena fazer para se livrar da fiação manual das dependências.

====* 7.2 Usando CounterService *

Vamos agora usar o CounterService para contar ocorrências para cada Código de Status:

@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. Exportar métricas usando MetricRepository *

Em seguida - precisamos exportar as métricas - usando o MetricRepository:

@Service
public class MetricService {

    @Autowired
    private MetricRepository repo;

    private List<ArrayList<Integer>> statusMetric;
    private List<String> statusList;

    @Scheduled(fixedDelay = 60000)
    private void exportMetrics() {
        Metric<?> metric;
        ArrayList<Integer> statusCount = new ArrayList<Integer>();
        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);
    }
}

Observe que estamos armazenando contagens de* códigos de status por minuto *.

7.4. Bota de primavera PublicMetrics

Também podemos usar o Spring Boot PublicMetrics para exportar métricas em vez de usar nossos próprios filtros - da seguinte maneira:

Primeiro, temos nossa tarefa agendada para exportar métricas por minuto :

@Autowired
private MetricReaderPublicMetrics publicMetrics;

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

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

Obviamente, precisamos inicializar a lista de códigos de status HTTP:

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

E então vamos atualizar as métricas com status code count :

private void updateMetrics(Metric<?> counterMetric, ArrayList<Integer> 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<Integer> statusCount) {
    if (!statusList.contains(status)) {
        statusList.add(status);
        statusCount.add(0);
    }
}

Observe que:

  • O nome do contador de status PublicMetics começa com "counter.status", por exemplo "counter.status.200.root"

  • Mantemos um registro da contagem de status por minuto em nossa lista statusMetricsByMinute

    *Podemos exportar nossos dados coletados para desenhá-los em um gráfico* - da seguinte forma:
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<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. Desenhar gráfico usando métricas *

Finalmente - vamos representar essas métricas por meio de uma matriz de duas dimensões - para que possamos representá-las graficamente:

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

E aqui está o nosso método Controller _getMetricData () _:

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

E aqui está uma resposta de amostra:

[
    ["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. Usando o Atuador Spring Boot 2.x *

No Spring Boot 2, as APIs do Spring Actuator testemunharam uma grande mudança.* As métricas da própria Spring foram substituídas por https://www..com/micrometer [Micrometer]. *Então, vamos escrever o mesmo exemplo de métrica acima com Micrometer.

8.1. Substituindo CounterService por MeterRegistry*

Como nosso aplicativo Spring Boot já depende do acionador de partida do atuador, o micrômetro já está configurado automaticamente. Podemos injetar MeterRegistry em vez de CounterService. Podemos usar diferentes tipos de Meter para capturar métricas. O Counter é um dos medidores:

@Autowired
private MeterRegistry registry;

private List<String> 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. Exportando contagens usando MeterRegistry *

No micrômetro, podemos exportar os valores Counter usando _MeterRegistry: _

@Scheduled(fixedDelay = 60000)
private void exportMetrics() {
    ArrayList<Integer> statusCount = new ArrayList<Integer>();
    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. Publicando métricas usando Meters *

Agora também podemos publicar métricas usando os medidores de _MeterRegistry: _

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

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

private void updateMetrics(final Meter counterMetric, final ArrayList<Integer> 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. Conclusão *

Neste artigo, exploramos algumas maneiras simples de desenvolver alguns recursos básicos de métricas em um aplicativo da Web Spring.

Observe que os contadores* não são seguros para threads * - portanto, eles podem não ser exatos sem usar algo como números atômicos. Isso foi deliberado apenas porque o delta deveria ser pequeno e 100% de precisão não é o objetivo - é melhor detectar tendências cedo.

É claro que existem maneiras mais maduras de registrar métricas HTTP em um aplicativo, mas essa é uma maneira simples, leve e super útil de fazê-lo sem a complexidade extra de uma ferramenta completa.

A implementação completa deste artigo pode ser encontrada em o projeto GitHub.