Quellgenerierung der Kompilierzeitprotokollierung

Mit .NET 6 wird der Typ LoggerMessageAttribute eingeführt. Dieses Attribut ist Teil des Namespace Microsoft.Extensions.Logging und führt bei Verwendung eine Quellgenerierung von APIs zur leistungsbasierten Protokollierung durch. Die Protokollierungsunterstützung für die Quellgenerierung wurde entwickelt, um eine hochverwertbare und äußerst leistungsfähige Protokollierungslösung für moderne .NET-Anwendungen zu bieten. Der automatisch generierte Quellcode basiert auf der ILogger-Schnittstelle in Verbindung mit der LoggerMessage.Define-Funktionalität.

Der Quellgenerator wird ausgelöst, wenn LoggerMessageAttribute für partial-Protokollierungsmethoden verwendet wird. Wenn sie ausgelöst wird, kann sie entweder die Implementierung der partial-Methoden automatisch erzeugen, die sie erweitert, oder sie kann Diagnosen zur Kompilierzeit mit Hinweisen zur richtigen Verwendung erstellen. Die Protokollierungslösung zur Kompilierzeit ist in der Regel zur Laufzeit deutlich schneller als vorhandene Protokollierungsansätze. Dies wird erreicht, indem Boxing, temporäre Zuordnungen und Kopien so weit wie möglich beseitigt werden.

Grundlegende Verwendung

Um LoggerMessageAttribute zu verwenden, müssen die nutzende Klasse und Methode partial sein. Der Codegenerator wird zur Kompilierzeit ausgelöst und generiert eine Implementierung der partial-Methode.

public static partial class Log
{
    [LoggerMessage(
        EventId = 0,
        Level = LogLevel.Critical,
        Message = "Could not open socket to `{HostName}`")]
    public static partial void CouldNotOpenSocket(
        ILogger logger, string hostName);
}

Im vorherigen Beispiel ist die Protokollierungsmethode static, und die Protokollebene wird in der Attributdefinition angegeben. Wenn Sie das Attribut in einem statischen Kontext verwenden, ist entweder die ILogger-Instanz als Parameter erforderlich, oder Sie ändern die Definition so, dass sie das this-Schlüsselwort verwendet, um die Methode als Erweiterungsmethode zu definieren.

public static partial class Log
{
    [LoggerMessage(
        EventId = 0,
        Level = LogLevel.Critical,
        Message = "Could not open socket to `{HostName}`")]
    public static partial void CouldNotOpenSocket(
        this ILogger logger, string hostName);
}

Sie können das Attribut auch in einem nicht statischen Kontext verwenden. Betrachten Sie das folgende Beispiel, in dem die Protokollierungsmethode als Instanzmethode deklariert wird. In diesem Kontext ruft die Protokollierungsmethode die Protokollierung ab, indem sie auf ein ILogger-Feld in der enthaltenden Klasse zugreift.

public partial class InstanceLoggingExample
{
    private readonly ILogger _logger;

    public InstanceLoggingExample(ILogger logger)
    {
        _logger = logger;
    }

    [LoggerMessage(
        EventId = 0,
        Level = LogLevel.Critical,
        Message = "Could not open socket to `{HostName}`")]
    public partial void CouldNotOpenSocket(string hostName);
}

Manchmal muss die Protokollebene dynamisch sein, anstatt statisch in den Code integriert zu werden. Sie können dies erreichen, indem Sie den Protokolliergrad aus dem Attribut weglassen und stattdessen als erforderlichen Parameter für die Protokollierungsmethode festlegen.

public static partial class Log
{
    [LoggerMessage(
        EventId = 0,
        Message = "Could not open socket to `{HostName}`")]
    public static partial void CouldNotOpenSocket(
        ILogger logger,
        LogLevel level, /* Dynamic log level as parameter, rather than defined in attribute. */
        string hostName);
}

Sie können die Protokollierungsmeldung weglassen, und String.Empty wird für die Meldung bereitgestellt. Der Zustand enthält die Argumente, die als Schlüssel-Wert-Paare formatiert sind.

