Für ein Tool hab ich kürzlich ein Logging in das Windows-EventLog benötigt. Natürlich gab es dazu auch ein paar Anforderungen.
1) Das Logging muss jederzeit deaktivierbar sein
2) In der Ereignisanzeige soll für das Log ein eigenes Protokoll eröffnet werden (unter Anwendungs- und Dienstprogramme)

Das hört sich alles eigentlich recht einfach an. Punkt 1 lässt sich super über die app.config umsetzen. Punkt 2… naja. Das macht es dann doch ein bisschen komplizierter, aber fangen wir erst einmal ganz von Vorne an.

Um in das EventLog zu schreiben, muss man sich ein Objekt vom Typ System.Diagnostics.EventLog anlegen. Hier gibt es zwei Properties Source und Log, mit denen man konfiguriert, wie das Protokoll heißen soll und welche Quelle verwendet werden soll.
Die Quelle muss man im Vorfeld anlegen, falls diese noch nicht existiert. Das Ganze sieht dann so aus:

if (!EventLog.SourceExists("Quelle"))
    EventLog.CreateEventSource("Quelle", "Log");
var log = new EventLog();
log.Source = "Quelle";
log.Log = "Log";
log.WriteEntry("Das ist ein Test", EventLogEntryType.Information);

Das starten wir jetzt gleich mal und… wir bekommen eine SecurityException.
Hintergrund ist, dass man einen EventSource nur als Admin anlegen kann. Das heißt, dass die Anwendung zumindest beim ersten Start als Admin laufen muss, sonst klappt das Ganze nicht. In meinem Fall macht das jetzt nichts aus, da es sich dabei um einen Windows Dienst handelt, der ohnehin mit Adminrechten läuft. Daher gehe ich jetzt gar nicht weiter darauf ein, sondern starte die Anwendung einfach mit entsprechenden Rechten und dann funktioniert es auch schon.

Wenn man jetzt die Ereignisanzeige neu startet (man muss sie wirklich neu starten, damit das neue Log sichtbar ist), sieht man unter “Anwendungs- und Dienstprogramme” ein neues Protokoll “Log” mit einem Eintrag darin.

Das schon mal als Grundlage für die Thematik. Nun kümmern wir uns mal um Punkt 1 und versuchen das Ganze über die app.config entsprechend zu konfigurieren.

Es gibt bereits einen passenden TraceListener im Namespace System.Diagnostics, den wir dafür verwenden können. Wir fügen also den entsprechenden Listener in der app.config hinzu:

<configuration>
    <system.diagnotics>
        <trace>
            <add name="eventlog" 
                 type="System.Diagnostics.EventLogTraceListener"
                 initializeData="TEST1" />
        </trace>
    </system.diagnotics>
</configuration>

Damit sollte jetzt also ein entsprechender TraceListener hinzugefügt werden. Der Konstruktur der Klasse EventLogTraceListsner erwartet einen String, der über initializeData übergeben wird. Der String enthält den Namen der Quelle, die verwendet werden soll. Laut Doku wird die Quell automatisch angelegt, falls sie noch nicht existiert. Das hört sich alles ganz gut an. Probieren wir also mit einem kleinen Testprogramm aus, ob es auch funktioniert.

static void Main(string[] args)
{
    Trace.WriteLine("writeline");
    Trace.TraceInformation("information");
    Trace.TraceWarning("warning");
    Trace.TraceError("error");
}

Das Ergebnis ist ernüchternd. Grundsätzlich funktioniert es und Punkt 1 der Anforderung ist auf jeden Fall schon mal erfüllt, denn es läuft auf jeden Fall über die app.config und ist somit auch jederzeit deaktivierbar. Aber in der Ereignisanzeige sieht es leider nicht so toll aus. Man findet die Einträge unter den Windows-Protkollen im Protokoll “Anwendung”. Da soll es ja eigentlich nicht sein, sondern es soll ein eigenes Protokoll angelegt werden.
Wenn man jetzt aber in der app.config statt “TEST1” den Namen “Quelle” verwendet, den wir oben beim ersten Test verwendet haben, landen die Meldungen auch im entsprechenden Protokoll. Der Hintergrund ist ganz einfach: Wir haben die Quelle am Anfang angelegt und ein entsprechendes Log zugeordnet. Über die app.config wird der Quelle, die man über initializeData angibt, aber vom System automatisch das Log “applications” zugeordnet und daher landen die Meldungen auch dort.
Wie kann man aber jetzt ein entsprechendes Log zuordnen? Ich hab ehrlich gesagt keine Lösung gefunden, wie man das über die app.config lösen kann. Die einzige Lösung, die ich gefunden habe ist, dass man einen eigenen TraceListener erstellt und das werden wir jetzt machen.

