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):
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:
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;
}
}
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
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?
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…
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 🙂
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 🙂
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);
}
}
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
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!
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.