using System.Text.Json;
using Microsoft.Extensions.Logging;

using ILoggerFactory loggerFactory = LoggerFactory.Create(
    builder =>
    builder.AddJsonConsole(
        options =>
        options.JsonWriterOptions = new JsonWriterOptions()
        {
            Indented = true
        }));

ILogger<SampleObject> logger = loggerFactory.CreateLogger<SampleObject>();
logger.PlaceOfResidence(logLevel: LogLevel.Information, name: "Liana", city: "Seattle");

readonly file record struct SampleObject { }

public static partial class Log
{
    [LoggerMessage(EventId = 23, Message = "{Name} lives in {City}.")]
    public static partial void PlaceOfResidence(
        this ILogger logger,
        LogLevel logLevel,
        string name,
        string city);
}

Betrachten Sie die Beispielprotokollierungsausgabe bei Verwendung des JsonConsole-Formatierers.

{
  "EventId": 23,
  "LogLevel": "Information",
  "Category": "\u003CProgram\u003EF...9CB42__SampleObject",
  "Message": "Liana lives in Seattle.",
  "State": {
    "Message": "Liana lives in Seattle.",
    "name": "Liana",
    "city": "Seattle",
    "{OriginalFormat}": "{Name} lives in {City}."
  }
}

Einschränkungen für Protokollmethoden

Bei Verwendung von LoggerMessageAttribute für Protokollierungsmethoden müssen einige Einschränkungen beachtet werden:

  • Protokollierungsmethoden müssen partial sein und void zurückgeben.
  • Protokollierungsmethodennamen dürfen nicht mit einem Unterstrich beginnen.
  • Parameternamen von Protokollierungsmethoden dürfen nicht mit einem Unterstrich beginnen.
  • Protokollierungsmethoden dürfen nicht in einem geschachtelten Typ definiert werden.
  • Protokollierungsmethoden dürfen nicht generisch sein.
  • Wenn eine Protokollierungsmethode ist static, ist die ILogger-Instanz als Parameter erforderlich.

Das Codegenerierungsmodell erfordert, dass Code mit einem modernen C#-Compiler (Version 9 oder höher) kompiliert wird. Der C# 9.0-Compiler wurde mit .NET 5 verfügbar. Bearbeiten Sie Ihre Projektdatei so, dass sie auf C# 9.0 ausgerichtet ist, um ein Upgrade auf einen modernen C#-Compiler durchzuführen.

<PropertyGroup>
  <LangVersion>9.0</LangVersion>
</PropertyGroup>

Weitere Informationen finden Sie unter C#-Sprachversionsverwaltung.

Struktur der Protokollierungsmethode

Die ILogger.Log-Signatur akzeptiert LogLevel und optional eine Exception (siehe folgende Darstellung).

public interface ILogger
{
    void Log<TState>(
        Microsoft.Extensions.Logging.LogLevel logLevel,
        Microsoft.Extensions.Logging.EventId eventId,
        TState state,
        System.Exception? exception,
        Func<TState, System.Exception?, string> formatter);
}

Als allgemeine Regel wird die erste Instanz von ILogger, LogLevel und Exception speziell in der Protokollierungsmethodensignatur des Quellgenerators behandelt. Nachfolgende Instanzen werden wie normale Parameter der Meldungsvorlage behandelt:

// This is a valid attribute usage
[LoggerMessage(
    EventId = 110, Level = LogLevel.Debug, Message = "M1 {Ex3} {Ex2}")]
public static partial void ValidLogMethod(
    ILogger logger,
    Exception ex,
    Exception ex2,
    Exception ex3);

// This causes a warning
[LoggerMessage(
    EventId = 0, Level = LogLevel.Debug, Message = "M1 {Ex} {Ex2}")]
public static partial void WarningLogMethod(
    ILogger logger,
    Exception ex,
    Exception ex2);

Wichtig

Die ausgegebenen Warnungen enthalten Details zur richtigen Verwendung von LoggerMessageAttribute. Im vorherigen Beispiel gibt WarningLogMethod einen DiagnosticSeverity.Warning-Wert von SYSLIB0025 an.

