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.