Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Very easy to use logger for ATL/MFC/NonMFC applications

0.00/5 (No votes)
2 Jan 2004 1  
Very easy to use logger for ATL/MFC/NonMFC applications.

Sample Image - EASYLogger.jpg

Introduction

There are many programmers who want to know what is behind their running application. For example: you successfully build you debug version, but in the release version something goes wrong and your application crashes. Now, you ask yourself: "What is wrong with my code?". The answer to this question, you can find yourself by doing some logging. You can do that by simply dumping your objects of interest into a file. This is not such a good idea because you must rewrite the same code over and over again. Another problem would be, for example, thread safety: 2 instances of the same application want to log on the same file or some sort of "one user" media. This is where my logger comes into place.

The logger itself

The idea is taken from Java. There you have a class named java.util.logging.Logger. It is an extremely powerful logging system. So the concepts copied from Java are:

  • Possibility for logging same message on multiple locations: file, database, socket, window, etc. My implementation contains file (console or file on disk), database (ANY database with standard SQL syntax) and window (a dialog will appear on your app start)
  • Multiple logging levels. Those are:
    • SEVERE is a message level indicating a serious failure. In general, SEVERE messages should describe events that are of considerable importance and which will prevent normal program execution. They should be reasonably intelligible to end users and to system administrators.
    • WARNING is a message level indicating a potential problem. In general, WARNING messages should describe events that will be of interest to end users or system managers, or which indicate potential problems.
    • INFO is a message level for informational messages. Typically, INFO messages will be written to the console or its equivalent. So the INFO level should only be used for reasonably significant messages that will make sense to end users and system admins.
    • CONFIG is a message level for static configuration messages. CONFIG messages are intended to provide a variety of static configuration information, to assist in debugging problems that may be associated with particular configurations.

      For example, CONFIG messages might include the CPU type, the graphics depth, the GUI look-and-feel, etc.

    • FINE is a message level providing tracing information. All of FINE, FINER and FINEST are intended for relatively detailed tracing. The exact meaning of the three levels will vary between subsystems, but in general, FINEST should be used for the most voluminous detailed output, FINER for somewhat less detailed output and FINE for the lowest volume (and most important) messages.

      In general, the FINE level should be used for information that will be broadly interesting to developers who do not have a specialized interest in the specific subsystem. FINE messages might include things like minor (recoverable) failures. Issues indicating potential performance problems are also worth logging as FINE.

    • FINER indicates a fairly detailed tracing message. By default, logging calls for entering, returning or throwing an exception are traced at this level.
    • FINEST indicates a highly detailed tracing message.
  • Possibility for logging or not logging a message. This choice is made based on the message logging level. For example, if we want to log only the INFO, WARNING and SEVERE messages, we will set the logger level of logging to INFO. Any message with greater or equal level will be logged. Any other message will be discarded.
  • Possibility for not logging at all. Just set LEVEL_OFF to the logger.
  • The logger is system wide (unique per system). Some of the advantages are:
    • All your applications use the same logger. This is for saving memory. The logger is an out-of-proc server. That means that the logger and the client application don't share the same memory space. This helps a lot when you make, by mistake, some buffer overrun in your app. The application terminates in the worst case but the logger remains.
    • Safe threading on the same logging media: file, window, database, etc.
  • Possibility to chose which application log on which logging media. For example: App1.exe (first instance)->log1.log; App2.exe->log1.log and log2.log; App3.exe->all available logging media; App1.exe (second instance)->log3.log and/or log1.log.
  • Each logging location has a separate logging level.
  • Possibility for removing or adding new logging locations without recompiling the application. You can even remove or add new logging locations while the client application is running.
  • Setting the level of logging for each logging location while the client application is running.
  • Double click on the logging window to go to the file and line at the specified message.
  • Completely remove all logging code from your sources with a simple define (but not recommended) I recommend you to preserve the USE_LOGGER define and don't add any logging locations if you don't want to log anything. Later, if you want to do some logging, you can add logging locations using the the visual interface of the logger, without recompiling your application. In this situation, you must start the logger first (don't do it if the logger is already active), add your logging locations and than start your application. If you start your application before starting the logger, the logger will start automatically, but first messages from your application will be lost because you don't have any logging locations defined (this situation will be soon solved by implementing save methods into the logger).

Prepare the project

  1. In the zip archive, you have one directory named ready_for_use. Copy it wherever you like best.
  2. Create a project (any type you want: ATL/MFC/Win32 Console etc)
  3. Add LoggerWrapper.h and LoggerWrapper.cpp files to your project and be sure that your project can access the files from that directory (see the picture below). If you don't have a stdafx.h file in your project, comment the #include "stdafx.h" from the top of the file LoggerWrapper.cpp. If you do, leave it there.
  4. Define _WIN32_DCOM (see the picture below).
  5. Add #include "LoggerWrapper.h" in stdafx.h from your project. If you don't have one, make sure that you add the #include line in the top most header from your project.
  6. Add the logging location as described below at your application initialization. For a MFC application, this point is located at the top of the InitInstance() function.
  7. Just do the logging as described below:

Sample screenshot

How to use the logger

As explained before, the logger is system wide. So every time you try to instantiate a logger, the server will give you the same instance. That means that any changes made by one application to the logger will impact all other applications that use the logger. This is very good but also bad. The good part is that you can modify the logging style of an application from another application. The bad part is that you can do that accidentally.

The logger can be used both visually and programmatically at the same time. Next, I will explain in detail, the programmatically used interface exposed by the logger (only 3 functions).

Functions for adding logging locations:

  1. int AddNewWindowLogLocation(BOOL customizeVisualy, DWORD ownerProcessID, LEVEL loggingLevel, DWORD* locationID, char* windowTitle,unsigned long int maxRecords, BOOL alwaysOnTop, BOOL autoScroll);

    Adds a window logging location. The logged messages will be displayed in a window; Double click on a record to go to file/line in MSDEV.

    Returns non-zero on success, 0 on fail;

    • customizeVisualy - If non-zero, tells the logger that this handler will be customized visually. All the remaining parameters (except locationID) will be discarded. If zero, the rest of the parameters will be taken into consideration as follows:
    • ownerProcessID - The PID of the process that will use this logging location. Can be any PID (process ID) in the system or -1. When -1 is specified, the location will be used by all the applications that do logs (running and future running). When an individual PID is specified (!=-1), only that PID will have access to this location. All other incoming logging requests will be discarded. Tip: Use GetCurrentProcessID() function;
    • loggingLevel - The level of logging on this location. Read the intro part for details;
    • locationID - This is the place where the function puts the ID of the newly created logging location. You can use this ID in changing the properties of the logging location (e.g.: level of logging, remove the logging location, etc.);
    • windowTitle - The title of the new logging window;
    • maxRecords - The maximum number of records in the window. If this number is reached, the list of present records will be deleted automatically to make space for the next incoming messages.
    • alwaysOnTop - If non-zero, the window logging location will be always on top. (Can't change this after the window has been created. This problem will be solved as soon as possible.)
    • autoScroll - If non-zero, the window will scroll itself. Pressing left mouse button will disable auto scrolling temporarily. Pressing right mouse button will enable the auto scroll again. Without this flag, left and right mouse clicks will do nothing.
  2. int AddNewFileLogLocation(BOOL customizeVisualy, DWORD ownerProcessID, LEVEL loggingLevel, DWORD* locationID, char *fileName, long maxSize, int allowRead, int allowWrite, int rotate);

    Adds a new file location. The incoming log messages will be saved in a file;

    • customizeVisualy, ownerProcessID, loggingLevel, locationID - see AdddNewWindowLogLocation
    • fileName - The name of the file where the logged messages will be saved. Do NOT provide any extension. *_%d.log is appended automatically. %d (a number) - see maxSize;
    • maxSize - The maximum size of the file. If the file grows bigger than this value, a new file will be created or the file will be overwritten. (See rotate for details);
    • allowRead - Reserved for now (will be implemented!!!);
    • allowWrite - Reserved for now (will be implemented!!!);
    • rotate - If non-zero, when the file wants to grow larger then the maxSize, the file is truncated to zero. If rotate==0, a new file will be created and a contour number will be appended to the end of the file. See fileName parameter;
  3. int AddNewDatabaseLogLocation(BOOL customizeVisualy, DWORD ownerProcessID, LEVEL loggingLevel, DWORD* locationID, char* connectingString, char* tableName, char* levelCol, char* timeCol, char* sequenceNumberCol, char* sourceFileCol, char* sourceLineCol, char* threadIdCol, char* processIdCol, char* messageCol);

    Adds a new database location. The incoming log messages will be stored into a database. I strongly recommend you to use the visual configuration at least once for correctly determining the connecting string. Just copy-paste the connecting string if you don't want to use the visual interface.

    • customizeVisualy, ownerProcessID, loggingLevel, locationID - see AdddNewWindowLogLocation.
    • connectingString - Connection string used to connect to the database. This string is very different from one database engine to another. If this string is incorrect, the logging location will not be appended to the logger. Use the visual interface to build the connecting string and copy-paste it if you want;
    • tableName - The name of the table in the database selected in the connection string, used for storing log messages;
    • levelCol - The name of the level column. This column will hold the level of the message to be stored. Must be a text column. Must be at least 32 characters (bytes) long.
    • timeCol - The name of the time column. This column will hold the time stamp of the message to be stored. Must be a date and time column. Must be complete date and time: year, month, day, hour, minute and second, in any order.
    • sequenceNumberCol - The name of the sequence number column. This column will hold the order of the messages to be stored. This order is relative to the logging location, not to the table. One or more database locations can log into the same table, so you can have records with the same sequenceNumber but from different logging locations. Must be a numeric column. Must be at least 4 bytes long;
    • sourceFileCol - The name of the source file column. This column will hold the source code file path of the messages to be stored. See __FILE__ in MSDN. Must be a text column. Must be at least 512 characters (bytes) long;
    • sourceLineCol - The name of the source line column. This column will hold the source code line number of the messages to be stored. See __LINE__ in MSDN. Must be a numeric column. Must be at least 4 bytes long;
    • threadIdCol - The name of the thread ID column. This column will hold the source thread of the messages to be stored. See GetCurrentThreadID() in MSDN. Must be a numeric column. Must be at least 4 bytes long;
    • processIdCol - The name of the process ID column. This column will hold the source process of the messages to be stored. See GetCurrentProcessID() in MSDN. Must be a numeric column. Must be at least 4 bytes long;
    • messageCol - The name of the message column. This column will hold the message itself. Must be a text column. Must be at least 65536 bytes long. If your database engine doesn't permit such a large text column, you are free to modify it, but you MUST also modify #define MAX_MESSAGELENGTH in LoggerWrapper.h (read the antet of the file for more info)

Now let's see some examples of adding logging locations:

/*Will add a database logging location using the visual 
method (All parameters ignored)*/
DWORD locationID1;
AddNewDatabaseLogLocation(TRUE, 0, LEVEL_ALL, 
    &locationID1, NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL, NULL, NULL);
/*Will add a database logging location programatically. 
YOU MUST CHANGE THE CONNECTING STRING!!!*/
DWORD locationID2;
AddNewDatabaseLogLocation(FALSE, -1, LEVEL_ALL, &locationID2,
    "Provider=MSDASQL.1;Persist Security Info=False;Data Source=MySQLTest",
    "logrecords", "levelCol", "timeCol", "sequenceNumberCol",
    "sourceFileCol", "sourceLineCol", "threadIdCol", "processIdCol",
    "messageCol");

/*Will add a new window logging location 
using the visual method (All parameters ignored)*/
DWORD locationID3;
AddNewWindowLogLocation(TRUE, 0, LEVEL_ALL, 
    &locationID3, NULL, 0, TRUE, TRUE);

/*Will add a new window logging location programatically.*/
DWORD locationID4;
AddNewWindowLogLocation(FALSE, -1, LEVEL_CONFIG, &locationID4,
    "Logging handler added programmatically", 1024, FALSE, FALSE);

/*Will add a new file logging location using 
the visual method (All parameters ignored)*/
DWORD locationID5;
AddNewFileLogLocation(TRUE,0,LEVEL_ALL, &locationID5, NULL,0,0,0,0);

/*Will add a new file logging location programatically.*/
DWORD locationID6;
AddNewFileLogLocation(FALSE,-1,LEVEL_ALL, 
    &locationID6, "./testlog",1024*1024,1,1,0);

Now we do the actual logging using some defines te make the job easy. PLEASE OBSERVE the double '(' and ')'. These defines are:

  • FINEST_LOG(());
  • FINER_LOG(());
  • FINE_LOG(());
  • CONFIG_LOG(());
  • INFO_LOG(());
  • WARNING_LOG(());
  • SEVERE_LOG(());

These macros are printf like macros. So you can do for example:

INFO_LOG(( "finest log test message: number: %d, text: '%s', double: %f",
    1234,
    "a simple text",
    12.9987 ));/*or*/INFO_LOG(("CMDIFrameWnd::OnCreate"));
if (CMDIFrameWnd::OnCreate(lpCreateStruct) == -1)
{
    SEVERE_LOG(("OnCreate function failed!!!"));
    return -1;
}

Visually using the logger

Using the logger visually is easy like killing a bunny rabbit with an axe (Carmagedon rulz!!!). I think that you alone can understand what is happening. I must remind you that the LoggerAddIn.dll file from ready_to_use directory is a MSDEV add-in. Just activate it: Tools->Customize, Add-Ins and macros tab -> and browse for the LoggerAddIn.dll. Once activated, if you double click in a window logging location and you have MSDEV open, the logger takes you to the file/line corresponding to the log message.

TODO List:

  • I\You must make the Getters and Setters for various operations on the logger. For example: Set-Get the logging level programmatically or Get the list of current handlers or remove a handler programmatically, or, or, or. All this is done by sending a few Windows messages to the LoggerMonitor from within. Bottom line: all visual interfaces must be programmatically exposed.
  • Put the debug window from MSDEV as a new logging location.
  • Any good idea that you have in your mind, please share it with me.

Updates #1

  • Now all the 3 functions for adding logging locations take an extra parameter named locationID. You can use this ID in operations like Set/Get level of logging.
  • Added Set/Get logging level programmatically.
  • Fixed some issues regarding creating modeless dialog from a foreign thread (not the thread who destroys the dialog).

EOF

Please excuse my bad English. If you have any observations regarding this problem, don't hesitate to contact me in order to correct the problems. Now I'm waiting for some feedback from you.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here