This link is no longer kept up-to-date. See this article instead.
Often times we want to create a log of what steps a program takes during run-time. Maybe our program works fine during a debug build but crashes during a release build. Or maybe we are developing on WinNT, but testing on Win98 without a development environment on the Win98 machine. Since we can't debug in these cases, it would be nice to have a log of all the steps a program took before it crashed.
This is a set of small projects (all working together) that can ease creating a log of your program’s code path at run-time. Some features include:
- Easy installation / integration into any project (MFC, Non-MFC, Console apps... all app types on Windows platforms).
- No need to create message strings ahead of time... log functions accept
sprintf-style messages with variable length parameter lists.
- Automatically include date/time, and code filename/line number in every message.
- Log to any file, or log straight to an MFC-based Log Window which opens automagically on demand during run-time (see picture below).
- When using the Log Window, double-click on any log entry to jump straight to that location in your code in MSDEV.
- Log Window will remain open when your app crashes/closes.
- Send different types of log messages (Debug, Release, Critical, Warning, Normal, Trace, or add your own fairly easily) and then filter out any types of messages you are not currently interested in, without re-compiling.
- For each message, individually specify if the message is to be sent in either debug, release, or both builds.
- For each message, individually specify if the message is to be sent to a file, the Log Window, or both.
- Have multiple simultaneous logs, specifying output location for each log individually (unique or shared file, and unique or shared Log Window, or both).
- Append logs over multiple runs, or erase log before starting each new run.
- Completely remove all logging code from your project by simply defining
_SS_LOG_INACTIVE in the pre-compiler (NOTE: this removes only the global log code, but support for removal of local logs is note entirely complete).
- SS_Log -- This project includes the main logging classes, and can be used without the other projects if you desire only to output your logs to a file.
- SS_Log_Window -- This project is the Log Window mentioned above, which will automatically open during your program’s run-time and start accepting logs from the
- SS_Log_AddIn -- This project is an MSDEV add-in that allows you to double-click in the Log Window and have MSDEV jump to the associated line of code in your project.
- SS_Log_Test -- This project is simply an example of how to use the
SS_Log logging class. It uses a global log and 2 local logs, and demonstrates a few of the features available.
- Utils -- This project is a helper library. It contains only a registry key class. Nothing too interesting here...
To install the projects, simply place the SS_Log_Window.exe and SS_Log_AddIn.dll files in your %SYSTEMROOT% folder (e.g.: c:\winnt or c:\windows). Don't worry about installing the add-in, as the first time you double-click on a log entry in the SS_Log_Window.exe application, it will ask you if you want to install the add-in.
To integrate the log into your projects, follow these three steps:
- Place the SS_Log_Include.h and SS_Log.h files in your project’s include directory.
- Add the SS_LogD.lib to your debug project, and add SS_Log.lib to your release project. (Project->Settings->"Link" tab-> Object/Library Modules).
#include "SS_Log_Include.h" in any file you would like to produce logs from (do not include SS_Log.h).
If you would like, you can add the SS_Log_Include.cpp, SS_Log.cpp, and SS_RegistryKey.cpp files to your project instead of adding SS_Log.lib and SS_LogD.lib as in step 2.
We will talk about 2 different log types:
- the global log, and
- local logs.
"SS_Log_Include.h" in your project, a global log is created for you (assuming you don't have
_SS_LOG_INACTIVE #defined). To use the global log, all you have to do is enter a line as follows:
Log("some message here, %d, %s", nValue, szText, ... );
This line will use the global log’s defaults, sending the message to the global log with a "CRITICAL" level. The message goes to a Log Window in debug builds and to the file \SS_Log.log during release builds by default. To override the global log’s defaults, use these calls:
LogFilter( DWORD dwFilter );
LogRemoveFilters( DWORD dwFilter );
LogAddFilters( DWORD dwFilter );
LogFilename( TCHAR* szFilename );
LogWindowName( TCHAR* szWindowName );
Log( TCHAR* pMessage, ... );
Log( DWORD dwFilter, TCHAR* pMessage, ... );
When the above global log functions are used, all messages in all threads will go to the same place (specified file, specified log window, or both). If you want to send some messages to a different location, you may create multiple simultaneous logs by defining "local logs" with the following code:
The following calls can be used for local logs:
myLog.Filter( DWORD dwFilter );
myLog.RemoveFilters( DWORD dwFilter );
myLog.AddFilters( DWORD dwFilter );
myLog.Filename( TCHAR* szFilename );
myLog.WindowName( TCHAR* szWindowName );
Log( SS_Log* pLog, TCHAR* pMessage, ... );
Log( SS_Log* pLog, DWORD dwFilter, TCHAR* pMessage, ... );
Note that if you don't call the
myLog.WindowName(...) functions, the local log will default to the same window and file that the global log defaults to, and hence, all messages from both logs will go to the same file/window. An example for a complete local log would be:
myLog.WindowName("My Local Log");
Log( &myLog, "message to local log here, %d, %s", nValue1, szText1 );
Log( "message to global log here, %s, %s", szText2, szText3 );
Log(...) call above will send its message to the local log, while the second one will send its message to the global log.
Filters are assigned to each log individually by default, but each log’s default values can be overridden. In addition, every message sent to a log can contain filters that override the log’s default filters. You may then turn each filter type "on" and "off" by setting registry values (created automatically by the
SS_Log class). Any message with a filter type that is turned off in the registry will not get sent to the log. This allows you to turn on and off the filters without re-compiling.
(Registry key: "HKEY_CURRENT_USER\Software\SS_Log". For all values, 1=on and 0=off. You can also set the default log file here.)
You set log filters with the
LogFilter(...) global calls. The following filters are pre-defined (the code includes instructions for adding your own filter types):
- Builds -- These are handled automatically. You typically won't need to change the Builds filters unless you want messages to be sent *only* in debug (or release) builds.
- Outputs -- These determine where the messages go during run-time. By default, debug builds send messages to Log Windows, and release builds send messages to a file (\SS_Log.log). You can have messages go to both a window and a file simultaneously, or you can specify that messages always go to the Log Window (or a file) as opposed being dependant on the build type.
- Levels -- You may specify one or more of these types (though I can't really think of why you'd specify more than one for any given message). By default, all messages get the
LOGTYPE_CRITICAL level. Note that unlike the Builds and Outputs types, the Levels types “combine”. That is, if a log or individual message has the
LOGTYPE_WARNING and the
LOGTYPE_TRACE types specified, then BOTH types must be turned on in the registry for the message to be sent.
LogRemoveFilters(...) calls work as you'd expect, the
LogFilter(...) call does not. The
LogFilter(...) does not "set" a log’s filter exactly as specified in the parameter list. Instead, the function is "smart" and tries to figure out what type of filter (Builds, Outputs, Levels, or some combination thereof) the user is trying to set. For example, if the user calls
LogFilter( LOGTYPE_TRACE ), the function will set only the Levels type, ignoring the Builds and Outputs types currently set in the filter. Specifically, the function would remove the
LOGTYPE_CRITICAL filter (which was there by default), add the
LOGTYPE_TRACE filter, and leave the Builds and Outputs types alone. Note the difference if the user had called
LogAddFilters( LOGTYPE_TRACE ) instead, where the function would have added the
LOGTYPE_TRACE filter and kept the
For more examples and detail, see the example project "SS_Log_Test".
A few problems that should be mentioned:
- Log Windows and log files are both appended by default. This means that over time, if a program is writing to some log file every time it runs, the file could grow quite large. You should call
LogEraseLog() at the beginning of every program in order to ensure that your files remain small. I will consider adding a set-able max file size if enough people request it.
_SS_LOG_INACTIVE will remove any code that accesses the global log from your project. However, it does not remove 100% of the code used by the local logs. The actual logging (file writing and windowing) gets removed, but the local logs will still create some variables on your stack even when
_SS_LOG_INACTIVE is defined.
- Typically, you will not need to call the three global log functions that have no parameters (i.e.
LogWindowName()). These functions return the associated value that the global log currently holds. Just be aware that if you use these three functions in your code, they will cause a compile error if
_SS_LOG_INACTIVE is ever
- Many file-writing functions (streams) will accept information to be written to a file but wait to write out to the file until a "convenient" time, which ensures fluid program flow. However, if the program crashes before the stream "flushes" its contents to the file, some information may not get written. In the case of our log messages, some final messages could get lost. This is a problem, so in order to be completely sure that all log messages get written out to their destinations before a program crash occurs, we tell the stream to flush after every message call. This, however, is inefficient use of system resources, and can slow your program’s execution substantially. I have no way around this problem. Furthermore, currently the registry is called to determine which filters are on and off for every message sent. This is also inefficient (and easily fix-able), but I've chosen to leave it this way so that the filters can be turned on and off during run-time (no need to restart the program). Just be aware of this fact: The
SS_Log logging class can significantly reduce your program’s performance (speed). The up-side is, you should regain 100% of your program’s efficiency by
"_SS_LOG_INACTIVE" for the pre-compiler (Note: this works 100% on the global log, but the code for the local logs is not 100% removed. The actual logging of messages is removed, but some local log objects and variables will still be created when
_SS_LOG_INACTIVE is defined).
- If you have multiple instances of MSDEV open, you might notice some problems with the jump-to-code feature of the SS_Log_Window.exe application. Specifically, double-clicking on a log entry might make one instance of MSDEV become the active window, but a different instance will be the one to open the file associated with the log entry. This is annoying to say the least. The rule is this... the first instance of MSDEV that is opened will always be the instance to open the associated code. However, the instance of MSDEV that is highest in the z-order (the one that was most recently the active window) will be the one that jumps to the front of the screen. This will probably work out to your satisfaction in most cases. In the future, if enough people request it, I may try to fix this problem.
Try it out for yourself... by the end of this 11 step tutorial, you will have seen several of the
SS_Log class' features, and just how quick and easy it is to integrate into your projects.
- First create a new project. Let's make an MFC-based SDI project named "Test". Go ahead to do that using the new project MFC AppWizard (select single document application).
- Give the project access to the SS_Log.h and SS_Log_Include.h headers simply by dumping those two files into the new Test project's main directory (Installation / Integration, Step 1).
- Place the SS_Log.lib and SS_LogD.lib into that same directory, and then add them to the project by selecting Project -> Settings -> Link -> Object/Library Modules. In that field, add SS_LogD.lib to the debug project and SS_Log.lib to the release project. (Installation / Integration, Step 2).
- Add the
OnPaint() method to the
CTestView class with the class wizard (press CTRL-W, under "class name", select "
CTestView", then double-click on the "
WM_PAINT" entry under "messages").
- In the testView.cpp file, add
#include "SS_Log_Include.h" to the top of the file, and add
Log("We are painting now!!"); and
Log(LOGTYPE_NORMAL, "We are painting now!!"); in the
CTestView::OnPaint() function. The
Log() calls each send a log message, the first with a "Critical" level (by default), and the second with a "Normal" level (overriding the default);
- Compile the debug version and run the program.
- When the program runs, two windows should pop up instead of just one. This is because the log file launched a Log Window to send the message you put in the
OnPaint function. Drag the "Untitled - test" window around on the screen (making sure that you make a portion of it move off the sides of the screen) and it will send numerous messages to the Log Window, two for each time the
OnPaint function is called.
- Open Regedit and go to "HKEY_CURRENT_USER\Software\SS_Log". Set the "Filter - Status Critical" value to "0". Now drag the "Untitled - test" window around again. You will see that only the "Normal" level messages are sent now, because we are filtering out the Critical ones.
- Now close the Log Window and move the "Untitled - test" window around again. It will open another Log Window since none are currently available.
- Close both windows and compile the release version of the program. Run it.
- No Log Window pops up for release version compiles (by default), but a log file is written to your hard drive. Look in your root directory for the SS_Log.log file and open it. It will contain the messages (it will contain only "Normal" level messages unless you already set the "Filter - Status Critical" registry entry back to "1").
This tutorial showed a few of the features found in the
SS_Log class. Experiment will local logs and different combinations of filters.
Steve Schaneville email@example.com