C#: Klassenübergreifendes Logging

Heute habe ich mir ein paar Gedanken zum Thema Logging mit C# gemacht. Für das umfangreiche Projekt, an dem ich zur Zeit arbeite, musste ich eine einfache und flexible Möglichkeit finden, verschiedene Meldungen (Hinweis, Erfolg, Fehler) zu loggen, ohne die zahlreichen Klassen von einer bestimmten Implementierung abhängig zu machen. Meine Lösung hierzu sieht wie folgt aus (das Diagramm habe ich mit ArgoUML erstellt):

UML-Diagramm meines C#-Loggers

Erklärung

  • Mein Entwurf basiert auf dem Singleton Logger über den an beliebiger Stelle in meinen verschiedenen Klassen einfach mittels Logger.Log(LogEintragTyp.Fehler, "Fehler!") Meldungen geloggt werden können. Hierzu ist lediglich der Verweis auf meine Assembly Logging nötig, da Log() eine statische Methode ist und Logger nicht instantiiert werden muss.
  • Der konkrete Logger, der die Ausgabe der Meldugen übernimmt, wird mittels Logger.SetLogger() gesetzt (geschieht dies nicht, wird halt nichts ausgegeben).
  • Das Interface ILogger muss von Klassen implementiert werden, die als Logger fungieren sollen. Meine Beispielimplementierung ConsoleLogger ist unten abgebildet.
  • Diese Implementierung weist eine große Flexibilität aus, weil ich zur Laufzeit den ausgebenden Logger wechseln kann (z.B. von Konsole zu Logdatei) und dank des Structs LogEintrag einfach weitere Attribute zu den Logmeldungen hinzugefügt werden können, ohne die vorhandenen Quelltexte großartig anpassen zu müssen. In meinen verschiedenen Klassen muss ich lediglich auf meine Assembly verweisen (mittels using Logging; und kann dann mit nur einer Zeile (ohne lästige Prüfung auf null etc.) einen Logeintrag erstellen.

Und so sieht das Ergebnis aus:

Beispiel-Ausgabe meines C#-Loggers

Quelltexte

Der Logger

/// <summary> /// Ein global verfügbarer Logger. /// </summary> public sealed class Logger { #region Statische Member private static readonly Logger _instanz = new Logger(); /// <summary> /// Loggt die übergebene Meldung. /// </summary> /// <param name="typ">Typ der Meldung.</param> /// <param name="meldung">Text der Meldung.</param> public static void Log(LogEintragTyp typ, string meldung) { _instanz.NeueMeldung(typ, meldung); } /// <summary> /// Setzt den Logger, der zur Ausgabe der Meldungen verwendet werden soll. /// </summary> /// <param name="logger"></param> public static void SetLogger(ILogger logger) { _instanz._logger = logger; } #endregion #region Instanzmember private ILogger _logger = null; private List<LogEintrag> _eintraege = new List<LogEintrag>(); private void NeueMeldung(LogEintragTyp typ, string meldung) { LogEintrag le = new LogEintrag(typ, meldung); this._eintraege.Add(le); if (this._logger != null) this._logger.NeueMeldung(le); } #endregion }

Logger-Implementierung

public interface ILogger { /// <summary> /// Wird aufgerufen, sobald eine neue Meldung geloggt wird. /// </summary> /// <param name="meldung">Die neue Meldung.</param> void NeueMeldung(LogEintrag meldung); /// <summary> /// Gibt die Liste aller geloggten Meldungen zurück. /// </summary> /// <returns>Die Liste aller geloggten Meldungen.</returns> List<LogEintrag> GetMeldungen(); }

Zur farbigen Ausgabe auf der Konsole habe ich die hier beschriebene Klasse verwendet: Putting colour/color to work on the console.
/// <summary> /// Ein Logger, der die Meldungen lediglich (farbig) auf der Konsole ausgibt. /// </summary> public class ConsoleLogger : ILogger { #region ConsoleColor /// <summary> /// Static class for console colour manipulation. /// (http://www.codeproject.com/csharp/console_apps__colour_text.asp) /// </summary> private class ConsoleColor { // source code of ConsoleColour from http://www.codeproject.com/csharp/console_apps__colour_text.asp } #endregion #region ILogger Member /// <summary> /// Gibt eine neue Meldung auf der Konsole aus. /// </summary> /// <param name="meldung">Die neue Meldung.</param> public void NeueMeldung(LogEintrag meldung) { switch (meldung.Typ) { case LogEintragTyp.Erfolg: ConsoleColor.SetForeGroundColour(ConsoleColor.ForeGroundColour.Green); break; case LogEintragTyp.Fehler: ConsoleColor.SetForeGroundColour(ConsoleColor.ForeGroundColour.Red); break; default: break; } Console.WriteLine(meldung.Zeitpunkt.ToString("HH:mm:ss.ffffff") + " " + meldung.Typ.ToString() + ": " + meldung.Text); ConsoleColor.SetForeGroundColour(); } /// <summary> /// Nicht implementiert. /// </summary> /// <returns>-</returns> public List<LogEintrag> GetMeldungen() { throw new Exception("The method or operation is not implemented."); } #endregion }

Log-Einträge

/// <summary> /// Mögliche Typen eines Log-Eintrags. /// </summary> public enum LogEintragTyp { /// <summary> /// Erfolgsmeldung. /// </summary> Erfolg, /// <summary> /// Fehlermeldung. /// </summary> Fehler, /// <summary> /// Hinweis. /// </summary> Hinweis } /// <summary> /// Eine Meldung für das Log. /// </summary> public struct LogEintrag { /// <summary> /// Der Typ der aktuellen Meldung. /// </summary> public LogEintragTyp Typ; /// <summary> /// Der Text der aktuellen Meldung. /// </summary> public string Text; /// <summary> /// Zeitpunkt zu dem die Meldung erstellt wurde. /// </summary> public DateTime Zeitpunkt; /// <summary> /// Konstruktor. /// </summary> /// <param name="typ">Der Typ der neuen Meldung.</param> /// <param name="text">Der Text der neuen Meldung.</param> public LogEintrag(LogEintragTyp typ, string text) { this.Typ = typ; this.Text = text; this.Zeitpunkt = DateTime.Now; } /// <summary> /// Gibt Typ und Text der aktuellen Meldung aus. /// </summary> /// <returns>Typ und Text der aktuellen Meldung.</returns> public override string ToString() { return this.Typ.ToString() + ": " + this.Text; } }

Über Stefan

Polyglot Clean Code Developer

10 Kommentare

  1. Hey Stefan,
    guter Ansatz über ein Interface lose zu koppeln. Das einzige was mich an der Lösung stören würde ist das Enum “LogEintragTyp”. Wäre es nicht geschickter eine Basisklasse “LogEintrag” mit der Methode “Print” oder so zu haben und die einzelnen Ausprägungen in Form von Klassen abzuleiten (ErfolgLogEintrag, WarnungLogEintrag, HinweisLogEintrag) und dann via Polymorphismus zur laufzeit entscheiden zu lassen, wie diese ausgegeben werden? Das würde dem open-close prinzip eher entsprechen denn es würde kein Enum mehr geben das bei jeder neuen LogSeverity geändert werden müsste. Was meinst du?
    Schöne Grüße,
    tobsen
    http://blog.tobsen.de

  2. Mhh… das wäre möglich.

    Aber die Ausgabe der Log-Einträge regelt bei meinem Entwurf ja die Implementierung des Loggers (z.B. ConsoleLogger). Evtl. gibt es z.B. einen Logger, der gar nichts ausgeben soll oder ein Logfile anlegt. Dann müsste ich in den abgeleiteten Klassen “LogEintrag” diese verschiedenen Möglichkeiten implementieren und hierzu evtl. diese “grundlegenden” Klassen häufig ändern/kompilieren. Die Ausgabe der Meldungen passt meiner Meinung nach semantisch besser in die Implementierung des Loggers…

    Bei meinem Entwurf muss ich lediglich das Enum erweitern, falls ich eine neue Art Meldungen hinzufügen möchte (was selten vorkommen dürfte), und zur Implementierung einer völlig neuen Art Logger (z.B. Ausgabe in einem Formular oder wie auch immer) in dem jeweiligen Projekt einen “ILogger” erstellen, der mir die Meldungen wie gewünscht ausgibt.

    So habe ich dann zwar mehrere Logger (ConsoleLogger, WindowLogger, TextfileLogger etc.), die aber nicht allzu umfangreich sein dürften, da sie nur die zwei Methoden des Interface implementieren. Bei deinem Vorschlag müsste ich stets mehrere Klassen (FehlerLogEintrag etc.) neu implementieren, oder nicht?

  3. Hab das ganze mal überschlafen und muß dir recht geben, denn die Häufigkeit in der sich das Enum ändern wird bleibt überschaubar. Bin zu schnell über das enum und die switch-Anweisung gestolpert (du mußt zugeben, dass sich LogEintragTyp sehr nach Type, also eher klasse anhört. Auch die association “ist-ein” passt…). Eine Frage wäre da noch: warum nimmst du nicht bereits vorhandene Logging-Bibliotheken (z.B.: http://blog.tobsen.de/2007/08/logging-bibliotheken.html )?
    Schöne Grüße,
    tobsen

  4. Ja ich muss dir Recht geben, das mit dem “Typ” hört sich sehr nach verkappter Klassenhierarchie an.

    Und deinen Beitrag habe ich mir schon auf die “To-Read”-Liste genommen 😉 Werde mir auf jeden Fall mal die von dir erwähnten Logging-Lösungen anschauen. Daran hatte ich vorher nicht gedacht…

  5. Warum eine eigene log Lösung entwickeln? Das Thema ist doch schon mehrfach, auf hohem Niveau gelöst..

    siehe: http://csharp-source.net/open-source/logging, da gibt es gleich 10 frameworks. wobei log4net sicher das Populärste ist. Das läst sich bequem über ein config file anpassen und unterstützt verschiedene Targets, die sich zur Laufzeit ändern lassen.

    Targets sind (von Telnet über ADO.NET bis hin zur Console, alles was man sich so vorstellen kann;)

    Wobei Deine Implentierung natürlich schick ist 🙂

  6. Interessant! Danke schön für den Link.

    Ich habe mir auch schon ein paar Logging-Bibliotheken angeschaut, aber zumindest die Apache-Lösung ist ja doch enorm umfangreich und für meine Zwecke wohl ein wenig überdimensioniert. Da braucht man ja erstmal ne Weile, bis man überhaupt die Konfiguration verstanden hat 🙂

  7. Der Artikel hat mich inspiriert einen Logger für kleine Applikationen zu schreiben. Einen Logger statisch zu programmieren (“klassenübergreifend”) ist eine super Idee!

    Falls jemand einen MiniLogger braucht der in Textdateien schreibt:

    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.IO;

    class Logger
    {
    private static string file = “log.txt”;

    private Logger(string file)
    {}

    public static void setFile(string file)
    {
    Logger.file = file;
    }

    public static void Write(string Message)
    {
    FileStream fs = new FileStream(file, FileMode.Append, FileAccess.Write);
    StreamWriter Writer = new StreamWriter(fs);

    Writer.Write(Message);
    Writer.Write(Writer.NewLine);

    Writer.Flush();
    Writer.Close();
    }
    public static void Write(string Message, Exception e)
    {
    string Result = Message + ” Exception: “+e.ToString();
    Logger.Write(Result);
    }

    }

  8. Ich hab ein sehr simples aber äußerst mächtiges Logging Tool für .NET 4.0 (programmiert in C#) geschrieben, das ohne Konfiguration benutzt werden kann.

    Beispiel für eine Windows Forms Anwendung:

    Logger.Add(new RichTextBoxLogger(richTextBox1));

    // …

    this.Log(“Hello World!”);

    Output:

    2010-05-10 19:09:25 – INFO @ SomeProject.Form1, Text: Form1 – Hello world!

    (es gibt aber auch vordefinierte logger für normale textfiles, WPF u.a.)

    this.Log… funktioniert über eine extension method für object. Das gilt mancherorts als “böse”, aber das möchte ich hier zur Diskussion stellen.

    Logger, Filter, Message Typen wie ERROR und INFO kann man selbst definieren wenn man will.

    Doku und Download: http://www.matthiasgruber.com/joomla/index.php/downloads

  9. Ich stimme Stefan zu log4Net ist für einfache Sachen überdimensioniert!
    Interessieren würde mich die Lösung von Matthias!
    Der Link scheint aber nicht mehr zu funktionieren!

  10. Ich habe das Beispiel mal übernommen, offensichtlich mache ich im Bereich SetLogger irgendetwas falsch. Ich bekomme keine Ausgabe auf der Console. Wenn ich Einzelschritt mache steigt er beim prüfen if (this._logger != null) aus.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax