Metriken für Ihre Spring REST-API

1. Überblick

In diesem Lernprogramm integrieren wir grundlegende Metriken in eine Spring-REST-API .

Wir entwickeln die Metrikfunktionalität zunächst mit einfachen Servlet-Filtern und dann mit einem Spring Boot Actuator.

2. Das web.xml

Beginnen wir mit der Registrierung eines Filters - " MetricFilter " - in der web.xml unserer App:

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

Beachten Sie, wie wir den Filter so abbilden, dass er alle eingehenden Anforderungen abdeckt - "/** " .

3. Der Servlet-Filter

Jetzt erstellen wir unseren benutzerdefinierten Filter:

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

Da es sich bei dem Filter nicht um eine Standard-Bean handelt, injizieren wir den metricService nicht, sondern rufen ihn manuell ab - über den ServletContext .

Beachten Sie auch, dass wir die Ausführung der Filterkette fortsetzen, indem Sie hier die doFilter -API aufrufen.

4. Metrik - Statuscode zählt

Weiter - werfen wir einen Blick auf unseren einfachen 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;
    }
}

Wir verwenden ein ConcurrentMap im Speicher, um die Zähler für jeden HTTP-Statuscode zu speichern.

Um diese grundlegende Metrik anzuzeigen, ordnen wir sie nun einer Controller -Methode zu:

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

Und hier ist eine Beispielantwort:

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

5. Metrik - Statuscodes nach Anforderung

Next - Lassen Sie uns Metriken für Counts by Request aufzeichnen:

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

Die Metrikergebnisse werden über die API angezeigt:

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

So sehen diese Metriken aus:

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

Gemäß dem obigen Beispiel hatte die API die folgende Aktivität:

  • "7" fordert zu "GET /users " auf.

  • "6" von ihnen führten zu "200" Statuscode-Antworten und nur zu einem in einem

"409".

6. Metrik - Zeitreihendaten

Gesamtzählungen sind in einer Anwendung etwas hilfreich, aber wenn das System längere Zeit in Betrieb war - ist es schwer zu sagen, was diese Metriken tatsächlich bedeuten .

Sie benötigen den Zeitkontext, damit die Daten sinnvoll sind und leicht interpretiert werden können.

Lassen Sie uns nun eine einfache zeitbasierte Metrik erstellen. Wir erfassen die Anzahl der Statuscodes pro Minute - wie folgt:

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

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

Wir werden dies nun der API zuordnen:

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

Und schließlich - wir werden es mit Google Charts rendern:

<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. Spring Boot Actuator verwenden

In den nächsten Abschnitten werden wir uns mit der Actuator-Funktionalität in Spring Boot beschäftigen, um unsere Metriken zu präsentieren.

Zuerst müssen wir die Aktuatorabhängigkeit zu unserer pom.xml hinzufügen:

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

8. Der MetricFilter

Weiter - wir können den MetricFilter - in eine echte Spring-Bohne verwandeln:

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

Dies ist natürlich eine geringfügige Vereinfachung - aber es lohnt sich, die bisher manuelle Verdrahtung von Abhängigkeiten zu beseitigen.

9. CounterService verwenden

Lassen Sie uns jetzt den CounterService verwenden, um Vorkommen für jeden Statuscode zu zählen:

@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. Metriken mit MetricRepository exportieren

Als Nächstes müssen wir die Metriken mithilfe des MetricRepository exportieren:

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

Beachten Sie, dass wir Zählungen von Statuscodes pro Minute speichern.

11. Spring Boot PublicMetrics

Wir können Spring Boot PublicMetrics auch verwenden, um Metriken zu exportieren, anstatt unsere eigenen Filter zu verwenden - wie folgt:

Erstens haben wir unsere geplante Aufgabe, Metriken pro Minute zu exportieren ** :

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

Wir müssen natürlich die Liste der HTTP-Statuscodes initialisieren:

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

Und dann werden wir die Metriken tatsächlich mit status code count aktualisieren:

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

Beachten Sie, dass:

  • PublicMetics Statuszähler beginnt mit " counter.status " für

Beispiel „ counter.status.200.root “ ** Wir halten die Anzahl der Status pro Minute in unserer Liste fest

statusMetricsByMinute

Wir können unsere gesammelten Daten exportieren, um sie in eine Grafik zu zeichnen - wie folgt:

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. Diagramm mit Metriken zeichnen

Schließlich - lassen Sie uns diese Metriken über ein zweidimensionales Array darstellen, damit wir sie grafisch darstellen können:

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

Und hier ist unsere Controller-Methode getMetricData () :

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

Und hier ist eine Beispielantwort:

----[   ["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. Fazit

In diesem Artikel haben wir einige einfache Möglichkeiten erläutert, um einige grundlegende Metrikfunktionen in Ihre Spring-Webanwendung zu integrieren.

Beachten Sie, dass die Zähler nicht fadensicher sind - daher sind sie möglicherweise nicht genau, ohne etwas wie Ordnungszahlen zu verwenden. Dies war beabsichtigt, nur weil das Delta klein sein sollte und eine 100% ige Genauigkeit nicht das Ziel ist - eher, Trends frühzeitig zu erkennen.

Es gibt natürlich mehr ausgereifte Methoden zum Aufzeichnen von HTTP-Metriken in einer Anwendung. Dies ist jedoch eine einfache, leichte und äußerst nützliche Möglichkeit, dies ohne die zusätzliche Komplexität eines vollwertigen Tools zu tun.

Die vollständige Implementierung dieses Artikels finden Sie in des GitHub-Projekts Seien Sie einfach zu importieren und auszuführen, wie es ist.