C#: Klassenübergreifendes Logging

URL dieses Beitrags: http://blog.stefan-macke.com/2007/08/16/c-klassenuebergreifendes-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

  1. /// <summary>
  2. /// Ein global verfügbarer Logger.
  3. /// </summary>
  4. public sealed class Logger
  5. {
  6.         #region Statische Member
  7.         private static readonly Logger _instanz = new Logger();
  8.  
  9.         /// <summary>
  10.         /// Loggt die übergebene Meldung.
  11.         /// </summary>
  12.         /// <param name="typ">Typ der Meldung.</param>
  13.         /// <param name="meldung">Text der Meldung.</param>
  14.         public static void Log(LogEintragTyp typ, string meldung)
  15.         {
  16.                 _instanz.NeueMeldung(typ, meldung);
  17.         }
  18.  
  19.         /// <summary>
  20.         /// Setzt den Logger, der zur Ausgabe der Meldungen verwendet werden soll.
  21.         /// </summary>
  22.         /// <param name="logger"></param>
  23.         public static void SetLogger(ILogger logger)
  24.         {
  25.                 _instanz._logger = logger;
  26.         }
  27.         #endregion
  28.  
  29.         #region Instanzmember
  30.         private ILogger _logger = null;
  31.         private List<LogEintrag> _eintraege = new List<LogEintrag>();
  32.  
  33.         private void NeueMeldung(LogEintragTyp typ, string meldung)
  34.         {
  35.                 LogEintrag le = new LogEintrag(typ, meldung);
  36.                 this._eintraege.Add(le);
  37.                 if (this._logger != null)
  38.                         this._logger.NeueMeldung(le);
  39.         }
  40.         #endregion
  41. }

Logger-Implementierung

  1. public interface ILogger
  2. {
  3.         /// <summary>
  4.         /// Wird aufgerufen, sobald eine neue Meldung geloggt wird.
  5.         /// </summary>
  6.         /// <param name="meldung">Die neue Meldung.</param>
  7.         void NeueMeldung(LogEintrag meldung);
  8.  
  9.         /// <summary>
  10.         /// Gibt die Liste aller geloggten Meldungen zurück.
  11.         /// </summary>
  12.         /// <returns>Die Liste aller geloggten Meldungen.</returns>
  13.         List<LogEintrag> GetMeldungen();
  14. }

Zur farbigen Ausgabe auf der Konsole habe ich die hier beschriebene Klasse verwendet: Putting colour/color to work on the console.

  1. /// <summary>
  2. /// Ein Logger, der die Meldungen lediglich (farbig) auf der Konsole ausgibt.
  3. /// </summary>
  4. public class ConsoleLogger : ILogger
  5. {
  6.         #region ConsoleColor
  7.         /// <summary>
  8.         /// Static class for console colour manipulation.
  9.         /// (http://www.codeproject.com/csharp/console_apps__colour_text.asp)
  10.         /// </summary>
  11.         private class ConsoleColor
  12.         {
  13.           // source code of ConsoleColour from http://www.codeproject.com/csharp/console_apps__colour_text.asp
  14.         }
  15.         #endregion
  16.  
  17.         #region ILogger Member
  18.         /// <summary>
  19.         /// Gibt eine neue Meldung auf der Konsole aus.
  20.         /// </summary>
  21.         /// <param name="meldung">Die neue Meldung.</param>
  22.         public void NeueMeldung(LogEintrag meldung)
  23.         {
  24.                 switch (meldung.Typ)
  25.                 {
  26.                         case LogEintragTyp.Erfolg:
  27.                                 ConsoleColor.SetForeGroundColour(ConsoleColor.ForeGroundColour.Green);
  28.                                 break;
  29.                         case LogEintragTyp.Fehler:
  30.                                 ConsoleColor.SetForeGroundColour(ConsoleColor.ForeGroundColour.Red);
  31.                                 break;
  32.                         default:
  33.                                 break;
  34.                 }
  35.  
  36.                 Console.WriteLine(meldung.Zeitpunkt.ToString("HH:mm:ss.ffffff") + " " + meldung.Typ.ToString() + ": " + meldung.Text);
  37.                 ConsoleColor.SetForeGroundColour();
  38.         }
  39.  
  40.         /// <summary>
  41.         /// Nicht implementiert.
  42.         /// </summary>
  43.         /// <returns>-</returns>
  44.         public List<LogEintrag> GetMeldungen()
  45.         {
  46.                 throw new Exception("The method or operation is not implemented.");
  47.         }
  48.         #endregion
  49. }

Log-Einträge

  1. /// <summary>
  2. /// Mögliche Typen eines Log-Eintrags.
  3. /// </summary>
  4. public enum LogEintragTyp
  5. {
  6.         /// <summary>
  7.         /// Erfolgsmeldung.
  8.         /// </summary>
  9.         Erfolg,
  10.         /// <summary>
  11.         /// Fehlermeldung.
  12.         /// </summary>
  13.         Fehler,
  14.         /// <summary>
  15.         /// Hinweis.
  16.         /// </summary>
  17.         Hinweis
  18. }
  19. /// <summary>
  20. /// Eine Meldung für das Log.
  21. /// </summary>
  22. public struct LogEintrag
  23. {
  24.         /// <summary>
  25.         /// Der Typ der aktuellen Meldung.
  26.         /// </summary>
  27.         public LogEintragTyp Typ;
  28.        
  29.         /// <summary>
  30.         /// Der Text der aktuellen Meldung.
  31.         /// </summary>
  32.         public string Text;
  33.  
  34.         /// <summary>
  35.         /// Zeitpunkt zu dem die Meldung erstellt wurde.
  36.         /// </summary>
  37.         public DateTime Zeitpunkt;
  38.  
  39.         /// <summary>
  40.         /// Konstruktor.
  41.         /// </summary>
  42.         /// <param name="typ">Der Typ der neuen Meldung.</param>
  43.         /// <param name="text">Der Text der neuen Meldung.</param>
  44.         public LogEintrag(LogEintragTyp typ, string text)
  45.         {
  46.                 this.Typ = typ;
  47.                 this.Text = text;
  48.                 this.Zeitpunkt = DateTime.Now;
  49.         }
  50.  
  51.         /// <summary>
  52.         /// Gibt Typ und Text der aktuellen Meldung aus.
  53.         /// </summary>
  54.         /// <returns>Typ und Text der aktuellen Meldung.</returns>
  55.         public override string ToString()
  56.         {
  57.                 return this.Typ.ToString() + ": " + this.Text;
  58.         }
  59. }

Kommentare zu diesem Beitrag

  1. Gravatar tobsen
    Am 18. August 2007 um 14:34 Uhr

    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. Gravatar Stefan
    Am 18. August 2007 um 17:19 Uhr

    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. Gravatar tobsen
    Am 19. August 2007 um 12:30 Uhr

    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. Gravatar Stefan
    Am 19. August 2007 um 15:20 Uhr

    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. Gravatar robert
    Am 7. September 2007 um 06:31 Uhr

    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. Gravatar Stefan
    Am 7. September 2007 um 12:47 Uhr

    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. Gravatar Martin
    Am 23. April 2009 um 15:23 Uhr

    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. Gravatar Matthias
    Am 11. Mai 2010 um 10:13 Uhr

    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. Gravatar Steve
    Am 22. Mai 2012 um 16:59 Uhr

    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. Gravatar Michael
    Am 30. März 2013 um 14:35 Uhr

    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.

Einen Kommentar schreiben

XHTML: Diese Tags sind erlaubt: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>