Home | FAQ | Contact me

Some JUnit logging...

Today, a subset of what I was working on was to provide a way to debug a statistics generator I'm writing. For some reason, the more elaborate log4j-based logging class I use in production mode would not work from my JUnit testing, so I threw this one together. This is not a serious attempt to replace that real facility, however, but just something I had to do by reason of an externally imposed impediment I won't bore you with.

Inside this code

In terms of beginning Java samples you have:

  • log4j's fundamental behavior (more on log4j)
  • Use of some date and time functionality (lines 42 and 219)
  • Some rudimentary file and directory I/O (lines 54, 88, 153 and 288)
  • Use of a static { ... } block that initializes the static portions of this class (line 39)
  • A tiny amount of reflexion (line 77)
Eclipse tip: Testing with main() or JUnit

When you create a class that you'll want to test lightly, be sure to tell Eclipse to create the main() method too. This will ensure that when you right-click on your class and choose Run As or Debug As, you'll get the option of Java Application and not have to open the Run dialog to configure it to do that.

If you're going to test it with JUnit, this isn't a problem, but there, be sure to use Eclipse's JUnit test class wizard to create your test. And be sure you have added the JUnit library using Build Path.


package com.etretatlogiciels.samples.logging;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Calendar;
import java.text.SimpleDateFormat;

/**
 * Quick and dirty replacement for log4j. The logging levels are slightly
 * different from log4j. I used TRACE, for instance, to profile the actual
 * sequence of method calls of a facility I was implementing and it worked
 * perfectly for this.
 *
 * - DEBUG
 * - TRACE
 * - INFO
 * - WARN
 * - ERROR
 */
public class JUnitLogger
{
  public static int     DEBUG = 1, TRACE = 2, INFO = 3, WARN = 4, ERROR = 5;
  public static int     TO_LOGFILE = 0, TO_CONSOLE = 1;

  private String        classname      = "[unknown class]";
  private int           level          = DEFAULT_LEVEL;
  private int           target         = DEFAULT_TARGET;

  private static int    DEFAULT_TARGET = TO_LOGFILE;
  private static int    DEFAULT_LEVEL  = INFO;

  private static String DEFAULT_PATH   = "/tmp/junit/logs";
  private static String DEFAULT_NAME   = "JUnit";
  private static String FILENAME       = null;

  static
  {
    // name the logger output file...
    Calendar          cal = Calendar.getInstance();
    SimpleDateFormat  sdf = new SimpleDateFormat( "yyyy-MM-dd" );

    FILENAME = DEFAULT_NAME + "-"
                            + sdf.format(  cal.getTime() )
                            + ".log";

    /* TODO: This caves in if the privileges aren't propitious for us on
     * "/tmp/junit/logs". If this code gets used for real work, it's going
     * to have to be redone including to support Windows. It's also complete
     * crap: rewrite it someday if and when it begins to matter.
     */
    File  whole = new File( DEFAULT_PATH );

    if( !whole.exists() )
    {
      File  junit = new File( "/tmp/junit" );

      if( !junit.exists() )
        junit.mkdir();

      File  logs = new File( DEFAULT_PATH );

      if( !logs.exists() )
        logs.mkdir();
    }
  }

  /**
   * Create a new logger and give it a name to display.
   *
   * @param callingClass—for use in the logging message (Class)
   */
  public JUnitLogger( Class< ? > callingClass )
  {
    if( callingClass != null )
      this.classname = callingClass.getName();
  }

  /**
   * Bounce the default logging file (delete it).
   */
  public static void bounceJUnitLogger()
  {
    File  f = new File( makeLogPath() );

    if( f != null )
      f.delete();
  }

  /**
   * Change the default target of all subsequent logger instances from going to
   * a file to going to the console.
   */
  public static void defaultToUseConsole() { DEFAULT_TARGET = TO_CONSOLE; }

  /**
   * Re-establish the level of logging in force or return to default setting.
   *
   * @param newLevel—new level or 0 to return to default (int)
   */
  public static void setDefaultLevel( int newLevel )
  {
    if( inrange( DEBUG, newLevel, ERROR ) )
      DEFAULT_LEVEL = newLevel;
    else if( newLevel == 0 )
      DEFAULT_LEVEL = INFO;
  }

  /**
   * Force this logger instance to write to console instead of to file if true
   * or back to the file if false.
   *
   * @param yesno—true if messages are to be output to the console (boolean)
   */
  public void useTarget( int tgt )
  {
    if( inrange( TO_LOGFILE, tgt, TO_CONSOLE ) )
      this.target = tgt;
  }

  /**
   * Change the logging level; this may be convenient, but it also
   * may be pernicious, so watch out.
   *
   * @param newLevel—at which to log messages from now on (int)
   */
  public void changeLevel( int newLevel )
  {
    if( inrange( DEBUG, newLevel, ERROR ) )
      this.level = newLevel;
  }

  /**
   * Establish the logfile path different from the one in force or return to
   * the default. The path must exists and the process have privileges or
   * logging will fail.
   *
   * TODO: If this becomes real code (unlikely), then fix this unpardonably
   * lazy attitude about the path existing and being accessible.
   *
   * @param path—new path or nil to return to default (String)
   */
  public static void setDefaultPath( String path )
  {
    if( path == null )
    {
      DEFAULT_PATH = "/tmp/junit/logs";
      return;
    }

    File  f = new File( path );
    if( !f.isDirectory() )
      return;

    DEFAULT_PATH = path;
  }

