log4net

Log4Net w pigułce4 stycznia 2016

Każdy system informatyczny na pewnym etapie zaawansowania i rozproszenia wymaga logowania informacji o tym co wykonuje. Czasami potrzeba wynika z wyłapania jakiegoś błędu, innym razem w grę wchodzą kwestie bezpieczeństwa, niekiedy analizy wymaga wydajność lub po prostu z różnych powodów zapisujemy historię zmian obiektu. Między innymi z tych powodów powstały takie biblioteki jak log4net, NLog, Enterprise Library Logging i inne. Dzisiaj na podstawie pierwszej z nich utworzę bibliotekę pozwalającą na przenoszenie między projektami. Prezentacja przeznaczona jest dla programistów, którzy mieli już do czynienia z biblioteką log4net, gdyż nie traktuje o podstawach i zasadach działania tej biblioteki.

Zaczynam od utworzenia nowej solucji WinForms. Niech nazywa się MyLoggingApplication. Tak naprawdę do działania log4net wystarczy biblioteka klas, ale dla potrzeb prezentacji wykorzystam właśnie WinForms. Finalnie utworzoną bibliotekę będzie można podpiąć do projektu napisanego w dowolnej technologii .NET np. WPF, WCF, ASP.NET. Pod podanym linkiem znaleźć można aktualnie obsługiwane wersje .NET Framework przez bibliotekę log4net. Ja użyję wersji 4.5.

Na głównym oknie utworzę dwa przyciski i wieloliniowe pole tekstowe.

zdjecie Form1 log4net
 

Pierwszy przycisk posłuży mi do wygenerowania błędu, a drugi do zalogowania tekstu wpisanego w pole tekstowe. Na chwilę obecną zrobiłem dwa eventy mouseclick. W jednym z nich rzucam niezaimplementowany wyjątek, który będzie później zalogowany, a w drugim tekst, który będę chciał zapisać.

namespace MyLoggingApplication
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

private void button1_Click(object sender, EventArgs e)
{
throw new NotImplementedException();
}

private void button2_Click(object sender, EventArgs e)
{
//log message later
}
}
}

Celem jest napisanie własnej rozszerzalnej pigułki, którą bez kłopotu będę mógł przenosić między projektami i solucjami, oraz która pozwoli mi na dokonanie zmian bez konieczności ponownego kompilowania całej solucji. Do solucji dodaję nowy projekt bibliotekę klas. Niech nazywa się ApplicationLogger.
Aby można było cokolwiek logować, trzeba ściągnąć bibliotekę log4net. Prezentowana solucja utworzona jest w Visual Studio 2012, dlatego do ściągnięcia log4net’a wykorzystam system dystrybucji bibliotek NuGet. Dla starszych wersji Visual Studio, odpowiedniej wersji biblioteki log4net należy szukać pod tym adresem.

 

zdjecie NuGetPackage
 

Bibliotekę instaluję tylko w projekcie ApplicationLogger:

 

zdjecie SelectProjects
 

Chcąc uniezależnić solucję od biblioteki log4net, muszę utworzyć własny interfejs, który z pomocą wzorca projektowego Adapter będzie udostępniał metody i właściwości interfejsu ILog z biblioteki log4net.

 

zdjecie ILog
 

W tym celu muszę przepisać wszystkie metody i właściwości widoczne na zdjęciu powyżej do własnego interfejsu. Niech nazywa się ILogger.

namespace ApplicationLogger
{
public interface ILogger
{
bool IsDebugEnabled { get; }
bool IsErrorEnabled { get; }
bool IsFatalEnabled { get; }
bool IsInfoEnabled { get; }
bool IsWarnEnabled { get; }

void Debug(object message);
void Debug(object message, Exception exception);
void DebugFormat(string format, object arg0);
//...
}
}

Następnym krokiem będzie implementacja interfejsu oraz strukturalnego wzorca Adapter. Pozwoli on na wywołanie metod biblioteki log4net „schowanych” przed innymi projektami i w tej sposób niedopinanie referencji log4net do każdego projektu solucji. Tworzę klasę LoggerAdapter, która dziedziczy z interfejsu ILogger oraz implementuję ten interfejs w sposób jak w przykładzie poniżej.

namespace ApplicationLogger
{
class LoggerAdapter : ILogger
{
private readonly ILog log;

internal LoggerAdapter(ILog log)
{
this.log = log;
}
#region ILogger Members

public bool IsDebugEnabled
{
get { return log.IsDebugEnabled; }
}

public bool IsErrorEnabled
{
get { return log.IsErrorEnabled; }
}
//...
}
}

Należy zwrócić uwagę, że konstruktor klasy LoggerAdapter jest dostępny z modyfikatorem wewnętrznym. Potrzebuję go używać w projekcie ApplicationLogger (implementacja w kolejnych krokach), ale nie chcę, aby ktoś go umyślnie lub nie użył w innym projekcie (wymagałoby to referencji do interfejsu ILog i finalnie do biblioteki log4net w projekcie, który miał nie widzieć tej biblioteki).
Powyższy konstruktor wymaga parametru z typem ILog. Uzyskam go wykorzystując jedną z metod GetLogger dostępnych w bibliotece log4net. Dla mojej aplikacji testowej niech to będzie przeciążenie z parametrem System.Type.

 

zdjecie LogManager
 

