I. Overview of the logging system▲
The logger will be split in three parts
First of all, we will find the atomic loggers. These are small specialized classes
which aims at catching a message and writing it somewhere else (terminal, file,
network...)
Next, we will find a common interface for every atomic logger.
Last, but not least, we will create a holder for all atomic loggers
that will have to catch every logging message, and dispatch it to
each atomic logger.
Here is a quick look at what it should look like:
LOG( "Hello from "
<<
'C'
<<
'+'
<<
'+'
<<
" logger release"
<<
1
<<
'.'
<<
0
)
For convenience, we will provide a specific preprocessor constant that will enable or disable messages logging (release builds). It will be called NLOG (no log).
If you plan to use Make/GCC, the only thing you will have to do is add a single line on top of your makefile, and pass it to GCC using the -D flag (i.e. defining a preprocessor constant) :
CC=
g++
CFLAGS=
-c -Wall
LOG=
NLOG
all:
hello
hello:
hello.o
$(CC)
hello.o -o hello -D$(LOG)
clean:
rm -rf *o hello
If you are using Visual Studio, head to your project properties, then C++/Préprocesseur and add NLOG:
II. Atomic loggers▲
The major purpose of creating a logging system based on
modularity, is that the atomic loggers should
be very easy to implement.
We have to keep this in mind when designing the logger
interface.
II-A. The common interface▲
We are going to design the common logger interface.
Basically, the only thing it has to provide, is a method
taking a string (the message to log), and write it somewhere.
That is what write() does.
#ifndef LOGGER_HH
#define LOGGER_HH
#include
<string>
class
Logger
{
public
:
virtual
~
Logger ( void
) {}
virtual
void
write( const
std::
string&
) =
0
;
}
;
#endif
// LOGGER_HH
This is really all we have to do for the logger interface. The real work will we achieved by the atomic loggers themthelves.
II-B. Atomic logger examples▲
We are going to illustrate the creation of atomic loggers, by implementing two small of them for terminal and file output.
The most obvious is the terminal. All it have to do is redirect the message to the standard output stream.
#ifndef LOGGERCONSOLE_HH
#define LOGGERCONSOLE_HH
#include
<string>
#include
<iostream>
#include
"logger.hh"
class
LoggerConsole : public
Logger
{
public
:
virtual
void
write( const
std::
string&
msg )
{
std::
cout <<
msg <<
std::
endl;
}
}
;
#endif
// LOGGERCONSOLE_HH
Now we are going to create a little more advanced logger, aiming at
writing messages on files.
The following implementation is Windows-dependent, but the use of
preprocessor constants helps a lot for an hypothetical port on
Linux or MacOSX.
Start by creating a file named platform.hh containing:
#if defined linux || defined __linux__ || defined __linux
# define PLATFORM_LINUX
# define PLATFORM_NAME
"Linux"
#endif
// Plateforme Windows
#if defined _WIN32 || defined WIN32 || defined __NT__ || defined __WIN32__
# define PLATFORM_WIN32
# define PLATFORM_NAME
"Windows"
#endif
// Plateforme MacOS X
#if ( defined __MWERKS__ && defined __powerc && !defined macintosh ) || defined __APPLE_CC__ || defined macosx
# define PLATFORM_MACOSX
# define PLATFORM_NAME
"MacOS X"
#endif
Now we can create some other helpers such as a class for handling time and date:
#ifndef TIME_HH
#define TIME_HH
#include
<string>
#include
"platform.hh"
#ifdef PLATFORM_WIN32
# include
<atltime.h>
#elif PLATFORM_LINUX
//TODO: handling linux
#elif PLATFORM_MACOSX
//TODO: handling macosx
#endif
class
Time
{
public
:
static
const
std::
string getTime()
{
std::
ostringstream oss;
#ifdef PLATFORM_WIN32
CTime time =
CTime::
GetCurrentTime();
oss <<
time.GetHour()
<<
":"
<<
time.GetMinute()
<<
":"
<<
time.GetSecond();
#else
//TODO: Linux and MacOS X
#endif
return
oss.str();
}
static
const
std::
string getDate()
{
std::
ostringstream oss;
#ifdef YASHMUP_PLATFORM_WIN32
CTime time =
CTime::
GetCurrentTime();
oss <<
time.GetDay()
<<
"/"
<<
time.GetMonth()
<<
"/"
<<
time.GetYear();
#else
//TODO: Linux and MacOS X
#endif
return
oss.str();
}
}
;
#endif
// TIME_HH
We should now have all the necessary tools for implementing our file logger.
#ifndef LOGGERFILE_HH
#define LOGGERFILE_HH
#include
<string>
#include
<fstream>
#include
"time.hh"
#include
"logger.hh"
class
LoggerFile : public
Logger
{
public
:
LoggerFile()
{
file_.open( "output.log"
, std::ios::
app );
file_.seekp( std::ios::
beg );
if
(!
file_.good()) return
;
file_ <<
" ===============================================
\n
"
<<
" Begin Output log ( "
<<
Time::
getDate()
<<
" at "
<<
Time::
getTime()
<<
" ): "
<<
PLATFORM_NAME
<<
"
\n
===============================================
\n\n
"
;
file_.flush();
}
virtual
~
LoggerFile()
{
if
(!
file_.good()) return
;
file_ <<
"
\n
===============================================
\n
"
<<
" End Output log ( "
<<
Time::
getDate()
<<
" at "
<<
Time::
getTime()
<<
" ): "
<<
PLATFORM_NAME
<<
"
\n
===============================================
\n\n
"
;
file_.flush();
file_.close();
}
virtual
void
write( const
std::
string&
msg )
{
file_ <<
msg;
file_.flush();
}
private
:
std::
ofstream file_;;
}
;
#endif
// LOGGERFILE_HH
Nothing uncommon about this class, the source is self explanatory. We can now foxus on the core of the logging system: the logger holder.
III. The logger holder▲
Now that we defined a logger interface, and that we have implemented
somme dummy atomic loggers, the next step is to create the class
that will contain all those loggers, and provide a centralized way to
log messages and dispatch them to loggers.
Before heading deeply in the core of LoggerHolder, we wille have to
create some helpers.
(1)
.
III-A. Helper classes▲
The first thing we will need is a way to store lists of type. So first of all we are going to define a TypeList class, along with some userful macros for declaring typelists.
Fonctionnal languages litterate readers will find this section rather obvious. A typelist is just a structure containing 2 classes, which might be wrapped by another, and so on. The list ends with the arbitrary defined class NullType.
class
NullType {}
;
template
<
class
H, class
T>
class
TypeList
{
typedef
H Head;
typedef
T Tail;
}
;
We are now able to define some helpful macros:
#define TYPELIST_1(T1)\
TypeList<T1, NullType>
#define TYPELIST_2(T1, T2)\
TypeList<T1, TYPELIST_1(T2)>
#define TYPELIST_3(T1, T2, T3)\
TypeList<T1, TYPELIST_2(T2, T3)>
#define TYPELIST_4(T1, T2, T3, T4)\
TypeList<T1, TYPELIST_3(T2, T3, T4)>
Following is a short example of defining a new typelist of four type: char, int, float, double.
TYPELIST_4( char
, int
, float
, double
)
III-B. The LoggerHolder▲
At last, we can start the creation of the LoggerHolder.
It will take a typelist as template parameter:
the atomic loggers to use.
Here is a first draft:
#ifndef LOGGERHOLDER_HH
#define LOGGERHOLDER_HH
#include
<list>
#include
<string>
#include
<sstream>
#include
"logger.h"
template
<
class
T>
class
LoggerHolder
{
private
:
public
:
LoggerHolder( void
) {}
~
LoggerHolder( void
) {}
template
<
class
U>
LoggerHolder<
T>&
operator
<<
( const
U&
message )
{
//TODO: Dispatcher les messages
return
*
this
;
}
}
;
#endif
// LOGGERHOLDER_HH
The major issue remains "how to iterate over our logger list and call the proper write method on each of them, from LoggerHolder's operator << ?
Ideally, we would have a container (such as a std::list)
that would store our loggers. Then we would only have to
iterate over it and call write.
We will call this
hypothetical class LoggerList.
We don't have to create LoggerList immediatly. In fact we are able to implement LoggerHolder's operator << since we know LoggerList will inherit from std::list.
template
<
class
T>
class
LoggerHolder
{
private
:
LoggerList<
T>
loggers_;
public
:
LoggerHolder( void
) {}
~
LoggerHolder( void
) {}
template
<
class
U>
LoggerHolder<
T>&
operator
<<
( const
U&
message )
{
std::
ostringstream oss;
oss <<
message;
for
( LoggerList<
T>
::
iterator it=
loggers_.begin();
it!=
loggers_.end();
++
it )
{
(**
it).write( oss.str() );
}
return
*
this
;
}
}
;
Now, it's time to focus on LoggerList.
As we said earlier, metaprogramming is close to functionnal
programming. This is mostly due to the heavy use of
recursivity, along with simple structures such as lists.
Imagine the same problem in Scheme (LISP dialect), here is what would be a typical solution:
(define
NullType () )
(define
LoggerConsole "LoggerConsole"
)
(define
LoggerFile "LoggerFile"
)
(define
TypeList
(lambda
(h d)
(cons
h d) ))
(define
TYPELIST_2 (TypeList LoggerConsole (TypeList LoggerFile NullType)))
(define
head
(lambda
(x)
(car
x) ))
(define
tail
(lambda
(x)
(cdr
x) ))
(define
list
.merge
(lambda
(list1 list2)
(cond
((equal?
NullType list2) list1)
(else
(list
.merge (TypeList (head list2) list1) (tail list2))) )))
(define
logger->write
(lambda
(logger)
(begin
(display
"Writing to "
) (display
logger) (newline
)) ))
(define
loggerholder->write
(lambda
(loggers)
(cond
((equal?
NullType (tail loggers)) (logger->write (head loggers)))
(else
(begin
(logger->write (head loggers)) (loggerholder->write (tail loggers)))) )))
We will proceed the same manner.
When metaprogramming, the stop case of our recursion
will be a partial template specialization. If we want to
do a particuliar action for the last element, we will
have to create a specialization based on NullType.
So here is the global LoggerList definition:
template
<
class
T>
struct
LoggerList : public
std::
list<
Logger*>
{}
;
And this will be our stop case:
template
<>
struct
LoggerList<
NullType>
: public
std::
list<
Logger*>{}
;
All we have to do now is the general case. The constructor
will have to push the head type in the list, and merge
it with the resulting list created with the tail of
the typelist.
The destructor will ... destruct the logger.
template
<
class
H, class
T>
struct
LoggerList<
TypeList<
typename
H, typename
T>
>
:
public
std::
list<
Logger*>
{
typedef
TypeList<
typename
H, typename
T>
List_t;
typedef
typename
H Head_t;
typedef
typename
T Tail_t;
LoggerList()
{
push_back( new
Head_t );
LoggerList<
Tail_t>
tmp;
merge( tmp );
}
~
LoggerList()
{
LoggerList<
List_t>
::
iterator it;
for
( it=
begin(); it!=
end(); ++
it )
delete
*
it;
}
}
;
That's all for the LoggerList, now all you have to do is test it.
IV. Debug mode▲
Since we defined a preprocessor constant earlier, we can wrap each call to LoggerHolder's operator<< in a macro, that would be defined to nothing if log is disabled.
#ifdef NLOG
# define LOG(msg)
#else
# define LOG(msg) (*LoggerHolder<TYPELIST_2( LoggerConsole, LoggerFile )>::getInstance()) << msg;
#endif
V. Criticisms▲
Whereas the overall concept of a static logger is quite attractive,
the practical benefits are insignificant compared to a dynamic one.
The major benifit resides in the capability to define pool of different
loggers that one wouldn't have to dynamically plug.
This article should in fact be considered as an introduction to metaprogramming
using C++.