Don't include a template for `ex` in the logging message since it is implicitly taken care of.

Unterstützung von Vorlagennamen ohne Unterscheidung nach Groß-/Kleinschreibung

Der Generator vergleicht Elemente in der Meldungsvorlage und Argumentnamen in der Protokollmeldung ohne Unterscheidung von Groß-/Kleinschreibung. Dies bedeutet, dass beim Aufzählen des Zustands durch ILogger das Argument von der Meldungsvorlage verwendet wird, wodurch die Protokolle besser verarbeitet werden können:

public partial class LoggingExample
{
    private readonly ILogger _logger;

    public LoggingExample(ILogger logger)
    {
        _logger = logger;
    }

    [LoggerMessage(
        EventId = 10,
        Level = LogLevel.Information,
        Message = "Welcome to {City} {Province}!")]
    public partial void LogMethodSupportsPascalCasingOfNames(
        string city, string province);

    public void TestLogging()
    {
        LogMethodSupportsPascalCasingOfNames("Vancouver", "BC");
    }
}

Betrachten Sie die Beispielprotokollierungsausgabe bei Verwendung des JsonConsole-Formatierers:

{
  "EventId": 13,
  "LogLevel": "Information",
  "Category": "LoggingExample",
  "Message": "Welcome to Vancouver BC!",
  "State": {
    "Message": "Welcome to Vancouver BC!",
    "City": "Vancouver",
    "Province": "BC",
    "{OriginalFormat}": "Welcome to {City} {Province}!"
  }
}

Unbestimmte Parameterreihenfolge

Es gibt keine Einschränkungen für die Reihenfolge von Protokollierungsmethodenparametern. Ein Entwickler könnte ILogger als letzten Parameter definieren, obwohl das etwas umständlich erscheinen kann.

[LoggerMessage(
    EventId = 110,
    Level = LogLevel.Debug,
    Message = "M1 {Ex3} {Ex2}")]
static partial void LogMethod(
    Exception ex,
    Exception ex2,
    Exception ex3,
    ILogger logger);

Tipp

Die Reihenfolge der Parameter für eine Protokollierungsmethode muss nicht der Reihenfolge der Vorlagenplatzhalter entsprechen. Stattdessen wird erwartet, dass die Platzhalternamen in der Vorlage mit den Parametern übereinstimmen. Betrachten Sie die folgende JsonConsole-Ausgabe und die Reihenfolge der Fehler.

{
  "EventId": 110,
  "LogLevel": "Debug",
  "Category": "ConsoleApp.Program",
  "Message": "M1 System.Exception: Third time's the charm. System.Exception: This is the second error.",
  "State": {
    "Message": "M1 System.Exception: Third time's the charm. System.Exception: This is the second error.",
    "ex2": "System.Exception: This is the second error.",
    "ex3": "System.Exception: Third time's the charm.",
    "{OriginalFormat}": "M1 {Ex3} {Ex2}"
  }
}

Zusätzliche Protokollierungsbeispiele

In den folgenden Beispielen wird veranschaulicht, wie Sie den Ereignisnamen abrufen, den Protokolliergrad dynamisch festlegen und Protokollierungsparameter formatieren. Die Protokollierungsmethoden sind:

  • LogWithCustomEventName: Abrufen des Ereignisnamens über das Attribut LoggerMessage
  • LogWithDynamicLogLevel: Dynamisches Festlegen der Protokollebene, damit die Protokollebene basierend auf Konfigurationseingaben festgelegt werden kann
  • UsingFormatSpecifier: Verwenden von Formatbezeichnern zum Formatieren von Protokollierungsparametern
public partial class LoggingSample
{
    private readonly ILogger _logger;

    public LoggingSample(ILogger logger)
    {
        _logger = logger;
    }

    [LoggerMessage(
        EventId = 20,
        Level = LogLevel.Critical,
        Message = "Value is {Value:E}")]
    public static partial void UsingFormatSpecifier(
        ILogger logger, double value);

