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();
2 Answers 2
First off, you are using std::
for everything else, might as well use it for std::uint32_t
and std::size_t
I'm not very familiar with variadic templates, but I feel like dropping into C's varargs is not the way to go.
- You have to do the messy
snprintf
to make the prefix (which now means you need the max prefix length) - Only primitive types can be logged.
About the Log levels, is it possible to add levels without access to the header? The way logLevelString()
works is beyond me.
So this some code for the way I might try to do it. The main thing is that I don't use varargs:
#include <iostream>
#include <cstdint>
#include <string>
#include <ctime>
struct LogLevel
{
const std::string name;
LogLevel( const std::string& n ): name( n ){}
virtual ~LogLevel( void ) = default;
};
struct Info : public LogLevel
{
Info( void ):LogLevel( "Info" ){}
};
template <typename T >
void print( T only )
{
std::cout << only << std::endl;
}
template <typename T, typename ... args >
void print( T current, args... next )
{
std::cout << current << ' ';
print( next... );
}
template < typename Level, typename ... args >
void print( args ... to_print )
{
std::cout << Level().name << ": ";
std::time_t time = std::time(nullptr);
std::string time_str = std::ctime( &time );
time_str.pop_back();
std::cout << time_str << ": ";
print( to_print... );
}
struct Point
{
int x; int y;
friend std::ostream& operator<<( std::ostream& out, const Point& p )
{ return out << '{' << p.x << ',' << p.y << '}'; }
};
int main( void )
{
Point p = { 1, 2 };
print< Info >( 1, "this", 1.2, p );
return 0;
}
-
\$\begingroup\$ I agree about the messy
snprintf
andvarargs
. I like your idea of a variadic print function instead of it, which would be able to handle non-primitive types, which is another good point. And no, if you didn't have access to the header, you couldn't add additional log levels as it is now. Perhaps I can turn it into a struct/class that you could inherit from if this was the case. \$\endgroup\$Chris– Chris2014年09月13日 19:03:22 +00:00Commented Sep 13, 2014 at 19:03
The main problem I have with this is:
void log (const char* format, ...)
Its not what you think.
The problem is that all the parameters have to be evaluated before the method is called. This can potentially be expensive (or it may be cheap but you are doing it a lot). If the logging is turned off this can be a lot of work with in the end no result being generated.
Most (all the good ones) logging systems I have seen, make sure that the parameters are only evaluated if the logging is actually going to be done. There is no point in doing work if you are not going to use the values in a message.
Explore related questions
See similar questions with these tags.
syslog
, but I'll have a look at it to see what I can learn from it. \$\endgroup\$__attribute__(format)
orSA_FormatString
to get some compile time checking for the format strings. \$\endgroup\$