bb.util.logging
Class LogUtil

java.lang.Object
  extended by bb.util.logging.LogUtil

public final class LogUtil
extends Object

Provides static constants and utility methods relating to the java.util.logging package.

The most important functionality offered by this class is the various makeLogger2 methods. A default Logger2 may be obtained by calling getLogger2. The remaining public methods offer all kinds of other services.

The makeLogger2 methods are especially powerful because they can draw on new logging properties supported by this class. The next sections of documentation explain the background, motivation, and behavior of these new properties.

JDK logging background

These resources provide an introduction to the JDK logging facility:

One characteristic of the java.util.logging package is that all named (i.e. non-anonymous) Loggers form a parent/child hierarchy. From the LogManager javadocs:

Loggers are organized into a naming hierarchy based on their dot separated names. Thus "a.b.c" is a child of "a.b", but "a.b1" and a.b2" are peers.
One reason for this hierarchy is that some behavior (the level and handlers properties) in child Loggers can be inherited in some sense from its parents.

Another characteristic of the java.util.logging package is that much of this behavior can be specified outside of code. In particular, the java.util.logging.config.file System property may be used to point to a logging properties file that specifies default behaviors. Consult LogManager as well as the links above for details. A sample logging properties file is the .../script/logging.properties file in this project.

Problems with the JDK logging properties

First note that the useParentHandlers property is never inherited at all. A Logger is by default created with useParentHandlers set to true. To set useParentHandlers set to false requires explicit configuration (i.e. the logging properties file must have a <logger>.level entry where <logger> exactly matches the name of the Logger, or else there must be Java code somewhere which calls setUseParentHandlers).

Unfortunately, even the "inheritable" properties (level and handlers) have issues. From Jason Hunter's JDK logging tutorial:

A child inherits the properties of its parent, or its parents parent, going to the root for global default if necessary. Inheritance works a little differently than in object-oriented programs... The tricky part of parentage is understanding that setting a parent property doesn't push the setting on its children. Child loggers have their own settings but at runtime look up the tree for a value if they ever have an unset property.

This form of "inheritance" is not a problem for the logging Level. For example, suppose your logging properties file only specifies the property com.level = INFO. Next suppose you subsequently only create a Logger named com.xyz, and rely only on the logging properties file for configuration (i.e. no explicit configuration in code). Then because there is no com.xyz.level property, the com.xyz Logger will initially have null for its Level, which means that every log request will cause a search up the Logger hierarchy until a Logger with a non-null Level is found. (Note that when LogManager registers a Logger, it will also create all parent Loggers which have a Level specified in the properties file.) Thus, the net effect is that the com.xyz Logger will at runtime always search for its Level, which it will find in its immediate parent, the com Logger.

In contrast, this form of "inheritance" may be a huge problem with certain logging Handlers. For instance, suppose the com.xyz package in some Java codebase has a bunch of classes inside it, and each class needs its own dedicated Logger in order to classify events and so simplify analysis. Furthermore, suppose that each of these per class Loggers needs a FileHandler to persist all logs to a file. Now if your logging properties file merely contained the entry com.xyz.handlers = java.util.logging.FileHandler what would happen is that only the parent Logger (i.e. the one named com.xyz) would have that FileHandler. All the child Loggers (e.g. com.xyz.foo, com.xyz.bar, etc) would use the same FileHandler instance in their parent (assuming their useParentHandlers is true). This fails the per class FileHandler requirement. You could achieve it by having dedicated property entries (e.g. com.xyz.foo.handlers = java.util.logging.FileHandler, com.xyz.bar.handlers = java.util.logging.FileHandler, etc) but this is very cumbersome and bug prone (e.g. if you refactor a class name, you have to remember to change the logging properties file too).

New properties supported by this class

This class supports these new logging properties:
  1. A property named logDirectory-NS. Defines the path of the parent directory of all log files created by this class. This directory will be created if currently unexisting. Log files created by this class include those explicitly created by makeLogFile, as well as files implicitly created by FileHandlers specified by the <logger>.handlers-NS property below.
  2. A property named <logger>.level-NS. Analogous to the existing <logger>.level property defined in LogManager but with differences described below.
  3. A property named <logger>.handlers-NS. Analogous to the existing <logger>.handlers property defined in LogManager but with differences described below.
  4. A property named <logger>.useParentHandlers-NS. Analogous to the existing <logger>.useParentHandlers property defined in LogManager but with differences described below.

These properties must be defined in the same logging properties file that is used to specify the java.util.logging properties for your project. These property names all end with -NS because they are non-standard properties: the JDK logging code (e.g. LogManager) does not understand them, only a special class like this one does. This class's static initializer will reread any logging properties file and process the above properties. Only Loggers created by the makeLogger2 method will have these non-standard properties applied to them.

The <logger>.level-NS, <logger>.handlers-NS, and <logger>.useParentHandlers-NS properties were introduced in order to fix the defects with the JDK's logging property "inheritance" noted above. These properties behave as follows:

  1. the <logger> part is actually a name prefix (i.e. the <logger> pattern is assumed to end with an implicit * wildcard). For example, specifying com.level-NS = ... will cause both a Logger named com as well as Loggers named com123 and com.xyz to match the pattern and be assigned the Level. This wildcard matching thus transends Logger parent/child boundaries. Continuing the example, the com Logger is the parent of the com.xyz Logger but is not the parent of the com123 Logger (its parent is the root "" Logger). This contrasts with the existing JDK logging properties, where the <logger> part is the full name of some Logger.
  2. the <logger> part may be a special universal match value of last resort. The value "+" matches any non-empty Logger name if no other prefix matches. For example, if your logging property file only defines +.level-NS = FINE, then every Logger (except the root Logger which has the empty name "") will have FINE as its Level. This pattern allows convenient setting of general behavior.
  3. they are inherited by explicit assignment. This is a consequence of the prefix matching described above: every Logger name which matches a prefix has that property applied to it.
This behavior allows properties for many differently named Loggers to all be set at once by specifying the properties of some common name prefix. Furthermore, because these properties are explicitly assigned to matching Loggers, you can now easily solve the log file per class problem described above.

One major issue with using a name prefix,however, is how to handle multiple matches. For example, if only the properties com.level-NS = INFO and com.xyz.level-NS = FINE are defined in the logging properties file, which one does a Logger named com.xyz.foo receive? After all, both the com and com.xyz prefixes match in this case. This class's policy is to use the longest length matching prefix, since that is likely to be the most specific. Continuing the example, the com.xyz.foo Logger would be assigned the FINE Level since com.xyz is longer than com. In the case of multiple "best matches", one is arbitrarily chosen. Note that the "+" universal match value mentioned above is only used if no other name prefix matched.

Comment on param substitution and reducing logging impact

Jason Hunter's JDK logging tutorial notes the following:
In Java we first saw variable substitution used for internationalization and localization, because in those cases simple string concatentation isn't possible. With simple log() methods you could rely on string concatentation like this: logger.log(Level.INFO, "Your file was " + size + " bytes long"); The problem with this approach is that the runtime must perform the string concatentation before executing the log() method call. That wastes CPU cycles and memory when the logger's going to reject the message for being at too low a level. The solution is to use variable substitution and avoid all concatentation: logger.log(Level.INFO, "Your file was {0} bytes long", size); This call executes directly and the substitution only happens after the message has passed the logger's quick level check.
This seems like a really clever use of param substitution, but it is not obvious that will actually reduce the cpu impact of logging very much. Reason: it is almost impossible to use params in one of the log/logp methods of Logger without causing an Object[] to be created. (The sole exception is the rare situation when all the params that you need are already in an array.) Thus, you always take a hit from creating that Object[], which is about as bad as creating a new String. Furthermore, if the level check is passed, then the work that needs to be done to actually carry out the param substitution is somewhat greater than doing simple String concatenation. So, you may not gain much performance. Since param substitution makes your code look even worse, you should only use param substitution for what it was originally designed for, namely, to support pre-localized generic messages that can subsequently be localized and then have params substituted in. This is discussed further here.

Multithread safety

This class is multithread safe: most of its immediate state is immutable, and the deep state of its fields is also either immutable or is multithread safe. The sole mutable field is logger2_default, which is protected by synchronized access.

Author:
Brent Boyer

Nested Class Summary
private static class LogUtil.NodeProp
           
static class LogUtil.UnitTest
          See the Overview page of the project's javadocs for a general description of this unit test class.
 
Field Summary
private static Level[] levels
           
private static File logDirectory
          Directory where log files are to be stored.
private static String logDirectory_key
           
private static String logDirectory_valueDefault
           
private static Logger2 logger2_default
          A default Logger2 instance that any user of this class may draw on via getLogger2 if they do not want to bother creating their own Logger.
private static ConcurrentMap<String,LogUtil.NodeProp> nameToNodeProps
           
private static String nsPatternUniversalMatch
          Serves as the universal matching pattern of last resort.
private static String nsSuffix
           
 
Constructor Summary
private LogUtil()
          This private constructor suppresses the default (public) constructor, ensuring non-instantiability.
 
Method Summary
static void addHandlers(Handler[] handlers, Logger logger)
          Adds every element of handlers to logger.
static void close(Logger logger)
          Immediately returns if logger == null.
private static File extract_logDirectory(Properties properties)
           
static void flush(Logger logger)
          Immediately returns if logger == null.
static Level[] getLevels()
          Returns an array of all the possible Levels, sorted into ascending order (according to Level.intValue).
static File getLogDirectory()
          Accessor for logDirectory.
static Logger2 getLogger2()
          Accessor for logger2_default.
static String getName(Class c, String suffix)
          Determines an appropriate Logger name for the arguments.
static String getPath(Class c, String suffix)
          Returns getPath( getName(c, suffix) ).
static String getPath(String name)
          Returns an appropriate path to a log file for name.
private static void init_nameToNodeProps(Properties properties)
           
private static Properties loadProperties()
           
static File makeLogFile(String childPath)
          Returns a new File instance which points to a location inside the log directory.
static Logger2 makeLogger2(Class c)
          Returns makeLogger2(c, null) ).
static Logger2 makeLogger2(Class c, String suffix)
          Returns makeLogger2( getName(c, suffix) ).
static Logger2 makeLogger2(String name)
          Returns makeLogger2( name, null ).
static Logger2 makeLogger2(String name, String resourceBundleName)
          Returns a new Logger2 created by a call to Logger2.getLogger2(name, resourceBundleName).
static PrintWriter makeLogWriter(File directory, String prefix, boolean autoFlush)
          Returns a new PrintWriter that ultimately writes to a file located in directory.
static PrintWriter makeLogWriter(String prefix)
          Returns makeLogWriter(prefix, false) (i.e. no auto flushing).
static PrintWriter makeLogWriter(String prefix, boolean autoFlush)
          Returns makeLogWriter(logDirectory, prefix, autoFlush) (i.e. the default log directory is used).
static void nsHandlers(Logger logger)
          Calls NodeProp.findBest to get the NodeProp whose associated key best matches logger's name and whose handlerClasses field is non-null.
static void nsLevel(Logger logger)
          Calls NodeProp.findBest to get the NodeProp whose associated key best matches logger's name and whose level field is non-null.
static void nsUseParentHandlers(Logger logger)
          Calls NodeProp.findBest to get the NodeProp whose associated key best matches logger's name and whose useParentHandlers field is non-null.
static Level parseLevel(String s)
          Returns the Level that corresponds to s.
static Handler[] removeHandlers(Logger logger)
          Removes all of logger's Handlers.
 
Methods inherited from class java.lang.Object
clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
 

Field Detail

levels

private static final Level[] levels

logDirectory_key

private static final String logDirectory_key
See Also:
Constant Field Values

logDirectory_valueDefault

private static final String logDirectory_valueDefault
See Also:
Constant Field Values

logDirectory

private static final File logDirectory
Directory where log files are to be stored.

Internally used by this class's getPath(String) method, and therefore implicitly used by the various makeLogger2 methods. May be used by external users in arbitrary ways.

Contract: is never null, and always exists.


nsPatternUniversalMatch

private static final String nsPatternUniversalMatch
Serves as the universal matching pattern of last resort. See the class javadocs concerning the -NS properties as well as findBest.

See Also:
Constant Field Values

nsSuffix

private static final String nsSuffix
See Also:
Constant Field Values

nameToNodeProps

private static final ConcurrentMap<String,LogUtil.NodeProp> nameToNodeProps

logger2_default

private static Logger2 logger2_default
A default Logger2 instance that any user of this class may draw on via getLogger2 if they do not want to bother creating their own Logger. This Logger always has the name "default".

Note: while Logger.global is a similar default Logger, there are several reasons why this field was introduced:

  1. as of JDK 1.5, sun, in their infinite wisdom, chose to not initialize Logger.global: the programmer must manually initialize it via a call to Logger.getLogger("global") which is annoying; in contrast, getLogger2 lazy initializes this field
  2. since getLogger2 initializes this field by a call to makeLogger2("default"), this means that the logging properties file can specify -NS properties for this field (use the Logger name "default"); this is particularly valuable if want it to write to a log file located in logDirectory, and with a timestamp in its file name.
  3. this field is a Logger2 instance because of that subclass's advanced features.

Constructor Detail

LogUtil

private LogUtil()
This private constructor suppresses the default (public) constructor, ensuring non-instantiability.

Method Detail

loadProperties

private static Properties loadProperties()
                                  throws Exception
Throws:
Exception

extract_logDirectory

private static File extract_logDirectory(Properties properties)
                                  throws SecurityException,
                                         IllegalStateException
Throws:
SecurityException
IllegalStateException

init_nameToNodeProps

private static void init_nameToNodeProps(Properties properties)
                                  throws IllegalStateException,
                                         ClassNotFoundException
Throws:
IllegalStateException
ClassNotFoundException

getLevels

public static Level[] getLevels()
Returns an array of all the possible Levels, sorted into ascending order (according to Level.intValue).

Contract: the result is a clone of an internal field, so the caller may safely modify the result.


getLogDirectory

public static File getLogDirectory()
Accessor for logDirectory.


getLogger2

public static Logger2 getLogger2()
                          throws IllegalStateException,
                                 SecurityException,
                                 RuntimeException
Accessor for logger2_default. Lazy initializes it if necessary.

Throws:
IllegalStateException - if a Logger with the name "default" already exists
SecurityException - if a security manager exists and if the caller does not have LoggingPermission("control")
RuntimeException - (or some subclass) if any other error occurs; this may merely wrap some other underlying Throwable

makeLogFile

public static File makeLogFile(String childPath)
                        throws IllegalArgumentException,
                               SecurityException,
                               RuntimeException
Returns a new File instance which points to a location inside the log directory.

The childPath arg is interpreted as a path relative to the log directory. It can be either a simple file name, or it can include one or more subdirtories before the file name. The only restriction in the latter case is that the canonical path that it resolves to must be inside the log directory (i.e. using ".." to move out of the log directory will be dtected and rejected).

Returns:
a new File constructed from childPath
Throws:
IllegalArgumentException - if childPath is blank; childPath resolves to a path that falls outside the log directory
SecurityException - if a security manager exists and denies read access to the file
RuntimeException - (or some subclass) if any other error occurs; this may merely wrap some other underlying Throwable

makeLogWriter

public static PrintWriter makeLogWriter(String prefix)
                                 throws IllegalArgumentException,
                                        RuntimeException
Returns makeLogWriter(prefix, false) (i.e. no auto flushing).

Throws:
IllegalArgumentException
RuntimeException

makeLogWriter

public static PrintWriter makeLogWriter(String prefix,
                                        boolean autoFlush)
                                 throws IllegalArgumentException,
                                        RuntimeException
Returns makeLogWriter(logDirectory, prefix, autoFlush) (i.e. the default log directory is used).

Throws:
IllegalArgumentException
RuntimeException

makeLogWriter

public static PrintWriter makeLogWriter(File directory,
                                        String prefix,
                                        boolean autoFlush)
                                 throws IllegalArgumentException,
                                        RuntimeException
Returns a new PrintWriter that ultimately writes to a file located in directory.

The PrintWriter immediately writes to a BufferedWriter to ensure top performance, and that in turn writes to an OutputStreamWriter which uses the platform's default Charset.

The file's name starts with prefix, and ends with an underscore ('_') followed by a timestamp followed by the extension ".txt". So, the complete filename is typically unique each time that this method is called, which obviates the need for a file append mode (this method always overwrites the file if it already exists).

Parameters:
directory - the parent directory of the file that will be written to by the result
prefix - the prefix of the filename that the result will write to
autoFlush - specifies wheter or not automatic flushing is enabled in the result
Throws:
IllegalArgumentException - if prefix is blank
RuntimeException - (or some subclass) if any other probolem occurs

makeLogger2

public static Logger2 makeLogger2(Class c)
                           throws IllegalArgumentException,
                                  IllegalStateException,
                                  SecurityException,
                                  RuntimeException
Returns makeLogger2(c, null) ).

Throws:
IllegalArgumentException - if c == null
IllegalStateException - if a Logger with getName(c, null) already exists
SecurityException - if a security manager exists and if the caller does not have LoggingPermission("control")
RuntimeException - (or some subclass) if any other error occurs; this may merely wrap some other underlying Throwable

makeLogger2

public static Logger2 makeLogger2(Class c,
                                  String suffix)
                           throws IllegalArgumentException,
                                  IllegalStateException,
                                  SecurityException,
                                  RuntimeException
Returns makeLogger2( getName(c, suffix) ).

Throws:
IllegalArgumentException - if c == null
IllegalStateException - if a Logger with getName(c, suffix) already exists
SecurityException - if a security manager exists and if the caller does not have LoggingPermission("control")
RuntimeException - (or some subclass) if any other error occurs; this may merely wrap some other underlying Throwable

makeLogger2

public static Logger2 makeLogger2(String name)
                           throws IllegalArgumentException,
                                  IllegalStateException,
                                  SecurityException,
                                  RuntimeException
Returns makeLogger2( name, null ).

Throws:
IllegalArgumentException - if name is blank
IllegalStateException - if a Logger with name already exists
SecurityException - if a security manager exists and if the caller does not have LoggingPermission("control")
RuntimeException - (or some subclass) if any other error occurs; this may merely wrap some other underlying Throwable

makeLogger2

public static Logger2 makeLogger2(String name,
                                  String resourceBundleName)
                           throws IllegalArgumentException,
                                  IllegalStateException,
                                  SecurityException,
                                  RuntimeException
Returns a new Logger2 created by a call to Logger2.getLogger2(name, resourceBundleName). So, only if this is the first Logger with name known by LogManager is it created. The result is a Logger2 instance because of that subclass's advanced features.

If the Logger2 is created, then nsLevel, nsHandlers, and nsUseParentHandlers are called on it.

No other configuration is subsequently performed (e.g. for the Level or Formatter of Logger2's handlers; all are left with their default settings, such as whatever has been specified by some logging properties file).

This method only throws unchecked Exceptions so that it can be used to initialize a field at its declaration.

Throws:
IllegalArgumentException - if name is blank
IllegalStateException - if a Logger with name already exists
SecurityException - if a security manager exists and if the caller does not have LoggingPermission("control")
RuntimeException - (or some subclass) if any other error occurs; this may merely wrap some other underlying Throwable

getName

public static String getName(Class c,
                             String suffix)
                      throws IllegalArgumentException
Determines an appropriate Logger name for the arguments. The fully qualified name of c (i.e. package included) always appears first. If suffix is non-null, then a '_' followed by suffix is appended. Examples:

Motivation: Loggers are often static fields of a class, so it is convenient to simply supply the Class object and have this method determine the name. The suffix arg allows optional further discrimination (e.g. a class may have multiple loggers for different kinds of events, or, if each instance has its own Logger, then each one's name should have some instance specific part).

Throws:
IllegalArgumentException - if c == null

getPath

public static String getPath(Class c,
                             String suffix)
                      throws IllegalArgumentException,
                             SecurityException,
                             IllegalStateException
Returns getPath( getName(c, suffix) ).

Throws:
IllegalArgumentException - if c == null
SecurityException - if a security manager exists and its SecurityManager.checkWrite method does not permit directoryLogDefault and all necessary parent directories to be created
IllegalStateException - if an attempt to create directoryLogDefault is made but fails

getPath

public static String getPath(String name)
                      throws IllegalArgumentException,
                             SecurityException,
                             IllegalStateException
Returns an appropriate path to a log file for name.

This is typically used with makeLogger2(Class, String). The result is located in the directory returned by getLogDirectory (see javadocs for possible side effects), and consists of name, a '_', a timestamp, and then ".log".

Throws:
IllegalArgumentException - if name is blank
SecurityException - if a security manager exists and its SecurityManager.checkWrite method does not permit directoryLogDefault and all necessary parent directories to be created
IllegalStateException - if an attempt to create directoryLogDefault is made but fails

nsLevel

public static void nsLevel(Logger logger)
                    throws IllegalArgumentException,
                           Exception
Calls NodeProp.findBest to get the NodeProp whose associated key best matches logger's name and whose level field is non-null. If a non-null result is found, then its level field is assigned to logger.

Throws:
IllegalArgumentException - if logger == null
Exception - (or some subclass) if some other problem occurs

nsHandlers

public static void nsHandlers(Logger logger)
                       throws IllegalArgumentException,
                              Exception
Calls NodeProp.findBest to get the NodeProp whose associated key best matches logger's name and whose handlerClasses field is non-null. If a non-null result is found, then its handlerClasses field is used to create new Handler instances that are added to logger.

Special case: if a FileHandler Class is encountered, its String arg constructor is called instead of its default constructor. The param passed to that constructor is a call to getPath( logger.getName() ).

Throws:
IllegalArgumentException - if logger == null
Exception - (or some subclass) if some other problem occurs

nsUseParentHandlers

public static void nsUseParentHandlers(Logger logger)
                                throws IllegalArgumentException,
                                       Exception
Calls NodeProp.findBest to get the NodeProp whose associated key best matches logger's name and whose useParentHandlers field is non-null. If a non-null result is found, then its useParentHandlers field is assigned to logger.

If an appropriate match is found, then its useParentHandlers boolean value is assigned to logger.

Throws:
IllegalArgumentException - if logger == null
Exception - (or some subclass) if some other problem occurs

parseLevel

public static Level parseLevel(String s)
                        throws IllegalArgumentException
Returns the Level that corresponds to s.

Throws:
IllegalArgumentException - if s is blank or no Level corresponds to s

close

public static void close(Logger logger)
Immediately returns if logger == null. Otherwise, retrieves all of logger's Handlers and closes them.

Contract: this method should never throw a Throwable. Any Throwable that is raised is caught and logged robustly to the default Logger.


flush

public static void flush(Logger logger)
                  throws IllegalArgumentException
Immediately returns if logger == null. Otherwise, retrieves all of logger's Handlers and flushes them.

Contract: this method should never throw a Throwable. Any Throwable that is raised is caught and logged robustly to the default Logger.

Throws:
IllegalArgumentException

addHandlers

public static void addHandlers(Handler[] handlers,
                               Logger logger)
                        throws IllegalArgumentException
Adds every element of handlers to logger.

Throws:
IllegalArgumentException - if handlers is null; logger is null

removeHandlers

public static Handler[] removeHandlers(Logger logger)
                                throws IllegalArgumentException
Removes all of logger's Handlers.

Returns:
all the Handlers that were removed from logger
Throws:
IllegalArgumentException - if logger == null