Pamiętając jednak, że nie chcę bezpośrednio pokazywać zewnętrznym projektom zawartości tej biblioteki, muszę utworzyć własnego menedżera. Na powyższym zdjęciu widać, że klasa LogManager jest sealed i nie pozwala na dziedziczenie z niej. Utworzę więc własny interfejs, który będzie udostępniał moją metodę GetLogger i mojego menedżera implementującego metodę GetLogger z biblioteki log4net.

Tworzę więc interfejs ILoggingManager i klasę LoggingManager. Zawartość interfejsu to tylko jedna metoda GetLogger.

namespace ApplicationLogger
{
public interface ILoggingManager
{
ILogger GetLogger(Type type);
}
}

Klasa LoggingManager dziedziczy po interfejsie ILoggingManager. W przykładzie poniżej widać zmienną loggingManager, która w statycznym konstruktorze klasy LoggingManager odczytuje konfigurację z pliku i tworzy nowy obiekt dla zmiennej loggingManager. Dalej widać statyczną metodę GetLogger. Jest to metoda, której będę używał do zalogowania błędu. Na końcu jest metoda, która poprzez utworzony wcześniej adapter komunikuje się z biblioteką log4net.

namespace ApplicationLogger
{
public class LoggingManager : ILoggingManager
{
static readonly ILoggingManager loggingManager;
static LoggingManager()
{
log4net.Config.XmlConfigurator.ConfigureAndWatch(new FileInfo("ApplicationLogger.config"));
loggingManager = new LoggingManager();
}

public static ILogger GetLogger()
{
return loggingManager.GetLogger(typeof(T));
}

public ILogger GetLogger(Type type)
{
var logger = log4net.LogManager.GetLogger(type);
return new LoggerAdapter(logger);
}
}
}

Moja konfiguracja będzie używała AdoNetAppender. Jednakże oprócz standardowych wartości uzupełnianych przez log4net chciałbym również, aby zalogowła się informacja o użytkowniku aplikacji. W tym celu do klasy LoggingManager dodam statyczną metodę rejestrującą dodatkowe parametry do zalogowania.

public static void SetColumnData(string key, object value)
{
log4net.GlobalContext.Properties[key] = value;
}

Do zalogowania potrzebna mi będzie baza. Struktura mojej bazy wygląda następująco:

 

zdjecie StrukturaBazy
 

Definiuję konfigurację appendera.
Warto zwrócić uwagę, że dodatkowa kolumna z użytkownikiem w definicji parametrów w tagu conversionPattern musi mieć taką samą nazwę jak nazwa klucza w metodzie SetColumnData. W mojej testowej aplikacji będzie to klucz User.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
</configSections>

<log4net>
<appender name="SqlAppender" type="log4net.Appender.AdoNetAppender">
<bufferSize value="1" />
<connectionType value="System.Data.SqlClient.SqlConnection, System.Data, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<connectionString value="data source=.;initial catalog=Log4netDB; Integrated Security = true" /> <commandText value="INSERT INTO ApplicationLog ([Date],[LogLevel],[Logger],[Message],[Exception],[User]) VALUES (@log_date, @log_level, @logger, @message, @exception, @user)" />

<!-- standard log4net parameters -->

<parameter>
<parameterName value="@user" />
<dbType value="String" />
<size value="100" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%property{User}" />
</layout>
</parameter>
</appender>
<root>
<level value="ALL"/>
<appender-ref ref="SqlAppender"/>
</root>
</log4net>
</configuration>

Abym nie musiał pisać dużo kodu podczas logowania błędu, utworzyłem sobię pomocniczą klasę GenericLoggingExtensions. Pozwoli mi ona skrócić kod zapisujący błąd.

namespace ApplicationLogger
{
public static class GenericLoggingExtensions
{
public static ILogger LogException(this T thing)
{
return LoggingManager.GetLogger();
}
}
}

 

W przypadku statycznych klas rozszerzenie to nie będzie użyteczne. Poniższy przykład prezentuje dwa sposoby implementacji logowania.

namespace MyLoggingApplication
{
public partial class Form1 : Form
{
private static readonly ILogger log = LoggingManager.GetLogger();

string UserFirstName = "Mark";
string UserLastName = "Anthony";

public Form1()
{
InitializeComponent();
LoggingManager.SetColumnData("User", UserFirstName + " " + UserLastName);
}

private void button1_Click(object sender, EventArgs e)
{
log.Error("Critical error message", new Exception());
}

private void button2_Click(object sender, EventArgs e)
{
this.LogException().Debug(textBox1.Text);
}

}
}

 

Dla użycia w klasach statycznych należy zadeklarować statyczną zmienną tylko do odczytu, jak powyżej, podając w metodzie GetLogger typ klasy, w której jest odpalana. W klasach, które maja swoją instancję wystarczy użyć this.LogException() i po kropce podać jaki ma być typ błędu. Należy tylko dodać do klasy namespace ApplicationLogger, aby rozszerzenie było widoczne w danej klasie.

Wynik działania wygląda następująco:

zdjecie Wynik
 

Projekt razem z przykładową bazą są dostępne na repozytorium github.

Interesujesz się programowaniem i tworzeniem dedykowanych aplikacji?

Pozostań z nami w kontakcie - polub nas w mediach społecznościowych!

Odwiedź nasz profil na FB!
Odwiedź nasz profil na FB!
Zobacz nasze filmy na YT!
Odwiedź nasz profil na LI!

Napisz komentarz

Zobacz inne wpisy: