Shuffleco.de

Lost in shuffled code

May 16, 2017 - 5 minute read - .Net C# WCF NLog

zentralisiertes Logging mit NLog und WCF

1. Die Grundidee

Ich möchte mich etwas näher mit NLog beschäftigen und habe dabei folgenden erdachten UseCase: Mehrere Anwendungen in einer Domäne nutzen NLog zum Loggen von Fehlern und dergleichen. Ich möchte nun die Logs an zentraler Stelle Sichten und auswerten können.

Eine Datenbank soll die Logs schnell durchsuchbar machen und basis für weitergehende Auswertungen sein. Anwendungsstabilität, Laufzeit, Nutzungsdauer oder so sind da mögliche Metriken.

Ein Web Dashboard soll auf einen Blick den Stand der “Lage” wiedergeben und einlaufende Meldungen in Echtzeit darstellen. Das Dashboard soll zunächt mal eine Tabelle der letzten 10 Fehlermeldungen und ein Liniendiagramm enthalten, das die Anzahl der Fehlermeldungen nach Typ auf einer Zeitachse aufträgt.

NLog uebersicht

Nlog bietet von Haus aus die Möglichkeit zu verschienden ‘Targets’ genannten Endpunkten zu loggen. Neben den üblichen Verdächtigen wie Loggen in die Console oder in ein LogFile unterstützt NLog auch das Loggen zu einem LogReceiverService genannten Target. Dieses Target verfügt über eine Schnittstelle, die als ServiceContract eines WCF Services dienen kann. Doch dazu unten mehr.

Als Framework für die WebApp habe ich mich für Microsofts SignalR entschieden, einfach weil ich damit bisher noch nichts gemacht habe, die Specs scheinen mir aber für meinen Use-Case zu passen.

2. Datenbank + Dienst

Da ich die Logmeldungen in einer Datenbank ablegen möchte, richte ich die erst einmal ein. Die Attribute orientieren sich an den von Nlog vorgegebenen Daten. Hierfür gibt es hierfür spezielle Layout Renderer. Das sind einfache Makros die später in der Nlog.config zu den Attributen gemappt werden.

Als Basis für den Dienst verwende ich einen verwalteten Windows Service. Folgende NuGet Pakete installiere ich:

    PM > Install-Package Nlog
    PM > Install-Package Nlog.config
    PM > Install-Package MySql.Data

Nach der Installation verfügt das Projekt über eine NLog.Config, in der ich die Datenbank als Logtarget eintragen kann.

Hier hinterlege ich neben dem Connectionstring auch den Commandtext enthalten, mit dem die Logs in die DB eingefügt werden. Hierfür ist innerhalb des <target> Elements ein <commandtext> Element vorgesehen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<target name="database" xsi:type="Database" 
        dbProvider="MySql.Data.MySqlClient.MySqlConnection, MySql.Data"
        connectionString="server=localhost;Database=nlog;user id=****;password=****">
        <commandText>
          insert into nlog.log (
          Application, Date, Level, Message,
          Logger, CallSite, Exception
          ) values (
          @Application, @Date, @Level, @Message,
          @Logger, @Callsite, @Exception
          );
        </commandText>
        <parameter name="@exception" layout="${exception:tostring}" />
        ...

Ein weiteres LogTarget in einer FallbackGroup sorgt dafür das keine Logs verloren gehen, sollte die Verbindung zur Datenbank einmal fehlschlagen. Diesen Mechanismus werde ich später auch in den Clients anwenden. Auch dort kann dann das klassische Logging in eine Datei alls Fallback für einen nicht erreichbaren LogServer dienen.

vereinfachte Konfiguration

Ohne viel in der app.config herumzuschrauben, nutze ich die vereinfachte Konfiguration für WCF, die ab .Net Framework 4.6.1 möglich ist. Hier wird einfach das <service> Element in der Konfiguration nicht mehr vorausgesetz. Danke Microsoft!

If you do not add a section or add any endpoints in a section and your service does not programmatically define any endpoints, then a set of default endpoints are automatically added to your service, one for each service base address …

