log.rofl(‘Fun with Groovy metaprogramming’)

Recently I saw a post by someone (I think it was @jbarnette, but it was retweeted to me) suggesting that there should be some alternate log levels, like fyi, omg, or even wtf. I thought that was pretty funny, but then it occurred to me I could probably implement them using Groovy metaprogramming.

As a first attempt, consider the following simple example that adds the fyi and omg methods to java.util.logging.Logger:

import java.util.logging.Logger

Logger.metaClass.fyi = { msg -> delegate.info msg }
Logger.metaClass.omg = { msg -> delegate.severe msg }

For those who haven’t used Groovy much, the metaClass property is associated with every class in Groovy, and allows you to add methods and properties to the class. Here the fyi method is defined by assigning it to a one-argument closure whose implementation is to invoke the (existing) info method in Logger, with the msg argument. Likewise, omg is assigned to the severe method. Therefore, an invocation like:

Logger log = Logger.getLogger(this.class.name)
log.fyi 'for your information'
log.omg 'oh my goodness'

results in

Dec 12, 2011 10:09:02 PM java_util_logging_Logger$info call
INFO: for your information
Dec 12, 2011 10:09:02 PM java_util_logging_Logger$severe call
SEVERE: oh my goodness

The methods work, but the output isn’t really what I want. The messages get passed through, but the output shows INFO and SEVERE rather than FYI and OMG.

It turns out it takes a bit of work to define a custom log level. Levels are defined using the java.util.logging.Level class, which predefines levels like Level.INFO, Level.WARNING, and Level.SEVERE. The Level class has a protected constructor which can be used to make new levels. I therefore adding a class called CustomLevel, as follows:

import java.util.logging.Level

class CustomLevel extends Level {
    CustomLevel(String name, int val) {
        super(name,val)
    }
}

Each level gets an integer value. On my Windows 7 system (sorry) using JDK 1.6, the actual values of some of the defined levels are:

import java.util.logging.Level

println "$Level.INFO: ${Level.INFO.intValue()}"
println "$Level.WARNING: ${Level.WARNING.intValue()}"
println "$Level.SEVERE: ${Level.SEVERE.intValue()}"

INFO: 800
WARNING: 900
SEVERE: 1000

My second attempt was then to define a Groovy category, so that I could replace a couple of the existing levels in a controlled fashion.

import java.util.logging.Level
import java.util.logging.Logger

class SlangCategory {
    static String fyi(Logger self, String msg) {
        return self.log(new CustomLevel('FYI',Level.INFO.intValue()),msg)
    }
    static String lol(Logger self, String msg) {
        return self.log(new CustomLevel('LOL',Level.WARNING.intValue()),msg)
    }
}

Logger log = Logger.getLogger(this.class.name)
use(SlangCategory) {
    log.fyi 'this seems okay'
    log.lol('snicker')
}

Each of the logging methods in the Logger class (like info() or warning()) delegate to the log() method, which takes two arguments — an instance of Level, and a message String. I therefore used the category to replace the INFO and WARNING levels with FYI and LOL. The output is now:

Dec 12, 2011 10:20:29 PM sun.reflect.NativeMethodAccessorImpl invoke0
FYI: this seems okay
Dec 12, 2011 10:20:29 PM sun.reflect.NativeMethodAccessorImpl invoke0
LOL: snicker

Once again, this is just replacing existing levels, though it does at least have the new level name in the output string.

To really do this right, though, I wanted to be able to define new levels arbitrarily without having to hardwire them. That meant overriding the methodMissing method in the metaClass, using what Jeff Brown describes as the “intercept, cache, invoke” pattern for metaprogramming. Here’s the result, which I’ll explain after the code.

import java.util.logging.*

Logger.metaClass.methodMissing = { String name, args ->
    def impl = { Object... varArgs ->
        int val = Level.WARNING.intValue() +
            (Level.SEVERE.intValue() - Level.WARNING.intValue()) * Math.random()
        def level = new CustomLevel(name.toUpperCase(),val)
        delegate.log(level,varArgs[0])
    }
    Logger.metaClass."$name" = impl
    impl(args)
}

Logger log = Logger.getLogger(this.class.name)
log.wtf 'no effin way'
log.whoa 'dude, seriously'
log.rofl "you're kidding, right?"

The methodMissing method of the metaClass takes two arguments: the name of the method, and the arguments passed to it. Whenever you invoke a method that doesn’t exist, methodMissing gets invoked. That’s the “intercept” part.