Wir erzeugen uns also eine Klassenbibliothek mit einer Klasse EventLogTraceListenerExtended und basteln uns das Ganze eben selbst.
Was brauchen wir dazu?
Eigentlich nicht viel. Wir müssen unsere Klasse von System.Diagniostics.TraceListener ableiten und müssen dann gleich die beiden abstrakten Methoden Write und WriteLine überschreiben. Das war es dann auch schon grundlegend. Im Konstruktor erstellen wir unser EventLog-Objekt und konfigurieren es entsprechend, so dass es unseren Anforderungen entspricht und wir wir es ganz zu Beginn des Artikels getestet haben. Den Namen des Logs übergeben wir über den Parameter initializeData, also als StringParameter im Konstruktor.
Das Ganze sieht dann so aus:

public class EventLogTraceListenerExtended : System.Diagnostics.TraceListener
{
    private readonly EventLog _eventLog;
    public EventLogTraceListenerExtended(string logname)
    {
        if (!EventLog.SourceExists(logname))
            EventLog.CreateEventSource(logname, logname);

        _eventLog = new EventLog();
        _eventLog.Source = logname;
        _eventLog.Log = logname;
    }
    public override void Write(string message)
    {
        _eventLog.WriteEntry(message, EventLogEntryType.Information);
    }

    public override void WriteLine(string message)
    {
        Write(message);
    }
}

Dann verwenden wir diesen Listener in der app.config:

<configuration>
    <system.diagnotics>
        <trace>
            <add name="eventlog" 
                 type="TraceListener.EventLogTraceListenerExtended, TraceListener"
                 initializeData="TEST1" />
        </trace>
    </system.diagnotics>
</configuration>

Und dann probieren wir das Ganze gleich nochmal.
Um die Quelle zu löschen, empfielt es sich, wenn man über die Powershell-Konsole diese entfernt. Dazu gibt man einfach den folgenden Befehl ein:

Remove-EventLog TEST1

Jetzt starten wir das Ganze und siehe da… wir haben unser eigenes Protokoll mit den Einträgen darin.

Jetzt gibt es nur noch einen Punkt, der mir persönlich nicht gefällt: Man sieht jetzt in der Ereignisanzeige nicht auf den ersten Blick, ob es sich um einen Fehler, Warnung etc. handelt. Das muss man ebenfalls noch im Tracelistener einbauen und man muss dazu die folgenden Methoden überschreiben bzw. hinzufügen:

public override void TraceData(TraceEventCache eventCache, 
    string source, TraceEventType eventType, int id, object data)
{
    _eventLog.WriteEntry(data.ToString(), GetEventLogType(eventType));
}

public override void TraceEvent(TraceEventCache eventCache, string source,
    TraceEventType eventType, int id, string format, params object[] args)
{
    _eventLog.WriteEntry(String.Format(format, args), GetEventLogType(eventType));
}

public override void TraceEvent(TraceEventCache eventCache, string source, 
    TraceEventType eventType, int id, string message)
{
    _eventLog.WriteEntry(message, GetEventLogType(eventType));
}

private EventLogEntryType GetEventLogType(TraceEventType source)
{
    EventLogEntryType type = EventLogEntryType.Information;
    switch (source)
    {
        case TraceEventType.Critical:
        case TraceEventType.Error:
            type = EventLogEntryType.Error;
            break;
        case TraceEventType.Warning:
        case TraceEventType.Resume:
        case TraceEventType.Start:
        case TraceEventType.Stop:
        case TraceEventType.Suspend:
        case TraceEventType.Transfer:
            type = EventLogEntryType.Warning;
            break;
        default:
            type = EventLogEntryType.Information;
            break;
    }
    return type;
}

Damit haben einen eigenen TraceListener, den wir nach belieben aktivieren und deaktivieren können. Falls er nicht mehr benötigt wird, wird einfach der Eintrag in der app.config entfernt und das war’s dann auch schon.
Auf diesem Weg kann man auch ohne Probleme weitere Trace-Ausgaben hinzufügen, wenn man zum Beispiel zusätzlich auch noch eine Datei schreiben will oder in eine Datenbank oder… Es gibt bereits viele vorgefertigte TraceListener im Framework selbst. Aber selbst wenn man eine eigene Implementation benötigt, sieht man hier ganz gut, dass es sich ohne viel Aufwand bewerkstelligen lässt.

Ein kleiner Hinweis noch am Schluss: Das hier gezeigte Beispiel ist in keinster Weise auf Thread-Sicherheit getestet. Unter Umständen kann es sein, dass man den Listener noch weiter anpassen muss, damit man keine Probleme bekommt, wenn man mehrere Threads parallel verwendet. Aber selbst das dürfte dann kein größeres Problem mehr darstellen.