Metriche per la tua API REST di primavera

REST Top

Ho appena annunciato il nuovo corso Learn Spring , incentrato sui fondamenti di Spring 5 e Spring Boot 2:

>> SCOPRI IL CORSO

1. Panoramica

In questo tutorial, integreremo le metriche di base in un'API REST di Spring .

Svilupperemo la funzionalità metrica prima utilizzando semplici filtri servlet, quindi utilizzando un attuatore Spring Boot.

2. Il file web.xml

Cominciamo registrando un filtro - " MetricFilter " - nel web.xml della nostra app:

 metricFilter org.baeldung.web.metric.MetricFilter   metricFilter /* 

Nota come stiamo mappando il filtro per coprire tutte le richieste in arrivo - "/ *" - che è ovviamente completamente configurabile.

3. Il filtro servlet

Ora, creiamo il nostro filtro personalizzato:

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

Poiché il filtro non è un bean standard, non inseriremo metricService ma lo recupereremo manualmente, tramite ServletContext .

Si noti inoltre che stiamo continuando l'esecuzione della catena di filtri chiamando qui l'API doFilter .

4. Metrica - Conteggi codici di stato

Successivamente, diamo un'occhiata al nostro semplice 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; } }

Stiamo usando una ConcurrentMap in memoria per contenere i conteggi per ogni tipo di codice di stato HTTP.

Ora, per visualizzare questa metrica di base, la mapperemo a un metodo Controller :

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

Ed ecco una risposta di esempio:

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

5. Metrica - Codici di stato su richiesta

Avanti: registriamo le metriche per i conteggi per richiesta :

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

Mostreremo i risultati della metrica tramite l'API:

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

Ecco come appaiono queste metriche:

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

Secondo l'esempio sopra, l'API aveva la seguente attività:

  • "7" richiede a "GET / users "
  • "6" di loro hanno dato come risultato "200" risposte del codice di stato e solo una in un "409"

6. Metrica - Dati di serie storiche

I conteggi complessivi sono in qualche modo utili in un'applicazione, ma se il sistema è in esecuzione per un periodo di tempo significativo, è difficile dire cosa significano effettivamente queste metriche .

È necessario il contesto temporale affinché i dati abbiano un senso e possano essere facilmente interpretati.

Creiamo ora una semplice metrica basata sul tempo; terremo un registro dei conteggi dei codici di stato al minuto, come segue:

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

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

Ora lo mapperemo all'API:

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

E infine, lo visualizzeremo utilizzando Google Charts:

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

7. Utilizzo dell'attuatore Spring Boot 1.x.

Nelle prossime sezioni, ci collegheremo alla funzionalità Attuatore in Spring Boot per presentare le nostre metriche.

Primo: dobbiamo aggiungere la dipendenza dell'attuatore al nostro pom.xml :

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

7.1. Il MetricFilter

Successivamente, possiamo trasformare MetricFilter in un vero 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); } }

Questa è, ovviamente, una piccola semplificazione, ma vale la pena fare per sbarazzarsi del cablaggio delle dipendenze precedentemente manuale.

7.2. Utilizzando CounterService

Usiamo ora il CounterService per contare le occorrenze per ogni codice di stato:

@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. Esporta metriche utilizzando MetricRepository

Successivamente, dobbiamo esportare le metriche, utilizzando 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); } }
    

Tieni presente che memorizziamo i conteggi dei codici di stato al minuto .

7.4. Spring Boot PublicMetrics

Possiamo anche utilizzare Spring Boot PublicMetrics per esportare le metriche invece di utilizzare i nostri filtri, come segue:

Innanzitutto, abbiamo la nostra attività pianificata per esportare le metriche al minuto :

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

Ovviamente dobbiamo inizializzare l'elenco dei codici di stato HTTP:

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

E poi aggiorneremo effettivamente le metriche con il conteggio del codice di stato :

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

Nota che:

  • PublicMetics status counter name start with “counter.status” for example “counter.status.200.root
  • We keep record of status count per minute in our list statusMetricsByMinute

We can export our collected data to draw it in a graph – as follows:

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 = j ? last.get(j - 1) : 0); } while (j < colCount) { result[i][j] = 0; j++; } last = minuteOfStatuses; } return result; }

7.5. Draw Graph Using Metrics

Finally – let's represent these metrics via a 2 dimension array – so that we can then graph them:

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

And here is our Controller method getMetricData():

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

And here is a sample response:

[ ["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. Using Spring Boot 2.x Actuator

In Spring Boot 2, Spring Actuator's APIs witnessed a major change. Spring's own metrics have been replaced with Micrometer. So let's write the same metrics example above with Micrometer.

8.1. Replacing CounterService With MeterRegistry

As our Spring Boot application already depends on the Actuator starter, Micrometer is already auto-configured. We can inject MeterRegistry instead of CounterService. We can use different types of Meter to capture metrics. The Counter is one of the Meters:

@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. Exporting Counts Using MeterRegistry

In Micrometer, we can export the Counter values using MeterRegistry:

@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. Publishing Metrics Using Meters

Now we can also publish Metrics using MeterRegistry's Meters:

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

In this article, we explored a few simple ways to build out some basic metrics capabilities into a Spring web application.

Note that the counters aren't thread-safe – so they might not be exact without using something like atomic numbers. This was deliberate just because the delta should be small and 100% accuracy isn't the goal – rather, spotting trends early is.

Ci sono ovviamente modi più maturi per registrare le metriche HTTP in un'applicazione, ma questo è un modo semplice, leggero e super utile per farlo senza la complessità aggiuntiva di uno strumento completo.

L'implementazione completa di questo articolo può essere trovata nel progetto GitHub.

REST fondo

Ho appena annunciato il nuovo corso Learn Spring , incentrato sui fondamenti di Spring 5 e Spring Boot 2:

>> SCOPRI IL CORSO