Mesures pour votre API REST Spring

1. Vue d’ensemble

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

Nous allons d’abord créer la fonctionnalité de métrique à l’aide de filtres de servlet simples, puis d’un actionneur de démarrage Spring.

2. Le web.xml

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

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

Notez comment nous mappons le filtre pour couvrir toutes les demandes entrantes - “/** ” - ce qui est bien entendu 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);
    }
}

Le filtre n’étant pas un bean standard, nous n’allons pas injecter le metricService , mais le récupérer manuellement - via le ServletContext .

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

4. Métrique - Nombre de codes de statut

Ensuite, jetons un coup d’œil à notre simple MetricService :

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

Nous utilisons un ConcurrentMap en mémoire pour contenir les nombres pour chaque type de code de statut HTTP.

Maintenant, pour afficher cette métrique de base, nous allons l’associer à 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

Suivant - enregistrons les métriques pour Nombre par demande :

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

Nous afficherons les résultats de 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 /users “.

  • «6» ont abouti à «200» réponses du code d’état et une seule réponse sur une

"409".

6. Métrique - Données de série chronologique

Les comptes globaux sont quelque peu utiles dans une application, mais si le système fonctionne depuis assez longtemps - 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 une trace du nombre de codes de statut par minute - comme suit:

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

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

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 restituer en utilisant 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. Utilisation de l’actionneur de démarrage à ressort

Dans les sections suivantes, nous allons intégrer la fonctionnalité Actionneur dans Spring Boot pour présenter nos métriques.

Premièrement, nous devrons ajouter la dépendance de l’actionneur à notre pom.xml :

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

8. Le MetricFilter

Ensuite - nous pouvons transformer le MetricFilter - en un véritable haricot 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);
    }
}

Il s’agit bien entendu d’une simplification mineure, mais qui mérite d’être supprimée du câblage précédemment manuel des dépendances.

9. Utiliser CounterService

Utilisons maintenant CounterService pour compter les occurrences de chaque code de statut:

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

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

Notez que nous enregistrons des comptes de codes d’état par minute .

11. Spring Boot PublicMetrics

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

Premièrement, nous avons notre tâche planifiée pour exporter les métriques par minute :

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

Nous devons bien sûr initialiser la liste des codes d’état 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;
}

Ensuite, nous mettrons à jour les métriques avec nombre de codes d’état :

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

Notez que:

  • Le nom du compteur de statut PublicMetics commence par « counter.status » pour

exemple “ counter.status.200.root “ ** Nous gardons une trace du nombre de statuts par minute dans notre liste

statusMetricsByMinute

Nous pouvons exporter nos données collectées pour les dessiner dans un graphique - comme suit:

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

12. Dessiner un graphique à l’aide de métriques

Enfin - représentons ces métriques via un tableau à 2 dimensions - pour pouvoir ensuite les représenter graphiquement:

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

Et voici notre méthode de contrôleur getMetricData () :

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

Et voici un exemple de réponse:

----[   ["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]]----

13. Conclusion

Dans cet article, nous avons exploré quelques méthodes simples pour développer certaines fonctionnalités de métriques de base dans votre application Web Spring.

Notez que les compteurs ne sont pas thread-safe - ils pourraient donc ne pas être exacts sans utiliser quelque chose comme des nombres atomiques. Cela a été délibéré simplement parce que le delta devrait être petit et que la précision à 100% n’est pas l’objectif recherché.

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

La mise en œuvre complète de cet article est disponible sur le projet GitHub - il s’agit d’un projet basé sur Maven, il devrait être facile à importer et à exécuter tel quel.