One of the first things I wrote for Deep Space was the logging system. The goal was to have minimal bloat and be very easy to use. The logging system I wrote for Box Bunny fit the easy to use part of the requirements, but it used rather long macros that failed when used outside of simple cases (the best example being that it relied on literal string concatenation in the pre-processor so logging using non-literal strings needed to be handled differently). While the new system does use macros for most of its interface, they are there merely to introduce breakpoints at the calling point instead of having to walk up the call stack to find the point where errors or warnings were found and to provide LogIf interfaces that only log if the first parameter is true.
For example LogError(format, ...) was defined like this in Box Bunny: #define LogDbg(type, format, ...)\ ::logger.Write(\ type, format "\n\t" __FILE__ "(%d)(" ARCHITECTURE "): %s\n",\ ##__VA_ARGS__, __LINE__, FUNC_NAME) #define LogError(format, ...){LogDbg(UT::LT_ERROR, format, ##__VA_ARGS__); BREAK(); abort();}The new logging system I created allowed the LogError define to be redefined as this and still provide all of the same information in the logs: #define LogError(format, ...){BREAK(); ::logger.Write(UT::LT_ERROR, format, ##__VA_ARGS__);}interesting code from UT/Logger.h namespace UT{
enum LogType{
LT_STD, // generic logging data
LT_INFO, // Data meant to be displayed (in the console)
LT_WARNING, // For non-fatal errors
LT_ERROR, // For fatal errors
LT_LUA_PRINT, // output from lua calls to print()
LT_LUA_STD, // LT_STD for code using lua
LT_LUA_INFO, // LT_INFO for code using lua
LT_LUA_WARNING, // LT_WARNING for code using lua
LT_LUA_ERROR, // LT_ERROR for code using lua
};
bool IsError( const LogType type);
bool IsWarning(const LogType type);
bool IsInfo( const LogType type);
struct LogData
{
LogType type;
// formatted_output contains a prefix with the logging type and
// when IsError(type) == true contains the calling function's info
char formatted_output[2048];
// output is the string as formatted by the caller
char output[2048];
Debug::SymbolInfo symbol_info;
};
class LogWriter;
class Logger
{
public:
Logger();
~Logger();
void Write(const LogType type, const char* format, ...);
void AddLogWriter(LogWriter* writer);
void RemoveLogWriter(LogWriter* writer);
private:
void FormatLog(LogData& data);
std::vector<LogWriter*> m_writers;
};
// Use the LogWriter interface to provide a new log output format.
class LogWriter
{
public:
LogWriter();
virtual ~LogWriter();
virtual void Write(const LogData& data) = 0;
};
}
#define Log( format, ...) ::logger.Write(UT::LT_STD, format, ##__VA_ARGS__)
#define LogInfo( format, ...) ::logger.Write(UT::LT_INFO, format, ##__VA_ARGS__)
#define LogWarningNB(format, ...) ::logger.Write(UT::LT_WARNING, format, ##__VA_ARGS__)
#define LogWarning( format, ...)\
{ DBG_BREAK(); ::logger.Write(UT::LT_WARNING, format, ##__VA_ARGS__); }
#define LogError( format, ...)\
{ BREAK(); ::logger.Write(UT::LT_ERROR, format, ##__VA_ARGS__); }
#define LogLua( format, ...) ::logger.Write(UT::LT_LUA_STD, format, ##__VA_ARGS__)
#define LogLuaPrint( format, ...) ::logger.Write(UT::LT_LUA_PRINT, format, ##__VA_ARGS__)
#define LogLuaInfo( format, ...) ::logger.Write(UT::LT_LUA_INFO, format, ##__VA_ARGS__)
#define LogLuaWarning(format, ...) ::logger.Write(UT::LT_LUA_WARNING, format, ##__VA_ARGS__)
#define LogLuaError( format, ...)\
{ BREAK(); ::logger.Write(UT::LT_LUA_ERROR, format, ##__VA_ARGS__); }
#define LogIf( x, format, ...) { if(x){ Log( format, ##__VA_ARGS__); } }
#define LogInfoIf( x, format, ...) { if(x){ LogInfo( format, ##__VA_ARGS__); } }
#define LogWarningIf( x, format, ...) { if(x){ LogWarning( format, ##__VA_ARGS__); } }
#define LogWarningIfNB(x, format, ...) { if(x){ LogWarningNB(format, ##__VA_ARGS__); } }
#define LogErrorIf( x, format, ...) { if(x){ LogError( format, ##__VA_ARGS__); } }
#define LogLuaIf( x, format, ...) { if(x){ LogLua( format, ##__VA_ARGS__); } }
#define LogLuaPrintIf( x, format, ...) { if(x){ LogLuaPrint( format, ##__VA_ARGS__); } }
#define LogLuaInfoIf( x, format, ...) { if(x){ LogLuaInfo( format, ##__VA_ARGS__); } }
#define LogLuaWarningIf(x, format, ...) { if(x){ LogLuaWarning(format, ##__VA_ARGS__); } }
#define LogLuaErrorIf( x, format, ...) { if(x){ LogLuaError( format, ##__VA_ARGS__); } }
#define ASSERT_TRUE(x) LogErrorIf(true != (x), "ASSERT_TRUE(" #x")")
#define ASSERT_FALSE(x) LogErrorIf(false != (x), "ASSERT_FALSE(" #x")")
#define ASSERT_NULL(x) LogErrorIf(nullptr != (x), "ASSERT_NULL(" #x")")
#define ASSERT_NOT_NULL(x) LogErrorIf(nullptr == (x), "ASSERT_NOT_NULL("#x")")
And the UT::Logger::Write function is defined like this From UT/Logger.cpp namespace UT{
void Logger::Write(const LogType type, const char* format, ...)
{
static LogData data;
data.type = type;
data.formated_output[0] = '\0';
data.output[0] = '\0';
data.symbol_info.Clear();
// Format the input into a useable string
va_list args;
va_start(args, format);
vsprintf(data.output, format, args);
va_end(args);
// If it's an error log gather some data about the calling function.
if(IsError(type)){
UT::Debug::AddressToSymbolInfo(data.symbol_info, ReturnAddress());
}
// FormatLog adds the log type prefixed to the string and
// if data.symbol_info is provided then it adds it to the string
FormatLog(data);
// call each LogWriter so that they can output the data however they need
for(auto i = m_writers.begin(); i != m_writers.end(); ++i){
(*i)->Write(data);
}
}
}
Using this system it is extremly easy to add new logging outputs to the system as seen by the message box example below. Currently we have about four log outputs including a file logger, console logger, OutputDebugString logger and Error Message boxes. From SYS/ErrorMsgBoxLogWriter.h namespace SYS{ class ErrorMsgBoxLogWriter : public UT::LogWriter { virtual void Write(const UT::LogData& data) { if(UT::IsError(data.type)){ MessageBoxA(nullptr, data.formated_output, "Error", MB_OK | MB_ICONERROR); } } }; } |