Monitoring Mendix using JMX

The following is only relevant if you run Mendix on-premises. If you do, you probably have standard monitoring tooling that you use to monitor all your applications.

For java applications most monitoring tools provide a way to hook into JMX to get information about the application. The following describes how you can use JMX to get information on your Mendix application.

Mendix doesn’t provide any runtime or application specific MBeans, which means that without writing your own MBeans you will only be able to monitor generic JVM statistics. In this post i’ll describe how you enable JMX on your Mendix runtime. I’ll also decribe how you can write custom MBeans to expose Mendix runtime information and application specific information.

This post will discuss the following:

  • Starting the Mendix runtime with JMX enabled
  • MBean exposing generic Mendix statistics
  • MBean exposing application specific statistics
  • Register MBeans using a Startup microflow

Starting the Mendix runtime with JMX enabled

To enable JMX on your Mendix runtime you can use the following javaopts to the m2ee.yaml for your Mendix runtime:

 javaopts: [
   "-Dfile.encoding=UTF-8", "-XX:MaxPermSize=64M", "-Xmx128M", "-Xms128M",
   "-Djava.io.tmpdir=/tmp",
   "-Dcom.sun.management.jmxremote",
   "-Dcom.sun.management.jmxremote.port=7845",
   "-Dcom.sun.management.jmxremote.local.only=false",
   "-Dcom.sun.management.jmxremote.authenticate=false",
   "-Dcom.sun.management.jmxremote.ssl=false",
   "-Djava.rmi.server.hostname=192.168.1.70",
 ]

After you restart the Mendix runtime you will be able to connect to it using tools like JConsole or VisualVM. Every JVM by default exposes information like memory usage, garbage collection and threading. If you want more, you can write you own MBeans.

MBean exposing generic Mendix statistics

The simplest way to expose management information is by writing a MBean interface and a Java class which implements the interface. You can define getters and setter, but you can also define methods which can be called from generic management tooling. I’ve used this for example to tell a running application to reload its configuration file.

Example of an interface which contains getters for some generic Mendix information, MxStatsMBean.java:

package jmx.actions;
public interface MxStatsMBean {
    public int getMaximumNumberConcurrentUsers() throws Exception;
    public int getActionQueueSize();
    public int getActiveActionCount();
    public int getScheduledActionCount();
    public long getNumberConcurrentSessions();
    public long getCurrentUserCount();
    public long getCompletedActionCount();
    public long getNamedUserCount();
}

And here is the implementation, MxStats.java. The methods just call the Mendix Core class, and return the value:

package jmx.actions;
import com.mendix.core.Core;
public class MxStats implements MxStatsMBean {
    public int getMaximumNumberConcurrentUsers() throws Exception {
            return Core.getMaximumNumberConcurrentUsers();
    }
    public int getActionQueueSize(){
        return Core.getActionQueueSize();
    }
    public int getActiveActionCount(){
        return Core.getActiveActionCount();
    }
    public int getScheduledActionCount(){
        return Core.getScheduledActionCount();
    }
    public long getNumberConcurrentSessions(){
        return Core.getNumberConcurrentSessions();
    }
    public long getCurrentUserCount(){
        return Core.getConcurrentUserCount(true);
    }
    public long getCompletedActionCount(){
        return Core.getCompletedActionCount();
    }
    public long getNamedUserCount(){
        return Core.getNamedUserCount();
    }
}

Here’s a screenshot of JConsole showing the values exposed by this MBean:

JConsole Mendix

MBean exposing application specific statistics

You can use the same approach, with an Interface and implementation class, to also expose application specific information. Here’s a different approach: one that exposes a dynamic set of values. You can do the same with methods, but the example only shows attributes to retrieve values.

The idea is that you will have a Java Action that you can call in a microflow, where you expose arbitrary key, value pairs. Here’s an example in the Mendix modeler:

Microflow to change application statistic

The implementation of the SetJmxValue looks like this:

package jmx.actions;
public class SetJmxValue extends CustomJavaAction<Boolean>
{
  private String name;
  private Long value;
  public SetJmxValue(IContext context, String name, Long value)
  {
    super(context);
    this.name = name;
    this.value = value;
  }
  @Override
  public Boolean executeAction() throws Exception
  {
    // BEGIN USER CODE
    MxAppStats.setLongValue(name,value);
    return true;
    // END USER CODE
  }
  /* .... */
}

The real meat is in the MxAppStats class. Here i use a HashMap to store all the key,value pairs that will be exposed. The HashMap is implemented as a static property, so this will be runtime instance specific (for production you’ll want to make this code thread-save, which it currently isn’t). GetMBeanInfo returns the data which tells the monitoring software which properties are exposed in this MBean.

package jmx.actions;

import javax.management.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Vector;

public class MxAppStats implements DynamicMBean {

    private static HashMap stats;

    @Override
    public Object getAttribute(String attribute) throws AttributeNotFoundException, MBeanException, ReflectionException {
        return getStats().get(attribute);
    }

    @Override
    public void setAttribute(Attribute attribute) throws AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException {
        getStats().put(attribute.getName(), attribute.getValue());
    }

    @Override
    public AttributeList getAttributes(String[] attributes) {
        AttributeList list = new AttributeList();
        for (Object name : getStats().keySet()) {
            list.add(new Attribute((String) name, getStats().get(name)));
        }
        return list;
    }

    @Override
    public AttributeList setAttributes(AttributeList attributes) {
        return null;
    }

    @Override
    public Object invoke(String actionName, Object[] params, String[] signature) throws MBeanException, ReflectionException {
        return null;
    }

    @Override
    public MBeanInfo getMBeanInfo() {
        //MBeanAttributeInfo[] attrs = new MBeanAttributeInfo[getStats().size()];
        ArrayList<MBeanAttributeInfo> attrs = new ArrayList();
        for (Object name : getStats().keySet()) {
            MBeanAttributeInfo attr = new MBeanAttributeInfo(
                    (String) name,
                    "java.lang.Long",
                    "Property " + name,
                    true,
                    true,
                    false
            );
            attrs.add(attr);
        }
        MBeanInfo info = new MBeanInfo(
                this.getClass().getName(),
                "Mendix App MBean",
                (MBeanAttributeInfo[]) attrs.toArray(new MBeanAttributeInfo[0]),
                null,
                null,
                null
        );
        return info;
    }

    public static void setLongValue(String name, Long value) {
        getStats().put(name, value);
    }

    private static HashMap getStats() {
        if (stats == null) {
            stats = new HashMap();
        }
        return stats;
    }
}

Register the MBeans using a Startup microflow

You need to register your MBean with the MBeanServer, before they can be used. We will call the initialization code in a Startup Microflow.

First we need to create a microflow:

JMX Setup Microflow

Now we can call it in the startup of the Mendix runtime:

After Startup JMX Microflow

And the java code for the InitJMX action:

@Override
public Boolean executeAction() throws Exception
{
  // BEGIN USER CODE
      initJmx();
      return true;
  // END USER CODE
}
// BEGIN EXTRA CODE
  private MBeanServer mbs = null;
  private void initJmx() throws NotCompliantMBeanException, InstanceAlreadyExistsException, MBeanRegistrationException, MalformedObjectNameException {
  mbs = ManagementFactory.getPlatformMBeanServer();
  /*
   * runtime statistics
   */
  MxStatsMBean mxStats = new MxStats();
  ObjectName mxMbeanName = new ObjectName("Mx:name=MxMbean");
  mbs.registerMBean(mxStats, mxMbeanName);
  /*
   * application statistics
   */
  MxAppStats appStats = new MxAppStats();
  ObjectName mxAppMbeanName = new ObjectName("Mx:name=MxAppMbean");
  mbs.registerMBean(appStats, mxAppMbeanName);
  }
// END EXTRA CODE

JMX is the default Java way to expose information to monitoring tooling, so there are lots of options besides JConsole and VisualVM. There’s command line tooling like jmxterm, or complete monitoring suites like Hyperic HQ, Hawt.io, and RHQ. Other well know monitoring tooling like Splunk and New Relic also offer ways to feed it JMX data. Or you can create something cutting edge using Logstash, jmxtrans, ElasticSearch and Kibana.

blog comments powered by Disqus