June 9, 2019

Implement Metrics with Java EE Interceptors and Exposing with MBean

Background

A crucial capability when deploying an application to production is to be able to monitor it and set alerts if application is running slow. This is the background for Eclipse Micropofile Metrics project, which most Java EE container does not yet support, but will probably in near future.

The other thing you need after added monitoring is to expose that data and many monitoring tools use MBean for that.

So here we are going to show how to add Timer Metrics in a Java EE way with Interceptors (previously called Aspect Oriented Programming, AOP) and to expose that data with MBean.

Reference:

Java EE Interceptors

The interceptor consist of one class, which is annotated with @javax.interceptor.Interceptor and have one method annotated with @javax.interceptor.AroundInvoke.


package se.magnuskkarlsson.metrics.control;

import java.util.logging.Logger;

import javax.inject.Inject;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;

import se.magnuskkarlsson.metrics.boundary.TimerResource;

@Interceptor
public class TimerInterceptor {

    private final Logger log = Logger.getLogger(TimerInterceptor.class.getName());
    @Inject
    protected TimerResource resource;

    // ----------------------- Logic Methods -----------------------

    @AroundInvoke
    public Object intercept(InvocationContext ctx) throws Exception {
        String className = ctx.getTarget().getClass().getName();
        String methodName = ctx.getMethod().getName();
        long start = System.currentTimeMillis();
        try {
            // call the target code
            return ctx.proceed();
        } finally {
            // target code can fail so we measure the duration with a finally block
            long duration = System.currentTimeMillis() - start;
            log.info(className + "#" + methodName + " " + duration + " ms");
            resource.getStatisticsMBean(className).add(methodName, duration);
        }
    }

    // ----------------------- Helper Methods -----------------------

    // ----------------------- Get and Set Methods -----------------------

}

Reference:

MBean

A MBean is a POJO, but with naming requirements:

  • First it needs an interface.
  • Second the interface name must end with MBean.
  • And finally the implementing class must be named as same as the interface, but without the MBean.

package se.magnuskkarlsson.metrics.control;

import java.util.Map;

public interface TimerMBean {

    public void add(String key, long value);

    public Map<String, String> getTimers();

}

package se.magnuskkarlsson.metrics.control;

import java.util.LongSummaryStatistics;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

public class Timer implements TimerMBean {

    private final ConcurrentHashMap<String, LongSummaryStatistics> timers = new ConcurrentHashMap<>();

    // ----------------------- Logic Methods -----------------------

    @Override
    public void add(String key, long value) {
        // "The entire method invocation is performed atomically"
        LongSummaryStatistics summary = timers.computeIfAbsent(key, k -> new LongSummaryStatistics());
        summary.accept(value);
    }

    @Override
    public Map<String, String> getTimers() {
        return timers.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().toString()));
    }

    // ----------------------- Helper Methods -----------------------

    // ----------------------- Get and Set Methods -----------------------

}

Reference:

Java EE Singleton

Finally we need to register these MBeans to you Java EE container and also unregister those when your application is undeployed. To keep track of these MBeans I use a Java EE singleton. The singleton pattern is generally not recommended, but for specific purpose it is good.

A Java EE Singleton is an EJB, which means it is transaction aware, available via dependency injection, but also it is default thread safe by the Java EE container, this is both good and bad. Good because it shields the developer from writing thread safe code, but also bad since locking is not good for performance.

You can choose which looking mode you will have with @javax.ejb.ConcurrencyManagement, the default value for it is javax.ejb.ConcurrencyManagementType.CONTAINER, which means the container will do the locking. But you can also control locking with @javax.ejb.Lock(javax.ejb.LockType.READ) and @javax.ejb.Lock(javax.ejb.LockType.WRITE) and locking timeouts with @javax.ejb.AccessTimeout.

See for example https://www.byteslounge.com/tutorials/java-ee-ejb-concurrency-concurrencymanagement-lock-and-locktype for examples.

But here we will manage the concurrency by yourself, with the help of java.util.concurrent.ConcurrentHashMap and it's method computeIfAbsent which is thread safe. The second argument in computeIfAbsent is a construction for new values if the key is not existing.


package se.magnuskkarlsson.metrics.boundary;

import java.lang.management.ManagementFactory;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.PreDestroy;
import javax.ejb.ConcurrencyManagement;
import javax.ejb.ConcurrencyManagementType;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.management.MBeanServer;
import javax.management.ObjectName;

import se.magnuskkarlsson.metrics.control.Timer;
import se.magnuskkarlsson.metrics.control.TimerMBean;

@Singleton
@Startup
// "Bean developer is responsible for managing concurrent access to the bean instance."
@ConcurrencyManagement(ConcurrencyManagementType.BEAN)
public class TimerResource {

    private final Logger log = Logger.getLogger(TimerResource.class.getName());
    private ConcurrentHashMap<String, TimerMBean> mbeans = new ConcurrentHashMap<>();

    // ----------------------- Logic Methods -----------------------

    public TimerMBean getStatisticsMBean(final String className) {
        // "The entire method invocation is performed atomically"
        return mbeans.computeIfAbsent(className, k -> registerMBean(className));
    }

    @PreDestroy
    public void preDestroy() {
        for (String key : mbeans.keySet()) {
            try {
                MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
                ObjectName objectName = new ObjectName("metrics.timer:type=" + key);
                mBeanServer.unregisterMBean(objectName);
                log.info("Successfully unregistered " + objectName);
            } catch (Exception e) {
                log.log(Level.WARNING, "Failed to unregister MBean.", e);
            }
        }
    }

    // ----------------------- Helper Methods -----------------------

    protected TimerMBean registerMBean(String className) {
        try {
            MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
            ObjectName objectName = new ObjectName("metrics.timer:type=" + className);
            Timer statistics = new Timer();
            mBeanServer.registerMBean(statistics, objectName);
            log.info("Successfully registered " + objectName);
            return statistics;
        } catch (Exception e) {
            throw new IllegalStateException("Failed to register MBean.", e);
        }
    }

    // ----------------------- Get and Set Methods -----------------------

}

Reference:

Usage

Now you can simply add the @javax.interceptor.Interceptors to your methods you want to get timer metrics from.


    @GET
    @Interceptors(TimerInterceptor.class)
    public List<Person> getAll() { }

Test

To verify the final result open JConsole or VisualVM and get MBean.

No comments: