2006/11/23
Design Pattern: The enum logger
Recently I have come up with this design pattern that solves many of the problems relating to logging in both small and large applications. Some of the problems are related to:
Ability to change logger (implementation, e.g. Log4j) Managing log statements in the application (e.g. knowing what is logged) Managing log levels in the application (e.g. what is logged at what level) Managing multiple loggers (different outputs, e.g. system log, service log, etc.)
My solution is to have the logger inside an enum, where the log statement is stored as a string. Additionally and optionally, the log levels and the logger itself can also be stored inside the enum, simplifying logging quite dramatically. Here is an example of an implementation that would be used for logging inside an RS232 component (serial communication).
Now, it is possible to have one of these enum loggers as a system-wide logger, or it is possible to have one for each single component in the system (however "component" is defined in the system at hand). The design pattern allows for log levels to be changed without the need to dig deep into code to find exactly where the log statement was logged. You can easily change the type of logger as well by simply changing the enum, while the code in the system itself remains untouched. Also, the parameterised parsing actually allows for unlimited number of parameters being replaced in the log string, though more than three is fairly unlikely. Moreover, if something like having the name of the enum as a prefix to the log message is preferable, then this can very easily be achieved by changing the log method(s) in the enum itself. Additionally, when using the enum logger all that have to be done to use it is simply doing static imports. Here is an example of how it can be used:
Some extra benefits that are not that obvious would be that when you hover the enum being used in an IDE, the JavaDoc neatly comes up and displays what is being logged and what the parameters are. What should also not be missed is the @LogStatements annotation attached to the enum, which could be used by a simple tool to gather all log statements in the system and use these to produce documentation. More importantly, if the enum implements an interface that defines the methods used for the logging, then they could all be populated from a single aspect using AOP. The main thing to consider is will the software ever log the exact same log statement more than once at different log levels? If so, the enum logger could still be used by having the same statements at different levels. Also it should be noticed that in this example there is only one "debug" element. This is mainly because debug messages normally do not need to be "managed" the way other log messages need to be, as the developers have access to the source code anyway. However, if it is required to have a clear separation between, say, system and service logging, then it could be specified at the enum level by having SERVICE_RS232_001 and SYSTEM_RS232_001 statements. The main positive thing with this design pattern is that when the log statements are defined, then you don't have to stop and carefully consider "is this debug, info, warn, or error level?", you simply just do enum.log();
My solution is to have the logger inside an enum, where the log statement is stored as a string. Additionally and optionally, the log levels and the logger itself can also be stored inside the enum, simplifying logging quite dramatically. Here is an example of an implementation that would be used for logging inside an RS232 component (serial communication).
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
@LogStatements
public enum RS232LogStatements {
/** Debug freetext.<BR> %0 = Any debug string */
RS232_DEBUG("%0", Level.DEBUG),
/** Trace freetext.<BR> %0 = Any trace string */
RS232_TRACE("%0", Level.TRACE),
/** Port %0 in use<BR> %0 = Port name */
RS232_001("Port %0 in use", Level.WARN),
/**
* Port %0 is currently owned by %1<BR> %0 = Port name
* <BR> %1 = Port owner name
*/
RS232_002("Port %0 is currently owned by %1", Level.INFO),
/** Port %0 does not exist.<BR> %0 = Port name */
RS232_003("Port %0 does not exist", Level.INFO);
private static final Logger logger = Logger.getLogger("RS232");
private final String logStatement;
private final Level logLevel;
private RS232LogStatements(final String logStatement,
final Level logLevel) {
this.logStatement = logStatement;
this.logLevel = logLevel;
}
public String toString() {
return logStatement;
}
public void log(final String ... values) {
if (logger.isEnabledFor(logLevel)) {
logger.log(logLevel, parse(values));
}
}
public void log(final Exception e) {
if (logger.isEnabledFor(logLevel)) {
logger.log(logLevel, "", e);
}
}
public void log(final Exception e, final String ... values) {
if (logger.isEnabledFor(logLevel)) {
logger.log(logLevel, parse(values), e);
}
}
private String parse(final String ... values) {
final int size = values.length - 1;
String returnString = logStatement;
for (int i = size; i >= 0; i--) {
returnString = returnString.replace("%" + i, values[i]);
}
return returnString;
}
}
Now, it is possible to have one of these enum loggers as a system-wide logger, or it is possible to have one for each single component in the system (however "component" is defined in the system at hand). The design pattern allows for log levels to be changed without the need to dig deep into code to find exactly where the log statement was logged. You can easily change the type of logger as well by simply changing the enum, while the code in the system itself remains untouched. Also, the parameterised parsing actually allows for unlimited number of parameters being replaced in the log string, though more than three is fairly unlikely. Moreover, if something like having the name of the enum as a prefix to the log message is preferable, then this can very easily be achieved by changing the log method(s) in the enum itself. Additionally, when using the enum logger all that have to be done to use it is simply doing static imports. Here is an example of how it can be used:
import static ...RS232LogStatements.*;
.
.
.
private SerialPort openSerialPort(CommPortIdentifier comm) {
try {
final SerialPort serialPort = (SerialPort) comm.open(appName,
timeout);
serialPort.setSerialPortParams(baudrate, databits,
stopbits, parity);
return serialPort;
} catch (PortInUseException e) {
RS232_001.log(e, comm.getName());
} catch (UnsupportedCommOperationException e) {
RS232_DEBUG.log(e);
}
return null;
}
Some extra benefits that are not that obvious would be that when you hover the enum being used in an IDE, the JavaDoc neatly comes up and displays what is being logged and what the parameters are. What should also not be missed is the @LogStatements annotation attached to the enum, which could be used by a simple tool to gather all log statements in the system and use these to produce documentation. More importantly, if the enum implements an interface that defines the methods used for the logging, then they could all be populated from a single aspect using AOP. The main thing to consider is will the software ever log the exact same log statement more than once at different log levels? If so, the enum logger could still be used by having the same statements at different levels. Also it should be noticed that in this example there is only one "debug" element. This is mainly because debug messages normally do not need to be "managed" the way other log messages need to be, as the developers have access to the source code anyway. However, if it is required to have a clear separation between, say, system and service logging, then it could be specified at the enum level by having SERVICE_RS232_001 and SYSTEM_RS232_001 statements. The main positive thing with this design pattern is that when the log statements are defined, then you don't have to stop and carefully consider "is this debug, info, warn, or error level?", you simply just do enum.log();
Copyright © 2006 Stein Gunnar Bakkeby