I. Aperçu général du logger▲
Notre logger va être découpé en 3 parties distinces.
Premièrement nous allons trouver les loggers atomiques. Ce sont de petites
classes très spécialisées qui ont pour unique but de récupérer un message,
et de l'inscrire quelque part (fichier, console, réseau, ...).
Ensuite, nous trouverons une interface, commune à chacun des petites modules
de log.
Dernièrement, nous aurons un conteneur pour tous ces loggers différents,
qui centralisera l'arrivée des messages, et fera un dispatch vers les
loggers atomiques.
Voici un aperçu de ce que nous souhaitons pouvoir faire:
LOG( "Hello from "
<<
'C'
<<
'+'
<<
'+'
<<
" logger release"
<<
1
<<
'.'
<<
0
)
Dernière chose tant que nous y sommes, il serait pratique que les messages
de log puissent être desactivés (lors d'une génération release par exemple).
Nous utiliserons pour celà une constante préprocesseur NLOG (messages
desactivés).
Si vous utilisez Make/GCC, il vous suffit pour celà de rajouter une ligne à votre makefile, par exemple :
CC=
g++
CFLAGS=
-c -Wall
LOG=
NLOG
all:
hello
hello:
hello.o
$(CC)
hello.o -o hello -D$(LOG)
clean:
rm -rf *o hello
Si vous utilisez Visual Studio, vous n'avez qu'à vous rendre dans les propriétés de votre projet sous l'onglet C++/Préprocesseur et d'ajouter NLOG ou LOG
II. Les loggers atomiques▲
L'intéret d'un système de log basé sur de multiples loggers
atomiques, est que ces derniers soient particulièrement simples
à implémenter. Si ce n'était le cas, alors autant créer un
logger monolithique.
Nous tacherons donc de minimiser au maximum les dépendances dans
nos loggers atomiques.
II-A. Une interface commune▲
Commençons par définir une interface qui sera commune à tous
nos loggers atomiques.
Typiquement, tout ce qu'elle a à faire, c'est de fournir une
méthode write pour écrire du texte.
#ifndef LOGGER_HH
#define LOGGER_HH
#include
<string>
class
Logger
{
public
:
virtual
~
Logger ( void
) {}
virtual
void
write( const
std::
string&
) =
0
;
}
;
#endif
// LOGGER_HH
C'est vraiment tout ce que nous avons à faire pour notre interface, tout le reste du travail, sera d'implémenter write pour ses classes filles.
II-B. Exemples de loggers atomiques▲
Nous allons tenter d'illustrer un peu la création de loggers atomiques, en implémentant deux petits modules de sortie pour la console et pour un fichier.
Le plus trivial est bien entendu la console, voici ce que celà donne:
#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
Essayons maintenant de fournir un logger atomique un peu plus évolué
prenant en charge l'écriture des messages dans un fichier de log.
L'implémentation donnée ici est spécifique à Windows, mais la place est
laissée pour une éventuelle version Linux et MacOSX.
Créez un fichier platform.hh contenant par exemple:
#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
Tant que nous sommes dans les classes utilitaires, nous pouvons donc ajouter un fichier time.hh contenant:
#ifndef TIME_HH
#define TIME_HH
#include
<string>
#include
"platform.hh"
#ifdef PLATFORM_WIN32
# include
<atltime.h>
#elif PLATFORM_LINUX
//TODO: gérer linux
#elif PLATFORM_MACOSX
//TODO: gérer 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
Nous avons donc maintenant tous les outils pour pouvoir créer un logger atomique chargé d'enregistrer les messages dans un fichier!
#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
Encore une fois, rien d'exceptionnel dans ce logger, le but étant de démontrer leur facilité de création. Il va maintenant falloir s'attaquer à la grosse partie: le conteneur de loggers atomiques.
III. Le conteneur de loggers▲
Maintenant que nous avons nos loggers, il va nous falloir une classe pour tous les contenir, et pour dispatcher les messages. Nous la sommerons LoggerHolder. Mais avant de rentrer directement dans le code, nous allons devoir définir quelques classes utilitaires (1) .
III-A. Classes utilitaires▲
La première chose dont nous aurons besoin, c'est d'un moyen de stocker des listes de types. Nous allons donc définir un type TypeList, ainsi que divers macros nous permettant de les construire simplement.
Pour les habitués des langages fonctionnels (LISP en particulier), il n'y aura rien de sorcier ici. Une liste est en fait une classe prenant deux paramètres templates, enveloppée dans une récursion s'arrêtant sur un type arbitrairement nommé NullType.
class
NullType {}
;
template
<
class
H, class
T>
class
TypeList
{
typedef
H Head;
typedef
T Tail;
}
;
Quelques macros pour en faciliter la création ne seront pas de trop:
#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)>
Et voilà, nous pouvons maintenant très simplement créer des listes de types. Voici un simple exemple d'utilisation:
TYPELIST_4( char
, int
, float
, double
)
III-B. Le LoggerHolder▲
Nous pouvons (enfin) débuter la création du conteneur
de loggers. Ce dernier prendra en paramètres une liste
de type: les loggers atomiques à utiliser.
Voici un premier jet:
#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
Reste la question principale: Comment itérer à travers nos loggers atomiques, et appeler leur méthode write sur le paramètre message de l'operateur << ?
Pour celà, l'idéal serait de disposer d'une classe de liste
(ressemblant à std::list) qui stockerait nos loggers. Il
ne resterait plus qu'à la parcourir et appelant write sur
chaque élément.
Lançons-nous donc dans sa création ! Pour plus de propreté,
nous avons choisi de déclarer LoggerList comme une
classe interne à LoggerHolder.
Avant de nous attaquer à LoggerList, nous pouvons déjà implémenter l'opérateur << du LoggerHolder, maintenant que nous savons comment seront stockés nos loggers atomiques:
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
;
}
}
;
Maintenant, attaquons LoggerList.
Nous l'avons déjà évoqué, la métaprogrammation se
rapproche souvent de la programmation fonctionnelle. Ceci
est en parti du aux structures de données qu'on y retrouve
(essentiellement des listes), et à l'usage intensif de la
récursivité.
Imaginez que vous vouliez parcourir une liste en Scheme ( dialecte de LISP), vous procéderiez comme suit:
(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)))) )))
Nous allons donc procéder de manière analogue. Dans une récursion en métaprogrammation, les cas d'arrêts sont des spécialisation partielles. Si l'on veut créer un traitement spécifique pour la fin d'une liste de types, il suffit donc de spécialiser partiellement la classe avec NullType.
Voici donc la déclaration générale de notre liste de loggers:
template
<
class
T>
struct
LoggerList : public
std::
list<
Logger*>
{}
;
Le cas d'arrêt de notre récursion, le voici:
template
<>
struct
LoggerList<
NullType>
: public
std::
list<
Logger*>{}
;
Il ne reste plus qu'à faire le cas général. A savoir
que dans le constructeur, nous créerons un nouveau logger,
et nous fusionnerons avec une LoggerList templatée avec
les arguments template de celle courante, sauf le premier.
Le destructeur quand à lui se contentera bien sur de détruire
le contenu de notre liste.
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;
}
}
;
C'est terminé. Il ne reste plus qu'à utiliser.
IV. Le mode debug▲
Grace à notre constante préprocesseur définie plus haut, nous pouvons englober les appels au LoggerHolder dans une macro, et ne rien faire lorsque le mode sans-messages est activé.
#ifdef NLOG
# define LOG(msg)
#else
# define LOG(msg) (*LoggerHolder<TYPELIST_2( LoggerConsole, LoggerFile )>::getInstance()) << msg;
#endif
V. Critiques▲
L'intéret pratique d'un logger presque entièrement statique comme celui-ci n'est au final pas indispensable. C'est en revanche un bon exercice d'introduction à la métaprogrammation, et celà permet de définir une batterie de types de loggers prets à l'emploi, sans avoir à s'embeter en les plugant dynamiquement sur un LoggerHolder.