The java.util.logging package
provides a highly flexible and easy-to-use logging framework for system
information, error messages, and fine-grained tracing (debugging) output.
With the logging package, you can apply filters to select log messages,
direct their output to one or more destinations (including files and
network services), and format the messages appropriately for their
consumers.
Most importantly, much of this basic logging configuration can be set up externally at runtime through the use of a logging setup properties file or an external program. For example, by setting the right properties at runtime, you can specify that log messages are to be sent both to a designated file in XML format and also logged to the system console in a digested, human-readable form. Furthermore, for each of those destinations, you can specify the level or priority of messages to be logged, discarding those below a certain threshold of significance. By following the correct source conventions in your code, you can even make it possible to adjust the logging levels for specific parts of your application, allowing you to target individual packages and classes for detailed logging without being overwhelmed by too much output. The Logging API can even be controlled remotely via Java Management Extensions MBean APIs.
Any good logging API must have at least two guiding
principles. First, performance should not inhibit the developer from
using log messages freely. As with Java language assertions (discussed
in Chapter 4), when log messages are turned
off, they should not consume any significant amount of processing time.
This means that there’s no performance penalty for including logging
statements as long as they’re turned off. Second, although some users
may want advanced features and configuration, a logging API must have
some simple mode of usage that is convenient enough for time-starved
developers to use in lieu of the old standby System.out.println(). Java’s Logging API
provides a simple model and many convenience methods that make it very
tempting.
The heart of the logging framework is the
logger, an instance of java.util.logging.Logger. In most cases,
this is the only class your code will ever have to deal with. A logger
is constructed from the static Logger.getLogger()
method, with a logger name as its argument. Logger names place loggers
into a hierarchy with a global, root logger at the top and a tree and
children below. This hierarchy allows the configuration to be
inherited by parts of the tree so that logging can be automatically
configured for different parts of your application. The convention is
to use a separate logger instance in each major class or package and
to use the dot-separated package and/or class name as the logger name.
For example:
packagecom.oreilly.learnjava;publicclassBook{staticLoggerlog=Logger.getLogger("com.oreilly.learnjava.Book");
The logger provides a wide range of methods to log messages; some take very detailed information, and some convenience methods take only a string for ease of use. For example:
log.warning("Disk 90% full.");log.info("New user joined chat room.");
We cover methods of the logger class in detail a bit later. The
names warning and info are two examples of logging levels;
there are seven levels ranging from SEVERE at the top to FINEST at the
bottom. Distinguishing log messages in this way allows us to select
the level of information that we want to see at runtime. Rather than
simply logging everything and sorting through it later (with negative
performance impact) we can tweak which messages are generated. We’ll
talk more about logging levels in the next section.
We should also mention that for convenience in very simple
applications or experiments, a logger for the name “global” is
provided in the static field Logger.global. You
can use it as an alternative to the old standby System.out.println() for those cases where
that is still a temptation:
Logger.global.info("Doing foo...")
Loggers represent the client interface to the logging
system, but the actual work of publishing messages to destinations
(such as files or the console) is done by handler
objects. Each logger may have one or more Handler objects associated with it, which
includes several predefined handlers supplied with the Logging API:
ConsoleHandler,
FileHandler,
StreamHandler, and
SocketHandler. Each
handler knows how to deliver messages to its respective destination.
ConsoleHandler is used by the
default configuration to print messages on the command line or system
console. FileHandler can direct
output to files using a supplied naming convention and automatically
rotate the files as they become full. The others send messages to
streams and sockets, respectively. There is one additional handler,
MemoryHandler, that
can hold a number of log messages in memory. MemoryHandler has a circular buffer, which
maintains a certain number of messages until it is triggered to
publish them to another designated handler.
As we said, loggers can be set to use one or more handlers. Loggers also send messages up the tree to each of their parent logger’s handlers. In the simplest configuration, this means that all messages end up distributed by the root logger’s handlers. We’ll soon see how to set up output using the standard handlers for the console, files, etc.
Before a logger hands off a message to its handlers or
its parent’s handlers, it first checks whether the logging level is
sufficient to proceed. If the message doesn’t meet the required level,
it is discarded at the source. In addition to level, you can implement
arbitrary filtering of messages by creating Filter classes that
examine the log message before it is processed. A Filter class can be applied to a logger
externally at runtime in the same way that the logging level,
handlers, and formatters, which are discussed next, can be. A Filter may also be attached to an individual
Handler to filter records at the
output stage (as opposed to the source).
Internally, messages are carried in a neutral format,
including all the source information provided. It is not until they
are processed by a handler that they are formatted for output by an
instance of a Formatter object. The
logging package comes with two basic formatters: SimpleFormatter and
XMLFormatter. The
SimpleFormatter is the default used
for console output. It produces short, human-readable summaries of log
messages. XMLFormatter encodes all
the log message details into an XML record format. The DTD for the
format can be found at http://java.sun.com/dtd/.
Table 11-9 lists the logging levels from most to least significant.
These levels fall into three camps: end user, administrator, and
developer. Applications often default to logging only messages of the
INFO level and above (INFO, WARNING, and SEVERE). These levels are generally seen by
end users and messages logged to them should be suitable for general
consumption. In other words, they should be written clearly so they make
sense to an average user of the application. Often these kinds of
messages are presented to the end user on a system console or in a
pop-up message dialog.
The CONFIG level should be used
for relatively static but detailed system information that could assist
an administrator or installer. This might include information about the
installed software modules, host system characteristics, and
configuration parameters. These details are important, but probably not
as meaningful to an end user.
The FINE, FINER, and FINEST levels are for developers or others
with knowledge of the internals of the application. These should be used
for tracing the application at successive levels of detail. You can
define your own meanings for these. We’ll suggest a rough outline in our
example, coming up next.
In the following (admittedly very contrived) example, we use all the logging levels so that we can experiment with logging configuration. Although the sequence of messages is nonsensical, the text is representative of messages of that type.
importjava.util.logging.*;publicclassLogTest{publicstaticvoidmain(Stringargv[]){Loggerlogger=Logger.getLogger("com.oreilly.LogTest");logger.severe("Power lost - running on backup!");logger.warning("Database connection lost, retrying...");logger.info("Startup complete.");logger.config("Server configuration: standalone, JVM version 1.5");logger.fine("Loading graphing package.");logger.finer("Doing pie chart");logger.finest("Starting bubble sort: value ="+42);}}
There’s not much to this example. We ask for a logger instance for our class using the static
Logger.getLogger() method, specifying
a class name. The convention is to use the fully qualified class name,
so we’ll pretend that our class is in a com.oreilly package.
Now, run LogTest. You should
see output like the following on the system console:
Jan6,20023:24:36PMLogTestmainSEVERE:Powerlost-runningonbackup!Jan6,20023:24:37PMLogTestmainWARNING:Databaseconnectionlost,retrying...Jan6,20023:24:37PMLogTestmainINFO:Startupcomplete.
We see the INFO, WARNING, and SEVERE messages, each identified with a date
and timestamp and the name of the class and method (LogTest main) from which they came. Notice
that the lower-level messages did not appear. This is because the
default logging level is normally set to INFO, meaning that only messages of severity
INFO and above are logged. Also note
that the output went to the system console and not to a logfile
somewhere; that’s also the default. Now we’ll describe where these
defaults are set and how to override them at runtime.
As we said in the introduction, probably the most important feature of the Logging API is the ability to configure so much of it at runtime through the use of external properties or applications. The default logging configuration is stored in the file jre/lib/logging.properties in the directory where Java is installed. It’s a standard Java properties file (of the kind we described earlier in this chapter).
The format of this file is simple. You can make changes to it, but you don’t have to. Instead, you can specify your own logging setup properties file on a case-by-case basis using a system property at runtime, as follows:
%java-Djava.util.logging.config.file=myfile.properties
In this command line, myfile is your properties file that contains the directive, which we’ll describe next. If you want to make this file designation more permanent, you can do so by setting the filename in the corresponding entry using the Java Preferences API described earlier in this chapter. You can go even further and instead of specifying a setup file, supply a class that is responsible for setting up all logging configuration, but we won’t get into that here.
A very simple logging properties file might look like this:
#Setthedefaultlogginglevel.level=FINEST#Directoutputtotheconsolehandlers=java.util.logging.ConsoleHandler
Here, we have set the default logging level for the entire
application using the .level (that’s
dot-level) property. We have also used the handlers property to
specify that an instance of the ConsoleHandler should be used (just like the
default setup) to show messages on the console. If you run our
application again, specifying this properties file as the logging setup,
you will now see all our log messages.
But we’re just getting warmed up. Next, let’s look at a more complex configuration:
#Setthedefaultlogginglevel.level=INFO#Ouputtofileandconsolehandlers=java.util.logging.FileHandler,java.util.logging.ConsoleHandler#Configurethefileoutputjava.util.logging.FileHandler.level=FINESTjava.util.logging.FileHandler.pattern=%h/Test.logjava.util.logging.FileHandler.limit=25000java.util.logging.FileHandler.count=4java.util.logging.FileHandler.formatter=java.util.logging.XMLFormatter#Configuretheconsoleoutputjava.util.logging.ConsoleHandler.level=WARNING#Levelsforspecificclassescom.oreilly.LogTest.level=FINEST
In this example, we have configured two log handlers: a ConsoleHandler with the
logging level set to WARNING and also an
instance of FileHandler that sends
the output to an XML file. The file handler is configured to log messages
at the FINEST level (all
messages) and to rotate logfiles every 25,000 lines, keeping a maximum
of four files.
The filename is controlled by the pattern property. Forward slashes in the
filename are automatically localized to backslash (\) if necessary. The
special symbol %h refers to the user
home. You can use %t to refer to the
system temporary directory. If filenames conflict, a number is appended
automatically after a dot (starting at zero). Alternatively, you can use
%u to indicate where a
unique number should be inserted into the name. Similarly, when files
rotate, a number is appended after a dot at the end. You can take
control of where the rotation number is placed with the %g identifier.
In our example, we specified the XMLFormatter class. We could also have used
the SimpleFormatter class to send the
same kind of simple output to the console. The ConsoleHandler also allows us to specify any
formatter we wish, using the formatter property.
Finally, we promised earlier that you could control logging levels for parts of your applications. To do this, set properties on your application loggers using their hierarchical names:
#Levelsforspecificlogger(class)namescom.oreilly.LogTest.level=FINEST
Here, we’ve set the logging level for just our test logger, by
name. The log properties follow the hierarchy, so we could set the
logging level for all classes in the oreilly package with:
com.oreilly.level=FINEST
Logging levels are set in the order in which they are read in the
properties file, so set the general ones first. Also note that the
levels set on the handlers allow the file handler to filter only the
messages being supplied by the loggers. So setting the file handler to
FINEST won’t revive
messages squelched by a logger set to SEVERE (only the
SEVERE messages will make it to the
handler from that logger).
In our example, we used the seven convenience methods named for the various logging levels. There are also three groups of general methods that can be used to provide more detailed information. The most general are:
log(Levellevel,Stringmsg)log(Levellevel,Stringmsg,Objectparam1)log(Levellevel,Stringmsg,Objectparams[])log(Levellevel,Stringmsg,Throwablethrown)
These methods accept as their first argument a static logging
level identifier from the Level class, followed
by a parameter, array, or exception type. The level identifier is one of
Level.SEVERE, Level.WARNING, Level.INFO, and so on.
In addition to these four methods, there are four corresponding
methods named logp() that also take a
source class and method name as the second and third arguments. In our
example, we saw Java automatically determine that information, so why
would we want to supply it? The answer is that Java may not always be
able to determine the exact method name because of runtime dynamic
optimization. The p in logp stands for “precise” and allows you to
control this yourself.
There is yet another set of methods named logrb()—which probably
should have been named logprb()—that
take both the class and method names and a resource bundle name. The
resource bundle localizes the messages (see the section Resource Bundles in Chapter 10). More generally, a logger may have a
resource bundle associated with it when it is created, using another
form of the getLogger method:
Logger.getLogger("com.oreilly.LogTest","logMessages");
In either case, the resource bundle name is passed along with the
log message and can be used by the formatter. If a resource bundle is
specified, the standard formatters treat the message text as a key and
try to look up a localized message. Localized messages may include
parameters using the standard message format notation and the form of
log(), which accepts an argument
array.
Finally, there are convenience methods called entering(), exiting(), and
throwing() that
developers can use to log detailed trace information.
In the introduction, we said that performance is a
priority of the Logging API. To that end we’ve described that log
messages are filtered at the source, using logging levels to cut off
processing of messages early. This saves much of the expense of handling
them. However, it cannot prevent certain kinds of setup work that you
might do before the logging call. Specifically, because we’re passing
things into the log methods, it’s common to construct detailed messages
or render objects to strings as arguments. Often this kind of operation
is costly. To avoid unnecessary string construction, you should wrap
expensive log operations in a conditional test using the Logger isLoggable() method to test whether you
should carry out the operation:
if(log.isLoggable(Level.CONFIG)){log.config("Configuration: "+loadExpensiveConfigInfo());}