Jeff Mesnil

JMX Scripts with Eclipse Monkey

May 23, 2007

Continuing the series about "writing JMX scripts in a dynamic language", after Ruby (part I & II), let's do that in JavaScript.

Aside of the use of a different scripting language, this example differs completely from the Ruby one by its context of execution: it will be integrated into Eclipse and called directly from its user interface (using Eclipse Monkey as the glue).

The example will:

  1. ask graphically the user for a logging level
  2. update all the JVM's loggers with this level
  3. display all the loggers of the JVM

in 50 lines of code.

This example is simple but it implies several interesting steps:

  • connect to a JMX Server
  • retrieve a MBean
  • retrieve value of MBean attributes
  • invoke operations on the MBean

There are many use cases where you have to perform theses steps in repetition. It's tedious to do that in a JMX console (e.g. jconsole or eclipse-jmx) and most of the time, it is not worth writing a Java application.

These use cases beg to be scripted.

We will again use jconsole as our managed java application (see this previous post to start jconsole with all System properties required to manage it remotely).

To run the example:

  1. install Eclipse Monkey
  2. Make sure that Eclipse is running on Java 1.5 or above
  3. copy the script just below and save it as logging_change_level.js in the scripts directory of an Eclipse project
  4. click on Scripts > Logging > Change level in Eclipse menu
  5. follow the instruction to install the required DOM
  6. only the JMX Monkey feature is required
  7. restart Eclipse
  8. click on Scripts > Logging > Change level
  9. in the dialog, input the logging level Dialog to input the logging level
  10. all the loggers have been updated with the given level Dialog to display the loggers

logging__change__level.js

/*
 * Menu: Logging > Change level
 * DOM: http://eclipse-jmx.googlecode.com/svn/trunk/net.jmesnil.jmx.update/net.jmesnil.jmx.monkey.doms
 */
    
function main() {
   mbsc = jmx.connect("localhost", 3000)
   logging = mbsc.getMBean("java.util.logging:type=Logging")
   
   level = ask_level()
   set_all(mbsc, logging, level)
   text = "All loggers are set to " + level + "\n\n"
   text += logger_levels(mbsc, logging)
   show(text)
}
    
function logger_levels(mbsc, logging) {
   var out = ""
   for each(loggerName in logging.LoggerNames) {
       lvl = mbsc.invoke(logging, "getLoggerLevel",
            [loggerName],
            ["java.lang.String"]);
       out += loggerName + " is at " + lvl + "\n"
   }
   return out
}
    
function set_all(mbsc, logging, level) {
   for each(loggerName in logging.LoggerNames) {
       mbsc.invoke(logging, "setLoggerLevel",
            [loggerName, level],
            ["java.lang.String", "java.lang.String"])
   }
}
    
function ask_level() {
   dialog = new Packages.org.eclipse.jface.dialogs.InputDialog(
          window.getShell(), 
          "Logging Level",
          "Which level? (SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST)",
          "INFO", null)
   result = dialog.open()
   if (result == Packages.org.eclipse.jface.window.Window.OK) {
       return dialog.getValue()
   }
}
    
function show(text) {
   Packages.org.eclipse.jface.dialogs.MessageDialog.openInformation(
           window.getShell(),
           "Update Logging",
           text
       )
}

code walkthrough

The main() function of the script is:

function main() {
  mbsc = jmx.connect("localhost", 3000)
  logging = mbsc.getMBean("java.util.logging:type=Logging")
     
   level = ask_level()
   set_all(mbsc, logging, level)
   text = "All loggers are set to " + level + "\n\n"
   text += logger_levels(mbsc, logging)
   show(text)
} 

where

  • mbsc = jmx.connect("localhost", 3000) creates a mbsc object connected to a local JMX server using the standard JMX Service URL.
  • logging = mbsc.getMBean("java.util.logging:type=Logging") creates a logging object representing the LoggingMXBean used to manage the JVM logging.
  • level = ask_level() opens an Eclipse input dialog where you can input the logging level and returns it as a String.
  • set_all(mbsc, logging, level) sets all the loggers at the given level
  • text += logger_levels(mbsc, logging) returns a String representation of all the loggers and their level
  • show(text) opens an Eclipse message dialog

The interesting methods are logger_levels() and set_all(). In both methods, we retrieve an attribute of a MBean and invoke an operation.

Let's focus on set_all():

function set_all(mbsc, logging, level) {
   for each(loggerName in logging.LoggerNames) {
      mbsc.invoke(logging, "setLoggerLevel",
            [loggerName, level],
            ["java.lang.String", "java.lang.String"])
   }
}  

logging represents a MBean (it is not a "real" MBean, more on that later) and mbsc represents a MBeanServerConnection (but it is not a "real" MBeanServerConnection, more on that later).
logging.LoggerNames returns the value of the LoggerNames attribute of the MBean (note that the first letter must be in upper case) which is an array of strings.
For each element of this array, we invoke the setLoggerLevel operation using mbsc.invoke().
This method is very similar to the "real" MBeanServerConnection.invoke() method:

  1. something representing a MBean (instead of an ObjectName)
  2. the MBean operation name
  3. the parameters of the operation
  4. the types of the parameters

jmx: an Eclipse Monkey DOM

What do I mean when I write that logging is not the "real" LogginMXBean and that mbsc is not the real MBeanServerConnection?

This 2 types of objects are created by the jmx object in the main() method. This jmx object is in fact an Eclipse Monkey DOM that is contributed by the plug-in listed in the DOM directive at the top of the script:

DOM: http://eclipse-jmx.googlecode.com/svn/trunk/net.jmesnil.jmx.update/net.jmesnil.jmx.monkey.doms

This plug-in was included in the "JMX Monkey" feature which was installed the first time you ran this script.

The jmx DOM has a single method connect(host, port) which connects to a JMX Server using the standard JMX Service URL.
The object returned by this method is a ScriptableMBeanServerConnection. This class encapsulates the "real" MBeanServerConnection (still available using the getObject() method) but only exposes its invoke().

It also exposes a getMBean() method which returns a ScriptableMBean. In turn this class exposes the attributes of the MBean as JavaScript attributes.

To sum up, these are the operations you can perform using the jmx DOM:

  • connect to a JMX server: mbsc = jmx.connect(host, port)
  • get a mbean: mbean = mbsc.getMBean(objectName)
  • get the value of a mbean attribute: val = mbean.AttributeName
  • get the "real" mbean server connection and use it: objectNames = mbsc.getObject().queryNames(name, query)
  • invoke an operation on a mbean: mbsc.invoke(mbean, operation, params, param_types)

Conclusion

This script example is simple but quite interesting thanks to its integration with Eclipse.

I believe there is an use for such scripts: repeatable management operations that needs to be tweaked from time to time. It's tedious to do that with a GUI and it's even more tedious to write Java applications to do so.

Last year, during EclipseCon'06, I blogged about an use case for scripting a RCP application using Eclipse Monkey. This is a concrete example: I'm using eclipse-jmx to manage Java applications that I develop. When I realize that I perform the same kind of management task, I write a monkey script which automates it.

Next time, you have to perform the same operation on many MBeans or many operations on the same MBean but you think it is not worth to write a Java application to automate it, ask yourselves if it can not be simply automated by a script such as the one in this post.