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.