Simple DSL
First we will chart out how we want our domain language to look like. DSL syntax can be done multiple ways. It is necessary that you play with the syntax to come up with one that works for the scenario. For our example, here is the first cut that we will use.
Essentially, the script reports processing time of all web modules defined on (in this example - JBoss) application server using Bar Chart.
jmx {
server "service:jmx:rmi://localhost/jndi/rmi://localhost:1090/jmxconnector" {
query "jboss.web:*" {
findAll "j2eeType=WebModule" {
chart{
chartType="Bar"
attributes={m-> [m.processingTime, m.path]}
labels=["Time per Webapp", "Webapp", "Time"]
options=[false, true, true]
windowTitle="JBoss Module Processing Time"
width=1200
height=700
refreshRate=5000
show()
}
}
}
}
}
Other Use cases
Target users for this DSL are system administrators who can write simple scripts to monitor and/or report on various aspects of app server instances graphically.
Some other use cases for this type of DSL
- Compare Processing Time of Web Application
- Compare Load Time of Servlets
- Compare Response Time
- Compare memory usage
- Compare total requests
With Groovy there are multiple ways to write domain specific language as described in groovy documentation here
We will use nested closure approach. More information about nested closure here.
Here we use ExpandoMetaClass and series of Delegate classes to handle each closure. In the sample script above read each node (eg. jmx, server, query) as a dynamic method call with one or two arguments. For example jmx has a string argument followed by closure argument. String argument is we just store in instance variable. For closure argument we delegate the handling to separate Delegate Class. We follow the same pattern for the rest of the nested nodes.
First up Engine
This is the main class for the DSL. It gets the file passed as command args and pass it to GroovyShell to create Script object. It uses ExpandoMetaClass to dynamically add methods/closure. It add adds jmx closure and sets properties for handling closure.
class JmxReportingDslEngine {
static main(String[] args){
if(args.length != 1)
{
println("Usage: JmxReportingDslEngine <ScriptFileName>")
}
runEngine(new File(args[0]))
}
static void runEngine(File dsl){
Script dslScript = new GroovyShell().parse(dsl.text)
dslScript.metaClass = createExpandoMetaClass(dslScript.class, {
ExpandoMetaClass emc ->
emc.jmx = {
Closure cl ->
cl.delegate = new JmxClosureDelegate()
cl.resolveStrategy = Closure.DELEGATE_FIRST
cl()
}
})
dslScript.run()
}
static ExpandoMetaClass createExpandoMetaClass(Class clazz, Closure cl){
ExpandoMetaClass emc = new ExpandoMetaClass(clazz, false)
cl(emc)
emc.initialize()
return emc
}
}
Delegates
Next up we write series of Delegate Class responsible for handling each node of the language.
JMXClosureDelegate
This handles closure passed to jmxtag. It instantiates MBeanServerConnection and passes the reference down to the delegate chain.
class JmxClosureDelegate {
//To avoid using "," between String and Closure argument
def methodMissing(String name, args) {
return [name, args[0]]
}
void server(param){
def (serverUrl, cl) = param
def server = JMXConnectorFactory.connect(new JmxUrl(serverUrl),env).MBeanServerConnection
cl.delegate = new JmxServerClosureDelegate(server)
cl.resolveStrategy = Closure.DELEGATE_FIRST
cl()
}
JMXServerClosureDelegate
This delegate handles closure passed to server tag.
JmxQueryClosureDelegate
class JmxServerClosureDelegate {
def server
JmxServerClosureDelegate(server){
this.server = server
}
def methodMissing(String name, args) {
return [name, args[0]]
}
void query(param){
def (objectName, cl) = param
def query = new ObjectName(objectName)
String[] allNames = server.queryNames(query, null)
cl.delegate = new JmxQueryClosureDelegate(allNames, server)
cl.resolveStrategy = Closure.DELEGATE_FIRST
cl()
}
}
and so on...
JmxFindAllClosureDelegate
class JmxQueryClosureDelegate {
def allNames
def server
JmxQueryClosureDelegate(allNames, server){
this.allNames = allNames
this.server = server
}
def methodMissing(String name, args) {
return [name, args[0]]
}
void findAll(param){
def (filter, cl) = param
def modules = allNames.findAll{ name ->
name.contains(filter)
}.collect{ new GroovyMBean(server, it) }
cl.delegate = new JmxFindAllClosureDelegate(modules)
cl.resolveStrategy = Closure.DELEGATE_FIRST
cl()
}
}
ChartDelegate
class JmxFindAllClosureDelegate {
def modules
JmxFindAllClosureDelegate(modules){
this.modules = modules
}
void chart(Closure cl){
cl.delegate = new ChartDelegate(modules)
cl.resolveStrategy = Closure.DELEGATE_FIRST
cl()
}
}
This class
- Creates the dataset from the MBean values
- Creates the chart with the dataset values
- Creates the external frame using SwingBuilder
Output
class ChartDelegate {
def modules
def chartType
def attributes
def labels
def options
def windowTitle
def width
def height
def refreshRate
def orientation = "VERTICAL"
def dataset
ChartDelegate(modules){
this.modules = modules
}
void show(){
switch(chartType){
case "Bar": drawBarChart(); break;
//TODO:Add more chart types
default: break;
}
}
void drawBarChart(){
calculateData()
def chart = ChartFactory.createBarChart(*labels, dataset, Orientation."${orientation}", *options)
def swing = new SwingBuilder()
def frame = swing.frame(title:windowTitle, defaultCloseOperation:WC.EXIT_ON_CLOSE){
panel(id:'canvas') {rigidArea(width:width, height:height)}
}
while(true){
calculateData()
chart.fireChartChanged()
frame.pack()
frame.show()
chart.draw(swing.canvas.graphics, swing.canvas.bounds)
sleep(refreshRate)
}
}
void calculateData(){
def newDataset = new DefaultCategoryDataset()
modules.each{ m ->
def dsCall = attributes.call(m)
newDataset.addValue dsCall[0], 0, dsCall[1]
}
this.dataset = newDataset
}
}
Let's run and look at sample output...
Click here for bigger images
Here is another script for Servlet Load Time
This one uses some intermediate groovy knowledge with attributes param taking a closure. It allows user to perform transformation on the MBean parameters. Below in the script it performs operates on long objectName attribute to get servlet name parameter.
and here is the output...
jmx {
server "service:jmx:rmi://localhost/jndi/rmi://localhost:1090/jmxconnector" {
query "jboss.web:*" {
findAll "j2eeType=Servlet" {
chart{
chartType="Bar"
attributes={m-> [m.loadTime, m.objectName.find("name=([^,]*)"){it[1]}]}
labels=["Load Time per Servlet", "Servlet", "Time"]
options=[false, true, true]
windowTitle="JBoss Servlet Processing Time"
width=1200
height=700
orientation="HORIZONTAL"
refreshRate=5000
show()
}
}
}
}
}
Click here for bigger image
Further you can,
- Add different type of chart support to the DSL - Bar, XY, trending, etc
- Use/Extend to work with different application server and/or application
Complete source code can be found at http://github.com/kartikshah/jmx-dsl
Blogged with the Flock Browser
1 comment:
Thanks for this post. I need to create simple text report from JMX data, and I thought it would be a good excuse to write my first Groovy DSL. I'm sure I'll learn a lot from your code as dig into it.
Post a Comment