Using an RTF Log File
by Eamonn Doherty


ADVERTISEMENT

This article is intended for anyone who is looking for a new debugging tool to add to their arsenal.

This article is divded into the following sections:

  1. The case for RTF log files
  2. The RTF file format
  3. Preparing an RTF file for output
  4. Writing formatted output
  5. Logging macros

1. The case for RTF log files

First of all, I'd like to let you know that if you don't have the time or inclination to read through all of this, theres a link at the end of this article to the source being discussed. Second, all code accompanying this article will be written as stand-alone functions. I personally use a C++ singleton class to handle logging, but I'm leaving it up to you to decide how you want to implement this technique in your own program.

I'm sure I don't have to convince you of the value of using a log file, but why go through all of the trouble to make an RTF log file when you could just use a regular text file, or even an HTML log file?

Here are a couple of points to consider:

  • Adding a bit of colour and formatting to text makes it a lot more readable

  • You don't have to worry about forgetting to close a tag like you do in HTML. You can have all formatting be reset after every outputted string (and I strongly recommend you do this). The last thing you want to be doing is debugging your debug output strings!

  • It's not particularly hard to write, and anyways, I've done it for you.

  • It can actually help save programming time. Looking through a large, plaintext log file for some impotant output can be a pain in the ass. If the log file is large enough, it can consume a fair amount of time if you have to do it often. With an RTF file, you can have all run-of-the-mill output be a dull grey colour, while the important stuff is a bright blue or red, making it much easier to pick out.

  • It just plain looks cooler

If you're still not convinced, I ask you to just give it a shot. What have you got to lose besides 5 minutes of coding time?

2. The RTF file format

After I made the decision to write an RTF log file, I went ahead and downloaded the RTF file spec documents. When I opened the RTF 1.7 specification file in Word, I nearly gave up right then and there. The file spec was 225 pages long and covered everything from "document variables" to foreign language support. All I wanted to do was make some text blue.

Fortunately, a bit of searching turned up a great site with a basic overview of the RTF file format : The RTF Cookbook

RTF files are organized into blocks denoted by { braces } and use formatting commands that begin with a back-slash and are delimited by a space, line-break, semi-colon, or the beginning of another command (the delimiter following a command does not show up in the actual text of the document). Braces and backslashes that are part the actual text have a backslash prepended to them and a space or line-break after them (i.e \\ , \{ . and \} ).

RTF files have a header, and it's fairly simple to construct. Well, it doesn't have to be simple (foreign language support, etc, etc), but the one I created is.

First, you need a line or two that lets a word processor know this is an RTF file. It looks something like this:

    {\rtf1
     \ansi
     \deff0
     \fonttbl{\f0 Courier New;}
     \fs20}

The rtf1, ansi, and deff0 commands are file format tags and nothing to worry about. The fonttbl command defines a table of all the fonts used in the file. For this tutorial, we will only be using one font: Courier New. The fs20 command sets the font size to 10. The syntax of this command is \fs<double the desired font size>. The command is formatted like this to add easy support for font sizes like "10.5" (which would be \fs21).

Next in the file comes the colour table. Any font colour you want to use in your files has to be in this table. Here's an example with three colours (we'll be using more the actual implementation of the logging class):

    {\colortbl;
     \red255\green255\blue255;
     \red128\green128\blue128;
     \red255\green0\blue0;
    }

The above colour table makes the colours black, grey, and red available for use in the rest of the document.

Now we get into the actual text of the document, and the formatting tags that go with it. Just like commands, text can be organized into blocks using braces. Any formatting attributes set inside a block will not affect text outside of the block. Here's where RTF files begin to look a little sexier than HTML files. If we surround every outputted line with braces, each line begins with a nice, plain black font. Also, there's a command we'll be using on each line as sort of a safeguard, "\pard", that removes all special formatting.

Here's an example of a line of text:

{\pard \cf5 \b <CGraphics::Init>\b0 Direct3D initialized successfully\par}

If the 6th entry (index of 5) in the colour table is blue, This will look like:

<CGraphics::Init> Direct3D initialized successfully

Here's a list of some common formatting commands, and what they do:

Command   Purpose
\plainTurn off all text formatting
\pardEnd paragraph / turn off all text formatting
\ulUnderline text
\bBold text
\iItalicize text
\subMake text a subscript
\superMake text a superscript
\f#Change font to given # in the font table
\fs#Change font size to # half-points
\cf#Change foreground font colour to # in the colour table
\cb#Change background font colour to # in the colour table
\strikeStrikethrough text

Note that many of these formatting flags can be turned off by writing them again with an appended zero (e.g. you can turn off italics by writing \i0)

We've covered the header of the file and the meat of the file. All that's left is the footer. Here's where things get really complicated. Below is an example of an RTF file footer:

    }

