Comment logger efficacement dans l’Event Viewer Windows avec log4net ?

La réponse est toute simple, il suffit d’un logger, et d’un fichier de config log4net. config de ce style ci :

<log4net>
  <appender name="eventlogAppender" type="log4net.Appender.EventLogAppender">
    <applicationName value="MySource" />
    <mapping>
      <level value="ERROR" />
      <eventLogEntryType value="Error" />
    </mapping>
    <mapping>
      <level value="FATAL" />
      <eventLogEntryType value="Error" />
    </mapping>
    <mapping>
      <level value="DEBUG" />
      <eventLogEntryType value="Information" />
    </mapping>
    <mapping>
      <level value="INFO" />
      <eventLogEntryType value="Information" />
    </mapping>
    <mapping>
      <level value="WARN" />
      <eventLogEntryType value="Warning" />
    </mapping>
    <logName value="Application" />
    <layout type="log4net.Layout.PatternLayout">
      <conversionPattern value="%message" />
    </layout>
    <securityContext type="log4net.Util.WindowsSecurityContext">
      <credentials value="Process" />
    </securityContext>
  </appender>
  <logger name="MyLogger">
    <level value="ALL"/>
    <appender-ref ref="eventlogAppender" />
  </logger>
</log4net>

Ensuite, il suffira d’une petite ligne de ce style dans son code, pour logger facilement un message d’erreur :

public void ExempleErrorFunction(string message, Exception ex)
{
    var logger = LogManager.GetLogger("MyLogger");
    if (logger.IsErrorEnabled)
    {
        logger.Error(message, ex);
    }
}

Avec ces simples lignes, vous êtes capables de logger une erreur dans l’Event Log Windows.

Maintenant, regardons plus en détails comment ceci fonctionne :

Mon logger « myLogger » définit l’appender utilisé (ici eventLogAppender), ainsi que le niveau de détail loggé.

Dans cet exemple, on récupère la totalité des messages pour les transmettre à l’appender.

L’appender, quant à lui, définit de quelle manière sera loggé le message.

Je lui donne comme paramètre

– son type (log4net. Appender. EventLogAppender correspondant à une écriture dans l’event log windows)

– son application name (ici mySource) correspondant à la source de l’événement loggé dans l’event viewer

