Introduction▲
Comme vu dans mon précédent article, log4net est un framework qui propose tout un ensemble de fonctionnalités pour gérer les logs de vos applications. Log4net propose une série d'appenders permettant d'enregistrer vos logs sous différents formats (fichier texte, mail, insertion en base de données ...). Malgré le grand nombre d'appenders existants, il se peut que vous ne trouviez pas celui qui correspond exactement à vos besoins. Pour cela, il suffit de développer son propre appender. Cet article vous montrera comment faire à travers deux exemples concrets. Le code source des exemples (C#, Visual Studio 2008) est disponible à la fin de ce document.
I. Un peu de théorie▲
I-A. La classe LoggingEvent▲
Cette classe est la représentation interne de tout événement à logger. Elle expose des propriétés telles que le nom du logger, le niveau de l'événement, le message à logger .
I-B. Les appenders▲
Tous les appenders dérivent de la classe abstraite AppenderSkeleton, qui implémente entre autres l'interface IAppender.
Cette interface déclare la méthode DoAppend qui prend en paramètre un LoggingEvent :
void
DoAppend
(
LoggingEvent loggingEvent);
Cette méthode est très importante pour nous car c'est elle qui est chargée de vérifier si l'événement doit être loggé (c'est-à-dire que les différents filtres ont été passés) et qui, le cas échéant, logge l'événement suivant l'appender utilisé.
Pour être plus précis, la fonction DoAppend vérifie les filtres puis appelle une fonction Append qui prend en paramètre un LoggingEvent :
abstract
protected
void
Append
(
LoggingEvent loggingEvent);
Chaque appender, héritant forcément de la classe AppenderSkeleton, devra donc implémenter cette méthode abstraite. C'est cette méthode qui loggera l'événement suivant l'appender utilisé. C'est donc sur l'implémentation de cette méthode qui nous porterons nos efforts lors du développement de notre propre appender.
Certains appenders sont bufferisés (ils dérivent de la classe abstract BufferingAppenderSkeleton, qui elle-même dérive de la classe abstraite AppenderSkeleton). Cela signifie que tant que la taille des messages à logger n'aura pas atteint la taille définie dans le fichier de configuration, alors ces messages seront ajoutés dans un buffer. Quand la taille définie aura été atteinte, tous les messages seront loggés d'un coup.
Pour ces appenders, c'est la fonction SendBuffer qui est chargé de logger les événements :
abstract
protected
void
SendBuffer
(
LoggingEvent[]
events);
Quant à elle, la fonction Append est chargée d'ajouter les événements au buffer et d'appeler la fonction SendBuffer quand le buffer atteint la taille limite.
I-C. Les propriétés des Appenders▲
Chaque appender a ses propres propriétés. Par exemple, l'appender SmtpAppender possède entre autres les propriétés From, To, Subject. On définit, si on le souahite, la valeur de ces propriétés dans le fichier de configuration de log4net. Pour le SmtpAppender, la configuration ressemble à :
<to
value
=
"destinataire@mail.com"
/>
<from
value
=
"expediteur@mail.com"
/>
<subject
value
=
"Alerte, tout a pété"
/>
Ces propriétés étant définies dans le code source de l'appender, les éléments xml associés porteront donc le même nom.
public
string
To
{
get
{
return
m_to;
}
set
{
m_to =
value
;
}
}
public
string
From
{
get
{
return
m_from;
}
set
{
m_from =
value
;
}
}
public
string
Subject
{
get
{
return
m_subject;
}
set
{
m_subject =
value
;
}
}
Log4net utilise la réflexion pour associer une valeur aux propriétés de l'appender à partir des éléments xml du fichier de configuration.
Pour ceux que cela intéresse, la fonction en charge de cette tâche est : log4net.Repository.Hierarchy.XmlHierarchyConfigurator.SetParameter
Ainsi, si vous voulez définir une propriété lors du développement de votre appender, il suffira de déclarer la propriété et de créer le noeud xml associé dans le fichier de configuration :
public
int
MyProperty
{
get
{
return
m_myproperty;
}
set
{
m_myproperty =
value
;
}
}
<myProperty
value
=
"12"
/>
Vous savez maintenant tout ce qu'il faut pour développer votre propre appender.
Nous allons maintenant mettre cela en pratique à travers différents exemples.
II. Un RollingFileAppender amélioré▲
Le RollingFileAppender permet de logger dans un fichier texte. Par rapport au FileAppender, il permet également de renommer le fichier contenant les logs suivant certains critères comme la date ou la taille du fichier. Lorsqu'on choisit de renommer suivant la date, il faut définir un datePattern. Prenons par exemple datePattern = "yyyyMMdd". Cela signifie que le 1er jour, les logs seront écrits dans log.txt (en supposant que la propriété File soit définie à log.txt). Au deuxième jour, le fichier log.txt de la veille sera renommé en log.txt.20090216 (pour une date du jour étant 17/02/2009).
Ce fonctionnement est très pratique pour avoir un fichier de log par jour. Par contre, la façon dont le fichier est renommé l'est beaucoup moi. De un, Windows ne reconnait pas l'extension et nous demande à chaque avec quel programme l'on souhaite ouvrir le fichier. De deux, cela ne permet pas de faire un tri alphabétique sur les fichiers. Il serait beaucoup plus pratique d'avoir un fichier log20090216.txt ou 20090216log.txt. Pour rendre le nom du fichier encore un plus lisible, on pourrait souhaiter une séparation entre la date et le nom du fichier, comme par exemple 20090216_log.txt.
C'est ce que je vous propose de réaliser dans la suite de ce chapitre.
Regardons dans un 1er temps le fonctionnement par défaut du RollingFileAppender.
Comme on peut le voir par exemple dans la fonction GetNextOutputFileName (ligne 605 de la classe RollingFileAppender), log4net se contente de rajouter la date formatée à la fin du nom du fichier :
fileName =
fileName +
m_now.
ToString
(
m_datePattern,
System.
Globalization.
DateTimeFormatInfo.
InvariantInfo);
m_datePattern correspond au formatage de la date que l'on a défini dans le fichier de configuration de log4net.
Pour notre appender customisé, l'idée est de repérer tous les endroits dans le code où log4net modifie le nom du fichier de log et de les adapter à notre besoin.
Commençons le développement à proprement dit de notre appender.
Après un parcours rapide de la classe RollingFileAppender, on remarque que log4net modifie le nom du fichier à plusieurs endroits, dont certains à l'intérieur de méthodes private. Il ne sera donc pas possible d'hériter de cette classe. La solution la plus simple est alors de reprendre intégralement le code de la classe RollingFileAppender et de le modifier.
Pour cela, il suffit de créer une classe nommée CustomRollingFileAppender par exemple et d'y copier le code intégral de la classe RollingFileAppender. On va ensuite y ajouter une fonction qui sera chargée de renommer le fichier de log afin de centraliser cette fonctionnalité.
De quoi a besoin cette fonction ?
- De la façon dont on va renommer le fichier
- De la chaîne de caractères qui fera office de séparation entre le nom du fichier et la date formatée.
En ce qui concerne la façon dont on va renommer le fichier, on peut imaginer 3 choix possibles :
- Celui par défaut => log.txt20090216
- En plaçant la date en premier => 20090216log.txt
- En plaçant la date avant l'extension => log20090216.txt
On peut donc créer une énumération pour ces 3 choix possibles :
public
enum
RenamingMode
{
Start =
0
,
BeforeExtension =
1
,
End =
2
}
Il faut maintenant déclarer une propriété afin de pouvoir spécifier dans le fichier de configuration la façon dont on souhaite renommer le fichier de log. Comme vu , cela donne :
private
RenamingMode m_renamingStyle =
RenamingMode.
End;
public
RenamingMode RenamingStyle
{
get
{
return
m_renamingStyle;
}
set
{
m_renamingStyle =
value
;
}
}
Et pour le fichier de configuration:
<renamingStyle
value
=
"BeforeExtension"
/>
On fait la même chose pour le séparateur :
private
string
m_separator =
string
.
Empty;
public
string
Separator
{
get
{
return
m_separator;
}
set
{
m_separator =
value
;
}
}
<separator
value
=
"_"
/>
Occupons nous maintenant de la fonction qui sera chargée de renommer le fichier de log. Cela donne:
private
string
GetRenamedName
(
string
fileName,
DateTime time)
{
switch
(
m_renamingStyle)
{
case
RenamingMode.
Start:
return
fileName.
Substring
(
0
,
fileName.
LastIndexOf
(
"
\\
"
) +
1
)
+
time.
ToString
(
m_datePattern,
DateTimeFormatInfo.
InvariantInfo)
+
m_separator +
fileName.
Remove
(
0
,
fileName.
LastIndexOf
(
"
\\
"
) +
1
);
case
RenamingMode.
End:
return
fileName +
m_separator +
time.
ToString
(
m_datePattern,
DateTimeFormatInfo.
InvariantInfo);
case
RenamingMode.
BeforeExtension:
return
fileName.
Substring
(
0
,
fileName.
LastIndexOf
(
"."
))
+
m_separator +
time.
ToString
(
m_datePattern,
DateTimeFormatInfo.
InvariantInfo)
+
fileName.
Remove
(
0
,
fileName.
LastIndexOf
(
"."
));
}
return
fileName;
}
L'étape suivante est de modifier le code de la classe afin d'appeler cette méthode à chaque fois que cela est nécessaire. Cette étape n'étant pas très intéressante, je vous passe le détail. Le code source est disponible à la fin de l'article.
La dernière étape est de paramétrer log4net afin d'utiliser notre appender customisé. Cela se fait très simplement via le fichier de configuration en modifiant légèrement la configuration classique d'un RollingFileAppender. La seule modification à apporter (en plus des nouvelles propriétés) est de spécifier le nouveau type de l'appender :
<appender
name
=
"CustomRollingFile"
type
=
"DVP.log4net.Appender.CustomRollingFileAppender, DVP.log4net"
>
<file
value
=
"Log.txt"
/>
<threshold
value
=
"INFO"
/>
<appendToFile
value
=
"true"
/>
<rollingStyle
value
=
"Date"
/>
<datePattern
value
=
"yyyyMMdd-HHmm"
/>
<separator
value
=
"_"
/>
<renamingStyle
value
=
"BeforeExtension"
/>
<layout
type
=
"log4net.Layout.PatternLayout"
>
<conversionPattern
value
=
"*%-10level %-30date %-25logger %message %newline"
/>
</layout>
</appender>
Une telle configuration générera les fichiers suivants :
Alors que l'appender RollingFileAppender par défaut aurait généré les fichiers suivants:
Le résultat répond ainsi aux besoins exprimés.
III. Un SmtpAppender amélioré▲
Le SmtpAppender permet de logger un événement en envoyant un mail.
Quelles sont les améliorations que l'on pourrait apporter ?
Tout d'abord, le sujet du mail envoyé est fixe. Il est défini en dur dans le fichier de configuration. Il serait intéressant de pouvoir le rendre dynamique, comme le message loggé.
Ensuite, le mail est envoyé au format texte. Avoir la possibilité d'envoyer un mail au format HTML permettrait d'avoir des possibilités de mise en page plus importantes.
Pour finir, il serait intéressant de pouvoir joindre un ou plusieurs fichiers au mail.
Nous allons mettre tout cela en place dans notre SmtpAppender amélioré. Contrairement au RollingFileAppender amélioré, nous allons cette fois-ci dériver la classe SmtpAppender.
III-A. Un sujet dynamique▲
Comme dit précédemment, le sujet du mail est défini en dur dans le fichier de configuration de log4net. Cela est assez limitant et il serait par exemple utile d'avoir le niveau du log dans le sujet du mail.
Cela est très facilement faisable en utilisant un Layout plutôt qu'une simple chaîne de caractères pour le sujet du mail. La modification se fait à 2 niveaux : au niveau de l'appender lui-même et au niveau du fichier de configuration.
Au niveau de l'appender, on définit une propriété SubjectLayout de type ILayout :
public
ILayout LayoutSubject {
get
;
set
;
}
Au niveau du fichier de configuration:
<
layoutSubject type=
"log4net.Layout.PatternLayout,log4net"
>
<
conversionPattern value
=
"[%level] Log from my app"
/>
</
layoutSubject>
Avec une telle configuration, le mail reçu lors d'une erreur aura le sujet suivant : [ERROR] Log from my app.
Au moment de l'envoi du mail, il reste à formater le sujet :
var
subjectWriter =
new
StringWriter
(
System.
Globalization.
CultureInfo.
InvariantCulture);
// On formate le sujet
LayoutSubject.
Format
(
subjectWriter,
loggingEvent);
Notez le fait que la propriété au niveau de l'appender porte le même nom que le noeud XML au niveau du fichier de configuration comme expliqué précédemment.
Remarquez aussi que je n'ai pas nommé la propriété « subject » comme c'est le cas pour le SmtpAppender par défaut. En effet, l'appender SmtpAppender possède une propriété « subject » de type chaîne de caractères. Le fait de la surcharger provoque l'erreur suivante lors du chargement de l'appender :
System.Reflection.AmbiguousMatchException : Correspondance ambiguë trouvée
III-B. Un mail au format HTML▲
Pour cela, on va juste ajouter une propriété « IsHtmlFormat» de type booléen.
Au niveau de l'appender :
public
bool
IsHtmlFormat {
get
;
set
;
}
Au niveau du fichier de configuration:
<isHtmlFormat
value
=
"true"
/>
Il reste à spécifier le format du mail lors de sa création. Cela se fait via la propriété IsBodyHtml de l'objet MailMessage.
var
mailMessage =
new
MailMessage
{
Body =
messageBody,
From =
new
MailAddress
(
From),
Subject =
messageSubject,
Priority =
Priority,
IsBodyHtml =
IsHtmlFormat
};
Dernier petit détail : lors du formatage du corps du mail, si on utilise le format HTML, il faut remplacer les sauts de ligne par la chaîne de caractères « <br/> ».
if
(
IsHtmlFormat)
{
messageBody =
messageBody.
Replace
(
Environment.
NewLine,
"<br/>"
);
}
III-C. Joindre un ou plusieurs fichiers▲
L'idée est de définir une propriété qui spécifiera les fichiers à joindre au mail. Le nom des fichiers à joindre seront stockés dans une chaîne de caractères, séparés par un ' ;'. Pour cela, on va créer une propriété « Attachment ».
Au niveau de l'appender :
public string Attachment { get; set; }
Au niveau du fichier de configuration:
<attachment
value
=
"aFile.txt;C:\Temp\anOtherFile.txt"
/>
Il reste maintenant à joindre les fichiers au mail. Pour chaque fichier :
- On regarde si le chemin défini dans le fichier de config est absolu ou non.
- Si non, on récupère le chemin absolu (en différenciant le cas d'une application web avec une application non web)
- On attache le fichier au mail
// on ajoute la ou les pièce(s) jointe(s)
if
(!
string
.
IsNullOrEmpty
(
Attachment))
{
var
attachments =
Attachment.
Split
(
';'
);
foreach
(
var
attach in
attachments)
{
if
(!
string
.
IsNullOrEmpty
(
attach.
Trim
(
)))
{
var
path =
attach.
Trim
(
);
try
{
var
context =
HttpContext.
Current;
if
(
context ==
null
) //context non web
{
//on teste s'il s'agit d'un chemin absolu ou non
if
(!
Path.
IsPathRooted
(
path))
{
path =
Directory.
GetCurrentDirectory
(
) +
"
\\
"
+
path;
}
if
(
File.
Exists
(
path))
{
//si le fichier spécifié existe bien, on l'ajoute en pièce jointe au mail
AttachFile
(
mailMessage,
path);
}
else
{
// Sinon, on écrit dans le corps du mail que le fichier n'a pas été trouvé
mailMessage.
Body +=
IsHtmlFormat ?
"<br/>"
:
Environment.
NewLine;
mailMessage.
Body +=
IsHtmlFormat ?
"<br/>"
:
Environment.
NewLine;
mailMessage.
Body +=
"Fichier non trouvé: "
+
attach;
}
}
else
//context web
{
//on teste s'il s'agit d'un chemin absolu ou non
if
(!
Path.
IsPathRooted
(
path))
{
path =
context.
Server.
MapPath
(
path);
}
if
(
File.
Exists
(
path))
{
//si le fichier spécifié existe bien, on l'ajoute en pièce jointe au mail
AttachFile
(
mailMessage,
path);
}
else
{
// Sinon, on écrit dans le corps du mail que le fichier n'a pas été trouvé
mailMessage.
Body +=
IsHtmlFormat ?
"<br/>"
:
Environment.
NewLine;
mailMessage.
Body +=
IsHtmlFormat ?
"<br/>"
:
Environment.
NewLine;
mailMessage.
Body +=
"Fichier non trouvé: "
+
attach;
}
}
}
catch
(
Exception ex)
{
// En cas d'erreur, on précise dans le corps du mail que le fichier n'a pu être joint
mailMessage.
Body +=
IsHtmlFormat ?
"<br/>"
:
Environment.
NewLine;
mailMessage.
Body +=
IsHtmlFormat ?
"<br/>"
:
Environment.
NewLine;
mailMessage.
Body +=
string
.
Format
(
"<b>Erreur lors de l'ajout de la piece jointe: {0}</b>"
,
attach);
// On logge l'erreur avec le logger interne de log4net
LogLog.
Error
(
string
.
Format
(
"Erreur lors de la pièe jointe: {0} (path: {1})"
,
attach,
path),
ex);
}
}
}
}
Veuillez noter l'utilisation de l'instruction LogLog.Error dans le catch. LogLog est une classe sealed du namespace Log4net.Util. Elle permet de logger les erreurs de log4net lorsque les logs internes sont activés (plus de détails ici).
La fonction pour joindre le fichier au mail est :
private
void
AttachFile
(
MailMessage mailMessage,
string
path)
{
var
stream =
new
FileStream
(
path,
FileMode.
Open,
FileAccess.
Read,
FileShare.
ReadWrite);
mailMessage.
Attachments.
Add
(
new
Attachment
(
stream,
Path.
GetFileName
(
Attachment)));
}
III-D. Dernier petit détail▲
L'appender SmtpAppender est un appender bufferisé. Cela signifie que l'on peut définir une taille minimum pour le message à envoyer. Tant que la taille du message est inférieure à la taille définie, les messages à logger sont mis bout à bout.
Dans notre cas, étant donné que l'on a créé un sujet dynamique, cette notion de buffer perd son intérêt. Imaginons le cas suivant : on a défini le sujet du mail de façon à y intégrer le niveau de l'événement à logger. Un certain nombre d'erreurs se produisent sans pour autant dépasser la taille du buffer. Suite à cela, un événement de niveau INFO se produit et fait dépasser la taille du buffer. Le mail est alors envoyé avec le niveau INFO dans le sujet alors que la plupart des événements contenus dans le corps du mail sont des événements de type ERROR. Pour cette raison, on va désactiver la partie bufferisée de l'appender SmtpAppender.
Pour cela, et contrairement à un appender bufférisé classique, on loggera l'événement dans la fonction Append et non dans la fonction SendBuffer.
Pour terminer, voilà un exemple de fichier de configuration pour utiliser cet appender :
<appender
name
=
"CustomSmtpAppender"
type
=
"DVP.log4net.Appender.NonBufferedSmtpAppenderWithSubjectLayoutAndAttachment, DVP.log4net"
>
<to
value
=
"destinataire@mail.com"
/>
<from
value
=
"expediteur@mail.com"
/>
<layoutSubject
type
=
"log4net.Layout.PatternLayout,log4net"
>
<conversionPattern
value
=
"[%level] Log from WebApp"
/>
</layoutSubject>
<smtpHost
value
=
"smtp.free.fr"
/>
<threshold
value
=
"DEBUG"
/>
<attachment
value
=
"Log/Log.txt"
/>
<isHtmlFormat
value
=
"true"
/>
<layout
type
=
"log4net.Layout.PatternLayout,log4net"
>
<conversionPattern
value
=
"LEVEL: %level %newlineDATE: %date %newlineLOGGER: %logger %newline%newline%message"
/>
</layout>
</appender>
Conclusion▲
Comme vous avez pu le constater, la création d'appenders personnalisés est très facile. Vous pouvez soit créer votre appender en partant de zéro, soit hériter d'un appender existant et modifier une partie de son comportement. Cela vous permet de customiser log4net à l'infini afin de l'adapter au moindre de vos besoins.
Liens▲
Remerciements▲
Je remercie l'équipe Dotnet pour leurs relectures attentives du document.