I suggest just copy-and-pasting that instead of trying to be some kind of programming hero and writing your own.

3. Preparing an RTF file for output

Here are the variables the logger will be using:

//External variables referenced by logger -- typically private class members
static BOOL bLoaded;     //File loaded flag
static HANDLE hFile;     //Handle to the file
static string sFilename; //Filename

Here is the initialization function for the logger:

//Open log file
int Init (const string& sFile)
{
    //Make sure log file is not already open
    if (bLoaded)
        return FALSE;

    //Open file

    hFile = CreateFile (sFile.c_str(), GENERIC_WRITE, 0, 0,
                        CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
    if (hFile == INVALID_HANDLE_VALUE)
        return FALSE;

    //Set file loaded flag
    bLoaded = TRUE;

    //Set filename
    sFilename = sFile;

    //Write RTF header
    WriteString ("{\\rtf1\\ansi\\deff0{\\fonttbl{\\f0 Courier New;}}\\fs20\n");

    //Write colour table
    WriteString ("{\\colortbl;\n");                     //Black
    WriteString ("\\red255\\green255\\blue255;\n");     //White
    WriteString ("\\red128\\green128\\blue128;\n");     //Grey
    WriteString ("\\red255\\green0\\blue0;\n");         //Red
    WriteString ("\\red0\\green255\\blue0;\n");         //Green
    WriteString ("\\red0\\green0\\blue255;\n");         //Blue
    WriteString ("\\red0\\green255\\blue255;\n");       //Cyan
    WriteString ("\\red255\\green255\\blue0;\n");       //Yellow
    WriteString ("\\red255\\green0\\blue255;\n");       //Magenta
    WriteString ("\\red128\\green0\\blue0;\n");         //Dark Red
    WriteString ("\\red0\\green128\\blue0;\n");         //Dark Green
    WriteString ("\\red0\\green0\\blue128;\n");         //Dark Blue
    WriteString ("\\red255\\green128\\blue128;\n");     //Light Red
    WriteString ("\\red128\\green255\\blue128;\n");     //Light Green
    WriteString ("\\red128\\green128\\blue255;\n");     //Light Blue
    WriteString ("\\red255\\green128\\blue0;\n");       //Orange
    WriteString ("}\n");

    //Write log header
    LogString ("*** Logging Started");
    LogString ("");

    //Success
    return TRUE;
}

Nothing overly complicated here. The function opens a file, drops in an RTF file header and colour table, and writes a string that says "*** Logging Started" into the file. The function LogString() used to write text in the file is shown later on in the article.

Here's a function that will close the file properly

//Close log file
int Close ()
{
    //Make sure log file is already opened
    if (!bLoaded)
        return FALSE;

    //Write closing line
    LogString ("");
    LogString ("*** Logging Ended");

    //Write closing brace (RTF file footer)
    WriteString ("}");

    //Close file
    CloseHandle (hFile);

    //Clear file loaded flag
    bLoaded = FALSE;

    //Success

    return TRUE;
}

If you use the logging file in a DLL, checking the file for the "*** Logging Ended" message can be a good way of assuring yourself the DLL is cleaning itself up properly.

4. Writing formatted output

Here comes the hard part: actually printing messages to the file. We will be using four functions for this.

First however, comes an enumeration. It represents the colour table written to the file in Init():

//RTF file's colour table
enum COLOURTABLE
{
    BLACK = 0,
    WHITE = 1,
    GREY = 2,
    RED = 3,
    GREEN = 4,
    BLUE = 5,
    CYAN = 6,
    YELLOW = 7,
    MAGENTA = 8,
    DARK_RED = 9,
    DARK_GREEN = 10,
    DARK_BLUE = 11,
    LIGHT_RED = 12,
    LIGHT_GREEN = 13,
    LIGHT_BLUE = 14,
    ORANGE = 15
};

The first of the four functions, WriteString() puts a plaintext string directly into the file. This should be a private function in your logging class. It is listed below:

//Write a string directly to the log file
void WriteString (const string& sText)
{
    DWORD bytesWritten;

    WriteFile (hFile, sText.c_str (), (int) sText.length (), &bytesWritten, NULL);
}

Nothing that really needs to be explained.

The second function, ReplaceTag() simply replaces all occurences of a formatting tag in a string with its RTF command equivalent. It allows us to use formatting tags such as [B] and [/B] rather than writing out the RTF commands. This function should be a private class member

//Replace all instances of a formatting tag with the RTF equivalent
void ReplaceTag (string* sText, const string& sTag, const string& sReplacement)
{
    unsigned int start = 0;
    while (sText->find(sTag, start) != string::npos)
    {
        start = sText->find(sTag, start);
        sText->replace (start, sTag.length (), sReplacement);
        start += sTag.length () + 1;
    }
}

This is basically just a substring-replacement function. Not particularly complicated. If you do not want to use special formatting tags, like [B], you should still include this function as it is later used to replace slashes and braces with the RTF comamdns that allow them to show up in the document.

The third function, DoFormatting(), is where the special formatting tags I've been going on about are actually implemented. It also allows you to easily include slashes and braces in your logging strings. Furthermore, it puts strings to be logged inside a set of braces and clears any previous formatting. This function should be a private class member.

//Replace formatting tags in a string with RTF formatting strings
void DoFormatting (string* sText)
{
    //Fix special symbols {, }, and backslash
    ReplaceTag (sText, "\\", "\\\\");
    ReplaceTag (sText, "{", "\\{");
    ReplaceTag (sText, "}", "\\}");

    //Colours
    ReplaceTag (sText, "[BLACK]", "\\cf0 ");
    ReplaceTag (sText, "[WHITE]", "\\cf1 ");
    ReplaceTag (sText, "[GREY]", "\\cf2 ");
    ReplaceTag (sText, "[RED]", "\\cf3 ");
    ReplaceTag (sText, "[GREEN]", "\\cf4 ");
    ReplaceTag (sText, "[BLUE]", "\\cf5 ");
    ReplaceTag (sText, "[CYAN]", "\\cf6 ");
    ReplaceTag (sText, "[YELLOW]", "\\cf7 ");
    ReplaceTag (sText, "[MAGENTA]", "\\cf8 ");
    ReplaceTag (sText, "[DARK_RED]", "\\cf9 ");
    ReplaceTag (sText, "[DARK_GREEN]", "\\cf10 ");
    ReplaceTag (sText, "[DARK_BLUE]", "\\cf11 ");
    ReplaceTag (sText, "[LIGHT_RED]", "\\cf12 ");
    ReplaceTag (sText, "[LIGHT_GREEN]", "\\cf13 ");
    ReplaceTag (sText, "[LIGHT_BLUE]", "\\cf14 ");
    ReplaceTag (sText, "[ORANGE]", "\\cf15 ");

    //Text style
    ReplaceTag (sText, "[PLAIN]", "\\plain ");
    ReplaceTag (sText, "[B]", "\\b ");
    ReplaceTag (sText, "[/B]", "\\b0 ");
    ReplaceTag (sText, "[I]", "\\i ");
    ReplaceTag (sText, "[/I]", "\\i0 ");
    ReplaceTag (sText, "[U]", "\\ul ");
    ReplaceTag (sText, "[/U]", "\\ul0 ");
    ReplaceTag (sText, "[S]", "\\strike ");
    ReplaceTag (sText, "[/S]", "\\strike0 ");
    ReplaceTag (sText, "[SUB]", "\\sub ");
    ReplaceTag (sText, "[/SUB]", "\\sub0 ");
    ReplaceTag (sText, "[SUPER]", "\\super ");
    ReplaceTag (sText, "[/SUPER]", "\\super0 ");
    ReplaceTag (sText, "[LEFT]", "\\ql ");
    ReplaceTag (sText, "[RIGHT]", "\\qr ");
    ReplaceTag (sText, "[CENTER]", "\\qc ");
    ReplaceTag (sText, "[FULL]", "\\qj ");
    ReplaceTag (sText, "[TITLE]", "\\fs40 ");
    ReplaceTag (sText, "[/TITLE]", "\\fs20 ");
    ReplaceTag (sText, "[H]", "\\fs24 ");
    ReplaceTag (sText, "[/H]", "\\fs20 ");

    //Add brackets and line formatting tags
    sText->insert (0, "{\\pard ");
    sText->insert (sText->length (), "\\par}\n");
}

If it looks a bit too unwieldy for your liking, you can remove a few of the formatting tags. I personally don't think speed is a big issue in logging. File output buffers make sure the log files dont slow down your program too much, and the cost of a few substring replacements is minimal. Anyways, you would probably want to disable logging in release builds (the next section details methods of making all of the logging function calls compile right out of the program).

The final function needed to get a fully-functioning RTF log file up and running is LogString(). This is the function you can call from anywhere to log messages to the file. It should be a public member of your logging class.

//Format and log a string
int LogString (string sText)
{
    //Make sure log file is already opened
    if (!bLoaded)
        return FALSE;
    //Format string
    DoFormatting (&sText);

    //Write string to file
    WriteString (sText);

    //Success
    return TRUE;
}

That does it. The log file should be running properly at this point. You should be able to open a file with Init(), write to it with LogString() and close it with Close().

5. Logging macros

In this section, a series of macros will be presented that allow access to the log file. If you use only these macros to interact with the logging functions, it is quite easy to have the preprocessor remove all logging code from a release build of your program.

Before we get to the macros, I would like to introduce a function that allows you to easily write messages that look like the following:

<Class::FunctionName> Some information
<Class::FunctionName> A notification of success
<Class::FunctionName> A warning
<Class::FunctionName> A fatal error

Here's a short snip from one of my log files that shows how this looks in practice:

<CMusicManager::Init> Attempting to load songs in directory: Music\
<CMusicManager::Init> Loaded song: (286394ms) Coheed and Cambria - Delirium Trigger (album).ogg
<CMusicManager::Init> Successfully loaded all songs in given directory
<CGraphics::Init> Initializing Direct3D...
<CGraphics::Init> Direct3D 8 object created
<CFake::Error> Nothing ever goes wrong with my code so I had to make a fake error
<CGraphics::CheckCaps> Examining adapter capabilities...
<CGraphics::CheckCaps> Adapter being examined: 3D Blaster GeForce2 MX
<CGraphics::CheckCaps> Video card supports textures as big as 1024x1024
<CGraphics::CheckCaps> Video card supports non-square textures
<CGraphics::CheckCaps> Video card does not support non-power-of-2 textures
<CGraphics::CheckCaps> Video card supports alpha channel in textures
<CGraphics::CheckCaps> Video card supports modulated texture blending
<CGraphics::CheckCaps> Video card supports source alpha blending
<CGraphics::CheckCaps> Video card supports destination alpha blending
<CGraphics::CheckCaps> Video card has required capabilities

As you can see, it is very easy to pick out warnings and errors from the rest of the messages.

Here is an enumeration the function depends on:

//Log message types
enum MESSAGETYPE
{
    MSG_SUCCESS = 1,
    MSG_INFO = 2,
    MSG_WARN = 3,
    MSG_ERROR = 4,
};

Here is the function:

//Write a specially formatted message to the log file
int CLogFile::LogMessage (const string &sSource,
           const string& sMessage, MESSAGETYPE messageType)
{
    string sString;
    switch (messageType)
    {
    case MSG_SUCCESS:
        sString = "[BLUE]";
        break;
    case MSG_INFO:
        sString = "[GREY]";
        break;
    case MSG_WARN:
        sString = "[ORANGE]";
        break;
    case MSG_ERROR:
        sString = "[RED]";
        break;
    }

    sString += "[B]<";
    sString += sSource;
    sString += ">[/B] ";
    sString += sMessage;

    //Format the string and write it to the log file
    return LogString (sString);
}

All it does is add a couple colour tags and some formatting, and passes it off to the LogString() function.

Now onto the macros. Here they are:

//Constant for disabling logging
const int LOGGING_DISABLED = 0x00330099;

//Logging definitions
#ifdef ENABLE_LOGGING
    #define openLog(fileName) Init(fileName)
    #define closeLog Close()
    #define writeLog(s) LogString(s)
    #define msgLog(message, type) LogMessage(__FUNCTION__, message, type)
    #define infoLog(message) LogMessage(__FUNCTION__, message, CLogFile::MSG_INFO)
    #define successLog(message) LogMessage(__FUNCTION__, message, CLogFile::MSG_SUCCESS)
    #define warnLog(message) LogMessage(__FUNCTION__, message, CLogFile::MSG_WARN)
    #define errorLog(message) LogMessage(__FUNCTION__, message, CLogFile::MSG_ERROR)
#else
    #define openLog(fileName) LOGGING_DISABLED
    #define closeLog LOGGING_DISABLED
    #define writeLog(s)
    #define msgLog(message, type)
    #define infoLog(message)
    #define successLog(message)
    #define warnLog(message)
    #define errorLog(message)
#endif

As should be apparent, logging will only compile into your program if ENABLE_LOGGING is defined. Otherwise, attempts to use openLog and closeLog will "return" LOGGING_DISABLED.

You can write to the file with the LogString() function via the writeLog macro. You can also write to the file with the LogMessage() function using the remaining 5 macros, unless you use Visual C++ 6.0. If you do, you don't have the luxury of the __FUNCTION__ macro. For Visual C++ 6 users (and for users of other compilers that do not support __FUNCTION__) I recommend rewriting the macros in this fashion:

#define msgLog(source, message, type) LogMessage(source, message, type)

This concludes my article. All of the functions in this article, have been compiled into the source files available here.

Please send any bugs, suggestions, or feedback to Eamonn@gdnMail.net

Discuss this article in the forums


Date this article was posted to GameDev.net: 4/12/2004
(Note that this date does not necessarily correspond to the date the article was written)

See Also:
Featured Articles
General

© 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
Comments? Questions? Feedback? Click here!