  /**
   * Insert a blank line in the log file. You saw it here first!
   */
  public void insertBlankLine()
  {
    writeOutput( "" );
  }

  /**
   * Insert a blank line in the log file. It will do this only if the
   * current logging level meets the threshold.
   *
   * @param threshold—below which this action will occur (int)
   */
  public void insertBlankLine( int threshold )
  {
    if( this.level <= threshold )
      writeOutput( "" );
  }

  /**
   * Insert a horizontal ruler in the log file. You saw it here first!
   * You can supply a string of characters to use as a horizontal ruler.
   */
  public void insertHorizontalRuler( String horizontalRule )
  {
    writeOutput( horizontalRule );
  }

  /**
   * Insert a horizontal ruler in the log file. It will do this only if the
   * current logging level meets the threshold.
   *
   * @param threshold—below which this action will occur (int)
   */
  public void insertHorizontalRuler( int threshold, String horizontalRule )
  {
    if( this.level <= threshold )
      writeOutput( horizontalRule );
  }

  public boolean isDebugEnabled() { return( this.level <= DEBUG ); }
  public boolean isTraceEnabled() { return( this.level <= TRACE ); }
  public boolean isInfoEnabled()  { return( this.level <= INFO  ); }
  public boolean isWarnEnabled()  { return( this.level <= WARN  ); }
  public boolean isErrorEnabled() { return( this.level <= ERROR ); }

  public void debug( String msg ) { doLogger( DEBUG, msg ); }
  public void trace( String msg ) { doLogger( TRACE, msg ); }
  public void info ( String msg ) { doLogger( INFO,  msg ); }
  public void warn ( String msg ) { doLogger( WARN,  msg ); }
  public void error( String msg ) { doLogger( ERROR, msg ); }

  /* ========================================================================
   * Private methods
   */

  private static String manufactureDateAndTimeStamp()
  {
    // return "2009-04-30 19:32:59.731"
    Calendar  cal    = Calendar.getInstance();
    long      milliseconds = cal.get( Calendar.MILLISECOND );
    String    year    = "" + cal.get( Calendar.YEAR );
    String    month   = "" + cal.get( Calendar.MONTH );
    String    day     = "" + cal.get( Calendar.DATE );
    String    hour    = "" + cal.get( Calendar.HOUR_OF_DAY );
    String    minute  = "" + cal.get( Calendar.MINUTE );
    String    second  = "" + cal.get( Calendar.SECOND );
    String    millis  = "" + milliseconds;

    /* Uniform width!
     * We found that despite HOUR_OF_DAY in place of DAY and all other
     * things being equal, this screws up and yields narrower widths
     * like single digits before 10 o'clock, etc. So, we ensure they
     * are double-wide. Also, we compensate for millisecond values not
     * occupying three places.
     */
    if( hour.length() == 1 )
      hour = " " + hour;
    if( minute.length() == 1 )
      minute = "0" + minute;
    if( second.length() == 1 )
      second = "0" + second;
    if( milliseconds < 10 )
      millis = "00" + millis;
    else if( milliseconds < 100 )
      millis = "0" + millis;

    String  timestamp = year
                + "-" + month
                + "-" + day
                + " " + hour
                + ":" + minute
                + ":" + second
                + "." + millis;

    return timestamp;
  }

  private static boolean inrange( int lo, int x, int hi )
  {
    return( x >= lo && x <= hi );
  }

  private static String makeLogPath()
  {
    return DEFAULT_PATH + "/" + FILENAME;
  }

  private String formatOutput( String msg )
  {
    String  output  = manufactureDateAndTimeStamp();
    output += " " + textForLevel( this.level );
    output += " " + this.classname;
    output += ": " + msg;
    return output;
  }

  private synchronized void writeOutput( String output )
  {
    if( this.target == TO_CONSOLE )
    {
      System.out.println( output );
      return;
    }

    try
    {
      // open file and append...
      FileWriter  f = new FileWriter( makeLogPath(), true );

      f.write( output + "\n" );
      f.close();
    }
    catch( IOException e )
    {
      e.printStackTrace();
    }
  }

  private final void doLogger( int threshold, String msg )
  {
    if( this.level <= threshold )
      writeOutput( formatOutput( msg ) );
  }

  private String textForLevel( int which )
  {
    switch( which )
    {
      case 1 :  return " DEBUG ";
      case 2 :  return " TRACE ";
      case 3 :  return " INFO  ";
      case 4 :  return " WARN  ";
      case 5 :  return " ERROR ";
      default : return " [unknown level] ";
    }
  }


  public static void main( String[] args )
  {
    JUnitLogger.bounceJUnitLogger();
    //JUnitLogger.defaultToUseConsole();
    JUnitLogger.setDefaultLevel( JUnitLogger.TRACE );

    JUnitLogger  log = new JUnitLogger( JUnitLogger.class );

    // the quickie test will output just this one line...
    log.trace( "Do stuff in main()" );
  }
}
// vim: set tabstop=2 shiftwidth=2 noexpandtab: