9.2 Protokollierung
Wenn ein Fehler nicht zu einer Ausnahme führt, ist er viel schwerer aufzuspüren. Dann bleibt nichts anderes als eine Analyse des Programmlaufes. Die meiste Zeit bei der Fehlersuche verbringt man mit der Suche nach der Stelle, die den Fehler ausgelöst hat. Hier können selbst definierte Ausgaben sehr viel helfen. Sie haben den Nachteil, nicht so »komfortabel« wie ein grafischer Debugger zu sein. Andererseits haben sie ein paar unschätzbare Vorteile:
- Sie bestimmen die ausgedruckte Information.
- Sie beeinflussen den Programmlauf nur minimal. (Das ist hilfreich bei zeitkritischen und nebenläufigen Anwendungen, in denen mehrere Threads gleichzeitig laufen.)
- Sie brauchen keinen Debugger. (Das ist hilfreich zur Fehlersuche bei einem Kunden.)
- Wenn sie entsprechend konfiguriert sind, können sie selbst bei Programmen helfen, die einen Debugger zum Absturz bringen.
- Auch andere Ausgaben können speziell protokolliert werden, die mit der eigentlichen Programmlogik nichts zu tun haben (zum Beispiel Dateizugriffe).
9.2.1 Ausgabe mit Debug
Die einfachste Art, Informationen auszudrucken, ist die Methode Console.WriteLine. Sie hat den Nachteil, dass Fehlerausgaben von normalen Ausgaben nicht getrennt sind. Außerdem müssen sie manuell vor Auslieferung der Software gelöscht werden. Daher bietet uns .NET die Klasse Debug im Namensraum System.Diagnostics an, mit der wir Ausgaben an einen eigenen Ausgabekanal schicken können. Standardmäßig ist das das Fenster Ausgabe in Visual Studio, das auch während der Kompilierung benutzt wird. Sie können es sich anzeigen lassen, indem Sie im Menü Ansicht das Untermenü Andere Fenster • Ausgabe wählen.
Protokoll anzeigen
Analog zur Klasse Console schreiben Sie mit Debug.Write eine Ausgabe, allerdings mit etwas weniger Überladungen. Die folgende Syntax zeigt alle Methoden zur Erzeugung einer unbedingten Ausgabe. Es fehlt insbesondere eine formatierte Ausgabe. Sie müssen die Ausgabe also selbst zu einer Zeichenkette zusammensetzen. Die Zeichenkette kategorie wird dem wert vorangestellt:
Public Shared Sub Ausgabe(wert As Art) Public Shared Sub Ausgabe(wert As Art, kategorie As String) Ausgabe: Write oder WriteLine, Art: String oder Object |
Analog zu diesen Ausgabemethoden gibt es Varianten, die nur dann eine Ausgabe machen, wenn die im ersten Parameter angegebene Bedingung erfüllt ist:
Public Shared Sub AusgabeIf(bedingung As Boolean, wert As Art) Public Shared Sub AusgabeIf(bedingung As Boolean, wert As Art, _ kategorie As String) Ausgabe: Write oder WriteLine, Art: String oder Object |
Im folgenden Beispiel wird bei jedem größeren Wert mit WriteLineIf eine Ausgabe im Ausgabe-Fenster produziert:
'...\Lauf\Protokollierung\Eingrenzung.vb |
Option Strict On
Namespace Lauf
Module Eingrenzung
Private rnd As New Random(1234)
Sub Ausgeben(ByRef wert As Integer, ByVal i As Integer)
Dim aus As Integer = rnd.Next(0, 7)
wert += aus
Debug.WriteLineIf(aus > 4, _
i & ": wieder eine Fünfer weg: " & wert, "Geld")
End Sub
Sub Test()
Dim wert As Integer
For i As Integer = 0 To 10
Ausgeben(wert, i)
Next
Console.WriteLine("Gesamtausgabe: {0}", wert)
Console.ReadLine()
End Sub
End Module
End Namespace
Die normale Ausgabe ist sehr kurz:
Gesamtausgabe: 40
Das Ausgabe-Fenster enthält nur die Ausgaben von Debug (siehe Abbildung 9.1). Der Kategorieparameter Geld steht vor den eigentlichen Ausgaben.
Abbildung 9.1 Ausgabe von »Debug.WriteLineIf«
Hinweis |
Sollten Sie im Ausgabe-Fenster nichts sehen, prüfen Sie in den Optionen des Debuggers (Menü Extras • Optionen), ob die Ausgabe in das Direktfenster umgeleitet wird. |
Einrücken der Ausgabe
Mit der Methode Indent wird die Einzugsebene um eins erhöht, mit Unindent wird sie um eins verringert. Mit der Eigenschaft IndentSize bestimmen Sie die Einheit. Standardmäßig sind es vier Leerzeichen. IndentLevel legt eine Einzugsebene direkt fest (ersetzt Indent-Aufrufe).
Debug.WriteLine("Ausgabe 1") Debug.Indent() Debug.WriteLine("Ausgabe 2") Debug.IndentLevel = 3 Debug.WriteLine("Ausgabe 3") Debug.Unindent() Debug.WriteLine("Ausgabe 4") Debug.IndentSize = 2 Debug.IndentLevel = 1 Debug.WriteLine("Ausgabe 5")
Der Code führt zu folgender Ausgabe:
Ausgabe 1 Ausgabe 2 Ausgabe 3 Ausgabe 4 Ausgabe 5
Prüfung mit Assert
Sie sollten die Methode Assert einsetzen, um Annahmen zu prüfen, von denen Sie eigentlich meinen, dass sie immer erfüllt sind. Die Methode ist also zur Erkennung schwerwiegender Fehler gedacht. Die Methode zeigt eine Fehlermeldung an, wenn ein Ausdruck mit False ausgewertet wird.
Debug.Assert(wert >= 0, "Negativer Wert")
Ist wert negativ, erscheint die Nachricht aus Abbildung 9.2.
Abbildung 9.2 Meldung der Methode »Debug.Assert«
Das Dialogfenster enthält neben der Zeichenfolge des zweiten Parameters auch Informationen darüber, in welcher Klasse und welcher Methode der Assertionsfehler aufgetreten ist.
9.2.2 Ausgabe mit Trace
Die Klasse Trace hat die gleiche Funktionalität wie Debug. Ein Unterschied macht sich bei einem Wechsel der Build-Konfiguration zwischen Release und Debug bemerkbar (siehe Abbildung 9.3).
Abbildung 9.3 Debug/Release-Build-Konfiguration
Mit der Standardeinstellung Debug erzeugen Debug und Trace Ausgaben, während bei Release Aufrufe von Debug ignoriert werden. Dies liegt daran, dass im zweiten Fall die Aufrufe gar nicht kompiliert werden und die Anwendung nicht aufblähen. Der Hintergrund ist das in Abschnitt 4.8.1, »Beispiel: Bedingte Kompilierung«, beschriebene Attribut.
Unterhalb des Verzeichnisses der Quellcodedateien legt die Entwicklungsumgebung das Verzeichnis \bin an, das – je nach eingestellter Build-Konfiguration – die beiden Verzeichnisse \Debug und \Release enthält und das entsprechende Kompilat aufnimmt. Die PDB-Dateien in diesen Verzeichnissen ermöglichen dem Debugger eine für Menschen lesbare Ausgabe.
9.2.3 Ausgabeziel mit TraceListener
Im Namensraum System.Diagnostics sind fünf Listener als Ziel der Protokollierung definiert, die alle aus der abstrakten Klasse TraceListener abgeleitet sind (siehe Tabelle 9.2):
Listener-Klasse | Ziel der Ausgabe |
ConsoleTraceListener |
Konsole |
DefaultTraceListener |
Entwicklungsumgebung (Standardeinstellung) |
EventLogTraceListener |
Windows-Ereignisprotokoll |
TextWriterTraceListener |
Stream, Textwriter oder Datei (siehe Kapitel 7, »Eingabe und Ausgabe«) |
Die folgende Klassenhierarchie zeigt die von TraceListener abgeleiteten Klassen in .NET.
System.Object +System.MarshalByRefObject +2--TraceListener +1--FileLogTraceListener +2-+DefaultTraceListener | +EventLogTraceListener | +TextWriterTraceListener | +2-+ConsoleTraceListener | +DelimitedListTraceListener | +EventSchemaTraceListener | +XmlWriterTraceListener +3--EventProviderTraceListener +4-+IisTraceListener +WebPageTraceListener 1: Microsoft.VisualBasic.Logging 2: System.Diagnostics 3: System.Diagnostics.Eventing 4: System.Web
Die Ausgabeziele von Debug und Trace sind in deren Eigenschaft Listeners vom Auflistungstyp TraceListenerCollection gespeichert. Sie können mit Add, Remove, Clear usw. die Liste nach Bedarf anpassen. Beachten Sie, dass die Eigenschaft Listeners beider Klassen dieselbe Auflistung bezeichnet. Im folgenden Codefragment wird die Liste geleert und dann die Konsole als einziges Ausgabeziel hinzugefügt. Debug und Trace schreiben damit nur noch in die Konsole.
Debug.Listeners.Clear() TextWriterTraceListener console = new TextWriterTraceListener(Console.Out) Debug.Listeners.Add(console)
Dateiausgabe mit TextWriterTraceListener
Mit diesem Listener leiten Sie einfach Ausgaben in eine Datei um. Übergeben Sie dem Konstruktor einen Dateipfad, werden die Ausgaben an die (gegebenenfalls neu erstellte) Datei angehängt. Brauchen Sie mehr Kontrolle, verwenden Sie einen anderen Konstruktor. Im folgenden Codefragment stellt Flush sicher, dass vor dem Schließen der Ausgabepuffer geleert wird. Alternativ setzen Sie AutoFlush=True. Da Debug und Trace dieselben Listener verwenden, werden zwei Zeilen in die angegebene Datei geschrieben.
Dim listener As New TextWriterTraceListener("C:\DebugProtocol.txt") Trace.Listeners.Add(listener) Debug.WriteLine("Debug.WriteLine-Anweisung") Trace.WriteLine("Trace.WriteLine-Anweisung") listener.Flush() listener.Close()
Das folgende Beispiel zeigt, wie mithilfe dieses Mechanismus datierte Fehlerinformationen auch über das Ende einer Anwendung hinaus gerettet werden können:
'...\Lauf\Protokollierung\PostMortem.vb |
Option Strict On Namespace Lauf Module PostMortem Sub Test() Debug.Listeners.Clear() Dim tl = New TextWriterTraceListener("C:\Temp\ErrorProtocol.txt") Debug.Listeners.Add(tl) Debug.AutoFlush = True Try Operation() Catch ex As Exception Trace.WriteLine("Fehler: " & Now & " – " & ex.Message) Finally tl.Close() End Try End Sub Sub Operation() Throw New Exception("Werch ein Illtum.") End Sub End Module End Namespace
Der Inhalt der Datei zeigt die Protokollierung der Ausnahme:
Fehler: 20.12.2008 20:52:02 – Werch ein Illtum.
Mehrere Listener verwalten
Debug und Trace schreiben in alle in Listeners gespeicherten Ausgabeziele. Um die Ziele zu beschränken, müssen die entsprechenden Listener (temporär) aus der Liste entfernt werden. Sie können leichter gefunden werden, wenn ihnen mit einem zweiten Konstruktorparameter ein Name gegeben wird.
Im folgenden Beispiel werden zwei benannte Listener registriert und es wird eine Meldung geschrieben. Anschließend wird ein Listener aus der TraceListenerCollection gelöscht. Die anschließende Meldung wird nur vom verbleibenden Listener weitergeleitet.
Dim ZielA As TextWriterTraceListener("C:\A.txt", "AL") Trace.Listeners.Add(ZielA) Dim ZielB As TextWriterTraceListener("C:\B.txt", "BL") Trace.Listeners.Add(ZielB) Trace.AutoFlush = True Trace.WriteLine("Erste Information") Trace.Listeners.Remove("BL") Trace.WriteLine("Zweite Information")
Nutzen Sie statt AutoFlush die Methode Flush, muss sie vor der Deregistrierung aufgerufen werden.
Ereignisprotokollausgabe
Mit dem Ziel EventLogTraceListener schreiben Sie in das Windows-Ereignisprotokoll.
Public Sub New() Public Sub New(eventLog As EventLog) Public Sub New(source As String) |
Im folgenden Codefragment wird ein Ereignisprotokoll namens MyProtocol auf der lokalen Maschine eingerichtet, die durch einen Punkt benannt ist. Mit der Eigenschaft Source geben Sie einen Bezeichner für die Ereignisquelle an. Innerhalb der Ereignisprotokolle von Windows muss dieser eindeutig sein. EventLog.GetEventLogs(".") listet alle bekannten Ereignisprotokolle auf. Windows definiert bereits Application, System und Security.
Dim log As New EventLog("MyProtocol", ".") log.Source = "MyApplication" Dim el New EventLogTraceListener(log) Debug.Listeners.Add(el) Debug.WriteLine("Ein Eintrag im Ereignisprotokoll") el.Flush()
Ereignisprotokolle sollten nur für wenige, besonders wichtige Meldungen verwendet werden.
9.2.4 Steuerung mit Konfigurationsdateien
Die Methoden WriteIf und WriteLineIf erlauben die bedingte Protokollierung. Die im ersten Parameter angegebene Bedingung ist der Schlüssel zur Steuerung. Normalerweise wird sie durch Code gesetzt. Wenn Sie eine von System.Diagnostics.Switch abgeleitete Klasse verwenden, können Sie die Bedingung in einer Konfigurationsdatei steuern.
Object +Switch ++BooleanSwitch +SourceSwitch +TraceSwitch
BooleanSwitch
Dieser Schalter kennt nur die Stellungen An und Aus. Dem Konstruktor übergeben Sie den Namen des Schalters und eine Beschreibung:
Dim meiner As New BooleanSwitch("MeinSchalter", "Ablaufverfolgung in MyApp")
Der Schalter startet in der Stellung False und kann mit meiner.Enabled = True angeschaltet werden. Diese Eigenschaft wird auch in den Protokollfunktionen verwendet.
Trace.WriteLineIf(meiner.Enabled, "Ablaufverfolgung:" & Now.ToString())
Die Schalterstellung können Sie auch in der Anwendungskonfigurationsdatei festlegen:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.diagnostics> <switches> <add name="MeinSchalter" value="1" /> </switches> </system.diagnostics> </configuration>
Jeder Schalter wird in einem eigenen add-Element dem switches-Knoten hinzugefügt. Das Attribut name gibt den Namen des Schalters an und das Attribut value die Schalterstellung. Dabei steht 1 für True und 0 für False. Beim Start der Anwendung wird die Konfigurationsdatei eingelesen und schafft eine Vorbelegung des Schalters, die im Code geändert werden kann. Durch Änderung der XML-Datei vor dem Programmstart wird die Anwendung gesteuert.
TraceSwitch
Dieser Schalter kennt nicht nur die Stellungen An und Aus, sondern verschiedene Einstellungen, die über die Eigenschaft Level gesetzt werden.
Public Property Level As TraceLevel |
Die Eigenschaft ist vom Typ der Enumeration TraceLevel (siehe Tabelle 9.3). Jeder Wert, bis auf Off, umfasst die niederwertigeren. Wenn zum Beispiel Info gesetzt ist, dann sind damit automatisch auch Error und Warning gesetzt.
Konstante | Wert | Art der ausgegebenen Meldungen |
Off |
0 |
Keine |
Error |
1 |
Fehlermeldungen |
Warning |
2 |
Fehler- und Warnmeldungen |
Info |
3 |
Fehler-, Warn- und Informationsmeldungen |
Verbose |
4 |
Alle Meldungen |
Der TraceSwitch-Konstruktor bekommt einen Namen und eine Beschreibung übergeben.
Dim meiner As New TraceSwitch("MeinSchalter", "Ablaufverfolgung in MyApp") meiner.Level = TraceLevel.Warning
Die Methoden WriteIf und WriteLineIf brauchen im ersten Parameter einen booleschen Wert. Daher definiert TraceSwitch die booleschen Eigenschaften TraceError, TraceWarning, TraceInfo und TraceVerbose.
Trace.WriteLineIf(meiner.TraceWarning, "Ablaufverfolgung:" & Now.ToString())
Analog zu BooleanSwitch kann ein Schalter in einer Konfigurationsdatei definiert und vorbelegt werden – im folgenden Beispiel mit TraceLevel.Info:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.diagnostics> <switches> <add name="MeinSchalter" value="3" /> </switches> </system.diagnostics> </configuration>
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.