Implementing My First Jenkins Plugin: AnsiColor

Back | jenkins, java | 9/7/2011 |

jenkins

I installed Jenkins last week for the very first time. A couple of days later I was able to publish my first plugin, called AnsiColor, which colorizes ANSI output. It’s the plugin you’ve all been waiting for.

The Jenkins plugin tutorial is quite good, I recommend you just follow it. It has a maven-based cookbook to generate a new project. But if you’re like me, you’ll reconstruct a plugin from scratch (and possibly trade time for a better understanding). I’ll just mention a few things that could have been helpful to me.

Basics

A plugin extends hudson.Plugin. This class isn’t even necessary, but it’s a good opportunity to setup a logger that’s going to tell us that the plugin is actually being loaded. Whether you’re testing the plugin locally or running a production instance of Jenkins, this will come handy.

  1. public class PluginImpl extends Plugin {
  2.     private final static Logger LOG = Logger.getLogger(PluginImpl.class.getName());
  3.  
  4.     public void start() throws Exception {
  5.         LOG.info("starting ansicolor plugin");
  6.     }
  7. }

Processing Build Output

Our goal is to process build output and insert HTML color markup. So the first task is to find a Jenkins extension point from here that exposes build log data. I found the very promising ConsoleLogFilter. A simple extension is marked with @Extension and I thought I was done.

  1. @Extension
  2. public class AnsiColorConsoleLogFilter extends ConsoleLogFilter {
  3.  
  4.     @SuppressWarnings("unchecked")
  5.     @Override
  6.     public OutputStream decorateLogger(AbstractBuild build, OutputStream logger)
  7.             throws IOException, InterruptedException {
  8.         return new AnsiColorizer(logger, build.getCharset());
  9.     }
  10.  
  11. }

What’s that AnsiColorizer? It’s a stream processing class that inherits from hudson.console.LineTransformationOutputStream that overrides a method called eol, called for each output line. It decorates our logger. Notice that the bytes passed into the eol method are pre-allocated, hence the len parameter. You get a lot more bytes than in the current line, but it’s garbage from previous output after len.

The following code will strip all ANSI markup.

  1. @Override
  2. protected void eol(byte[] b, int len) throws IOException {
  3.     String ansiEncodedString = new String(b, 0, len);
  4.     AnsiString ansiString = new AnsiString(ansiEncodedString);
  5.     String plainString = ansiString.getPlain().toString();
  6.     byte[] plainBytes = plainString.getBytes();
  7.     out.write(plainBytes, 0, plainBytes.length);
  8. }

Pretty simple, right? Well, it only works if we want to remove stuff or add text and doesn’t work for HTML. Console output gets HTML-encoded as it passed through subsequent filtering, so inserting HTML, such as color, will end up encoded too. Sad face.

Console Notes

Jenkins has another extension point, BuildWrapper. It will add an option to every build project to enable the decoration of the build logger to which we can attach a ConsoleAnnotationDescriptor. All this is rather convoluted, but constructed with good intentions of being able to stream data. As a recent Rubyist I raised all of my eyebrows time-and-again – I forgot how much people love factories in Java. Anyway, that lets you insert ConsoleNote elements before and after a line of log output. The note is HTML. But ANSI characters can be anywhere in the string, so how is this helpful?

Lets use the extra brain cells that didn’t die while sorting out wrappers, factories, decorators and annotators. Given a string, such as “Hello ]32mCruel Java World”, how do we make it display “Hello <span style=”color: green”>Cruel Java World</span>” given that we can only prepend and append text? Like this.

  1. Hello <span style=”color: green”>Cruel Java World</span>
  2. <span style="display: none">Hello ]32mCruel Java World</span>

I know, it’s a total hack, but it works and nobody will complain.

  1. String colorizedData = colorize(this.data);
  2. if (! colorizedData.contentEquals(this.data)) {
  3.     text.addMarkup(charPos, colorizedData);
  4.     text.addMarkup(charPos, charPos + text.length(), "<span style=\"display: none;\">", "</span>");
  5. }

ansicolor

Source Code

Full plugin source code is here on Github.