Somit ist ist die Konfiguration für einen funktionierenen Dienst mit wenigen Zeilen Code erledigt. Der ServiceTyp “typeof(NLogServer)” ist die Klasse, die die Serviceschnittstelle Implementiert. Diese ist durch Nlog vorgegeben. Die beiden Uris sind die base addresses, für die das WCF Framework die Endpunkte definieren wird.

Manuell könnte man die Konfiguration zum Beispiel vornehmen indem man innerhalb der Klasse NLogServer die Methode Configure Implementiert, diese wird vom Framework automatisch aufgerufen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public static void Configure(ServiceConfiguration config)
{
    ServiceEndpoint se = new ServiceEndpoint(
       new ContractDescription("ILogReceiverServer"), 
       new BasicHttpBinding(), 
       new EndpointAddress("")
    );
    se.Behaviors.Add(new MyEndpointBehavior());
    config.AddServiceEndpoint(se);
    config.Description.Behaviors.Add(new ServiceMetadataBehavior { HttpGetEnabled = true });
}

Intuitiver fand ich aber den Ansatz die Endpunkte und Konfiguration auf Default zu belassen und Änderungen ggfs. in der app.config zu überschreiben. Ich gebe also 2 Addressen an und verlasse mich im wesentlichen auf das Framework.

1
2
3
4
5
6
_host = new ServiceHost(
    typeof(NLogServer),
    new Uri("http://localhost:9999"), 
    new Uri("net.tcp://localhost:9998")
);
_host.Open();

Die beiden URis könnte ich später für je einen anderen Zweck nutzen, den TCP Endpoint für Applikationen innerhalb des LANs, der http Endpoint könnte apäter externen Instanzen als Logtarget dienen.

http://localhost:9999

net.tcp://localhost:9998

Um die Metadaten innerhalb der Umgebung (angenommen das ist kein Sicherheitsrisiko) zu veröffentlichen, muss ich eine Einstellung in der App.Config Datei vornehmen um das Verhalten des WCF Dienstes meinem Wunsch entsprechend anzupassen. In der Default Konfiguration ist die Infoseite auch aktiviert und nachdem der Service gestartet wurde, kann ich im Browser schon sehen ob alles läuft.

http://localhost:9999

zeigt mir die Infoseite des Dienstes an. Erfolg!

wcf_browser.png

3. Nlog in den Applikationen konfigurieren

Die Applikationen, die NLog bereits nutzen müssen in der vorhandenen config erweitert werden. Das bestehende Log wird um den Service als Target erweitert, innerhalb einer FallbackGroup gekapselt: Die Logrule kann entsprechend angepasst werden um nur Events mit speziellem Level an den Service zu senden. In diesem Fall möchte ich ab Debug aufwärts alles an den WCF dienst senden.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<targets>
    <target xsi:type="FallbackGroup" name="loggingtarget">
         <target xsi:type="LogReceiverService"
            name="NLogServer"
            endpointAddress="http://localhost:9999"
            useBinaryEncoding="false"
            clientId="demo1"
            includeEventProperties="true">
         </target>
      <target xsi:type="File" fileName="log.txt"  name="fileLogger"/>
    </target>
</targets>
<rules>
<logger name="*" minlevel="Debug" writeTo="loggingtarget" />
</rules>

Das ist schon alles! Um das Setup zu testen schreibe ich eine Konsolen-Applikation das nichts tut ausser zu Loggen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static void Main(string[] args)
{
 logger.Trace("trace" );
 logger.Debug("debug");
 logger.Info("info");
 logger.Warn("warning");
 logger.Error("error" );
 logger.Fatal("fatal error");
 logger.Log(LogLevel.Info, " informational ");
 logger.Log(LogLevel.Info,"fooo!");
}

Die erste Meldung wird gemäß unserer Rule nicht an den Service gesendet. Die anderen kann ich kurze Zeit später kann ich in der Datenbank des WCF Service finden:

Datenbank

Das war einfacher als gedacht. Ich denke das es durchaus Sinn macht sich nicht auf die Webservice Variante zu verlassen, sondern immer zusätzlich ein Fallback zu konfigurieren. Die in NLog eingebauten Mechanismen und Routingfunktionen helfen hierbei und sind Flexibel.

Die SignalR WebApp ist noch nicht fertig, die werde ich bei Gelegenheit Nachbloggen :)