Die ersten beiden Teile der Reihe haben die Grundlagen zur Ablaufprotokollierung behandelt. Trace, Debug, Schalter, Switches und Listener sind jetzt bekannt, ebenso wie das Verhalten der Ablaufverfolgung über die app.config beeinflusst wird. Dabei wurde bislang ausschließlich mit den Bordmitteln des .NET Frameworks gearbeitet.
Wenn nun aber die Standardausgabe nicht ausreicht, ist es Zeit selbst Hand anzulegen und einen eigenen TraceListener zu schreiben! Dies ist das Thema des dritten Teils dieser Reihe.
Überblick
Auch wenn das .NET Framework schon einige Listener von Haus aus bereitstellt, so gibt es doch immer Fälle in denen dies nicht ausreicht. Sei es weil eine besondere Funktionalität benötigt wird (z. B. Übergabe an einen Web-Dienst) oder auch nur, um das Ausgabeformat anzupassen.
Im Folgenden soll ein konfigurierbarer Trace-Listener entstehen, der die Ausgabe formatiert in eine Datei schreibt und zudem noch ein paar Komfortfunktionen bietet.
Anforderungen
Folgenden Funktionsumfang wird der TraceListener haben:
- Ausgabe in eine Textdatei
- Konfigurierbarer Dateiname
- Neue Ausgabedatei bei Erreichen einer bestimmten Dateigröße bzw. pro Tag/ Woche/Monat
- Formatierte Ausgabe mit (nahezu) fester Spaltenbreite oder als CSV-Datei
Grundlagen
Bevor es an den Code geht müssen noch einige Abläufe in den Klassen TraceSource und TraceListener betrachtet werden.
Die TraceSource Klasse besitzt die Eigenschaft Listeners in der für ein TraceSource Objekt eine Auflistung der angeschlossenen TraceListener gespeichert wird. Wird im Programm nun z. B. die Methode TraceEvent aufgerufen so wird die Liste der Listener durchlaufen und die entsprechende TraceEvent Methode des jeweiligen Listeners aufgerufen.
In der TraceListener Klasse selbst enden Aufrufe der Methoden TraceEvent, TraceData und TraceTransfer in Aufrufen der (in der Basisklasse abstrakten) Methoden Write und WriteLine. Der Vollständigkeit halber sei noch erwähnt, dass die TraceTransfer Methode wiederum die TraceEvent Methode aufruft.
Ein einfaches Beispiel wird diese Abläufe verdeutlichen:
Dazu erstellen wir zunächst eine neue von TraceListener abgeleitete Klasse. Bei dieser müssen die beiden abstrakten Methoden Write(string message) sowie WriteLine(string message) überladen werden. Die Ausgabe erfolgt im Konsolenfenster; zur Unterscheidung welche Methode die Ausgabe erzeugt hat, wird die Ausgabe unterschiedlich eingefärbt.
using System; using System.Diagnostics; namespace Logging { public class BlogTraceListener : TraceListener { public override void Write(string message) { Console.Write("Write: " + message); } public override void WriteLine(string message) { Console.ForegroundColor = ConsoleColor.White; Console.WriteLine("WriteLine: " + message); Console.ForegroundColor = ConsoleColor.Gray; } } }
Im Beispielprogramm wird eine Instanz des neuen TraceListeners erstellt und der Listeners Kollektion des TraceSource Objekts hinzugefügt. Anschließend werden über die TraceSource einige Trace Nachrichten ausgegeben.
using System; using System.Diagnostics; namespace Logging { class Program { static void Main(string[] args) { Console.WriteLine("Beispiel 14"); Console.WriteLine(""); SourceSwitch loggingSwitch = new SourceSwitch("testSwitch"); loggingSwitch.Level = SourceLevels.All; BlogTraceListener listener = new BlogTraceListener(); TraceSource loggingSource = new TraceSource("MeineTraceSource"); loggingSource.Switch = loggingSwitch; loggingSource.Listeners.Add(listener); loggingSource.TraceInformation("Logging: Eintrag mit TraceInformation"); loggingSource.TraceEvent(TraceEventType.Information, 1, "Logging: Eintrag mit TraceEvent/Information"); loggingSource.TraceEvent(TraceEventType.Verbose, 2, "Logging: Eintrag mit TraceEvent/Verbose"); loggingSource.TraceEvent(TraceEventType.Warning, 3, "Logging: Eintrag mit TraceEvent/Warning" + Environment.NewLine + "und Zeilenumbruch"); try { int i = 0; int r = 5 / i; } catch (Exception ex) { loggingSource.TraceData(TraceEventType.Critical, 4, ex); loggingSource.TraceData(TraceEventType.Error, 5, new object[] {ex, loggingSource, new DateTime(DateTime.Now.Ticks) } ); } loggingSource.TraceTransfer(6, "Logging: Eintrag mit TraceTransfer", Guid.NewGuid()); loggingSource.Close(); Console.ReadLine(); } } }
Wird das Beispielprogramm ausgeführt, erhält man folgende Ausgabe:
Dabei fällt auf, dass die Ausgaben stets durch einen Aufruf von Write mit Ablaufverfolgungsinformationen (Event Typ und ID) sowie einem Aufruf von WriteLine mit der eigentlichen Nachricht erfolgen.
Daraus folgt, dass allein mit dem Überschreiben der Write/WriteLine Methoden die gewünschte Funktionalität nicht zu erreichen ist, sondern dass auch TraceData, TraceEvent und TraceTransfer angepasst werden müssen.
Die app.config Datei
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.diagnostics> <sources> <source name="MeineTraceSource" switchName="MeinSchalter"> <listeners> <add name="blogTraceListener" /> <remove name="Default" /> </listeners> </source> </sources> <sharedListeners> <add name="blogTraceListener" type="Logging.BlogTraceListener, Logging" initializeData="C:TempLogFile-{0}-{1}-{2:D8}-{3:D8}.log;T,FIX" /> </sharedListeners> <switches> <add name="MeinSchalter" value="Information,ActivityTracing" /> </switches> </system.diagnostics> </configuration>
Der neue Listener wird wie üblich im Abschnitt eingebunden. Auch wenn die Parameter schon durch die Standard Listener bekannt sind, werfen wir diesmal einen genaueren Blick auf sie:
name=“blogTraceListener“:
Wie bei allen anderen Listenern – das Kind braucht einen Namen.
type=“Logging.BlogTraceListener, Logging“:
Hier muss der vollständig gekennzeichnete Typname den Listeners angegeben werden.
Hinweis: Für alle Beispielprojekte wurde in den Projekteigenschaften für Assemblyname und Standardnamespace Logging eingetragen.
initializeData=“C:TempLogFile-{0}-{1}-{2:D8}-{3:D8}.log;T;FIX“:
Die hier angegebene Zeichenkette wird an den Konstruktor des Listeners übergeben.
Die BlogTraceListener-Klasse
Um also den neuen Listener mit den angegebenen Daten zu initialisieren muss der Konstruktor TraceListener(string name) implementiert werden.
public BlogTraceListener(string initializeData) { initData(initializeData); }
In der Variable initializeData wird genau der String-Wert übergeben, der in der app.config angegeben wurde; im Beispiel oben also “C:TempLogFile-{0}-{1}-{2:D8}-{3:D8}.csv;T;EXCSV". initData verarbeitet dann diesen String, um Variablen zu initialisieren, den Dateinamen aufzubauen, erste Informationen in die Log-Datei zu schreiben, etc. Details dazu können den Kommentaren im Quellcode entnommen werden.
Die formatierte Ausgabe in die Protokolldatei erfolgt ausschließlich mit der nachfolgenden Methode writeLine().
private void writeLine(string message, string category, long id, DateTime dateTime, int threadId, int processId) { fileOpen(); checkSwitchReason(); if (category.Length > maxCategoryNameLength) category = category.Substring(0, maxCategoryNameLength); string formatString; if (outputFormat == OutputFormat.CSV || outputFormat == OutputFormat.ExcelCSV) { if (outputFormat == OutputFormat.ExcelCSV) formatString = messageFormatStringCSV.Replace(",", ";"); else formatString = messageFormatStringCSV; category = string.Format(""{0}"", category); if (message.IndexOfAny(new char[] { '"', ',', ';' }) != -1) message = message.Replace(""", """"); message = string.Format(""{0}"", message); } else formatString = messageFormatStringFixedFirstLine; if (outputFormat == OutputFormat.CSV || outputFormat == OutputFormat.ExcelCSV) { logFileStream.WriteLine(formatString, category.ToUpper(), id, threadId, processId, dateTime.ToString("yyyy-MM-dd HH:mm:ss"), message); } else { string[] lines = message.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); bool firstLine = true; foreach (string line in lines) { if (firstLine) { logFileStream.WriteLine(formatString, category.ToUpper(), id, threadId, processId, dateTime.ToString("yyyy-MM-dd HH:mm:ss"), line); firstLine = false; } else logFileStream.WriteLine(messageFormatStringFixedLines, line); } } loggedMessages++; fileClose(); }
Der Ablauf ist denkbar einfach:
- Protokolldatei öffnen
- Prüfen, ob die Protokolldatei gewechselt werden soll – wenn ja:
- Informationen zum Wechsel in die Protokolldatei schreiben
- Datei schließen und verschieben
- Neue Protokolldatei öffnen
- Je nach Ausgabeformat die Information in die Protokolldatei schreiben
- Protokolldatei schließen
Nun zum “eigentlichen” Protokollieren, d. h. zu Write/WriteLine und den verschiedenen Trace Methoden. Da ja ausschließlich writeLine() für die Ausgabe in die Datei verantwortlich ist, erfolgt in diese Methoden ein entsprechender Aufruf.
Write
Da eine der Anforderungen an den Listener ist, dass jeder Trace Eintrag in einer eigenen Zeile steht, werden sämtliche Write-Methoden so überschrieben, dass sie das entsprechende WriteLine Pendant aufrufen:
public override void Write(string message) { this.WriteLine(message); } public override void Write(string message, string category) { this.WriteLine(message, category); } public override void Write(object o) { this.WriteLine(o); } public override void Write(object o, string category) { this.WriteLine(o, category); }
WriteLine
Der Aufruf von writeLine() erfolgt in der Methode WriteLine(string message, string category). Die anderen WriteLine-Methoden rufen eben diese Methode auf.
public override void WriteLine(string message) { this.WriteLine(message, defaultCategory); } public override void WriteLine(string message, string category) { int threadId = Thread.CurrentThread.ManagedThreadId; int processId = Process.GetCurrentProcess().Id; this.writeLine(message, category, 0, DateTime.Now, threadId, processId); } public override void WriteLine(object o) { this.WriteLine(o, defaultCategory); } public override void WriteLine(object o, string category) { this.WriteLine(o.ToString(), category); }
TraceData, TraceEvent und TraceTransfer
Wie weiter oben schon beschrieben müssen die Methoden überschrieben werden, da die Ausgabe in der Basisklasse über mehrere Write/WriteLine Aufrufe erfolgt und somit nicht die Anforderungen nicht erfüllt. Die überschriebenen Methoden rufen daher direkt writeLine() auf.
public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, object data) { this.writeLine(source + ": " + data.ToString(), eventType.ToString(), id, eventCache.DateTime, Convert.ToInt32(eventCache.ThreadId), eventCache.ProcessId); } public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, params object[] data) { string message = ""; foreach (object obj in data) { if (message != "") message += ", "; message += obj.ToString(); } this.writeLine(source + ": " + message, eventType.ToString(), id, eventCache.DateTime, Convert.ToInt32(eventCache.ThreadId), eventCache.ProcessId); } public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id) { this.TraceEvent(eventCache, source, eventType, id, ""); } public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message) { if ((eventType == TraceEventType.Error || eventType == TraceEventType.Critical) && eventCache.Callstack != "") { if (message != "") message += Environment.NewLine; message += eventCache.Callstack; } this.writeLine(source + ": " + message, eventType.ToString(), id, eventCache.DateTime, Convert.ToInt32(eventCache.ThreadId), eventCache.ProcessId); } public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string format, params object[] args) { if (args != null) this.TraceEvent(eventCache, source, eventType, id, string.Format(format, args)); else this.TraceEvent(eventCache, source, eventType, id, format); } public override void TraceTransfer(TraceEventCache eventCache, string source, int id, string message, Guid relatedActivityId) { TraceEventType eventType = TraceEventType.Transfer; this.writeLine(source + ": " + relatedActivityId.ToString() + ": " + message, eventType.ToString(), id, eventCache.DateTime, Convert.ToInt32(eventCache.ThreadId), eventCache.ProcessId); }
Der BlogTraceListener im Einsatz
Im folgenden Beispielprogramm kommt der neue Listener dann zum Einsatz:
using System; using System.Diagnostics; namespace Logging { class Program { static void Main(string[] args) { Console.WriteLine("Beispiel 15"); Console.WriteLine(""); TraceSource loggingSource = new TraceSource("MeineTraceSource"); loggingSource.TraceInformation("Logging: Eintrag mit TraceInformation"); loggingSource.TraceInformation("Logging: Eintrag mit TraceInformation"); loggingSource.TraceEvent(TraceEventType.Information, 1, "Logging: Eintrag mit TraceEvent/Information"); loggingSource.TraceEvent(TraceEventType.Verbose, 2, "Logging: Eintrag mit TraceEvent/Verbose"); loggingSource.TraceEvent(TraceEventType.Warning, 3, "Logging: Eintrag mit TraceEvent/Warning" + Environment.NewLine + "und Zeilenumbruch"); try { int i = 0; int r = 5 / i; } catch (Exception ex) { loggingSource.TraceData(TraceEventType.Critical, 4, ex); loggingSource.TraceData(TraceEventType.Error, 5, new object[] { ex, loggingSource, new DateTime(DateTime.Now.Ticks) }); } loggingSource.TraceTransfer(6, "Logging: Eintrag mit TraceTransfer", Guid.NewGuid()); loggingSource.Close(); Console.ReadLine(); } } }
Lässt man das Programm laufen, findet man im Verzeichnis C:Temp die Protokolldatei. Diese sieht dann in etwa wie folgt aus:
TRACE 0000 00010 04336 2013-05-08 16:34:32 Trace gestartet - DebugVersion TRACE 0000 00010 04336 2013-05-08 16:34:32 initializeData="C:TempLogFile-{0}-{1}-{2:D8}-{3:D8}.log;T,FIX" TRACE 0000 00010 04336 2013-05-08 16:34:32 Dateiwechsel täglich TRACE 0000 00010 04336 2013-05-08 16:34:32 Assembly: 1.0.0.0 TRACE 0000 00010 04336 2013-05-08 16:34:32 Framework: 4.0.30319.18034 TRACE 0000 00010 04336 2013-05-08 16:34:32 Anwender: urunkel TRACE 0000 00010 04336 2013-05-08 16:34:32 Domain: RUNKEL TRACE 0000 00010 04336 2013-05-08 16:34:32 Hostname: VM-DEV TRACE 0000 00010 04336 2013-05-08 16:34:32 ------------------------------------------------------------------------- TRACE 0000 00010 04336 2013-05-08 16:34:32 Thread: 10 TRACE 0000 00010 04336 2013-05-08 16:34:32 Process: 4336 TRACE 0000 00010 04336 2013-05-08 16:34:32 Session: 1 TRACE 0000 00010 04336 2013-05-08 16:34:32 ------------------------------------------------------------------------- TRACE 0000 00010 04336 2013-05-08 16:34:32 INFORMATION 0000 00010 04336 2013-05-08 14:34:32 MeineTraceSource: Logging: Eintrag mit TraceInformation INFORMATION 0000 00010 04336 2013-05-08 14:34:32 MeineTraceSource: Logging: Eintrag mit TraceInformation INFORMATION 0001 00010 04336 2013-05-08 14:34:32 MeineTraceSource: Logging: Eintrag mit TraceEvent/Information WARNING 0003 00010 04336 2013-05-08 14:34:32 MeineTraceSource: Logging: Eintrag mit TraceEvent/Warning und Zeilenumbruch CRITICAL 0004 00010 04336 2013-05-08 14:34:32 MeineTraceSource: System.DivideByZeroException: Es wurde versucht, durch 0 (null) zu teilen. bei Logging.Program.Main(String[] args) in C:UsersuwruSkyDriveDokumenteLoggingTeil 3Logging_Samples_CSLogging_15Program.cs:Zeile 24. ERROR 0005 00010 04336 2013-05-08 14:34:32 MeineTraceSource: System.DivideByZeroException: Es wurde versucht, durch 0 (null) zu teilen. bei Logging.Program.Main(String[] args) in C:UsersuwruSkyDriveDokumenteLoggingTeil 3Logging_Samples_CSLogging_15Program.cs:Zeile 24., System.Diagnostics.TraceSource, 08.05.2013 16:34:32 TRANSFER 0006 00010 04336 2013-05-08 14:34:32 MeineTraceSource: 73c0943a-4e5a-47dc-8436-e999b8b9d92e: Logging: Eintrag mit TraceTransfer TRACE 0000 00002 04336 2013-05-08 16:34:34 Trace beendet
Wir passen nun in der app.config den initializeData Parameter an
<sharedListeners> <add name="blogTraceListener" type="Logging.BlogTraceListener, Logging" initializeData="C:TempLogFile-{0}-{1}-{2:D8}-{3:D8}.csv;T,EXCSV" /> </sharedListeners>
und starten das Programm dann noch einmal. Diesmal erhält man folgende Datei:
"TRACE";0;11;6712;2013-05-08 16:37:15;"Trace gestartet - DebugVersion" "TRACE";0;11;6712;2013-05-08 16:37:15;"initializeData=""C:TempLogFile-{0}-{1}-{2:D8}-{3:D8}.csv;T,EXCSV""" "TRACE";0;11;6712;2013-05-08 16:37:15;"Dateiwechsel täglich" "TRACE";0;11;6712;2013-05-08 16:37:15;"Assembly: 1.0.0.0" "TRACE";0;11;6712;2013-05-08 16:37:15;"Framework: 4.0.30319.18034" "TRACE";0;11;6712;2013-05-08 16:37:15;"Anwender: urunkel" "TRACE";0;11;6712;2013-05-08 16:37:15;"Domain: RUNKEL" "TRACE";0;11;6712;2013-05-08 16:37:15;"Hostname: VM-DEV" "TRACE";0;11;6712;2013-05-08 16:37:15;"-------------------------------------------------------------------------" "TRACE";0;11;6712;2013-05-08 16:37:15;"Thread: 11" "TRACE";0;11;6712;2013-05-08 16:37:15;"Process: 6712" "TRACE";0;11;6712;2013-05-08 16:37:15;"Session: 1" "TRACE";0;11;6712;2013-05-08 16:37:15;"-------------------------------------------------------------------------" "TRACE";0;11;6712;2013-05-08 16:37:15;"" "INFORMATION";0;11;6712;2013-05-08 14:37:15;"MeineTraceSource: Logging: Eintrag mit TraceInformation" "INFORMATION";0;11;6712;2013-05-08 14:37:15;"MeineTraceSource: Logging: Eintrag mit TraceInformation" "INFORMATION";1;11;6712;2013-05-08 14:37:15;"MeineTraceSource: Logging: Eintrag mit TraceEvent/Information" "WARNING";3;11;6712;2013-05-08 14:37:15;"MeineTraceSource: Logging: Eintrag mit TraceEvent/Warning und Zeilenumbruch" "CRITICAL";4;11;6712;2013-05-08 14:37:15;"MeineTraceSource: System.DivideByZeroException: Es wurde versucht, durch 0 (null) zu teilen. bei Logging.Program.Main(String[] args) in D:devblogLoggingTeil 3Logging_Samples_CSLogging_16Program.cs:Zeile 24." "ERROR";5;11;6712;2013-05-08 14:37:15;"MeineTraceSource: System.DivideByZeroException: Es wurde versucht, durch 0 (null) zu teilen. bei Logging.Program.Main(String[] args) in D:devblogLoggingTeil 3Logging_Samples_CSLogging_16Program.cs:Zeile 24., System.Diagnostics.TraceSource, 08.05.2013 16:37:15" "TRANSFER";6;11;6712;2013-05-08 14:37:15;"MeineTraceSource: 2eb08cc9-0a2f-4236-9943-9b2ebe30bbcd: Logging: Eintrag mit TraceTransfer" "TRACE";0;2;6712;2013-05-08 16:37:16;"Trace beendet"
Diese CSV-Datei lässt sich dann auch problemlos mit Excel öffnen.
Bleibt noch die Frage, wozu auch Write/WriteLine überschreiben, werden diese von TraceData, TraceEvent und TraceTransfer doch gar nicht mehr aufgerufen? Ganz einfach: werden für die Ausgabe Debug oder Trace (statt einer TraceSource) benutzt, ruft beispielsweise Trace.Write() die Write() Methode des Listeners auf.
Wie das dann funktioniert zeigt das nächste Beispiel:
using System; using System.Diagnostics; namespace Logging { class Program { static void Main(string[] args) { BlogTraceListener listener = new BlogTraceListener("C:\Temp\TraceFile-{0}-{1}-{2:D8}-{3:D8}.log;T,FIX"); Trace.Listeners.Add(listener); Trace.Listeners.Remove("Default"); Trace.AutoFlush = true; Console.WriteLine("Beispiel 17"); TraceSwitch traceSwitch = new TraceSwitch("MeinSchalter", "Ablaufprotokollierung mit Schalter"); traceSwitch.Level = TraceLevel.Info; Trace.WriteLine("Beispiel 17 gestartet: " + DateTime.Now.ToString()); Trace.Write("========================================="); Trace.WriteLineIf(traceSwitch.TraceError, "Logging: Eintrag mit TraceLevel Error"); Trace.WriteLineIf(traceSwitch.TraceWarning, "Logging: Eintrag mit TraceLevel Warning"); Trace.WriteLineIf(traceSwitch.TraceInfo, "Logging: Eintrag mit TraceLevel Info"); Trace.WriteLineIf(traceSwitch.TraceVerbose, "Logging: Eintrag mit TraceLevel Verbose"); Console.ReadLine(); } } }
Die Ausgabe sieht dann wie folgt aus:
TRACE 0000 00008 05640 2013-05-08 16:41:18 Trace gestartet - DebugVersion TRACE 0000 00008 05640 2013-05-08 16:41:18 initializeData="C:TempTraceFile-{0}-{1}-{2:D8}-{3:D8}.log;T,FIX" TRACE 0000 00008 05640 2013-05-08 16:41:18 Dateiwechsel täglich TRACE 0000 00008 05640 2013-05-08 16:41:18 Assembly: 1.0.0.0 TRACE 0000 00008 05640 2013-05-08 16:41:18 Framework: 4.0.30319.18034 TRACE 0000 00008 05640 2013-05-08 16:41:18 Anwender: urunkel TRACE 0000 00008 05640 2013-05-08 16:41:18 Domain: RUNKEL TRACE 0000 00008 05640 2013-05-08 16:41:18 Hostname: VM-DEV TRACE 0000 00008 05640 2013-05-08 16:41:18 ------------------------------------------------------------------------- TRACE 0000 00008 05640 2013-05-08 16:41:18 Thread: 8 TRACE 0000 00008 05640 2013-05-08 16:41:18 Process: 5640 TRACE 0000 00008 05640 2013-05-08 16:41:18 Session: 1 TRACE 0000 00008 05640 2013-05-08 16:41:18 ------------------------------------------------------------------------- TRACE 0000 00008 05640 2013-05-08 16:41:18 NONE 0000 00008 05640 2013-05-08 16:41:18 Beispiel 17 gestartet: 08.05.2013 16:41:18 NONE 0000 00008 05640 2013-05-08 16:41:18 ========================================= NONE 0000 00008 05640 2013-05-08 16:41:18 Logging: Eintrag mit TraceLevel Error NONE 0000 00008 05640 2013-05-08 16:41:18 Logging: Eintrag mit TraceLevel Warning NONE 0000 00008 05640 2013-05-08 16:41:18 Logging: Eintrag mit TraceLevel Info TRACE 0000 00002 05640 2013-05-08 16:41:20 Trace beendet
Fazit
Neben dem Grundverständnis für die Ablaufprotokollierung in den Teilen Eins und zwei wurde in diesem letzten Teil der Blog-Reihe ein eigener TraceListener erstellt. Dabei soll der hier vorgestellte Listener wiederum nur Grundlage sein. Erweiterte Funktionalitäten wie z. B. Thread-Sicherheit und vieles, vieles mehr wurden dabei nicht berücksichtigt.
Wie so oft lohnt es auch sich etwas umzuschauen, um das Rad nicht neu erfinden zu müssen. Es gibt unzählige und wirklich exzellente (OpenSource) Logging-Frameworks. NLog, log4net, ObjectGuy Framework oder der Logging Application Block der Enterprise Library, nur um einige wenige zu nennen.
Eine Übersicht über zahlreiche Logging Frameworks findet man z. B. hier. Dort gibt es auch einen Vergleich eines kommerziellen (das des Seitenbetreibers) mit einigen der bekanntesten OpenSource Frameworks.
Dateien:
Sourcecode der Beispiele (C#)
Sourcecode der Beispiele (VB.NET)
Ablaufprotokollierung Teil 3 im PDF-Format