I needed a C++ logger, but I was not sure which of the many logging implementations I should use. So I decided to pick one, and implement a wrapper over that logger. In case I needed to change the implementation, I only need to change the wrapper. For my wrapper class, I shall support five standard log levels:
enum LogLevel { TRACE, DEBUG, INFO, WARN, ERROR };
Let's call my wrapper class Logger. In this Logger class, I need a class member function to perform a logging action. Perhaps it can be defined as:
void Logger::log(LogLevel level, const std::string& message);
For example, this statement
logger.log(INFO, "Starting server");
produces a log record (either on the standard output or a log file) perhaps formatted like:
[INFO] Starting server
There is nothing really wrong with this design. However, log messages are often constructed from a concatenation of constant strings and other values, such as integers. So it is quite cumbersome having to always format the message string using sprintf() or something similar before performing a logging action.
After all, loggers should have an easy to use interface. Otherwise, it is often easier to just print to standard output for quick debugging purposes. Is it possible to design and implement a logger that is as easy to use as printf() or std::cout?
The answer is yes. My logger interface allows for a log record to be generated by the following statement:
logger.log(INFO) << "Connection from " << host << ":" << port;
However, the design of the logger implementation is somewhat tricky. In particular, the logger needs to know where the record has ended so that it known when to output the log record. In the above example, it is immediately after the variable port.
A solution is to have the overloaded log() class member function return an object, such that the lifetime of this object is until the end of the statement. Right at the semicolon after the port variable, at the end of the log record, the object gets destructed. So put the logic to output the complete log record in the destructor of this object. As for the << operator, all the object does is to store the argument on the right hand side and return the reference to the object itself. The operator<<() class member function can be implemented as a C++ template. Putting important code inside the destructor is somewhat counterintuitive, but is key in this solution.
I implement the LoggerStream class as follows:
class LoggerStream {
public:
~LoggerStream() {
const std::string message = stream.str();
if (!message.empty())
logger.log(level, message);
}
template
LoggerStream& operator<<(const T& value) {
stream << value;
return (*this);
}
private:
friend class Logger;
LoggerStream(Logger& logger, LogLevel level) : logger(logger), level(level) {
}
Logger& logger;
LogLevel level;
std::ostringstream stream;
};
As I have no need for LoggerStream to be constructed anywhere else, I made its constructor private with Logger as a friend class.
Then the log() class member function of the Logger class simply returns a new instance of LoggerStream:
LoggerStream Logger::log(LogLevel level) {
return LoggerStream(*this);
}
Notice the return value of this function is not a reference, otherwise the destructor of LoggerStream will never get called. Also, due to return value optimization, the C++ compiler omits the object copying and destruction when the LoggerStream instance is returned from the class member function. However, the check for empty message in the destructor exists in case the C++ compiler does not apply the return value optimization.
An added advantage of designing the logger this way is that the two-argument version of the log() class member function is guaranteed to handle the entire log record. This is particularly important in multithreaded applications which use the same Logger instance, for which a synchronization measure such as mutex can be employed in this log() class member function if the underlying logger implementation does not protect against concurrent access. I certainly do not want to see logger output like the following!
[INFO] Connection from [INFO] Connection from two.example.com:one.example.com:1234
5678
This logger design of mine was partially inspired by the log4cplus project, in which they defined logging macros that allowed the << syntax by the use of std::ostringstream in the macro. I am virtually certain that my logger design is not new, although I have yet to encounter another place where this design is used.
In fact, this design can be seen as an application of the builder design pattern. The object being created is the log record (or at least its formatted message string), the << operator is used to configure the object prior to its creation, and the destructor is used to finally create the log record. Additionally, the << operator provides a fluent interface that allows for multiple << operators to be chained together in a single C++ statement.