    [LoggerMessage(
        EventId = 9,
        Level = LogLevel.Trace,
        Message = "Fixed message",
        EventName = "CustomEventName")]
    public partial void LogWithCustomEventName();

    [LoggerMessage(
        EventId = 10,
        Message = "Welcome to {City} {Province}!")]
    public partial void LogWithDynamicLogLevel(
        string city, LogLevel level, string province);

    public void TestLogging()
    {
        LogWithCustomEventName();

        LogWithDynamicLogLevel("Vancouver", LogLevel.Warning, "BC");
        LogWithDynamicLogLevel("Vancouver", LogLevel.Information, "BC");

        UsingFormatSpecifier(logger, 12345.6789);
    }
}

Betrachten Sie die Beispielprotokollierungsausgabe bei Verwendung des SimpleConsole-Formatierers:

trce: LoggingExample[9]
      Fixed message
warn: LoggingExample[10]
      Welcome to Vancouver BC!
info: LoggingExample[10]
      Welcome to Vancouver BC!
crit: LoggingExample[20]
      Value is 1.234568E+004

Betrachten Sie die Beispielprotokollierungsausgabe bei Verwendung des JsonConsole-Formatierers:

{
  "EventId": 9,
  "LogLevel": "Trace",
  "Category": "LoggingExample",
  "Message": "Fixed message",
  "State": {
    "Message": "Fixed message",
    "{OriginalFormat}": "Fixed message"
  }
}
{
  "EventId": 10,
  "LogLevel": "Warning",
  "Category": "LoggingExample",
  "Message": "Welcome to Vancouver BC!",
  "State": {
    "Message": "Welcome to Vancouver BC!",
    "city": "Vancouver",
    "province": "BC",
    "{OriginalFormat}": "Welcome to {City} {Province}!"
  }
}
{
  "EventId": 10,
  "LogLevel": "Information",
  "Category": "LoggingExample",
  "Message": "Welcome to Vancouver BC!",
  "State": {
    "Message": "Welcome to Vancouver BC!",
    "city": "Vancouver",
    "province": "BC",
    "{OriginalFormat}": "Welcome to {City} {Province}!"
  }
}
{
  "EventId": 20,
  "LogLevel": "Critical",
  "Category": "LoggingExample",
  "Message": "Value is 1.234568E+004",
  "State": {
    "Message": "Value is 1.234568E+004",
    "value": 12345.6789,
    "{OriginalFormat}": "Value is {Value:E}"
  }
}

Zusammenfassung

Mit der Einführung von C#-Quellgeneratoren ist das Schreiben von hochleistungsfähigen Protokollierungs-APIs viel einfacher. Die Verwendung des Quellgeneratoransatzes hat mehrere wichtige Vorteile:

  • Ermöglicht das Beibehalten der Protokollierungsstruktur und die genaue Formatsyntax, die für Meldungsvorlagen erforderlich ist
  • Ermöglicht das Angeben alternativer Namen für die Vorlagenplatzhalter und die Verwendung von Formatbezeichnern
  • Ermöglicht die Übergabe aller unveränderten Originaldaten ohne Komplikationen bei der Art ihrer Speicherung, bevor sie in irgendeiner Weise verarbeitet werden (anders als das Erstellen einer string).
  • Stellt protokollierungsspezifische Diagnosen bereit und gibt Warnungen für doppelte Ereignis-IDs aus.

Darüber hinaus gibt es Vorteile gegenüber der manuellen Verwendung von LoggerMessage.Define:

  • Kürzere und einfachere Syntax: deklarative Attributverwendung anstelle von Codebausteinen
  • Geführter Entwicklungsprozess: Der Generator gibt Warnungen aus, um Fehler bei der Entwicklung zu vermeiden.
  • Unterstützung für eine beliebige Anzahl von Protokollierungsparametern LoggerMessage.Define unterstützt maximal sechs.
  • Unterstützung für dynamische Protokollebene Dies ist heute noch nicht mit LoggerMessage.Define allein möglich.

Siehe auch