http://www.developer.com/

Back to article

Taking on Custom Ant Logging


November 16, 2006

This article is the third and final article in my series on Ant customizations. My first article, Introduction to Custom Ant Tasks, explained the basic mechanics of writing a simple custom task in Java, packaging the Java class file and a Java properties file into a JAR file, and using this in your own Ant script. My second article, More on Custom Ant Tasks, discussed the details of custom "types" as well as the mechanics and issues associated with nesting custom elements and standard Ant elements together.

In this article, I will show you how to take control of Ant's default logging mechanism and write your own log message handlers. Specifically, I will discuss:

  • Ant's Built-in Logging Facilities
  • Loggers versus Listeners
  • A Custom Syslog Listener
  • A Custom XML Logger
  • Warnings on Sub-builds and Loggers

Of course, I will also try to add some insight about how Ant works with these concepts based on my own "lessons learned" and my own forays into the Ant API documentation.

Assumptions

I am assuming you have a moderate level of experience with Ant on your platform-of-choice. (I use Windows 2000 during the day and Mac OS X when I'm at home.) Also, my code examples use some of the newer Java syntax bits such as enhanced-for and generics.

Ant's Built-in Logging Facilities

Most people use Ant content with the output that results from the default logger... that is, log messages are plain text and look similar to the following:

Buildfile: build.xml

demo:
 Hello, World!

BUILD SUCCESSFUL
Total time: 0 seconds
Note: For this, I am using the Ant script from my first "hello world" custom task example. Refer to my first article in this series.

Following a banner which includes the Ant script file being executed, target names are shown on their own line with a colon following them, and output from specific tasks are shown on their own lines. This is usually enough for simple projects, but the output from different tasks is left more to the whims of the task writer, so there is not a lot of structural consistency. Also you might wonder whether that "Hello, World!" message is the result of a simple echo tag, or something else? (Where does one tag stop and the next tag start?)

Ant does, however, provide a few built-in logging options, and you can control exactly which logging option you want to use on the command line via the "-logger" command line option. When you use this command line option, follow it with the fully resolved name of some Java class that you want to use to handle the logging output. The built-in options include org.apache.tools.ant.DefaultLogger, which we have just seen, and org.apache.tools.ant.XmlLogger which looks like the following:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="log.xsl"?>

<build time="0 seconds">
  <task location="/Article1/ex1_files/build.xml:3: " 
        name="taskdef" time="0 seconds"></task>

  <target name="demo" time="0 seconds">
    <task location="/Article1/ex1_files/build.xml:6: " 
          name="helloworld" time="0 seconds">
      <message priority="info"><![CDATA[Hello, World!]]></message>
    </task>
  </target>
</build>

Larger build projects can definitely benefit from some form of an XML-based log output, especially if any downstream processing is needed. However, my personal opinion about Ant's built-in XML logger is that it really isn't quite as useful as I would like: when something goes wrong, the Ant-level error messages are placed in elements that are siblings to, not children of, the task elements that caused them, and the only way to relate which error belongs to which task is to read into a CDATA section. This alone makes automated log file analysis more cumbersome that it needs to be. You also only get one stacktrace element for the entire build, even if you are running Ant in "keep going" mode and several problems result. (Specifically, you get a stacktrace element for the first problem.)

This isn't to say you can't get what you want, just that the default XML-based logger currently included with Ant might not suit your needs. Fortunately, taking control of the logging facility to generate log messages in a format you require is rather easy. In fact, the need to perform hands-off, post-build analysis and decision making in a nightly build system eventually led me to my first experience with custom XML-based logging.

Loggers versus Listeners

In addition to a logger, Ant also has a log message handling category known as a listener. A logger basically is a listener. The first key difference is that a logger is given the ability to write to the log output device, which is either the console or some file specified with the "-logfile" command line argument. The second key difference results from the first: your Ant script can have multiple listeners but only one logger. The logger is that which Ant connects to the System.out and System.err streams. In fact, the logger neither knows nor cares about the exact nature or destination of these streams. Given this, I shall reiterate the Ant manual's warning to not directly write to these streams -- your logger might cause an infinite loop.

Developer's Diversion: Ant's Project class has a method called getBuildListeners which returns a Vector. The logger in use seems to be the first element in this vector, and additional listeners, if any, comprise the remainder of the vector. However, I am not one of the Ant developer's, so this is not an authoritative statement.

In addition to the two built-in loggers mentioned above, you can use the "-listener" command line argument to connect your Ant script to these built-in listener classes: org.apache.tools.ant.listener.CommonsLoggingListener, and org.apache.tools.ant.listener.Log4Listener. These listeners connect to the Apache "commons logging" facility and the Log4j logging facility, respectively. I will not be discussing how to configure those logging facilities as they are separate projects, each likely meriting its own article series. I mainly mention the existence of these built-in listeners more for completeness, since the standard Ant manual doesn't cover them too obviously outside of the API documentation itself. Also apparently not clear to a few people I've encountered on forums: if you want multiple listeners, add each with their own "-listener" argument.

A Custom Syslog Listener

Ant Listener Overview

Implementing a custom listener is a matter of creating a Java class that implements the following methods from the org.apache.tools.ant.BuildListener interface:

buildStarted(BuildEvent e)
buildFinished(BuildEvent e)
targetStarted(BuildEvent e)
targetFinished(BuildEvent e)
taskStarted(BuildEvent e)
taskFinished(BuildEvent e)
messageLogged(BuildEvent e)

Of course, your implementation for some of these methods can simply "do nothing" if you are only concerned with capturing certain log information. Regardless, with an implementation class available somewhere in the classpath, just run your Ant script and use the "-listener" argument to specify a listener which should receive logging messages.

Note: A custom listener is being discussed. A custom logger will be discussed later. Your listener will function in addition to whichever logger is being used.

The BuildEvent argument to all of the interface methods is another Ant class. This class allows you to get a reference to the Project, Target, and Task the script is running in, the actual message text itself, the Ant-level priority of the message (verbose, warning, etc.) as emitted by the task code, and a Throwable object. By convention, this Throwable will be null under normal circumstances. When it is non-null, this is generally due to a BuildException having been emitted by task code. You definitely want to watch for a non-null Throwable and send information about it somewhere as an error-level message, but you should really only expect to receive non-null throwables on the xxxFinished interface methods.

To provide some value to the discussion, assume we want to implement a custom listener that sends messages to a remote "syslog" server via UDP packets.

Syslog Overview

Syslog is a common network service that allows a machine on the network to be a recipient of logging messages from multiple other machines on the network. For the current discussion, RFC 3164 is relevant and serves as the basis for my syslog implementation classes. One point to bear in mind is that, in addition to message severity levels (error, warning, etc), the syslog protocol also defines a "facility" which is rather like a category that the application that is emitting the log messages belongs to. A syslog server is commonly configured to send messages from different log facilities into different files (or possibly to forward them to another syslog server) so the exact destination of your syslog messages depends on the whim of the network administrator. (If this isn't you, then you might be relegated to simply read along.) Also, many facility values are reserved for special applications, although eight "local" facilities (notionally designed "local0" through "local7") are defined for your own use. Lastly, note that syslog servers can be configured to either listen for or disregard UDP-based messages. Mac OS X 10.4, for example, needs explicit configuration adjustments to log UDP syslog messages. I have included a link to a quick "howto" explaining what steps are needed for OS X 10.4. For most UNIX-derivatives, running the syslog daemon with the "-u" argument seems to be necessary, plus configuration of a file typically in the "/etc" directory.

Note: Mac OS X 10.4 seems to use the "local0" facility for firewall-based messages, so it would be wise to use one of the other unclaimed local facilities. You might spare yourself some grief by first checking if your syslog configuration is already using a particular local facility for some meaningful purpose before you hard-wire a custom listener to use it.

Syslog Implementation Classes

In the zip file accompanying this article are two classes: SyslogMessage and SyslogMessageFactory. I wrote these classes to get a rough implementation for a UDP-based remote syslog facility. The SyslogMessage class itself contains, among other things, the actual message text being logged and the message priority level. It also contains the working code to transmit a UDP packet to some syslog server. The other class, SyslogMessageFactory, is a factory-pattern helper class designed mainly to take some of the effort out of creating and configuring lots of SyslogMessage objects. A very simplified usage pattern for these follows:

  1. Create a new a SyslogMessageFactory object. Using the simplest constructor option, pass in some short name you want attached to your log messages. This constructor will assume the syslog server is running on your (the local) machine.
  2. Call the setFacility method on your factory object to set the facility value. Note that your syslog server may need explicit configuration to handle messages matching this facility value. You should use one of the eight ogMessage object. Valid options for code for these classes. I will admit up front, however, that comments are rather sparse.

The Ant BuildListener Implementation

Also included in the zip file accompanying this article is a class called SyslogListener. This is simply a class that directly implements Ant's BuildListener interface. A no-arg constructor is used to initialize a SyslogMessageFactory object before actual Ant log messages are dealt with. Ant then calls the various BuildListener interface methods as interesting events occur during the build. These methods in turn analyze the BuildEvent and use the factory object reference to create SyslogMessage objects, which are then transmitted. The following is a snippet of code from the SyslogListener class:

public class SyslogListener implements BuildListener {

  private SyslogMessageFactory factory = null;


  /* no-arg constructor called by Ant during startup */
  public SyslogListener() {
    factory = new SyslogMessageFactory("ant_demo");
    factory.setFacility(SyslogMessage.FACILITY_LOCAL_7);
  }


  /* convenience method to catch send()-related exceptions */
  private void deliverMessage(SyslogMessage message) {
    try {
      message.send();
    }
    catch (IOException ioe) {
      /* ignored for simplicity */
    }
  }


  /* BuildListener implementation methods start... */

  public void buildStarted(BuildEvent event) {
    String projectName = event.getProject().getName();
    deliverMessage(factory.createInfoMessage("Build starting for "
                   +projectName));
  }

  public void buildFinished(BuildEvent event) {
    Throwable t = event.getException();
    String projectName = event.getProject().getName();
    if (t!=null) {
      deliverMessage(factory.createErrorMessage("Build exception in "
                     +projectName+": "
                     +t.toString()));
    }
    deliverMessage(factory.createInfoMessage("Build finished for "
                   +projectName));
  }

  // ADDITIONAL METHODS NOT SHOWN
}

Refer to the source code in this article's accompanying zip file for the complete version.

Trying it Out

Assuming your syslog server is configured to accept and log UDP messages for the "local7" facility, place the above class into your classpath (or adjust your CLASSPATH system environment variable) and call Ant with the "-listener org.roblybarger.SyslogListener" argument. You will get the standard console output for your build file, and your syslog server's log files should have some new messages as well. If not, check the syslog server's configuration and restart the syslog service. Failing that, check for firewall rules causing an obstruction.

A Custom XML Logger

The BuildLogger interface in Ant is a subinterface of the BuildListener interface we have just discussed. The BuildLogger interface adds a few more methods:

setOutputPrintStream(PrintStream)
setErrorPrintStream(PrintStream)
setMessageOutputLevel(int)
setEmacsMode(boolean)

The first two pass in a PrintStream object which Ant will connect for you and which you should use in place of System.out and System.err calls. These PrintStream objects might be connected to an output file by the user at script runtime via the "-logfile" command line option, so dealing with these PrintStream objects is the right thing to do. You should save the references to some instance variables in your logger's class. The third method listed is the level of severity that the user wants messages displayed for. (The user specified this via "-v" or "-d" command line arguments.) Should you wish to honor the user's choice -- you are certainly under no obligation to do so -- you should also save this value to a instance variable and compare it to the priority level in each BuildEvent you are given later. The final method is rather curious: it signals to your logger class that the user wants output in an emacs-friendly format. (For those completely unaware, emacs is one of two very popular text-mode editors available in UNIX systems.) I'll admit mild confusion as to the utility of this method since I tend to supply do-nothing implementations for it. The Ant API is also not terribly more informative about it either.

A New XML Logger

Near the outset of this article, I expressed mild dissatisfaction with the built-in XML logger. I now address my complaints by redesigning the format of the XML data itself. If a task has an exception, it will show up as a child of that task. I dispense with the timing information and I write elements as they happen rather than all-at-once upon completion. (It is better, in my opinion and experience, to have half a log than none at all.) To get on-the-fly elements -- and to simplify the mechanics for this article -- I am actually using direct file I/O rather than use a formal XML API. I also dispense with CDATA sections and instead directly write log messages into text nodes. These decisions, of course, suit my individual needs. Before undertaking a full-blown, XML-based custom Ant logger, you should decide what information you need and how you might want to process it before you decide how to structure the XML data itself. (To quote a professor from my collegiate days: "You can do anything you want, but whatever you do, know what you are doing.")

With this all being said, I am only showing snippets of the full logger implementation here. As always, refer to this articles accompanying zip file for the complete version. First, a few instance variables and the implementations for the BuildLogger interface methods:

OutputStreamWriter out;
PrintStream err;
int ouputLevel = Project.MSG_INFO;
public void setMessageOutputLevel(int level) {
  outputLevel = level;
}

public void setOutputPrintStream(PrintStream psOut) {
  out = new OutputStreamWriter(psOut,
                               Charset.forName("UTF-8"));
}

public void setErrorPrintStream(PrintStream psErr) {
  err = psErr;
}

public void setEmacsMode(boolean b) {
  /* ignored */
}

I have simply saved the message output level for use later in the messageLogger method. The error PrintStream reference is saved as-is, but the output PrintStream is instead connected to an OutputStreamWriter with UTF-8 encoding -- a common encoding in most XML files.

From here, I provide a convenience method to write to the output stream and catch wayward IO exceptions. All of the BuildListener interface methods will use this convenience method:

private void writeOutput(String s) {
  try {
    out.write(s);
    out.flush();
  }
  catch (IOException ioe) {
    err.println("Exception encountered: "+ioe);
    ioe.printStackTrace(err);
  }
}

Another convenience method will format a build exception to include the exception's top-level message and the complete stacktrace:

private void writeThrowable(Throwable t) {
  if (t!=null) {
    writeOutput("<exception>n");
    writeOutput("<message>"+t.toString()+"</message>n");
    writeOutput("<stacktrace>n");
    for(StackTraceElement e : t.getStackTrace()) {
      writeOutput("<element>"+e.toString()+"</element>n");
    }
    writeOutput("</stacktrace>n</exception>n");
  }
}

Finally, implementation for the BuildListener interface methods:

int exceptionCount = 0;
boolean inTask = false;

public void buildStarted(BuildEvent be) {
  writeOutput("<?xml version="1.0" charset="utf-8"?>n");
  writeOutput("<build>n");
}

public void buildFinished(BuildEvent be) {
  writeOutput("<buildsummary exceptionCount=""+
               exceptionCount+""/>n");
  writeOutput("</build>n");
}

public void targetStarted(BuildEvent be) {
  String name = be.getTarget().getName();
  writeOutput("<target name=""+name+"">n");
  Enumeration e = be.getTarget().getDependencies();
  if (! e.hasMoreElements()) {
    return;
  }
  writeOutput("<dependencies>n");
  while (e.hasMoreElements()) {
    Object o = e.nextElement();
    writeOutput("<dependency>"+o.toString()+"</dependency>n");
  }
  writeOutput("</dependencies>n");
}

public void targetFinished(BuildEvent be) {
  writeOutput("</target>n");
}

public void taskStarted(BuildEvent be) {
  String name = be.getTask().getTaskName();
  writeOutput("<task name=""+name+"">n");
  inTask = true;
}

public void taskFinished(BuildEvent be) {
  Throwable t = be.getException();
  if (t!=null) {
    writeThrowable(t);
    exceptionCount++;
  }
  writeOutput("</task>n");
  inTask = false;
}

public void messageLogged(BuildEvent be) {
  if (!inTask) { return; }
  int priority = be.getPriority();
  if (priority <= outputLevel) {
    String message = be.getMessage();
    writeOutput("<message priority=""+priority+"">"+
                 message+"</message>n");
  }
}

There you have it -- a complete, if minimalist, XML-formatted custom Ant logger. A few comments are worth discussing.

First, if a task throws a BuildException, then the taskFinished, targetFinished, and buildFinished will all receive BuildEvents that return a non-null Throwable for their getException method. I have decided to only catch possible exceptions at the task level where they originate.

Second, the Enumeration element returned in a Target's getDependencies method stores String objects, not Target objects. If you would rather have the actual Target object, you might experiment with the Project class's getTargets method, which returns a Hashtable. The Ant API docs, however, do not really document what any particular returned Collection is storing though, so let getClass().getName() be your guide. Also seemingly absent is a way to query the Project class for the actual Target the user is requesting to run.

Third, as I mentioned above, I am using the value stored in the outputLevel instance variable and comparing that against the priority level of the BuildEvent in the messageLogger method. If you run an Ant script that, for example, sets an Ant property, you will get additional output when running in debug mode detailing the action of the property task.

Admittedly, this is a fairly minimal starting point for a truly useful XML log file. I did, however, use just such a starting point as this to write a rather complex logger for my current employer. For example, certain tasks such as javac had more tailored output format (such as how many source files were marked for compilation and how many were actually compiled) while other tasks used a generic output format similar to the above. The result of these XML logs were then fed through some XSL transformations and Ant scripts to perform automated post-build diagnostics as well as to create developer-friendly HTML-formatted output files. Your own requirements for a custom logger will assuredly take you in different directions.

Warning on Sub-builds and Loggers

If your Ant script calls other Ant scripts internally (a sub-build), then take warning: the current version of Ant (1.6.5) unfortunately hardcodes the sub-Project's logger to a DefaultLogger, does not propagate BuildListeners, and does not pass the message output level through to the sub-ant build. Brave souls might take a look at a source distribution of Ant and look over the Ant.java task code for possible modification. Before my Ant-related project was complete, I eventually created a custom "Ant" task that allowed me to specify these values as attributes in my main build file.

Summary

In this three-article series, I have led you on a trip deep into Ant customization territory. I have shown you how to take control of just about every facet of Ant: writing your own task code, adding new conditionals and file selectors, creating your own supporting types as nested elements as well as using Ant's own types in your tasks, and finally taking complete control over even the logging system itself. While I continue to have some minor, personal complaints about Ant's design, user's manual, and API documentation, it can hardly be argued that the Ant developers have made a very flexible system. With any luck, some of my hard-won knowledge will save you some effort.

Remember, these articles include working example code. Consider the examples to merely be a jumping-off point for your own experimentation and problem solving.

Resources

Article #1 : Introduction to Custom Ant Tasks
Article #2 : More on Custom Ant Tasks
RFC3164 (BSD syslog protocol)
Configuring the syslog service in Mac OS X 10.4

About the Author

Rob Lybarger is a software engineer who has been using Java for nearly a decade and using Ant since its earliest days. He has used various versions of Windows, various distributions of Linux, and, most recently, Mac OS X. While knowing a wide array of software languages, the cross-platform nature of Java and Ant has proven to be a valuable combination for his personal and professional efforts.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date