Introduction
Sometimes it is useful and necessary to find out what is going on during the execution of
a program but it is not always feasible to use the debugger for tracking down defects. Instead
we can put log comments inside the source code, to retrieve useful information about the inner
workings of the code. The classes we have created, are designed to be easy to use and making
the log comments easy to switch on and off at will.
A Log Comment Example
We will implement a test harness that has a few log comments which will be written
to a predefined log file. Then we will show how to accomplish this by creating the logging class.
Note that this is only tested on MS Windows XP.
Main method
The listing below, testharness.cpp, consists of a few header file includes and two log comments. Notice how the Log.h is placed
after the constants.h file, which contains the define macro that switches on or off the logging. By putting the define macro in a separate
file, it will be propagated to all involved header files. If a source code file needs logging even if the macro is not defined, it is possible to
customise that one by adding a define macro in that particular file.
The log file will contain two entries in this example. Each entry consists of the file name it was placed in,
the line number and of course the log text itself, see listing (testharness.cpp).
#include <tchar.h>
#include "constants.h"
#include <Log.h>
int _tmain(int argc, _TCHAR* argv[])
{
_Log("FirstLogTest");
_Log("SecondLogTest");
return 0;
}
The Constants Header File
The only line of code in constants.h (see listing below) is the macro:
#define LOGLOG. As mentioned before, this macro can be commented out to
prevent any logging.
The Log Class Header File
Below is the listing with the header file (Log.h). The idea is to include this header in every source
file that should have log comments, as mentioned in the testharness.cpp above. Notice that if the
LOGLOG macro is not defined, the preprocessor will not insert any code in the source files. The benefit
is that we do not need to do error prone cut and pasting every time we would like to include logging.
#pragma once
#include <string>
namespace Logging
{
class Log
{
public:
Log(std::string aLogDirectory, std::string aLogFileName) throw ();
~Log(void);
void Output(std::string aFileNameStr,
int aLineNumber,
std::string aLogText);
private:
FILE* iFile;
bool iOutputAvailable;
};
}
#ifdef LOGLOG
extern Logging::Log logObj;
#define _Log(x) logObj.Output(__FILE__, __LINE__, x);
#else
#define _Log(x) // do nothing...
#endif
The Log Source Code
There is a global object in the source code. This is initialised when the program starts running.
The benefit from this is that we can start putting log comments in the source code direct, without
having to think about objects and initialisations. This is one of the criterias to make this kind
of log commenting easy and useful to have. There is basically no need for understanding what the code
does, it is just to include the header file and write the log comments. Thats all!
The constant strings that points to the directory and file name are put in the source code because
this class was intended to be a statically linked library. Obviously, the directory and/or file name
can be customised to point at any desired directory and file.
The method that writes the text to the log file is placed in Log::Output(). This can also be customised
for personalised output.
#include "Log.h"
#include "Utils.h"
#include <iostream>
#include <exception>
const std::string KLogDirectory = "C:\\Temp\\Log\\";
const std::string KLogFileName = "LogFile.txt";
Logging::Log logObj(KLogDirectory, KLogFileName);
namespace Logging
{
Log::Log(std::string aLogDirectory,
std::string aLogFileName) throw ()
: iFile(NULL), iOutputAvailable(true)
{
try{
std::string fullPath = aLogDirectory + aLogFileName;
Utils::CreateDirectory(aLogDirectory);
Utils::OpenFileForAppending(fullPath, iFile);
}
catch (std::string err)
{
std::cout << err.c_str() << std::endl;
iOutputAvailable = false;
}
}
Log::~Log(void)
{
_fcloseall();
}
void Log::Output(std::string aFileStr,
int aLineNo,
std::string aLogText)
{
if (iOutputAvailable)
{
std::string strOut = aFileStr + " : " + Utils::IntToStr(aLineNo);
strOut += "\n";
strOut += aLogText;
strOut += "\n";
strOut += "------------------------------- \n";
fputs(strOut.c_str(), iFile);
fflush(iFile);
}
}
}
The Helper Code - Header File
We have put the code that is not directly involved in the logging
as a separate class. This makes the code easier to understand and maintain.
We have used the c runtime library to perform all the utility handling.
#pragma once
#pragma warning( disable : 4290 )
#include <string>
#include <stdio.h>
class Utils
{
public:
static void CreateDirectory(std::string aDirPath) throw(std::string);
static void OpenFileForAppending(std::string aFileName,
FILE*& aOutFile) throw (std::string);
static std::string IntToStr(int aInt);
};
The Helper Code - Source File
The implementation takes advantage of std::strings rich and safe api.
We have also added some simple exception handling as the file system
may not always behave as expected.
#include "Utils.h"
#include <direct.h>
#include <errno.h>
void Utils::CreateDirectory(std::string aDirPath) throw(std::string)
{
if (_mkdir(aDirPath.c_str()) != 0)
{
errno_t err;
_get_errno(&err);
if (err != EEXIST)
{
std::string errStr =
"Error: cannot create directory: " + aDirPath;
throw errStr;
}
}
}
void Utils::OpenFileForAppending(std::string aFileName, FILE*& aOutFile)
throw (std::string)
{
aOutFile = _fsopen(aFileName.c_str(), "a+", _SH_DENYNO);
if (aOutFile == NULL)
{
std::string err = "Error: cannot open: " + aFileName;
throw err;
}
}
std::string Utils::IntToStr(int aInt)
{
char c[20];
_itoa_s(aInt, c, sizeof(c), 10);
std::string s(c);
return s;
}
|