The implementation is to define a closure that takes any number of arguments. Inside the closure, I computed a random value between Level.WARNING and Level.SEVERE, and then used that value to instantiate a custom level. The defined level and the new value were used in the log method on the closure’s delegate property (in this case, the logger) to log the message at the new level.

Finally, the "$name" method to the metaClass (which evaluates the name variable — otherwise the method added would just be called name) is assigned to the impl closure. That’s the “cache” part. Finally, the implementation is called, which is the “invoke” part.

Now I can use a log level with whatever name I want. The output of this script is
Dec 12, 2011 10:31:35 PM sun.reflect.NativeMethodAccessorImpl invoke0
WTF: no effin way
Dec 12, 2011 10:31:35 PM sun.reflect.NativeMethodAccessorImpl invoke0
WHOA: dude, seriously
Dec 12, 2011 10:31:35 PM sun.reflect.NativeMethodAccessorImpl invoke0
ROFL: you're kidding, right?

The demos are nice, but this really ought to be tested. I’m reasonably comfortable with this level (snicker) of metaprogramming, but I’d feel a lot better if I had a real test for it.

That took a fair amount of digging. It turns out that the inimitable Dierk Koenig (lead author of Groovy in Action, known as #regina on Twitter; second edition available now through the Manning Early Access Program) wrote a class called groovy.lang.GroovyLogTestCase. That class has a static method called stringLog. The GroovyDocs say:

“Execute the given Closure with the according level for the Logger that is qualified by the qualifier and return the log output as a String. Qualifiers are usually package or class names. Existing log level and handlers are restored after execution.”

It took me a while figure out how to use that, but eventually I got it to work. It automatically captures the console appender output, so the resulting test looks like this:

import java.util.logging.Level
import java.util.logging.Logger

class LoggingTests extends GroovyLogTestCase {
    String baseDir = 'src/main/groovy/metaprogramming'
    
    void testWithoutCustomLevel() {
        def result = stringLog(Level.INFO, without_custom_levels.class.name) {
            GroovyShell shell = new GroovyShell()
            shell.evaluate(new File("$baseDir/without_custom_levels.groovy"))
        }
        assert result.contains('INFO: for your information')
        assert result.contains('SEVERE: oh my goodness')
    }
    
    void testSlangCategory() {
        def result = stringLog(Level.INFO, use_slang_category.class.name) {
            GroovyShell shell = new GroovyShell()
            shell.evaluate(new File("$baseDir/use_slang_category.groovy"))
        }
        assert result.contains('FYI: this seems okay')
        assert result.contains('LOL: snicker')
    }

    void testEMC() {
        def result = stringLog(Level.INFO, use_emc.class.name) {
            GroovyShell shell = new GroovyShell()
            shell.evaluate(new File("$baseDir/use_emc.groovy"))
        }
        assert result.contains('WTF: no effin way')
        assert result.contains('WHOA: dude, seriously')
        assert result.contains("ROFL: you're kidding, right?")
    }
}

I should mention a couple of minor points. First, the class associated with a script has the same name as the file containing it, so the classes in the stringLog method are the script names. Second, in case you were wondering, the emc part of the script name stands for ExpandoMetaClass.

I spent a very pleasant evening working on this, and learned a few things:
1. Groovy metaprogramming is fun,
2. Now I know how to use GroovyLogTestCase, which is not at all well documented, and
3. I’ll go to all sorts of trouble to avoid working on what I’m supposed to be working on, especially if it involves a joke. 🙂

I added all this to my book’s source code. What book, you ask? Why, Making Java Groovy, available through the Manning Early Access Program (MEAP) at http://manning.com/kousen. I don’t know, however, if I’ll have room in the text to include all this.

log.marketing('Please forgive the mandatory advertising for my book')

About Ken Kousen
I teach software development training courses. I specialize in all areas of Java and XML, from EJB3 to web services to open source projects like Spring, Hibernate, Groovy, and Grails. Find me on Google+ I am the author of "Making Java Groovy", a Java / Groovy integration book published by Manning in the Fall of 2013, and "Gradle Recipes for Android", published by O'Reilly in 2015.

One Response to log.rofl(‘Fun with Groovy metaprogramming’)

  1. Pingback: GroovyMag - the magazine for Groovy and Grails developers about Groovy programming and Grails development

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: