Home > Articles > Programming > Java

Java Reference Guide

Hosted by

Toggle Open Guide Table of ContentsGuide Contents

Close Table of ContentsGuide Contents

Close Table of Contents

Tailer Library

Last updated Mar 14, 2003.

If youโ€™re looking for more up-to-date information on this topic, please visit our Java article, podcast, and store pages.

You are probably familiar with the UNIX concept of tailing a file, or, more specifically, using the tail command with the –f option to following a running file. In case you haven't encountered it before: the purpose of a following tail is to watch the contents of a file as the file is being created. It's usually a log file; it is common, in the UNIX world, to follow a server process log file as it is starting, to ensure that the server starts properly.

As I am writing my next book about Java EE Performance Management, I was reminded of an article I wrote, describing how to convert a verbose garbage collection log to XML for better integration into a monitoring solution. However, there is a problem with parsing an entire file. It gives you good information after-the-fact, but it does not allow you to see things in real-time.

The solution to this problem is to build a log file tailer that can follow a running verbose garbage collection log file and fire events as new lines are written. But as I am always looking to write reusable code, I abstracted this functionality into a generic log file tailer class that I will later use to tail verbose garbage collection logs. For now, you can use this class and infrastructure to follow any text-based file.

Before diving into code, let's review the requirements for a tailer program. A tailer program must unobtrusively open a text file (so that the owner of the file can continue to write to it); wait for new lines to be written to it; and notify someone when new lines are added.

The next question is: how can we do this with Java? We are used to reading text files using readers, but those are meant to read a stream until completion and stop; we need another way. And that way is the RandomAccessFile class. Our strategy is to open a file as a RandomAccessFile in read-only mode, seek to the end of the file, and then watch the file. Unfortunately, there is no easy way to watch a file: it would be nice if a notification mechanism could tell us when the file is updated, but in lieu of that, we fall back to polling. On a fixed interval we check the length of the file. If it is greater than our file pointer, we seek to our file pointer, read until the end of the file, and then update the file pointer. Simple enough, no?

Listing 1 shows the code for the LogFileTailer class.

Listing 1. LogFileTailer.java

package com.javasrc.tuning.agent.logfile;

import java.io.*;
import java.util.*;

/**
 * A log file tailer is designed to monitor a log file and send notifications
 * when new lines are added to the log file. This class has a notification
 * strategy similar to a SAX parser: implement the LogFileTailerListener interface,
 * create a LogFileTailer to tail your log file, add yourself as a listener, and
 * start the LogFileTailer. It is your job to interpret the results, build meaningful
 * sets of data, etc. This tailer simply fires notifications containing new log file lines, 
 * one at a time.
 */
public class LogFileTailer extends Thread 
{
  /**
   * How frequently to check for file changes; defaults to 5 seconds
   */
  private long sampleInterval = 5000;

  /**
   * The log file to tail
   */
  private File logfile;

  /**
   * Defines whether the log file tailer should include the entire contents
   * of the exising log file or tail from the end of the file when the tailer starts
   */
  private boolean startAtBeginning = false;

  /**
   * Is the tailer currently tailing?
   */
  private boolean tailing = false;

  /**
   * Set of listeners
   */
  private Set listeners = new HashSet();

  /**
   * Creates a new log file tailer that tails an existing file and checks the file for
   * updates every 5000ms
   */
  public LogFileTailer( File file )
  {
    this.logfile = file;
  }

  /**
   * Creates a new log file tailer
   * 
   * @param file         The file to tail
   * @param sampleInterval    How often to check for updates to the log file (default = 5000ms)
   * @param startAtBeginning   Should the tailer simply tail or should it process the entire
   *               file and continue tailing (true) or simply start tailing from the 
   *               end of the file
   */
  public LogFileTailer( File file, long sampleInterval, boolean startAtBeginning )
  {
    this.logfile = file;
    this.sampleInterval = sampleInterval;
  }

  public void addLogFileTailerListener( LogFileTailerListener l )
  {
    this.listeners.add( l );
  }

  public void removeLogFileTailerListener( LogFileTailerListener l )
  {
    this.listeners.remove( l );
  }

  protected void fireNewLogFileLine( String line )
  {
    for( Iterator i=this.listeners.iterator(); i.hasNext(); )
    {
      LogFileTailerListener l = ( LogFileTailerListener )i.next();
      l.newLogFileLine( line );
    }
  }

  public void stopTailing()
  {
    this.tailing = false;
  }

  public void run()
  {
    // The file pointer keeps track of where we are in the file
    long filePointer = 0;

    // Determine start point
    if( this.startAtBeginning )
    {
      filePointer = 0;
    }
    else
    {
      filePointer = this.logfile.length();
    }

    try
    {
      // Start tailing
      this.tailing = true;
      RandomAccessFile file = new RandomAccessFile( logfile, "r" );
      while( this.tailing )
      {
        try
        {  
          // Compare the length of the file to the file pointer
          long fileLength = this.logfile.length();
          if( fileLength < filePointer ) 
          {
            // Log file must have been rotated or deleted; 
            // reopen the file and reset the file pointer
            file = new RandomAccessFile( logfile, "r" );
            filePointer = 0;
          }

          if( fileLength > filePointer ) 
          {
            // There is data to read
            file.seek( filePointer );
            String line = file.readLine();
            while( line != null )
            {
              this.fireNewLogFileLine( line );
              line = file.readLine();
            }
            filePointer = file.getFilePointer();
          }

          // Sleep for the specified interval
          sleep( this.sampleInterval );
        }
        catch( Exception e )
        {
        }
      }

      // Close the file that we are tailing
      file.close();
    }
    catch( Exception e )
    {
      e.printStackTrace();
    }
  }
}

The LogFileTailer class extends java.lang.Thread. To use it, you must create an instance of it, and call its start() method. This begins the execution of the LogFileTailer's run() method in a new thread. As the LogFileTailer class is constructed, it is passed a file to tail; a sampling interval (defaulted to 5 seconds) to check the file for updates; and a boolean flag specifying whether the tail should parse the entire existing file and then start tailing, or simply start tailing from the end (the default behavior).

The LogFileTailer class maintains a Set of listeners, implementing the LogFileTailerListener interface, that are notified when the log file is notified. Listing 2 shows the code for the LogFileTailerListener interface.

Listing 2. LogFileTailerListener.java

package com.javasrc.tuning.agent.logfile;

/**
 * Provides listener notification methods when a tailed log file is updated
 */
public interface LogFileTailerListener
{
  /**
   * A new line has been added to the tailed log file
   * 
   * @param line   The new line that has been added to the tailed log file
   */
  public void newLogFileLine( String line );
}

The LogFileTailerListener interface defines a single method, newLogFileLine(), that is called once for each line added to the tailed log file.

The LogFileTailer class's run() method begins by determining whether the file pointer should point to the beginning of the file or the end of the file (from the startAtBeginning boolean parameter passed to the constructor). Next, it opens the log file to be tailed and then sits in a loop checking the file length. The file length can fall into one of three modes:

  • The file length is the same as the file pointer value. This denotes that the file has not been updated.
  • The file length is greater than the file pointer. This denotes that the file has been updated.
  • The file length is less than the file pointer. This denotes that the log file has been deleted or (more likely) rotated, so we need to start from the beginning of the file again.

In the first case, the LogFileTailer does not do anything; it just sleeps for the sample interval.

In the second case, the LogFileTailer calls the RandomAccessFile class's seek() method to position the file pointer appropriately. It then repeatedly calls readLine() until it reaches the end of the file stream. For each line that it reads, the program fires an event notification to its listeners.

If the final case, the program reopens the log file and resets the file pointer back to the beginning of the log file. It then processes the entire file.

As an example of using this framework to tail a text file, I created a Java console-based application that performs functionality similar to the tail –f command in UNIX. Its source code is shown in listing 3.

Listing 3. Tail.java

package com.javasrc.tuning.agent.logfile;

// Import the Java classes
import java.util.*;
import java.io.*;

/**
 * Implements console-based log file tailing, or more specifically, tail following:
 * it is somewhat equivalent to the unix command "tail -f"
 */
public class Tail implements LogFileTailerListener
{
  /**
   * The log file tailer
   */
  private LogFileTailer tailer;

  /**
   * Creates a new Tail instance to follow the specified file
   */
  public Tail( String filename )
  {
    tailer = new LogFileTailer( new File( filename ), 1000, false );
    tailer.addLogFileTailerListener( this );
    tailer.start();
  }

  /**
   * A new line has been added to the tailed log file
   * 
   * @param line   The new line that has been added to the tailed log file
   */
  public void newLogFileLine(String line)
  {
    System.out.println( line );
  }

  /**
   * Command-line launcher
   */
  public static void main( String[] args )
  {
    if( args.length < 1 )
    {
      System.out.println( "Usage: Tail <filename>" );
      System.exit( 0 );
    }
    Tail tail = new Tail( args[ 0 ] );
  }
}

Listing 3 demonstrates the simplicity of implementing a tail follower program. The Tailer class simply creates a LogFileTailer instance that follows the specified log file. It registers itself as a LogFileTailerListener and prints out new lines as they appear.

In retrospect, to create a GUI tailer, it would be a simple matter of creating a Swing GUI to redirect these messages to a JTextArea or even to create a set of JInternalFrame instances to tail multiple log files simultaneously. And, to take it a step further, you could build context-sensitive processors. They could color code text based on the type of message being received (e.g. tailing a WebLogic log might color fatal and emergency messages red, warning messages orange, and informational messages blue). Or they build a JTable that interprets and parses log messages for better sorting and filtering. The possibilities are vast. If I get around to building some of this functionality, I will surely share it with you!

Summary

Cool tools: it's a collection of cool tools that I develop and find on the Internet. Thus far, I have shared my LogFileTailer class with you.

If you have any cool tools that you would like to share with everyone, send me an e-mail at lygado@yahoo.com with your code. I will do my best to post an article about it. Sorry, there's no financial compensation; but if you want to contribute your tools to the world or gain some visibility, if it is cool enough then I would love to help you!