I usually post about Spring stuff on Twitter - you can follow me there:
Follow @baeldung1. Overview
In this tutorial we’ll integrate basic Metrics into a Spring REST API.
We’ll build out the metric functionality first using simple Servlet Filters, then using a Spring Boot Actuator.
2. The web.xml
Let’s start by registering a filter – “MetricFilter” – into the web.xml of our 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>
Note how we’re mapping the filter to cover all requests coming in – “/*” – which is of course fully configurable.
3. The Servlet Filter
Now – let’s create our custom 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); } }
Since the filter isn’t a standard bean, we’re not going to inject the metricService but instead retrieve it manually – via the ServletContext.
Also note that we’re continuing the execution of the filter chain by calling the doFilter API here.
4. Metric – Status Code Counts
Next – let’s take a look at our simple MetricService:
@Service public class MetricService { private Map<Integer, Integer> statusMetric; 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; } }
We’re using an in memory Map to hold the counts for each type of HTTP status code.
Now – to display this basic metric – we’re going to map it to a Controller method:
@RequestMapping(value = "/status-metric", method = RequestMethod.GET) @ResponseBody public Map getStatusMetric() { return metricService.getStatusMetric(); }
And here is a sample response:
{ "404":1, "200":6, "409":1 }
5. Metric – Status Codes by Request
Next – let’s record metrics for Counts by Request:
@Service public class MetricService { private Map<String, HashMap<Integer, Integer>> metricMap; public void increaseCount(String request, int status) { HashMap<Integer, Integer> statusMap = metricMap.get(request); if (statusMap == null) { statusMap = new HashMap<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; } }
We’ll display the metric results via the API:
@RequestMapping(value = "/metric", method = RequestMethod.GET) @ResponseBody public Map getMetric() { return metricService.getFullMetric(); }
Here’s how these metrics look like:
{ "GET /users": { "200":6, "409":1 }, "GET /users/1": { "404":1 } }
According to the above example the API had the following activity:
- “7” requests to “GET /users“.
- “6” of them resulted in “200” status code responses and only one in a “409”.
6. Metric – Time Series Data
Overall counts are somewhat useful in an application, but if the system has been running for a significant amount of time – it’s hard to tell what these metrics actually mean.
You need the context of time in order for the data to make sense and be easily interpreted.
Let’s now build a simple time-based metric; we’ll keep record of status code counts per minute – as follows:
@Service public class MetricService{ private Map<String, HashMap<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()); HashMap<Integer, Integer> statusMap = timeMap.get(time); if (statusMap == null) { statusMap = new HashMap<Integer, Integer>(); } Integer count = statusMap.get(status); if (count == null) { count = 1; } else { count++; } statusMap.put(status, count); timeMap.put(time, statusMap); } }
And the 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; Map<Integer, Integer> tempMap; for (Entry<String, HashMap<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; }
We’re now going to map this to the API:
@RequestMapping(value = "/metric-graph-data", method = RequestMethod.GET) @ResponseBody public Object[][] getMetricData() { return metricService.getGraphData(); }
And finally – we’re going to render it out using 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. Using Spring Boot Actuator
In the next few sections, we’re going to hook into the Actuator functionality in Spring Boot to present our metrics.
First – we’ll need to add the actuator dependency to our pom.xml:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
8. The MetricFilter
Next – we can turn the MetricFilter – into an actual Spring bean:
@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); } }
This is of course a minor simplification – but one that’s worth doing to get rid of the previously manual wiring of dependencies.
9. Using CounterService
Let’s now use the CounterService to count occurrences for each Status Code:
@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. Export Metrics using MetricRepository
Next – we need to export the metrics – using the 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); } }
Note that we’re storing counts of status codes per minute.
11. 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<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; }
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] ]
12. Conclusion
This article we explored a few simple ways to build out some basic metrics capabilities into your 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.
There are of course more mature ways to record HTTP metrics in an application, but this is a simple, lightweight and super-useful way to do it without the extra complexity of a full-fledged tool.
The full implementation of this article can be found in the github project – this is an Eclipse based project, so it should be easy to import and run as it is.