Saturday, September 06, 2008

JMX for Scripts : RSS Adapter

Last week, I added scheduling and monitoring capabilities to the basic MBean server to manage scripts I described the week before. Now imagine that the server was deployed to a bank of 10 or 50 or more machines on which scripts are being run. If you were to depend on the notifications alone, this would be fine, but there may still be situations where you want a "bird's eye view" of your entire system, perhaps to show your boss, or even for yourself. This post describes an RSS adapter that returns an RSS feed of the status of all the scripts being managed by its containing MBean server.

Anyway, back to the RSS Adapter. I initially figured that it should be modelled after the HTML Server Adapter, so I peeked at the OpenDMK sources (which is where the JMX tools code originally came from), but it seemed to be too much infrastructure for what I had in mind, so I fell back to using Jetty. I ended up creating an MBean that that instantiates a Jetty Handler listening on port 9081. Code inside the Handler queries the MBean server for the ScriptAdapter MBeans, gets the Status attribute values, then creates and serializes a SyndFeed object using ROME. Here is the code:

// Source: src/main/java/com/mycompany/myapp/RssAdapterServerMBean.java
package com.mycompany.myapp;

public interface RssAdapterServerMBean {
  
  public void start();
  public void stop();
}

Here is the implementation for the RssAdapterServer MBean. The code queries the MBean server for the ScriptAdapters - information about the mechanics of which came from Eamonn McManus's blog, which is also where I got the pointer about OpenDMK being the source for the JMX tools project.

// Source: src/main/java/com/mycompany/myapp/RssAdapterServer.java
package com.mycompany.myapp;

import java.io.IOException;
import java.io.PrintWriter;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.management.MBeanServer;
import javax.management.MBeanServerFactory;
import javax.management.ObjectName;
import javax.management.Query;
import javax.management.QueryExp;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.mortbay.jetty.Handler;
import org.mortbay.jetty.HttpStatus;
import org.mortbay.jetty.Request;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.handler.AbstractHandler;

import com.sun.syndication.feed.WireFeed;
import com.sun.syndication.feed.synd.SyndContent;
import com.sun.syndication.feed.synd.SyndContentImpl;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndEntryImpl;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.feed.synd.SyndFeedImpl;
import com.sun.syndication.io.FeedException;
import com.sun.syndication.io.WireFeedOutput;

public class RssAdapterServer implements RssAdapterServerMBean {

  private class StatusTriple {
    public ObjectName script;
    public URL httpUrl;
    public String status;
  };
  
  private int port;
  private String httpAdapterHostPort;
  
  private Server rssServer;
  
  public void setPort(int port) {
    this.port = port;
  }

  public void setHttpAdapterHostPort(String httpAdapterHostPort) {
    this.httpAdapterHostPort = httpAdapterHostPort;
  }
  
