Instrumenting a Java Web Application with JMX
Telemetry java jmx
Published: 2013-11-25
Instrumenting a Java Web Application with JMX

Java Management Extensions (JMX) is a technology for managing and monitoring Java applications. JMX instructs application developers how to instrument their applications to expose metrics and management mechanisms, and instructs operations teams how to collect metrics and manage Java applications. JMX is ubiquitous: there are a large number of off-the-shelf commercial and open source tools which speak JMX.

This article is intended for application developers to introduce JMX, to teach them how to create some simple metrics via JMX, and to encourage them to thoroughly instrument their applications with JMX.; This article uses only J2SE 5 and the Java Servlet specification. There may be better, more efficient, or more appropriate ways to add JMX counters to applications using other frameworks. For example, Spring developers should read http://static.springsource.org/spring/docs/2.0.x/reference/jmx.html.

Why to Instrument Your Application Using JMX

High-quality monitoring extends far beyond OS-level metrics such as CPU, memory, and disk space. Every non-trivial application has a collection of metrics which are critical to understanding how the application is behaving in production. There is no way for any off-the-shelf tool to collect these metrics, as they are internal to the application. Some examples of these metrics are:

  • Cache hits and misses for an internal, in-memory cache
  • Number of currently logged-in users
  • Authentication successes and failures

Standardizing the way applications perform instrumentation allows us to easily integrate tools such as Graphite to enable rapid data aggregation, charting, dashboarding, and eventing. For example, the below custom dashboard was built in Graphite in less than an hour:

Graphite Dashboard

JMX for Application Instrumentation

JMX is a complicated technology which covers far more than just application instrumentation. However, this tutorial focuses on the basics of what an application developer needs to know to implement instrumentation for his application. If you would like to know more, please read the Java JMX tutorial.

To instrument an application, the developer must do the following:

  1. Write one or more Managed Beans, or MBeans, which are Java objects which contain the metrics that the application exposes
  2. Register the MBeans with the platform MBean server

A Standard MBean is defined by writing a Java interface whose name ends with MBean and a Java class which implements this interface. For example:

1
2
3
4
5
6
7
8
9
public interface HelloMBean
{
    public int getCacheSize();
}

public class Hello implements HelloMBean
{
    public int getCacheSize() { /* return cache size */ }
}

For the MBean to be visible to management applications, it must be registered with the platform MBean server. This is typically done with code that looks like:

1
2
3
4
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
ObjectName name = new ObjectName("...");
Hello mbean = new Hello();
mbs.registerMBean(mbean, name);

MBeans may also generate notifications to signal state changes, detected events, or problems. This can be used to move from a polling to a push-based update mechanism. This is done by having the MBean class inherit from NotificationBroadcasterSupport and calling notify().

JmxCalcService – A Simple Web Application Instrumented with JMX

We will start with a simple web service called JmxCalcService which supports two URLs, add and subtract. These URLs are implemented by two separate servlets, com.morningstar.jmxcalcservice.servlet.AddServlet and com.morningstar.jmxcalcservice.servlet.SubtractServlet.

Here is the implementation of AddServlet (SubtractServlet is virtually identical):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.morningstar.jmxcalcservice.servlet;

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class AddServlet extends HttpServlet
{
    public void doGet(HttpServletRequest request,
                      HttpServletResponse response)
        throws ServletException, IOException
    {
        int val1 = Integer.parseInt(request.getParameter("val1"));
        int val2 = Integer.parseInt(request.getParameter("val2"));
        int result = val1+val2;
        PrintWriter out = response.getWriter();
        out.println(result);
        out.flush();
        out.close();
    }
}

These servlets are bound to URLs using the WEB-INF/web.xml file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE web-app PUBLIC
                  "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
                  "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
    <display-name>jmxcalcservice -- A simple calculation web service instrumented via JMX</display-name>
    <servlet>
        <servlet-name>add</servlet-name>
        <servlet-class>com.morningstar.jmxcalcservice.servlet.AddServlet</servlet-class>
    </servlet>
    <servlet>
        <servlet-name>subtract</servlet-name>
        <servlet-class>com.morningstar.jmxcalcservice.servlet.SubtractServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>add</servlet-name>
        <url-pattern>/add</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>subtract</servlet-name>
        <url-pattern>/subtract</url-pattern>
    </servlet-mapping>
</web-app>

JmxCalcService may be tested as follows:

1
2
3
% mvn jetty:run
% curl 'http://localhost:8080/add?val1=5&val2=8'
% curl 'http://localhost:8080/subtract?val1=9&val2=4'

View JMX Instrumentation Using JConsole

The JVM is already instrumented with JMX, and the Servlet container often is (although typically the instrumentation on the latter must be explicitly enabled). We can view this instrumentation using JConsole. Let’s start up the web service and then view its instrumentation. Be sure to start the web service first so that JConsole can find the process:

1
2
% mvn jetty:run
% jconsole

Find the Jetty process and click “Connect”:

Find Jetty Process

JConsole opens up a window which is updated in real-time with the state of the application:

JConsole Window

You can view individual MBeans by clicking on the MBeans tab. For example, we can view the value of the java.lang.ClassLoading.LoadedClassCount attribute as follows:

MBeans Tab

Adding Metrics to JmxCalcService

We will add two metrics to JmxCalcService: the number of times that add is called, and the number of times that subtract is called. These metrics will be implemented in a MBean whose interface is called com.morningstar.jmxcalcservice.mbean.JmxCalcServiceMBean:

1
2
3
4
5
6
7
package com.morningstar.jmxcalcservice.mbean;

public interface JmxCalcServiceMBean
{
    public int getNumAdds();
    public int getNumSubtracts();
}

The implementation of the MBean is in the class com.morningstar.jmxcalcservice.mbean.JmxCalcService This class also includes two additional functions, incrementNumAdds() and incrementNumSubtracts() These functions are called by the servlets each time their methods are called. These functions are not exposed through the MBean interface and are not available to JMX management applications.

JmxCalcService is implemented as a Singleton with thread-safe counters, as we only want one instance of the data and increments can happen on any thread. It’s implementation as below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package com.morningstar.jmxcalcservice.mbean;

import java.util.concurrent.atomic.AtomicInteger;

public class JmxCalcService implements JmxCalcServiceMBean
{
    private static JmxCalcService s_instance = new JmxCalcService();
    public static JmxCalcService getInstance()
    {
        return s_instance;
    }

    private AtomicInteger numAdds = new AtomicInteger(0);
    private AtomicInteger numSubtracts = new AtomicInteger(0);
    private JmxCalcService() { }

    public int getNumAdds() { return this.numAdds.get(); }
    public int getNumSubtracts() { return this.numSubtracts.get(); }
    public int incrementNumAdds() { return this.numAdds.incrementAndGet(); }
    public int incrementNumSubtracts() { return this.numSubtracts.incrementAndGet(); }
}

The JmxCalcService MBean must be registered with the platform MBean server. The proper way to register MBeans varies based on application type or servlet container. For Jetty, the following technique works well:

First, create a ServletContextListener which registers the MBean at application startup time and deregisters it at application shutdown time:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.morningstar.jmxcalcservice;

import java.lang.management.ManagementFactory;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import com.morningstar.jmxcalcservice.mbean.JmxCalcService;

public class JmxCalcServiceContextListener implements ServletContextListener
{
    private static final String mbeanName = "com.morningstar.jmxcalcservice:type=JmxCalcService";

    public void contextInitialized(ServletContextEvent event) {
        try {
            MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
            ObjectName objName = new ObjectName(this.mbeanName);
            JmxCalcService mbean = JmxCalcService.getInstance();
            mbs.registerMBean(mbean, objName);
        } catch (Exception ex) {
            System.err.println("Error registering JmxCalcService MBean: " + ex.toString());
        }
    }

    public void contextDestroyed(ServletContextEvent event) {
        try {
            MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
            ObjectName objName = new ObjectName(this.mbeanName);
            mbs.unregisterMBean(objName);
        } catch (Exception ex) {
            // Ignore unregistration failures
        }
    }
}

Second, add JmxCalcServiceContextListener to the web.xml file:

1
2
3
4
5
6
7
8
9
...
<web-app>
    <display-name>jmxcalcservice -- A simple calculation web service instrumented via JMX</display-name>
    <listener>
        <listener-class>com.morningstar.jmxcalcservice.JmxCalcServiceContextListener</listener-class>
    </listener>
    <servlet>
        <servlet-name>add</servlet-name>
        <servlet-class>com.morningstar.jmxcalcservice.servlet.AddServlet</servlet-class>

We must also change AddServlet and SubtractServlet to call incrementNumAdds() or incrementNumSubtracts() at the end of the function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class AddServlet extends HttpServlet {
    public void doGet(HttpServletRequest request,
                      HttpServletResponse response)
        throws ServletException, IOException
    {
        int val1 = Integer.parseInt(request.getParameter("val1"));
        int val2 = Integer.parseInt(request.getParameter("val2"));
        int result = val1+val2;
        PrintWriter out = response.getWriter();
        out.println(result);
        out.flush();
        out.close();
        JmxCalcService.getInstance().incrementNumAdds();
    }
}

After all this, we can start the application, hit the URLs a few times, load JConsole and see the values:

Viewing values in JConsole

What’s Next

After you instrument your application with JMX, consider hooking up your application to your centralized monitoring system or using jmxtrans to collect and send metrics to Graphite so that you can chart and trend data over time. Of course, if Graphite is your ultimate goal, statsd might be a better choice.