Flexible Logging System with filters

By Icebreaker

Intro

Logging always comes handy, and if it's done right, then you can fully benefit from it. In this article I'll present a logging system I'm currently using, and it does the job perfectly.

It's simple, but yet quite powerful. It is implemented as a singleton C++ class, and it can be ditched into any application easily. Logging is useful if we want to watch closely what's happening while debugging, or if our application crashed the user could send back the log file which could help us to analyze and hopefully fix the bug/issue.

Code for 'breakfast'

As always, let's see some code first ...

class Log
{
    public:
        Log();
        virtual ~Log();
       
        virtual void setFilter( const int filter = Any );
        virtual int log( const int level, const char *msg, ... ) const;
       
        static Log &getInstance( void )
        {
            static Log staticLog;
            return staticLog;
        };
       
    // ================== Public Constants ==================
    public:
       enum
        {
            Text,
            Info,
            Warning,
            Error,  
            Any
        };

    protected:
        int filter;
};

Here we have 4 level or filters or 'verbosity' if you wish. So let's see an sample usage scenario:

Log &logger = Log::getInstance();
logger.log(Log::Warning, "Your video card doesn't seem to support"
                         " Pixel Shader 3.0!");

This is pretty straightforward ... if we called for example logger.setFilter(Log::Error) before then this message wouldn't appear, which is useful if you don't want to view all the warnings, etc.

Code for 'dinner'

The actual implementation ...

Log::Log() : filter( Log::Any )
{

}

Log::~Log()
{

}

void Log::setFilter( const int filter )
{
    this->filter = filter;
}

int Log::log( const int level, const char *msg, ... ) const
{
    va_list ap;
    char text[ 2048 ] = {0,};
    int done;
       
    // is filtering turned on?
    if( filter != Log::Any && filter != level )
        return 0;
 
    va_start(ap, msg);
        done = vsprintf(text, msg, ap);
    va_end(ap);
   
    // make sure to don't add line-end if already exists
    if( !strchr(text,'\n') )
        sprintf(text,"%s\n",text);
 
    switch( level )
    {
        case Log::Info:
            fprintf(stderr, "INFO: %s", text );               
            break;

        case Log::Warning:
            fprintf(stderr,"WARNING: %s", text );               
            break;
           
        case Log::Error:
            fprintf(stderr,"ERROR: %s", text );               
            break;
               
        default:
            fprintf(stderr,"%s", text );               
            break;
    }

    return done;
}

The heart of the logger is a simple functions taking variable number of parameters ' la printf' and formatting the text accordingly to match the given level, or just bailing out if filtering is turned on and the currently given filter doesn't pass ...

Additional filters can be added easily, like "Fatal Errors", or "Critical Errors", etc. based on your personal needs. This is a consistent and elegant way of writing text to a console, instead of printfs everywhere, typing "WARNING:" all the time, or even calling some myError() type of thing.

Linux vs. Windows or Linux+Windows

What we have so far works great under Linux, all the 'messages' will appear right on the terminal if the application was started from a terminal, but under Windows, you can't see anything.

Under Windows, I like to spawn a console window on-demand. No, no ... you don't have to compile your application as a 'console application' in order to do this ... you can make the magic happen with a few lines of code. So we spawn a console window and redirect all the common pipes to it. (e.g: stderr, stdin, stdout)

void consoleShow( const TCHAR *title, int matrix = 0, maxLines = 2048 )
{
    int hConHandle;
    long lStdHandle;
 
    CONSOLE_SCREEN_BUFFER_INFO coninfo;
    FILE *fp;
 
    AllocConsole();
    SetConsoleTitle( title );
 
    GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE),&coninfo);
    coninfo.dwSize.Y = maxLines;
 
    if( matrix )
        SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), 
                                FOREGROUND_GREEN | FOREGROUND_INTENSITY);

    SetConsoleCtrlHandler( HandlerRoutine, true );
 
    SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE),coninfo.dwSize);
    lStdHandle = (long)GetStdHandle(STD_OUTPUT_HANDLE);
    hConHandle = _open_osfhandle(lStdHandle, _O_TEXT);
 
    fp = _fdopen( hConHandle, "w" );
    *stdout = *fp;
    setvbuf( stdout, NULL, _IONBF, 0 );
 
    lStdHandle = (long)GetStdHandle(STD_INPUT_HANDLE);
    hConHandle = _open_osfhandle(lStdHandle, _O_TEXT);
 
    fp = _fdopen( hConHandle, "r" );
    *stdin = *fp;
    setvbuf( stdin, NULL, _IONBF, 0 );
 
    lStdHandle = (long)GetStdHandle(STD_ERROR_HANDLE);
    hConHandle = _open_osfhandle(lStdHandle, _O_TEXT);
 
    fp = _fdopen( hConHandle, "w" );
    *stderr = *fp;
    setvbuf( stderr, NULL, _IONBF, 0 );
}
 
void consoleHide( void )
{
    SetConsoleCtrlHandler( HandlerRoutine, false );
    FreeConsole();
}
 
// callback
BOOL WINAPI HandlerRoutine( DWORD dwCtrlType )
{
    return true;
}

We can extend the Log class by adding two more methods like 'init' and 'shutdown' and guard the code with #ifdef WIN32, a sample usage scenario would look like this:

int WINAPI WinMain( HINSTANCE inst, HINSTANCE prevInst, LPTSTR cmd, int showCmd )
{
    Log &logger = Log::getInstance();
    logger.init(); // this will spawn the console window under Win32

    // do some work
    logger.log(Log::Info,"Chicken butts are friggin' cute!");

    logger.shutdown(); // this will destroy the console window under Win32
}

Outro

The main advantage of the singleton class is that we can access it from anywhere, and we don't have to use global variables, with externs, etc.... in one word it's elegant :)

For instance, if our program had an internal 'Quake' like console we could also write on it, or we could just open a file in the "init" and closing it in the "shutdown" logging everything to a file. The possibilities are endless :)

I hope that you enjoyed this piece of text as much as I did writing it ...

Until next time, good night and happy coding!

Icebreaker