  public void start() {
    Handler handler = new AbstractHandler() {
      public void handle(String target, HttpServletRequest request,
          HttpServletResponse response, int dispatch) throws IOException,
          ServletException {
        response.setContentType("text/xml");
        PrintWriter writer = response.getWriter();
        writer.println(getScriptStatusRss());
        writer.flush();
        writer.close();
        response.setStatus(HttpStatus.ORDINAL_200_OK);
        ((Request) request).setHandled(true);
      }
    };
    this.rssServer = new Server(port);
    rssServer.setHandler(handler);
    try {
      rssServer.start();
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  public void stop() {
    try {
      rssServer.stop();
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  private final String getScriptStatusRss() {
    List<StatusTriple> triples = new ArrayList<StatusTriple>();
    List<MBeanServer> mbeanServers = 
      MBeanServerFactory.findMBeanServer(null);
    if (mbeanServers == null || mbeanServers.size() == 0) {
      System.out.println("No MBean servers found in JVM");
      return getRss(triples);
    }
    MBeanServer mbeanServer = mbeanServers.get(0);
    QueryExp query = Query.isInstanceOf(Query.value(
      ScriptAdapter.class.getName()));
    Set<ObjectName> objectNames = mbeanServer.queryNames(null, query);
    for (ObjectName objectName : objectNames) {
      try {
        StatusTriple triple = new StatusTriple();
        triple.script = objectName;
        triple.status = 
          (String) mbeanServer.getAttribute(objectName, "Status");
        triple.httpUrl = new URL("http://" + httpAdapterHostPort + 
          "/ViewObjectRes//" + 
          URLEncoder.encode(objectName.getCanonicalName(), "UTF-8"));
        triples.add(triple);
      } catch (Exception e) {
        System.out.println("Cannot invoke getStatus on " + 
          objectName.getCanonicalName());
        e.printStackTrace();
        continue;
      }
    }
    System.out.println("Reporting on " + triples.size() + " MBeans");
    return getRss(triples);
  }
  
  @SuppressWarnings("unchecked")
  private String getRss(List<StatusTriple> triples) {
    SyndFeed feed = new SyndFeedImpl();
    feed.setFeedType("rss_2.0");
    feed.setTitle("Status of Scripts running on: " + httpAdapterHostPort);
    feed.setDescription("Status of scripts running on: " + 
      httpAdapterHostPort);
    feed.setLink("http://localhost:" + port);
    for (StatusTriple triple : triples) {
      SyndEntry entry = new SyndEntryImpl();
      entry.setTitle(triple.script.getCanonicalName());
      entry.setLink(triple.httpUrl.toExternalForm());
      SyndContent description = new SyndContentImpl();
      description.setType("text/plain");
      description.setValue(triple.status);
      entry.setDescription(description);
      feed.getEntries().add(entry);
    }
    WireFeedOutput outputter = new WireFeedOutput();
    WireFeed wirefeed = feed.createWireFeed("rss_2.0");
    try {
      return outputter.outputString(wirefeed);
    } catch (FeedException e) {
      e.printStackTrace();
      System.out.println("Feed exception trying to deserialize to RSS");
      return "";
    }
  }
}

And here is how it is registered with the MBean Server in the ScriptAgent.java code. For brevity, I only show the calls to register and start this server, please refer to previous articles to see the entire code for the ScriptAdapter.java. The snippet shown below should appear right after the block where the HTTP Adapter Server gets instantiated, registered to the MBean server and started. Notice that we choose a hardcoded (aka convention :-)) port number for the Rss Adapter Server as 1000 + the port number for the HTTP Adapter server.

// Source: src/main/java/com/mycompany/myapp/ScriptAgent.java
package com.mycompany.myapp;
...
public class ScriptAgent {
  ...
  protected void init() throws Exception {
    ...
    // load RSS Adapter
    RssAdapterServer rssAdapter = new RssAdapterServer();
    rssAdapter.setPort(DEFAULT_AGENT_PORT + 1000);
    rssAdapter.setHttpAdapterHostPort("localhost:" + DEFAULT_AGENT_PORT);
    server.registerMBean(rssAdapter, new ObjectName("adapter:protocol=RSS"));
    rssAdapter.start();
  }
  ...
}

Here are some screenshots of the RSS Adapter in action:

The Agent View - notice that the RSS Adapter server appears in the List of MBeans of type "adapter".
This is the output of the RSS Adapter server for the Agent shown above. This provides us with a single high-level view of the status of the scripts in the MBean server.
The links on the script ObjectNames in the previous screenshot points to the actual MBean view of the ScriptAdapter.

I'm sure you see where this is going, right? Now that we have an RSS feed from one MBean server, it is trivial to write a feed aggregator that shows the results of all these feeds on a single web page. You could also simply use one of the many freely available feed readers, but I prefer a custom web application doing the aggregation because that way all the end-user needs is a web browser.

I do suggest making your HTTP adapter have some sort of basic authentication, since anyone can now potentially open up the MBean viewer from the RSS feed page, so the convenience for the ops guys may end up becoming a security hole if the MBean viewer is not password protected.

The only thing left (at least from my point of view) is to make this whole thing more configurable, there are too many conventions floating around in this app at the moment. I plan to use Spring for the configuration, I will describe this in a future blog post.

Be the first to comment. Comments are moderated to prevent spam.