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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 à :

 
Sélectionnez
<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.

 
Sélectionnez
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 :

 
Sélectionnez
public int MyProperty 
{
	get { return m_myproperty; }
	set { m_myproperty = value; }
}
 
Sélectionnez
<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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
private RenamingMode m_renamingStyle = RenamingMode.End;
public RenamingMode RenamingStyle
{
	get { return m_renamingStyle;  }
	set { m_renamingStyle = value;  }
}

Et pour le fichier de configuration:

 
Sélectionnez
<renamingStyle value="BeforeExtension" />

On fait la même chose pour le séparateur :

 
Sélectionnez
private string m_separator = string.Empty;
public string Separator
{
	get { return m_separator; }
	set { m_separator = value; }
}
 
Sélectionnez
<separator value="_"/>

Occupons nous maintenant de la fonction qui sera chargée de renommer le fichier de log. Cela donne:

 
Sélectionnez
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 :

 
Sélectionnez
  <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 :

Image non disponible

Alors que l'appender RollingFileAppender par défaut aurait généré les fichiers suivants:

Image non disponible

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 :

 
Sélectionnez
public ILayout LayoutSubject { get; set; }

Au niveau du fichier de configuration:

 
Sélectionnez
<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 :

 
Sélectionnez
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 :

 
Sélectionnez
public bool IsHtmlFormat { get; set; }

Au niveau du fichier de configuration:

 
Sélectionnez
<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.

 
Sélectionnez
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/> ».

 
Sélectionnez
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:

 
Sélectionnez
<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
 
Sélectionnez
// 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 :

 
Sélectionnez
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 :

 
Sélectionnez
<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.

2 commentaires Donner une note à l'article (4.5)

Liens

Remerciements

Je remercie l'équipe Dotnet pour leurs relectures attentives du document.