Microsoft Extensions Logging

7. Mai 2020

Nach Konfiguration und Dependency Injection müssen wir uns noch mit dem dritten großen Querschnittsthema auseinandersetzen: Dem Logging.

Von System.Diagnostics

Logging ist im klassischen .NET Framework nicht sehr konsistent ausgebaut. Es gibt System.Diagnostics.Trace mit etwas Infrastruktur dahinter, mit WebForms kann man auf Page.Trace zurückgreifen. ServiceBase nutzt das EventLog.

Das hatte alles seinen Platz, aber insbesondere das Schreiben von Logdateien oder in Datenbanken hat sich mit diesen Mechanismen nie breit etabliert. Und damit hat die BCL auch keine einheitliche Lösung für Logging geboten. Diese Nische haben Logging-Frameworks wie log4net und NLog ausgefüllt. Das führte dann zu solchen Fragmenten im eigenen Code (hier mit log4net, aber NLog arbeitet ähnlich):

   1: public class Foo

   2: {

   3:     private static readonly ILog _log = LogManager.GetLogger(typeof(Foo));

   4:

   5:     public Foo()

   6:     {

   7:         _log.Debug("c'tor");

   8:     }

Zusammen mit der notwendigen Konfiguration lassen sich damit Logs mit unterschiedlichen Informationen in unterschiedliche Ziele schreiben.

Der Pferdefuß an dieser Lösung ist, dass sich die Abhängigkeit zu einer Bibliothek quer durch den eigenen Code zieht – ich hatte schon mehr als ein Projekt, in dem sich das später zu einem Problem entwickelt hat.

… zu den Microsoft Extensions

Ähnlich wie bei Dependency Injection basiert der Lösungsansatz der Microsoft Extensions auf einem Mittelweg: Für die Nutzung die notwendigen Schnittstellen zur Verfügung stellen. Und die Umsetzung über ein Provider-Konzept abstrahieren, so dass man bei Bedarf das Logging-Framework seiner Wahl einklinken kann.

Logging ist dabei voll in den Dependency Injection Mechanismus integriert, nutzt für die Anmeldung eines Providers aber wieder das Builder-Pattern, wie schon Konfiguration und Dependency Injection selbst:

   1: var services = new ServiceCollection();

   2:

   3: services.AddLogging(loggingBuilder =>

   4: {

   5:     loggingBuilder.AddConsole();

   6:     loggingBuilder.AddDebug();

   7: });

Für die Nutzung gibt es zwei Wege:

Entweder man lässt sich über über Dependency Injection ein ILoggerFactory geben. Der Methode CreateLogger() kann eine Kategorie übergeben werden, ähnlich wie in log4net, und als Ergebnis bekommt man ein ILogger. Die eigentlichen Logging-Methoden sind als Extension Methoden für diese Schnittstelle definiert.

Der übliche Weg ist aber, sich per Dependency Injection einen ILogger<TCategoryName> geben zu lassen und als TCategory den eigenen Typ anzugeben. Die Kategorie ergibt sich dabei aus dem qualifizierten Klassennamen:

   1: class MyClass

   2: {

   3:     private ILogger _logger;

   4:     private ILogger _sqlLogger;

   5:

   6:     public MyClass(ILogger<MyClass> logger, ILoggerFactory loggerFactory)

   7:     {

   8:         _logger = logger;

   9:         _sqlLogger = loggerFactory.CreateLogger("SQL");

  10:     }

  11: }

Natürlich bietet Logging die üblichen Möglichkeiten bzgl. LogLevel, Konfiguration, Filterung von Ausgaben und mehr; wer schon mit log4net oder ähnlichem gearbeitet hat, wird hier aber keine Überraschungen erleben, weshalb ich es bei den Verweisen belasse.

Konventionen

Wie schon bei den anderen Bereichen gibt es auch beim Logging die Konvention, dass Erweiterungsmethoden, die sich auf ILoggingBuilder oder auf ILogger beziehen, im Namespace Microsoft.Extensions.Logging liegen. Eine andere Konvention ist das Injizieren des generischen ILogger<> mit dem Typ als Kategorie, wie oben dargestellt.

Man kann sich zudem seine eigene Konvention schaffen, zum Beispiel wenn man ein spezielles Log für besondere Inhalte führen will. Wenn man etwa Logger mit Kategorie “SQL” für die Protokollierung von SQL-Aufrufen verwenden will, kann man natürlich über die LoggerFactory einen entsprechenden Logger abrufen, wie im Beispiel oben. Man kann sich aber auch einen eigenen ISqlLogger bereitstellen. Die Schnittstelle benötigt nur einen simplen Wrapper:

   1: public interface ISqlLogger : ILogger { }

   2:

   3: public class SqlLogger : ISqlLogger

   4: {

   5:     private readonly ILogger _logger;

   6:

   7:     public SqlLogger(ILoggerFactory loggerFactory)

   8:     {

   9:         _logger = loggerFactory.CreateLogger("SQL");

  10:     }

  11:

  12:     public bool IsEnabled(LogLevel logLevel)

  13:     {

  14:         return _logger.IsEnabled(logLevel);

  15:     }

  16:     [...]

  17: }

Die passende Registrierung (natürlich im Einklang mit den Konventionen)

   1: namespace Microsoft.Extensions.DependencyInjection

   2: {

   3:     public static class SqlLoggerServiceCollectionExtensions

   4:     {

   5:         public static IServiceCollection AddSqlLogger(this IServiceCollection services)

   6:         {

   7:             services.AddSingleton<ISqlLogger, SqlLogger>();

   8:             return services;

   9:         }

  10:     }

  11: }

Und man kann das Beispiel von oben vereinfachen:

   1: class MyClass

   2: {

   3:     private ILogger _logger;

   4:     private ILogger _sqlLogger;

   5:

   6:     public MyClass(ILogger<MyClass> logger, ISqlLogger sqlLogger)

   7:     {

   8:         _logger = logger;

   9:         _sqlLogger = sqlLogger;

  10:     }

  11: }

Diesen Aufwand wird man natürlich nicht sehr häufig treiben, aber für bestimmte Fälle macht das durchaus Sinn. In einem laufenden Projekt nutze ich das zur Unterscheidung von technischen und fachlichen Logs.

Logger Provider

Wie bei Konfiguration auch, kann man über das Provider-Konzept alternative Provider anbieten. Anders als bei Konfiguration muss man das oft auch tatsächlich tun, denn die Auswahl an Providern in den Microsoft Extensions selbst ist leider etwas beschränkt. So gibt es weder einen File-Logger, noch ein Logging in eine Datenbank. Und die bestehenden Logger sind wenig konfigurierbar. Der Console-Logger liefert die von ASP.NET Core vielleicht bekannte Ausgabe:

image

Aber diese Ausgabe ist vom Aufbau her wie sie ist; konfigurieren lässt sich da nichts.

Einerseits ist das etwas unbefriedigend, andererseits aber auch verständlich. Übermäßige Konfigurierbarkeit wird gerne mal zum Selbstzweck und konkurrierende Zugriffe auf Log-Files sind nicht gerade trivial – selbst bei log4net muss man hier aufpassen.

Es ist also durchaus sinnvoll, sich nach alternativen Providern umzuschauen. Microsoft führt Listen hier und hier. Und wer einen eigenen Provider erstellen will – auch das ist kein Hexenwerk – kann den DebugLoggerProvider als Vorlage verwenden. Der Console-Logger ist hingegen erstaunlich komplex, da er Windows und Linux adressiert und zudem asynchron arbeitet. Wer sich mit einem File-Provider auseinandersetzen will, findet vielleicht bei den Providern für AzureAppServices einige Anregungen.

Fazit

Konzeptionell lässt sich Logging mit Konfiguration und Dependency Injection vergleichen: Trennung von Nutzung und eigentlicher Umsetzung über Schnittstellen, Provider-Konzept für Erweiterbarkeit, Builder-Pattern und Konventionen für Konsistenz. Es stellt – anders als das .NET Framework – eine umfassende Lösung bereit. Und anders als 3rd Party Bibliotheken sorgt die Abstraktion dafür, dass sich die Abhängigkeit zwischen Nutzung und eigentlichem Provider in Grenzen hält.

Das ist umso wichtiger, da die verfügbaren Provider doch sehr limitiert sind. Man wird in vielen Projekten nicht darum herumkommen, eigene Provider zu schreiben oder einen Provider für ein gängiges Logging-Framework zu nutzen.

Hier wie immer die Links zu den Quelltexten und zur Dokumentation:

autor Alexander Jung

Alexander Jung

Chief eXpert Alexander.Jung@sdx-ag.de