|
YOUR FEEDBACK Did you read today's front page stories & breaking news?
SYS-CON.TV |
TOP THREE LINKS YOU MUST CLICK ON Java Basics Building the Ultimate Logging Solution
...and solving common interoperability issues
By: Jerason Banes
May. 5, 2004 12:00 AM
Anyone who has dealt with complex enterprise applications knows the value of a good logging solution. Features such as consolidating log files, separating events, and turning debugging on or off all come free with a good logging API. As a result, developers have been converting their existing System.out logging to many of the advanced solutions that have appeared on the Java scene. This article attempts to demonstrate how to convert your System.out logging to the new Java 1.4 logging APIs. Along the way, new concepts and algorithms will be introduced to solve common interoperability issues. Using the 1.4 Logger API While many applications will only deal with a single Logger instance, it may make sense for some applications to have multiple loggers. An example of this is a job scheduling server that creates a separate log file for each task (see Figure 1). To split log messages between files, the server assigns a new Logger instance when the task is created. The result is a much cleaner separation of events - something that can be invaluable when tracing a problem. To obtain a Logger instance, we need a unique name. Keep in mind that loggers exist for the life of the application, so it's very important to choose a unique name. Failure to do so can result in strange or unexpected behavior. Since class names must be unique, a common solution is to use the primary class name. Thus a Logger for the class com.example.MyServer might be obtained via the following code: Logger logger = Logger.getLogger("com.example.MyServer"); Log Records Explained To log a message, we need to create a new instance of the java.util.LogRecord class. This class encompasses all the necessary information about the message, including the text and the "level." Once created, the object can be passed to the Logger's log() method, which will either output the message or discard it. The following example prints "This is an informational message." to the log: LogRecord record = new LogRecord(Level.INFO, "This is an informational message."); Levels of Logging Logging levels are defined by the java.util.logging.Level class. This class wraps an integer that defines the current level. While you can create new instances of Level class, it's generally much easier to use one of the predefined levels. The predefined levels (from highest to lowest) are as follows: Level.SEVERE FINE, FINER, and FINEST are usually for various levels of debugging information and would normally be turned off in a production environment. CONFIG is for printing information on the current server configuration; INFO is for informational messages such as those usually emitted via System.out; WARNING is for possible problems; and SEVERE is for outright program errors. Custom levels are usually higher than SEVERE. The default is to log Level.INFO or higher. This can be changed by making a call to Logger.setLevel(), or by modifying the "lib/ logging.properties" file in the JRE directory. Look up java.util.logging. Log Manager in the Javadocs for more information on modifying the logging.properties file. Here's an example of using the FINEST level to log debugging info: Logger logger = Logger.getLogger("com.example.MyServer"); Redirecting to a File After checking the logging level, the Logger class calls a subclass of java.util. logging.Handler. The Handler is responsible for writing the LogRecord to the console, a file, or even a network stream. For the purposes of this article, we'll be using the java.util.logging.FileHandler class to log to a file. To use the FileHandler class, we need to create a new instance and call Logger- .setHandler(). The name of the log file can be specified by passing a string to the FileHandler constructor. Here is an example of logging to a file named "test.log": Logger logger = Logger.getLogger("com.example.MyServer"); Formatting for Nicer Output To demonstrate what I mean, here's the output from an earlier example: Jan 26, 2004 12:04:34 AM JavaLoggerTest main Using this format, we end up with two lines in the log for every one line we output! In theory, there might be circumstances where we write multiple lines in a single logging message (such as Stack Traces). In practice, most of our log records will be no more than one line. To correct this, we'll configure the logger so that each line of the log message is prefixed with the message info. Since my preferred format is [yyyy-MM-dd HH:mm:ss.S] <LEVEL>: <Message>, our example will use that as the format. Formatting is handled by subclasses of the java.util.logging.Formatter class. By overriding the format(LogRecord) method, we can develop a custom look to our logs. Formatters are attached to Handlers so that the format can be customized based on the destination. See Listing 1 for our example format. After adding in our new formatting class, we now have the following example code: Logger logger = Logger.getLogger("com.example.MyServer"); The output has now changed to look like the following: [2004-01-29 20:36:53.705] FINEST: Value of 'myvar' is ABC123 As you can see, the output is now much cleaner and easier to read. It also sets the stage for sending multiline output one line at a time. This will be important later when we attempt to redirect OutputStreams to our log. Converting Old Code The secret to adding proper logging to your application is configuring the Logger ahead of time. Once the configuration is out of the way, System.out statements can be easily replaced with similar one-line code statements. Let's take the lessons from above and add the proper setup code to a main() method:
public static void main(String[] args)
{
...
Logger logger = Logger.getLogger("com.example.MyServer");
Handler handler = new FileHandler("test.log");
Formatter formatter = new SingleLineFormatter();
handler.setFormatter(formatter);
logger.setHandler(handler);
...
}
An alternative is to modify the "lib/logging.properties" file. In fact, the logging.properties file is how most "real" applications commonly handle the setup of the Loggers. I won't cover the format of the file in this article, but it's well documented in the Javadocs for the LogManager class. Now that we've configured our logger, we can convert some code. Here's a typical example of the type of code we'll be converting:
try
{
System.out.println("This is a debug statement.");
System.err.println("This is an error statement.");
}
catch(Exception e)
{
e.printStackTrace();
}
The first output statement is pretty easy to convert based on what we know: Logger.getLogger("com.example.MyServer").log(new LogRecord(Level.FINEST, "This is a debug statement.")); The second output statement is more of the same, with the primary difference of a higher logging level: Logger.getLogger("com.example.MyServer").log(new LogRecord(Level.SEVERE, "This is an error statement.")); The third and final output statement is far more interesting. Many programmers would be tempted to log the message of the exception but continue to send the stack trace to the standard error stream. Thankfully there's a better way. The Sun developers recognized the need to log exceptions and thus added a special Logger.log() method. This method takes the standard LogRecord parameter, but adds a new parameter of type Throwable. Since all exceptions and errors extend the Throwable class, all exceptions can be logged with this method. The final converted code looks like Listing 2. What Are Thread Locals?
public class ThreadLocalHashtable extends Hashtable
{
public void set(Object value)
{
Thread thread = Thread.currentThread();
super.put(thread, value);
}
public Object get()
{
Thread thread = Thread.currentThread();
return super.get(thread);
}
}
Notice how the "set" method is passed a value Object, but no key Object? That's because the the Thread.currentThread() method is used to find the current thread. That Thread object is then used as a key to the hash table. This allows a thread to store an object that cannot be changed or accessed by any other thread. Tip: InheritableThreadLocal Dealing with Libraries In a perfect world, there would be an easy way to make all libraries use our logger. There are a few things we can do, though. In the following sections I'll cover a way to interface System.out and System.err with our logger. Redirecting the Standard Output Streams While you may feel free to write your own solution, I think you'll find that the class in Listing 3 will meet your needs quite nicely. Despite its size, the class is actually rather robust and will work for replacing both System.out and Systemerr. This class only needs to be installed once at the beginning of your program. The code below demonstrates its usage. You'll note that I've assigned System.out to the Level.INFO level and System.err to the Level.SEVERE level. This allows the two streams to be easily distinguishable in the log file.
public static void main(String[] args)
{
...
System.setOut(new PrintWriter(new LoggerOutputStream("com.example.MyServer")));
System.setErr(new PrintWriter(new LoggerOutputStream("com.example.MyServer",
Level.SEVERE)));
...
}
Assigning Multiple Loggers to the Standard Streams One way of solving this is to use a ThreadLocal object. By associating a thread to its Logger, we can effectively create a lookup table of the output streams to be used. Let's take our previous example of a job scheduling server. If we assume that each task is run inside its own thread, we can assign a LoggerOutputStream to the task when it's created. Obviously, this solution wouldn't work if we decided not to use multithreading, but it should be workable under most circumstances. While not a perfect solution, it solves 90% of the cases where we use multiple loggers. See Listing 4 for code that demonstrates this concept. (Listings 4 and 5 can be downloaded from www.sys-con.com/java/sourcec.cfm.) Listing 5 shows how our fictional job scheduling server might use the MultiOutputStream class to associate Loggers with tasks. It's important that the code to associate the OutputStream happens as soon as the Thread is created. If you wait, you may run the risk of a NullPointerException. Final Thoughts YOUR FEEDBACK
LATEST JAVA STORIES & POSTS
SUBSCRIBE TO THE WORLD'S MOST POWERFUL NEWSLETTERS SUBSCRIBE TO OUR RSS FEEDS & GET YOUR SYS-CON NEWS LIVE!
|
SYS-CON FEATURED WHITEPAPERS MOST READ THIS WEEK SPONSORED BY INFRAGISTICS
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||