I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE COURSE

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

We’re using an in memory ConcurrentMap 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 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;
    }
}

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

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

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

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

private ArrayList<Integer> initializeStatuses(int size) {
    ArrayList<Integer> counterList = new ArrayList<Integer>();
    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<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);
    }
}

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

13. 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 a Maven-based project, so it should be easy to import and run as it is.

I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE LESSONS

newest oldest most voted
Notify of
Hinotori
Guest
Hinotori

great! is there a way to do this with java config, or perhaps without an old and useless web.xml???
tks

Eugen Paraschiv
Guest

The web.xml can be entirely replaced – the only thing we’re doing with it is registering the filter, so yes – it’s certainly possible to register the filter by using a WebApplicationInitializer. Hope that helps. Cheers,
Eugen.

bob woodward
Guest

if your container supports it, you can use the @WebFilter annotation to register your filter.

Eugen Paraschiv
Guest

Yeah, I considered going XML-less but I figured it wasn’t really the point of the article, and even today XML config is a bit more common, so I went with that. I do have the no-XML servlet config on my TODO list for a while though, I might get to that soon. Cheers,
Eugen.

bob woodward
Guest

i’ve done something similar. but i exposed the stats through JMX. spring’s @ManagedAttribute makes it easy. jconsole, visualvm or many standard monitoring tool can display the results and even alert on them.

Eugen Paraschiv
Guest

JMX is definitely a good way to go – and this isn’t replacing that necessarily. I like having these kinds of simple, easily accessible and lightweight metrics over HTTP, but on a production system, I’d expose them via JMX as well.
Any suggestions for other metrics to cover? Cheers,
Eugen.

bob woodward
Guest

I like to gather metrics on all my service methods.
count, elapsed time (min, max, total) and exceptions by type and count.
I also track the currently executing service(s)
spring aop is perfect for this.

Eugen Paraschiv
Guest

Yeah, AOP makes sense if you’re tracking these at the service level. Cheers,
Eugen.

David Kiss
Guest
David Kiss

In some cases it’s better to roll out your own metrics collecting framework, and in others it makes sense to use an existing tool. For these other cases, I’ve been happy using an open source tool called JavaMelody (https://code.google.com/p/javamelody/). It integrates well with Spring using AOP and can provide extensive metrics on HTTP requests, Spring beans, SQL queries, Quartz, etc.

Eugen Paraschiv
Guest

I’ve seen Java Melody in past projects and it looked useful, although I have little first hand experience with. Might be interesting to explore in an article.

Mark Thomas
Guest
Mark Thomas

I personally like to use https://dropwizard.github.io/metrics/3.1.0/ along with some of the 3rd-party integrations (i.e. Spring). Also, the Time Series Data code has a threading issue as DateFormat and SimpleDateFormat are not thread-safe.

Stephane
Guest
Stephane

I couldn’t see the relevant source code in the linked to git hub repository. Also, I wonder how the above code does not trigger on NPE on the uninstantiated metricMap member for example. On a side note, I wonder why my metrics service bean seams to be instantiated more than once.

Eugen Paraschiv
Guest

Hey Stephane – these are good questions. First – I double checked and the code does exist in the linked repo – what part are you referring to exactly? Second – the map is instantiated in the constructor – I just skipped over some of this low level detail in the example here, because overall the full implementation is quite long (as you can see over in github). Not super long – just a bit to long for an article. However, I added the constructor above so that there’s no confusion about that part. Finally – the service isn’t defined… Read more »

krocodl
Guest
krocodl

About MetricService:
1) why do you use HashMap in multi thread code??? It is not thread safe collection
2) counter update is such manner is not thread safe

Eugen Paraschiv
Guest

No it’s definitely not; however – two notes here. One – this rough metrics implementation isn’t meant to be exact. If strict, exact metrics are a requirement for your particular system – then using a ConcurrentHashMap here would be fine. But for a POC, the focus is getting the data and the trends, and skipping a data point is not a big issue.
Also – the conclusion of the article makes that clear (hopefully). Cheers,
Eugen.

Brian
Guest
Brian

Agree accuracy is not important, but it used to be that HashMap could infinite loop if two threads did an add and both caused the table to be expanded. Not sure w/ Java 8, HashMap is too challenging to analyze quickly.

Brian
Guest
Brian

btw, great post here though. HashMap issue is just picking at nits. 🙂

Eugen Paraschiv
Guest

I wasn’t aware of the infinite loop problem – in that case, it would make more sense to upgrade the solution – thanks for pointing that out. Also – this kind of feedback is the way things get better, so definitely appreciated. Cheers,
Eugen.