Introduction
Good grief. Does the world really need another library for printing
trace messages? Probably not, but this one addresses some of the problems that tend
to afflict the squillions of other libraries out there:
- This library lets you use a stream-style syntax i.e. you can write your message
like this:
LOG_MSG( "The answer is " << iFoo ) ;
- You can control exactly which messages get printed and which don't. A lot
of libraries have the concept of low, medium and high priority messages but the
days of my wading through pages and pages of low priority messages searching for
the one that I actually wanted are long gone.
- Uses an ostream-derived class. Why this is cool probably deserves its own
section - read on...
Good logging is an essential part of any developer's toolkit. You don't always have
the luxury of being able to run your app from an IDE (e.g., NT services or CGI processes)
and if you don't know exactly what your program is doing, well, you're in trouble!
Furthermore, while this library gives you the option of disabling compilation of
all logging code, I'm a big fan of leaving it in for release builds. Then, when (not if!)
your customers start to have problems, you can just turn logging on via some hidden
switches and then have at least a clue as to what's going on.
In summary, these are the features offered by this library:
- Log messages can be enabled/disabled at runtime.
- Output can be sent to any ostream-derived destination.
- Optional inclusion of message number, date/time stamps and source file location.
- Automatic indentation of messages (useful for nested or recursive calls).
- Compile-time switch to enable/disable compilation of logging code.
Why an ostream-based solution is cool
First, a quick primer for those of you who are new to streams. An ostream
(or output stream) is simply somewhere where you can send data. That's it! Well, not quite,
but I'm not going to go into the differences between a stream and a streambuf here.
Look it up. The point is that the code sending the data doesn't have to know the mechanics
of how the data gets to where it's going, or even where it's going to. It just gives
the ostream a pile of data and says "deal with it!".
So, if you wrote a function like this:
void foo( ostream& os )
{
os << "Hello world!" << endl ;
}
this accepts an ostream and sends the message "Hello world!" to it.
cout
is a special ostream that sends its output to stdout, so writing:
foo( cout ) ;
would print "Hello world!" to the console.
Similarly, ofstream
is an ostream-derived class that sends its output to a file, so this would send the message to the specified file:
ofstream outputFile( "greeting.txt" ) ;
foo( outputFile ) ;
So, how does all of this relate to this article? CMessageLog
is my class
that manages log messages by forwarding them on to an ostream
object that you specify.
By passing cout
to the CMessageLog
constructor, you can print your trace messages
to the console, but by installing an ofstream
object, you can send your trace messages to a file.
But wait, there's more! I have in the past written an ostream-derived class that sends its data over a socket,
so with a single line of code, you could plug one of those babies into this library and have
instant remote logging. Cool! Or you could install a stringstream to keep your log messages
in memory. Or one that records log messages as rows in a database. One of the guys
I work with wants to write a ostream
wrapper for OutputDebugString()
so that we can send
log messages to the debugger (hi Pete - is it ready yet?). I've even written a library to generate PDF's
that had a stream-based interface and tried plugging that into this library. Sending trace messages
to a PDF: totally useless but a neat validation of the power of streams :-)
Examples
Time for some examples.
This is how to use the library in its simplest form:
#define _LOG #include "log/log.hpp"
CMessageLog myLog( cout ) ;
myLog.enableTimeStamps( true ) ;
myLog.enableDateStamps( true ) ;
myLog << "Hello world!" << endl ;
This produces the following output:
01jan02 12:48:19 | Hello world!
Most applications will typically only need the one log and so a global instance is provided
for you as a convenience. This object can be accessed via the global function theMessageLog()
.
Some macros have been defined as well to send messages to this
global object:
ofstream logFile( "log.txt" ) ;
theMessageLog().setOutputStream( logFile ) ;
LOG_MSG( "Hello world!" ) ;
Now we'll create some message groups, that is, groups of messages that can be
enabled or disabled individually at runtime.
CMessageGroup gMsgGroup1 ;
CMessageGroup gMsgGroup2 ;
CMessageGroup gMsgGroup3 ;
gMsgGroup1.enableMsgGroup( true ) ;
gMsgGroup2.enableMsgGroup( true ) ;
gMsgGroup3.enableMsgGroup( false ) ;
LOG_GMSG( gMsgGroup1 , "This is a message from group 1." ) ;
LOG_GMSG( gMsgGroup2 , "This is a message from group 2." ) ;
LOG_GMSG( gMsgGroup3 , "This is a message from group 3." ) ;
In this example, only the first two messages would appear. The third would not because its
group has been disabled. Note that I used the LOG_GMSG()
macro instead of LOG_MSG()
.
The former will check to see if the message group is enabled before outputting
the message, the latter doesn't check anything and unconditionally logs the message.
Full example
Now for a real-life example. Let's say I'm writing a server application that
accepts requests on a socket, does some processing and sends back a response.
I might want to set up three message groups, one to log incoming requests, one
for the processing, and one to log the responses being sent back. Using the helper
macros, I might define them like this:
DEFINE_MSG_GROUP( gReqMsgGroup , "req" , "Log incoming requests." )
DEFINE_MSG_GROUP( gProcMsgGroup , "proc" , "Log request processing." )
DEFINE_MSG_GROUP( gRespMsgGroup , "resp" , "Log outgoing responses." )
Note that I gave each group a name which can be used identify each group in addition
to their automatically-assigned numeric ID's. Each one also has a brief description
which will be printed out if you call CMessageGroup::dumpMsgGroups()
.
Take a look at the demo to see how this works.
I also usually define some helper macros of my own to log messages:
#define LOG_REQ_MSG( msg ) LOG_GMSG( gReqMsgGroup , msg )
#define LOG_PROC_MSG( msg ) LOG_GMSG( gProcMsgGroup , msg )
#define LOG_RESP_MSG( msg ) LOG_GMSG( gRespMsgGroup , msg )
Now, I could write my server to be something like this:
void main( int argc , char* argv[] )
{
CMessageGroup::disableAllMsgGroups( true ) ;
if ( argc > 1 )
CMessageGroup::enableMsgGroups( argv[1] , true ) ;
for ( ; ; )
{
string req = acceptRequest() ;
LOG_REQ_MSG( "Received a request: " << req ) ;
string resp = processRequest( req ) ;
LOG_RESP_MSG( "Sending response: " << resp ) ;
}
}
string processRequest( const string& req )
{
LOG_PROC_MSG( "Processing request: " << req ) ;
return req ;
}
Now, when I start my server app, I can specifiy which message groups I want
enabled:
server.exe req,resp <== log requests & responses only, no processing
I would also add command line switches to turn on date/time stamping, etc.
You could also, of course, add a UI to dynamically enable or disable message
groups by calling enableMsgGroup()
for the appropriate CMessageGroup
objects. Or perhaps periodically reload the settings from an INI file.
Summary
I've been lurking around CodeProject for a long time and figured it was about time
I got off my butt and put something back in. This is one of the hardest-working
libraries in my toolkit and while the implementation is a bit clunky - it was written
way back in '97, pretty early on in my C++ days - I hope you guys find it useful.
Cheers :-)
Revision History
- 4 Nov 2002 - Initial editing.