Métriques pour votre API Spring REST

Haut REST

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS

1. Vue d'ensemble

Dans ce didacticiel, nous allons intégrer des métriques de base dans une API Spring REST .

Nous allons créer la fonctionnalité métrique d'abord en utilisant de simples filtres de servlet, puis en utilisant un actionneur Spring Boot.

2. Le web.xml

Commençons par enregistrer un filtre - « MetricFilter » - dans le web.xml de notre application:

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

Notez comment nous mappons le filtre pour couvrir toutes les demandes entrantes - «/ *» - qui est bien sûr entièrement configurable.

3. Le filtre de servlet

Maintenant, créons notre filtre personnalisé:

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

Puisque le filtre n'est pas un bean standard, nous n'allons pas injecter le metricService, mais le récupérer manuellement - via le ServletContext .

Notez également que nous continuons l'exécution de la chaîne de filtres en appelant ici l'API doFilter .

4. Métrique - Nombre de codes d'état

Ensuite - jetons un coup d'œil à notre simple 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; } }

Nous utilisons un ConcurrentMap en mémoire pour contenir les décomptes pour chaque type de code d'état HTTP.

Maintenant - pour afficher cette métrique de base - nous allons la mapper à une méthode Controller :

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

Et voici un exemple de réponse:

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

5. Métrique - Codes de statut par demande

Ensuite - enregistrons les métriques pour les comptes par demande :

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

Nous afficherons les résultats de la métrique via l'API:

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

Voici à quoi ressemblent ces métriques:

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

Selon l'exemple ci-dessus, l'API avait l'activité suivante:

  • "7" demandes à "GET / utilisateurs "
  • «6» d'entre eux ont donné lieu à «200» réponses au code de statut et une seule à «409»

6. Métrique - Données de séries chronologiques

Les décomptes globaux sont quelque peu utiles dans une application, mais si le système fonctionne depuis un certain temps, il est difficile de dire ce que ces métriques signifient réellement .

Vous avez besoin du contexte temporel pour que les données aient un sens et soient facilement interprétées.

Construisons maintenant une métrique simple basée sur le temps; nous garderons un enregistrement du nombre de codes d'état par minute - comme suit:

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

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

Nous allons maintenant mapper ceci à l'API:

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

Et enfin - nous allons le rendre à l'aide de 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. Utilisation de l'actionneur Spring Boot 1.x

Dans les prochaines sections, nous allons nous connecter à la fonctionnalité Actuator de Spring Boot pour présenter nos métriques.

Tout d'abord, nous devrons ajouter la dépendance d'actionneur à notre pom.xml :

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

7.1. Le MetricFilter

Ensuite - nous pouvons transformer le MetricFilter - en un véritable haricot de printemps:

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

C'est, bien sûr, une simplification mineure - mais qui vaut la peine d'être faite pour se débarrasser du câblage auparavant manuel des dépendances.

7.2. Utilisation de CounterService

Utilisons maintenant le CounterService pour compter les occurrences pour chaque code d'état:

@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. Exporter des métriques à l'aide de MetricRepository

Ensuite - nous devons exporter les métriques - en utilisant le 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); } }
    

Notez que nous stockons le nombre de codes d'état par minute .

7.4. Spring Boot PublicMetrics

Nous pouvons également utiliser Spring Boot PublicMetrics pour exporter des métriques au lieu d'utiliser nos propres filtres - comme suit:

Tout d'abord, nous avons notre tâche planifiée pour exporter les métriques par minute :

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

Nous devons bien sûr initialiser la liste des codes d'état HTTP:

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

Et puis nous allons mettre à jour les métriques avec le nombre de codes d'état :

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

Notez que:

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

Il existe bien sûr des moyens plus matures pour enregistrer des métriques HTTP dans une application, mais c'est un moyen simple, léger et super utile de le faire sans la complexité supplémentaire d'un outil à part entière.

L'implémentation complète de cet article se trouve dans le projet GitHub.

REST bas

Je viens d'annoncer le nouveau cours Learn Spring , axé sur les principes de base de Spring 5 et Spring Boot 2:

>> VOIR LE COURS