I've been exploring design based around some of the more advanced C++11 features lately, and some of them are turning out to be rather useful for some projects I'm working on. One is this policy-based, variadic logger class that supports any combination of policies to determine where it logs output to.
Most loggers I've seen that adopt this design pattern only accept one, and there are cases where you would want more, so this expands on that without having to instantiate several loggers each with a distinct policy.
It's also easily extensible, flexible, being able to add additional logging policies and log levels.
/*! @brief Defines level severities for logging. */
enum class LogLevel : uint32_t
{
Info = 0,
Warning = 1,
Error = 2
};
template <class ... Policies>
class Logger
{
typedef std::tuple<Policies...> policies_t;
private:
policies_t _policies;
uint32_t _numWarnings;
uint32_t _numErrors;
static const size_t s_MaxPrefixSize = 128;
static const size_t s_MaxBufferSize = 1024;
public:
Logger ()
: _policies { std::forward<Policies>(Policies{})... }
, _numWarnings(0)
, _numErrors(0)
{}
static const size_t numPolicies = sizeof...(Policies);
template <typename T>
T* getPolicy ()
{
// Implementation of FindTypeIndex omitted for brevity.
static const auto index = FindTypeIndex<policies_t, T>::value;
return getPolicy<index>();
}
template <size_t Index>
auto getPolicy () -> decltype(&std::get<Index>(_policies))
{
static_assert(Index < numPolicies, "policy index out of range");
return &std::get<Index>(_policies);
}
template <LogLevel L>
void log (const char* format, ...)
{
std::time_t time = std::time(nullptr);
std::string timeString = std::ctime(&time);
char prefixString[s_MaxPrefixSize];
snprintf(prefixString, s_MaxPrefixSize, "[ %s ] : %s ",
timeString.substr(0, timeString.size() - 1).c_str(), // Strip off newline character.
logLevelString(Int2Type<(size_t)L>()).c_str());
va_list args;
va_start(args, format);
char buffer[s_MaxBufferSize];
vsnprintf(buffer, s_MaxBufferSize, format, args);
va_end(args);
std::string message;
message += prefixString;
message += buffer;
message += "\n";
doPolicy<0>(_policies, message);
}
template <LogLevel L>
void log (std::string msg)
{
log<L>(msg.c_str());
}
void logCounts ()
{
char buffer[s_MaxBufferSize];
snprintf(buffer, s_MaxBufferSize, "\nWarnings: %d\nErrors: %d\n", _numWarnings, _numErrors);
std::string countsString = buffer;
doPolicy<0>(_policies, countsString);
}
private:
// Int2Type implementation omitted for brevity.
std::string logLevelString (Int2Type<(size_t)LogLevel::Info>)
{
return std::string("< INFO >");
}
std::string logLevelString (Int2Type<(size_t)LogLevel::Warning>)
{
_numWarnings++;
return std::string("< WARNING >");
}
std::string logLevelString (Int2Type<(size_t)LogLevel::Error>)
{
_numErrors++;
return std::string("< ERROR >");
}
template <size_t I>
typename std::enable_if<I != numPolicies>::type doPolicy (policies_t& p, const std::string& msg)
{
std::get<I>(p).write(msg);
doPolicy<I+1>(p, msg);
}
template <size_t I>
typename std::enable_if<I == numPolicies>::type doPolicy (policies_t& p, const std::string& msg) {}
};
And then two simple logger policies as a sample. Additional policies could be added for network logging, user display, etc.
/*! @class ConsoleLogPolicy
@brief Logger policy that outputs messages to the debug console.
*/
class ConsoleLogPolicy
{
protected:
template <class ... T> friend class Logger;
void write (const std::string& buffer)
{
std::cout << buffer;
}
};
/*! @class FileLogPolicy
@brief Logger policy that outputs messages to an external text file.
@details The file must be opened by calling \ref open, with the path and filename. When a file stream goes out of scope,
or is destroyed, it will automatically flush its contents, so it is not absolutely necessary to call \ref close.
*/
class FileLogPolicy
{
public:
FileLogPolicy () {}
FileLogPolicy (const FileLogPolicy& copy) = delete;
FileLogPolicy (FileLogPolicy&& other) : _fileStream(std::move(other._fileStream)) {}
~FileLogPolicy ()
{
close();
}
bool open (const std::string& path, const std::string& fileName)
{
_fileStream.open(path + fileName);
if ( ! _fileStream.is_open() ) {
std::cerr << "FileLogPolicy Error: Failed to open file '" << fileName << "' at " << path << "\n";
return false;
}
_fileStream << "-- File Log Started --\n\n";
return true;
}
void close ()
{
if (_fileStream.is_open()) {
_fileStream << "\n-- File Log Ended --\n\n";
_fileStream.close();
}
}
protected:
std::ofstream _fileStream;
template <class ... T> friend class Logger;
void write (const std::string& buffer)
{
if (_fileStream) {
_fileStream << buffer;
} else {
std::cerr << "FileLogPolicy Error: File stream is not open for writing.\n";
}
}
};
Finally, a usage example of a logger that outputs to both the console and a file:
Logger<ConsoleLogPolicy, FileLogPolicy> logger;
logger.getPolicy<FileLogPolicy>()->open("/Users/Me/path/", "theLog.txt");
// ..
logger.log<LogLevel::Info>("Something informative %d", aValue);
// ..
std::string aMessage("Warning.");
logger.log<LogLevel::Warning>(aMessage);
// ..
logger.log<LogLevel::Error>(anException.description());
// ..
logger.logCount();
logger.getPolicy<1>()->close();