Wednesday, 29 May 2013

Scala code snippet: How to easily add JMX monitoring to your Scala application

Clearly one of the best features of Scala is the ability to mix-in code into your classes. This post shows how I have used Scala traits and Java annotations to automatically enable JMX monitoring to your application. The idea is the following:
  • Create a Java method annotation, @EnableForMonitoring, which will allow the JMX trait to discover all methods that should be exposed to JMX
  • Create a Scala trait which will use the scala.reflect.runtime.universe api to discover the annotated methods
  • Expose those methods via JMX using RequiredModelMBean
Let's start with the simple method annotation:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface EnableForMonitoring {
}
The Scala trait:
trait JMXAble { self =>
  import scala.reflect.runtime.{ universe => ru }

  val dynMon = DynMon
  dynMon.register(self)
}

object DynMon extends TLogger {
  import javax.management.Attribute
  import javax.management.AttributeList

  val mbs = ManagementFactory.getPlatformMBeanServer

  def register(jmxe: JMXAble) = {
    val rm = runtimeMirror(getClass.getClassLoader)
    val mType = rm.classSymbol(jmxe.getClass).selfType
    var ops = List[ModelMBeanOperationInfo]()
    mType.declarations.foreach(symbol => {
      symbol.annotations.find(a => a.tpe == ru.typeOf[EnableForMonitoring]) match {
        case Some(_) => {
          import language.reflectiveCalls
          val cmx = rm.asInstanceOf[{ def methodToJava(sym: scala.reflect.internal.Symbols#MethodSymbol): java.lang.reflect.Method }]
          val jm = cmx.methodToJava(symbol.asMethod.asInstanceOf[scala.reflect.internal.Symbols#MethodSymbol])
          ops = ops :+ new ModelMBeanOperationInfo(jm.toString, jm)
        }
        case None =>
      }
    })
    val mbeansOps = ops.toArray
    val modelMBeanInfoSupport = new ModelMBeanInfoSupport(
    getClass().getName(), 
    jmxe + " Model MBean", 
    null, null, mbeansOps, null)
    val requiredModelMBean = new RequiredModelMBean(modelMBeanInfoSupport);
    requiredModelMBean.setManagedResource(jmxe, "ObjectReference");
    val mBeanServer = ManagementFactory.getPlatformMBeanServer();
    mBeanServer.registerMBean(requiredModelMBean, new ObjectName("org.ws:type="+jmxe));
  }
}
ModelMBeanOperationInfo takes a Java method, and because of SI-7317, you have to use a hack to convert a Scala MethodSymbol to a Java method. This is how to use this trait:
class D extends JMXAble with TLogger {
  def m1 = {}
  @EnableForMonitoring def m2(i: Int) = {}
  def m3(s: String): Int = -1
  @EnableForMonitoring def m4(d: Double) = {}

  @EnableForMonitoring
  def time:String = (new Date).toGMTString 
}
And that's it. Another point to note, if you want to expose a map of [String, String] for example, you would need to convert your Scala map to a Java map. For example:
  @EnableForMonitoring def displayRoutees: java.util.Map[String, String] = {
    import scala.collection.JavaConversions._
    val data = routees.map(r => (r.path.toString, r.toString)).toMap
    new java.util.HashMap[String, String](data) 
  }

No comments:

Blog Archive