Freigeben über


Hochleistungsprotokollierung in .NET

Verwenden Sie für Hochleistungsprotokollierungsszenarien in .NET 6 und höheren Versionen die LoggerMessageAttribute mit Quellcode-Generierung zur Kompilierzeit. Durch die Eliminierung von Boxing, temporären Zuordnungen und dem Parsing von Nachrichtenvorlagen zur Laufzeit bietet dieser Ansatz die beste Leistung.

Die quellengenerierte Protokollierung bietet die folgenden Leistungsvorteile gegenüber Logger-Erweiterungsmethoden, wie z.B. LogInformation und LogDebug:

  • Eliminierung von Boxen: Logger-Erweiterungsmethoden erfordern das "Boxing" (die Konvertierung) von Werttypen, wie z. B. int, in object. Die quellgenerierte Protokollierung vermeidet das Boxen durch die Verwendung stark typisierter Parameter.
  • Analysiert Vorlagen zur Kompilierungszeit: Logger-Erweiterungsmethoden müssen die Nachrichtenvorlage (benannte Formatzeichenfolge) jedes Mal analysieren, wenn eine Protokollnachricht geschrieben wird. Quellgenerierte Protokollierung parst Vorlagen einmal während der Kompilierungszeit.
  • Reduziert Zuordnungen: Der Quellgenerator erstellt optimierten Code, der die Objektzuordnungen und die temporäre Speicherauslastung minimiert.

Die Beispiel-App veranschaulicht leistungsfähige Protokollierungsfunktionen mit einem Arbeitsdienst für die Verarbeitung von Prioritätswarteschlangen. Die App verarbeitet Arbeitsaufgaben in der Prioritätsreihenfolge. Wenn diese Vorgänge auftreten, werden Protokollmeldungen mithilfe der vom Quell generierten Protokollierung generiert.

Tipp

Der gesamte Quellcode des Protokollierungsbeispiels steht im Beispielbrowser zum Download zur Verfügung. Weitere Informationen finden Sie unter Durchsuchen von Codebeispielen: Protokollierung in .NET.

Definieren von Loggernachrichten mit Quellgenerierung

Um leistungsfähige Lognachrichten in .NET 6 und höher zu erstellen, definieren Sie partial-Methoden, die mit LoggerMessageAttribute dekoriert sind. Der Quellgenerator erstellt die Implementierung zur Kompilierzeit.

Grundlegende Protokollierungsmethode

Definieren Sie für eine einfache Protokollnachricht eine Partielle Methode mit dem Attribut, das die Ereignis-ID, Protokollebene und Nachrichtenvorlage angibt:

public static partial class Log
{
    [LoggerMessage(
        EventId = 13,
        Level = LogLevel.Critical,
        Message = "Epic failure processing item!")]
    public static partial void FailedToProcessWorkItem(
        ILogger logger, Exception ex);
}

Die Nachrichtenvorlage verwendet Platzhalter, die mit Methodenparametern gefüllt sind. Platzhalternamen sollten die Vorlagen übergreifend beschreibend und konsistent sein. Sie dienen als Eigenschaftsnamen in strukturierten Protokolldaten. Es wird die Pascal-Schreibweise für Platzhalternamen empfohlen. Beispielsweise {Item}, {DateTime}.

Rufen Sie die Protokollierungsmethode aus Ihrem Code auf. Beispiel: Wenn während der Verarbeitung von Arbeitsaufgaben eine Ausnahme auftritt:

try
{
    // Process work item.
}
catch (Exception ex)
{
    Log.FailedToProcessWorkItem(logger, ex);
}

Dieser Code erzeugt konsolenausgabe wie:

crit: WorkerServiceOptions.Example.Worker[13]
      Epic failure processing item!
      System.Exception: Failed to verify communications.

Protokollierung mit Parametern

Um Parameter an eine Protokollnachricht zu übergeben, fügen Sie sie als Methodenparameter hinzu. Die Parameternamen entsprechen den Platzhaltern in der Nachrichtenvorlage:

public static partial class Log
{
    [LoggerMessage(
        EventId = 1,
        Level = LogLevel.Information,
        Message = "Processing priority item: {Item}")]
    public static partial void PriorityItemProcessed(
        ILogger logger, WorkItem item);
}

Rufen Sie die Methode mit den Protokollier- und Parameterwerten auf:

var workItem = queue.Dequeue();
Log.PriorityItemProcessed(logger, workItem);

Dieser Code erzeugt konsolenausgabe wie:

info: WorkerServiceOptions.Example.Worker[1]
      Processing priority item: Priority-Extreme (50db062a-9732-4418-936d-110549ad79e4): 'Verify communications'

Strukturierte Protokollspeicher können den Ereignisnamen zusammen mit der Ereignis-ID verwenden, um die Protokollierung zu verbessern. Beispielsweise verwendet Serilog den Ereignisnamen.

Definieren des Logger-Nachrichtenbereichs mit der Quellgenerierung

Sie können Protokollbereiche definieren, um eine Reihe von Protokollnachrichten mit zusätzlichem Kontext umzuschließen. Bei der quellgenerierten Protokollierung kombinieren Sie die LoggerMessageAttribute Methoden mit der Standardmethode ILogger.BeginScope .

Aktivieren Sie IncludeScopes im Logger-Abschnitt der Konsole von appsettings.json:

{
    "Logging": {
        "Console": {
            "IncludeScopes": true
        },
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        }
    }
}

Erstellen Sie quellenerzeugte Protokollierungsmethoden, und umschließen Sie sie in einem Scope mithilfe von BeginScope.

public static partial class Log
{
    [LoggerMessage(
        EventId = 1,
        Level = LogLevel.Information,
        Message = "Processing priority item: {Item}")]
    public static partial void PriorityItemProcessed(
        ILogger logger, WorkItem item);
}

Verwenden Sie die Protokollierungsmethode innerhalb eines Bereichs in Ihrem Anwendungscode:

using (_logger.BeginScope("Processing scope, started at: {DateTime}", DateTime.Now))
{
    Log.PriorityItemProcessed(_logger, workItem);
}

Überprüfen Sie die Protokollmeldungen in der Konsolenausgabe der App. Das folgende Ergebnis zeigt die Prioritätsordnung von Protokollnachrichten mit der eingeschlossenen Protokollbereichsnachricht:

info: WorkerServiceOptions.Example.Worker[1]
      => Processing scope, started at: 04/11/2024 11:27:52
      Processing priority item: Priority-Extreme (7d153ef9-8894-4282-836a-8e5e38319fb3): 'Verify communications'

Legacyansatz: LoggerMessage.Define (für .NET Framework und .NET Core 3.1)

Bevor die quellengenerierte Protokollierung in .NET 6 eingeführt wurde, war der empfohlene Ansatz für leistungsstarke Protokollierung, die LoggerMessage.Define Methode zur Erstellung zwischenspeicherbarer Delegates zu verwenden. Obwohl dieser Ansatz weiterhin aus Gründen der Abwärtskompatibilität unterstützt wird, sollte der neue Code stattdessen die vom Quellcode generierte Protokollierung verwenden LoggerMessageAttribute .

Die LoggerMessage Klasse bietet Funktionalität zum Erstellen zwischenspeicherbarer Stellvertretungen, die weniger Objektzuordnungen erfordern und den rechenintensiven Aufwand im Vergleich zu Logger-Erweiterungsmethoden wie LogInformation und LogDebug. LoggerMessage bietet die folgenden Leistungsvorteile gegenüber Logger-Erweiterungsmethoden:

  • Protokollierungserweiterungsmethoden erfordern das Konvertieren von Werttypen wie int in object (sogenanntes Boxing). Das LoggerMessage-Muster verhindert das Konvertieren mithilfe von statischen Action-Feldern und mithilfe von Erweiterungsmethoden mit stark typisierten Parametern.
  • Protokollierungserweiterungsmethoden müssen die Meldungsvorlagen (sogenannte Formatzeichenfolgen) jedes Mal analysieren, wenn eine Protokollmeldung geschrieben wird. LoggerMessage erfordert das Analysieren einer Vorlage nur einmal beim Festlegen der Meldung.

Hinweis

Wenn Sie Code pflegen, der LoggerMessage.Define verwendet, sollten Sie die Migration zur quellegenerierten Protokollierung in Betracht ziehen. Für .NET Framework- oder .NET Core 3.1-Anwendungen verwenden Sie weiterhin LoggerMessage.Define.

Definieren einer Protokolliernachricht

Verwenden Sie Define(LogLevel, EventId, String), um einen Action Delegaten zum Protokollieren einer Nachricht zu erstellen. Define Überladungen ermöglichen das Übergeben von bis zu sechs Typparametern an eine benannte Formatzeichenfolge (Vorlage).

Die für die Define Methode bereitgestellte Zeichenfolge ist eine Vorlage und keine interpolierte Zeichenfolge. Platzhalter werden in der Reihenfolge ausgefüllt, in der die Typen angegeben sind. Platzhalternamen in der Vorlage sollten beschreibend und einheitlich in allen Vorlagen sein. Sie dienen als Eigenschaftsnamen in strukturierten Protokolldaten. Wir empfehlen die Pascal-Schreibweise für Platzhalternamen. Beispielsweise {Item}, {DateTime}.

Jede Protokollnachricht wird Action in einem statischen Feld gespeichert, das von LoggerMessage.Define erstellt wurde. Beispielsweise erstellt die Beispiel-App ein Feld, um eine Protokollmeldung für die Verarbeitung von Arbeitsaufgaben zu beschreiben:

private static readonly Action<ILogger, Exception> s_failedToProcessWorkItem;

Geben Sie für den Action Folgendes an:

  • Die Protokollebene.
  • Ein eindeutiger Ereignisbezeichner (EventId) mit dem Namen der statischen Erweiterungsmethode.
  • Die Nachrichtenvorlage (benannte Formatzeichenfolge).

Wenn Arbeitsaufgaben aus der Warteschlange zur Verarbeitung entfernt werden, legt die Worker-Dienstanwendung fest, dass:

  • Protokollebene bis LogLevel.Critical.
  • Ereignis-ID zu 13 mit dem Namen der FailedToProcessWorkItem Methode.
  • Nachrichtenvorlage (benannte Formatzeichenfolge) in eine Zeichenfolge umwandeln.
s_failedToProcessWorkItem = LoggerMessage.Define(
    LogLevel.Critical,
    new EventId(13, nameof(FailedToProcessWorkItem)),
    "Epic failure processing item!");

Die LoggerMessage.Define Methode wird verwendet, um einen Action Delegaten zu konfigurieren und zu definieren, der eine Protokollmeldung darstellt.

Strukturierte Protokollierungsspeicher können den Ereignisnamen verwenden, wenn dieser zusammen mit der Ereignis-ID bereitgestellt wird, um die Protokollierung zu erweitern. Beispielsweise verwendet Serilog den Ereignisnamen.

Die Action Methode wird über eine stark typisierte Erweiterungsmethode aufgerufen. Die PriorityItemProcessed Methode protokolliert eine Nachricht jedes Mal, wenn eine Arbeitsaufgabe verarbeitet wird. FailedToProcessWorkItem wird aufgerufen, wenn und wann eine Ausnahme auftritt:

protected override async Task ExecuteAsync(
    CancellationToken stoppingToken)
{
    using (IDisposable? scope = logger.ProcessingWorkScope(DateTime.Now))
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                WorkItem? nextItem = priorityQueue.ProcessNextHighestPriority();

                if (nextItem is not null)
                {
                    logger.PriorityItemProcessed(nextItem);
                }
            }
            catch (Exception ex)
            {
                logger.FailedToProcessWorkItem(ex);
            }

            await Task.Delay(1_000, stoppingToken);
        }
    }
}

Überprüfen Sie die Konsolenausgabe der App:

crit: WorkerServiceOptions.Example.Worker[13]
      Epic failure processing item!
      System.Exception: Failed to verify communications.
         at WorkerServiceOptions.Example.Worker.ExecuteAsync(CancellationToken stoppingToken) in
         ..\Worker.cs:line 27

Um Parameter an eine Protokollmeldung zu übergeben, definieren Sie beim Erstellen des statischen Felds bis zu sechs Typen. In der Beispiel-App werden die Arbeitsaufgabendetails protokolliert, wenn Elemente verarbeitet werden, indem ein WorkItem Typ für das Action Feld definiert wird:

private static readonly Action<ILogger, WorkItem, Exception> s_processingPriorityItem;

Die Lognachrichtenvorlage des Delegierten erhält ihre Platzhalterwerte von den bereitgestellten Typen. Die Beispiel-App definiert einen Delegat zum Hinzufügen einer Arbeitsaufgabe, bei der der Elementparameter ein WorkItem:

s_processingPriorityItem = LoggerMessage.Define<WorkItem>(
    LogLevel.Information,
    new EventId(1, nameof(PriorityItemProcessed)),
    "Processing priority item: {Item}");

Die statische Erweiterungsmethode für die Protokollierung, dass ein Arbeitselement verarbeitet wird, PriorityItemProcessed empfängt den Argumentwert des Arbeitselements und übergibt ihn an den Action-Delegaten:

public static void PriorityItemProcessed(
    this ILogger logger, WorkItem workItem) =>
    s_processingPriorityItem(logger, workItem, default!);

In der Methode des Workerdiensts ExecuteAsync, wird PriorityItemProcessed aufgerufen, um die Nachricht zu protokollieren.

protected override async Task ExecuteAsync(
    CancellationToken stoppingToken)
{
    using (IDisposable? scope = logger.ProcessingWorkScope(DateTime.Now))
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                WorkItem? nextItem = priorityQueue.ProcessNextHighestPriority();

                if (nextItem is not null)
                {
                    logger.PriorityItemProcessed(nextItem);
                }
            }
            catch (Exception ex)
            {
                logger.FailedToProcessWorkItem(ex);
            }

            await Task.Delay(1_000, stoppingToken);
        }
    }
}

Überprüfen Sie die Konsolenausgabe der App:

info: WorkerServiceOptions.Example.Worker[1]
      Processing priority item: Priority-Extreme (50db062a-9732-4418-936d-110549ad79e4): 'Verify communications'

Geschützte Optimierungen auf Protokollebene

Sie können die Leistung optimieren, indem Sie den LogLevel mit ILogger.IsEnabled(LogLevel) überprüfen, bevor Sie die entsprechende Log*-Methode aufrufen. Wenn die Protokollierung für das angegebene LogLevel nicht konfiguriert ist, wird ILogger.Log nicht aufgerufen. Darüber hinaus werden Werttyp-Boxen und eine Zuordnung von object[] (zur Darstellung der Parameter) vermieden.

Weitere Informationen finden Sie unter:

Siehe auch