Metriken für Ihre Spring REST-API

REST Top

Ich habe gerade den neuen Learn Spring- Kurs angekündigt , der sich auf die Grundlagen von Spring 5 und Spring Boot 2 konzentriert:

>> Überprüfen Sie den Kurs

1. Übersicht

In diesem Tutorial werden grundlegende Metriken in eine Spring REST-API integriert .

Wir werden die Metrikfunktionalität zuerst mit einfachen Servlet-Filtern und dann mit einem Spring Boot Actuator ausbauen.

2. Die web.xml

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

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

Beachten Sie, wie wir den Filter so zuordnen, dass er alle eingehenden Anforderungen abdeckt - "/ *" - was natürlich vollständig konfigurierbar ist.

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 der Filter keine Standard-Bean ist, wird der metricService nicht eingefügt, sondern manuell abgerufen - über den ServletContext .

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

4. Metrik - Statuscode zählt

Als nächstes werfen wir einen Blick auf unseren einfachen 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; } }

Wir verwenden eine ConcurrentMap im Speicher , um die Anzahl für jeden Typ von HTTP-Statuscode zu speichern .

Um diese grundlegende Metrik anzuzeigen, werden wir sie jetzt einer Controller- Methode zuordnen :

@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 auf Anfrage

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

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

Wir zeigen die Metrikergebnisse über die API an:

@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" -Anfragen an "GET / Benutzer "
  • "6" von ihnen führten zu "200" Statuscode-Antworten und nur eine in einem "409"

6. Metrik - Zeitreihendaten

Die Gesamtanzahl ist in einer Anwendung etwas nützlich, aber wenn das System längere Zeit ausgeführt wurde, 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 werden die Anzahl der Statuscodes pro Minute wie folgt aufzeichnen:

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

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

Wir werden dies jetzt 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:

  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. Verwenden des Spring Boot 1.x-Stellantriebs

In den nächsten Abschnitten werden wir uns mit der Actuator-Funktionalität in Spring Boot befassen, um unsere Metriken vorzustellen.

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

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

7.1. Der MetricFilter

Als nächstes können wir den MetricFilter in eine echte Spring Bean 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 kleine Vereinfachung - aber eine, die es wert ist, getan zu werden, um die zuvor manuelle Verkabelung von Abhängigkeiten zu beseitigen.

7.2. Mit Counter

Verwenden wir jetzt den CounterService, um Vorkommen für jeden Statuscode zu zählen:

@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. Export Metrics Using MetricRepository

Next – we need to export the metrics – using the 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); } }
    

Note that we're storing counts of status codes per minute.

7.4. Spring Boot PublicMetrics

We can also use Spring Boot PublicMetrics to export metrics instead of using our own filters – as follows:

First, we have our scheduled task to export metrics per 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); }
    

We, of course, need to initialize the list of HTTP status codes:

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

And then we're going to actually update the metrics with status code count:

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

Note that:

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

Es gibt natürlich ausgereiftere Möglichkeiten, HTTP-Metriken in einer Anwendung aufzuzeichnen, aber dies ist 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 im GitHub-Projekt.

REST unten

Ich habe gerade den neuen Learn Spring- Kurs angekündigt , der sich auf die Grundlagen von Spring 5 und Spring Boot 2 konzentriert:

>> Überprüfen Sie den Kurs