– Le pattern d’écriture du message dans la balise layout (ici, on écrit tout simplement le message envoyé 😉

– Son contexte d’exécution de sécurité (dans securityContext)

– Le nom du fichier de log (ici Application) dans logName

– Et finalement tous les mappings des types de messages de log vers les types de l’event viewer (moins nombreux).

Puis, mon code C# récupère le logger en question et écrit un message d’erreur avec celui-ci.

On remarque que le test logger. IsErrorEnabled vérifie que mon logger a bien le droit d’écriture en erreur.

Dans cet exemple, il n’est pas particulièrement utile, mais il peut le devenir, par exemple, si vous créez une méthode ExempleErrorFunctionFormat ( … . , params object[] args), auquel cas, vous pouvez utiliser ce test pour ne pas réaliser le String. Format lorsque la configuration du logger ne permet pas d’écrire dans le fichier.

Si j’avais décidé de modifier mon niveau de log dans mon fichier de configuration vers <level value=”FATAL”/>, le test aurait retourné « false », et rien n’aurait été loggé.

Ceci peut arriver assez souvent, un administrateur pouvant modifier la valeur du logger à tout moment ( log4net permet d’initialiser le logger avec un FileWatcher permettant de prendre en compte les modifications du fichier de configuration pendant l’execution).

Configuration Avancée

L’histoire se complique lorsque l’on souhaite spécifier également les catégories de ces messages.

En effet, non seulement le logger ne propose pas de moyen direct et codé d’envoyer une catégorie, mais en plus, celle-ci est stockée sous forme de short, et non de chaine de caractères.

Il nous faut donc changer notre code de Log pour insérer une catégorie, et trouver un moyen simple et élégant d’effectuer un mapping short => string dans notre code.

Une enum semble toute indiquée.

De plus, en décompilant la dll log4net (ou en cherchant sur divers forums) comment ajouter une catégorie, on voit qu’il suffit de setter « category » dans le dictionnaire de propriétés du contexte courant pour que celle ci soit envoyée a l’event Viewer.

Pour ne pas avoir d’effet de bord sur d’éventuels autres logger de mon application, j’utiliserai le LogicalThreadContext.

On obtient donc le code suivant :

public enum LogCategories{
           Cat1 = 1,
           Cat2 = 2,
           Cat3 = 3
        }

public void ExempleDebugFunction(string message, Exception ex, LogCategories category )
{
     var logger = LogManager.GetLogger("myLogger");
     if (logger.IsDebugEnabled)
     {
         LogicalThreadContext.Properties["category"] = (short)category;
         logger.Debug(message, ex);
         LogicalThreadContext.Properties["category"] = null;
     }
}

Maintenant, on a les catégories qui s’affichent sous forme de chiffre entre parenthèses dans la colonne “catégorie”.

C’est un bon début.

Voyons comment transformer ce nombre en chaîne de caractères ?

Pour ce faire, il nous faudra créer un fichier pour les catégories ET un fichier pour les event Ids .

En effet, même si vous ne souhaitez pas utiliser les eventIds (une autre information présente dans l’event viewer), ne pas surcharger le fichier proposé par défaut dans la base de registre fera ignorer votre fichier de catégories sur les versions récentes de Windows Server (pour ce que j’ai testé, c’est au moins vrai pour 2008 R2).

Les fichiers ont l’extension . mc, et peuvent être créés à partir d’un éditeur de texte.

Je les ai créés ainsi dans mon exemple :

Categories. mc :

MessageId=0x1
Language=English
Cat 1
.
 
MessageId=0x2
Language=English
Cat 2
.
 
MessageId=0x3
Language=English
Cat 3
.

EventMessages. mc :

MessageId=1000
SymbolicName=SOME_ERROR_OR_OTHER
Language=English
%1
.
 
MessageId=0
SymbolicName=MESSAGE_ZERO
Language=English
%1
.
 
MessageId=+1
SymbolicName=MESSAGE
Language=English
%1
.

Dans les grandes lignes, on précise le short correspondant à l’ID de la catégorie derrière MessageId, puis la langue utilisée, puis le texte de la catégorie.

Finalement, on termine sa déclaration par un point sur la ligne suivante.

Dans les eventMessages même principe, avec comme subtilité que +1 signifie l’id suivant après la ligne proposée précédemment, et qu’il y a un symbolicName, correspondant à une constante symbolique C/C++ associé au message et que le message est composé uniquement de %1, ce qui signifie « colle le 1er paramètre », en l’occurrence notre message.

Pour un formatage plus complexe, vous pouvez préciser un ParameterMessageFile, qui vous premettra de définir plus de paramètres pour vos messages.

Pour plus de détails sur les fichiers . mc, allez directement voir la msdn:

Ensuite, on va compiler les . mc en dlls, et les ajouter au registre.

Pour cela, un petit batch qui build les fichiers dont j’ai besoin.

@echo off
set installfolder=C:\le Chemin vers mon dossier ou se trouve mes fichiers Mc
 
cd "C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin" 
 
mc "%installfolder%\Categories.mc" -v
 
move /Y ".\MSG00001.bin" "%installfolder%"  
move /Y ".\Categories.h" "%installfolder%\"
move /Y ".\Categories.rc" "%installfolder%\" 
 
rc /r "%installfolder%\Categories.rc"
 
echo dummy > dummy.txt
al /win32res:"%installfolder%\Categories.res" /t:lib /out:"%installfolder%\Categories.dll" /embed:dummy.txt,dummy
del dummy.txt
 
mc "%installfolder%\EventMessages.mc" -v
 
move /Y ".\MSG00001.bin" "%installfolder%\"
move /Y ".\EventMessages.h" "%installfolder%\"
move /Y ".\EventMessages.rc" "%installfolder%\"
 
rc /r "%installfolder%\EventMessages.rc"
 
echo dummy > dummy.txt
al /win32res:"%installfolder%\EventMessages.res" /t:lib /out:"%installfolder%\EventMessages.dll" /embed:dummy.txt,dummy
del dummy.txt

Ce que fait mon script :

Il se place dans le dossier des binaires du sdk windows, compile les fichiers de messages (*. mc) à l’aide de MC. exe, déplace les résultats dans mon dossier de départ (de manière à ne pas polluer le dossier des sdk), puis AL. EXE prend mes fichiers de ressources compilés pour en faire une assembly.

Par contre, AL. exe demande à ce qu’on utilise le switch /embed ou /link, et non uniquement /win32res.

Pour ce faire, on lui donne une fausse ressource .Net, le fichier dummy. txt.

Attention : ce script modifie les fichiers . dlls que vous auriez éventuellement créés plus tôt. Si ceux ci sont déjà liés à des évènements de l’event viewer et que vous effectuez une mise à jour, veillez à bien fermer l’event viewer windows, ou son execution faillira.

Finalement il ne reste plus qu’à ajouter ces deux ressources au registre. Pour ce faire vous pouvez :

– Créer les entrées à la main.

– Utiliser power Shell

– Créer un petit Exécutable

Nous ne parlerons pas ici de la création manuelle.

Sachez juste que vous devez créer vos clefs à l’Uri suivante du registre :

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\eventlog\Application\MySource

Pour ce qui est des deux autres solutions, le powerShell semble le plus simple, car il suffit de lancer la commande suivante :

new-eventlog -source MySource -logname Application -MessageResourceFile $fileMsg -CategoryResourceFile $fileCat

On pourra également réverter cette commande et supprimer notre source via la commande suivante :

remove-eventlog -source MySource

Enfin … c’est ce que l’on peut penser. En fait, cette méthode ne spécifie pas la variable “category count”, que l’on doit donc setter dans le registre a la main. Du coup, il faut se retourner vers un script un peu plus complet, mais qui évite totalement les actions manuelles, tel que :

$MySource = "MySource"
$MyLogName = "Application"

$eventSourceData = new-object System.Diagnostics.EventSourceCreationData("$MySource", "$MyLog") $eventSourceData.CategoryCount = 3 $eventSourceData.MessageResourceFile = $fileMsg $eventSourceData.CategoryResourceFile = $fileCat


If (![System.Diagnostics.EventLog]::SourceExists($eventSourceData.Source))
{      
[System.Diagnostics.EventLog]::CreateEventSource($eventSourceData)
} 

Write-Host "Press any key to continue..."
$x = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")

En code, un petit exe en ligne de commande qui contient les lignes suivantes dans le Main de son program. cs fera l’affaire :

if (args.Length < 2)
{
     Console.WriteLine("Please pass the Category Message File and Event Messages DLL paths as a parameter");
     Console.ReadKey();
     return;
}

string name = "MySource";
int count = 3;

if (!EventLog.SourceExists(name))
     EventLog.CreateEventSource(name, "Application");

RegistryKey hklm = Registry.LocalMachine;
string keyName = @"SYSTEM\CurrentControlSet\Services\EventLog\Application\"  + name;
RegistryPermission perm = new RegistryPermission(
RegistryPermissionAccess.Create | RegistryPermissionAccess.Write, @"HKEY_LOCAL_MACHINE\" + keyName);
RegistryKey el = hklm.OpenSubKey(keyName, true);
try
{
    el.SetValue("CategoryMessageFile", args[0]);
    // important : even if you don't use this file, not overridding it will make your Categories file not being used by windows events viewer.
    el.SetValue("EventMessageFile", args[1]);
    el.SetValue("CategoryCount", count);
}
catch (Exception ex ) {
    Console.WriteLine("Error while creating the source for EventLogger", ex);
    Console.ReadKey();
    return;
}

Nous lancerons donc cet executable ainsi :

Cmd> ExecutablePath FileCatPath FileMessagesPath

Attention, pour que votre executable fonctionne bien, il vous faudra fermer l’event viewer. Dans le cas contraire, le registre ne sera pas édité.

Voila, vous savez tout !

Bonne journée et bon log.

Ne ratez plus aucunes actualités avec la newsletter mensuelle de SoftFluent