Condividi tramite


Registrazione ad alte prestazioni in .NET

Per scenari di registrazione ad alte prestazioni in .NET 6 e versioni successive, usare LoggerMessageAttribute con la generazione di origine in fase di compilazione. Questo approccio offre prestazioni ottimali eliminando boxing, allocazioni temporanee e parsing dei modelli di messaggio in fase di esecuzione.

La registrazione generata dall'origine offre i vantaggi seguenti per le prestazioni rispetto ai metodi di estensione del logger, ad esempio LogInformation e LogDebug:

  • Elimina il boxing: I metodi di estensione del logger richiedono il "boxing" (conversione) di tipi di valore, come int, in object. Il log generato dal codice sorgente evita il boxing usando parametri fortemente tipizzati.
  • Analizza i modelli in fase di compilazione: I metodi di estensione logger devono analizzare il modello di messaggio (stringa di formato denominato) ogni volta che viene scritto un messaggio di log. La registrazione generata dall'origine analizza i modelli una sola volta in fase di compilazione.
  • Riduce le allocazioni: Il generatore di origine crea codice ottimizzato che riduce al minimo le allocazioni di oggetti e l'utilizzo temporaneo della memoria.

L'app di esempio illustra le funzionalità di registrazione ad alte prestazioni con un servizio di lavoro di elaborazione delle code con priorità. L'app elabora gli elementi di lavoro in ordine di priorità. Quando si verificano queste operazioni, i messaggi di log vengono generati usando la registrazione generata dall'origine.

Suggerimento

Tutto il codice sorgente di esempio di registrazione è disponibile nel browser Samples per il download. Per altre informazioni, vedere Esplorare gli esempi di codice: Registrazione in .NET.

Definire i messaggi del logger con la generazione di sorgenti

Per creare messaggi di log ad alte prestazioni in .NET 6 e versioni successive, definire partial i metodi decorati con LoggerMessageAttribute. Il generatore di origine crea l'implementazione in fase di compilazione.

Metodo di registrazione di base

Per un messaggio di log semplice, definire un metodo parziale con l'attributo che specifica l'ID evento, il livello di log e il modello di messaggio:

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);
}

Il modello di messaggio usa segnaposto compilati dai parametri del metodo. I nomi segnaposto devono essere descrittivi e coerenti tra i modelli. Fungono da nomi di proprietà all'interno di dati di log strutturati. Raccomandiamo PascalCasing per i nomi segnaposto. Per esempio, {Item}, {DateTime}.

Chiamare il metodo di log dal codice. Ad esempio, quando si verifica un'eccezione durante l'elaborazione degli elementi di lavoro:

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

Questo codice genera un output della console simile al seguente:

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

Registrazione con parametri

Per passare parametri a un messaggio di log, aggiungerli come parametri del metodo. I nomi dei parametri corrispondono ai segnaposto nel modello di messaggio:

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);
}

Chiama il metodo con il logger e i valori dei parametri:

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

Questo codice genera un output della console simile al seguente:

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

Gli archivi di log strutturati possono usare il nome dell'evento quando viene fornito con l'ID dell'evento per potenziare il log. Ad esempio, Serilog usa il nome dell'evento.

Definire l'ambito del messaggio del logger con la generazione dell'origine

È possibile definire gli ambiti di log per eseguire il wrapping di una serie di messaggi di log con contesto aggiuntivo. Con la registrazione generata dall'origine, si combinano i LoggerMessageAttribute metodi con il metodo standard ILogger.BeginScope .

Abilitare IncludeScopes nella sezione del logger della console di appsettings.json:

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

Creare metodi di logging generati dalla sorgente ed eseguirne il wrapping in un contesto usando 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);
}

Usare il metodo di registrazione all'interno di un ambito nel codice dell'applicazione:

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

Esaminare i messaggi di log nell'output della console dell'app. Il risultato seguente mostra l'ordinamento prioritario dei messaggi di log con il messaggio di ambito del log incluso:

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'

Approccio storico: LoggerMessage.Define (per il .NET Framework e il .NET Core 3.1)

Prima dell'introduzione della registrazione generata dall'origine in .NET 6, l'approccio consigliato per la registrazione ad alte prestazioni consiste nell'usare il LoggerMessage.Define metodo per creare delegati memorizzabili nella cache. Anche se questo approccio è ancora supportato per la compatibilità con le versioni precedenti, il nuovo codice dovrebbe usare il logging generato dalla sorgente con LoggerMessageAttribute.

La LoggerMessage classe espone la funzionalità per creare delegati memorizzabili nella cache che richiedono un minor numero di allocazioni di oggetti e un sovraccarico di calcolo ridotto rispetto ai metodi di estensione del logger, ad esempio LogInformation e LogDebug. LoggerMessage offre i vantaggi seguenti per le prestazioni rispetto ai metodi di estensione del logger:

  • I metodi di estensione del logger richiedono una "conversione boxing" dei tipi di valori, ad esempio int, in object. LoggerMessage evita la conversione boxing usando campi Action statici e metodi di estensione con parametri fortemente tipizzati.
  • I metodi di estensione del logger devono analizzare il modello di messaggio (stringa di formato denominata) ogni volta che viene scritto un messaggio del log. Solo LoggerMessage richiede una sola analisi del modello durante la definizione del messaggio.

Annotazioni

Se si gestisce il codice che usa LoggerMessage.Define, valutare la possibilità di eseguire la migrazione alla registrazione generata dal codice sorgente. Per le applicazioni .NET Framework o .NET Core 3.1, continuare a usare LoggerMessage.Define.

Definire un messaggio di logger

Usare Define(LogLevel, EventId, String) per creare un Action delegato per la registrazione di un messaggio. Define Gli overload consentono di passare fino a sei parametri di tipo a una stringa di formato con nome (template).

La stringa fornita al Define metodo è un modello e non una stringa interpolata. I segnaposto vengono compilati nell'ordine in cui vengono specificati i tipi. I nomi segnaposto nel modello devono essere descrittivi e coerenti tra i modelli. Fungono da nomi di proprietà all'interno di dati di log strutturati. Raccomandiamo PascalCasing per i nomi segnaposto. Per esempio, {Item}, {DateTime}.

Ogni messaggio di log è un Action oggetto contenuto in un campo statico creato da LoggerMessage.Define. Ad esempio, l'app di esempio crea un campo per descrivere un messaggio di log per l'elaborazione degli elementi di lavoro:

private static readonly Action<ILogger, Exception> s_failedToProcessWorkItem;

Per Action, specificare:

  • Livello di registrazione.
  • Identificatore di evento univoco (EventId) con il nome del metodo di estensione statico.
  • Modello di messaggio (stringa di formato con nome).

Man mano che gli elementi di lavoro vengono dequeuati per l'elaborazione, l'app del servizio di lavoro imposta:

  • Imposta livello di log a LogLevel.Critical.
  • ID evento per 13 con il nome del metodo FailedToProcessWorkItem.
  • Modello di messaggio (stringa di formato denominato) in una stringa.
s_failedToProcessWorkItem = LoggerMessage.Define(
    LogLevel.Critical,
    new EventId(13, nameof(FailedToProcessWorkItem)),
    "Epic failure processing item!");

Il LoggerMessage.Define metodo viene usato per configurare e definire un Action delegato, che rappresenta un messaggio di log.

Gli archivi di log strutturati possono usare il nome dell'evento quando viene fornito con l'ID dell'evento per potenziare il log. Ad esempio, Serilog usa il nome dell'evento.

Action viene richiamato tramite un metodo di estensione fortemente tipizzato. Il PriorityItemProcessed metodo registra un messaggio ogni volta che viene elaborato un elemento di lavoro. FailedToProcessWorkItem viene chiamato se e quando si verifica un'eccezione:

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);
        }
    }
}

Verificare l'output della console dell'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

Per passare parametri a un messaggio di log, definire fino a sei tipi durante la creazione del campo statico. L'app di esempio registra i dettagli dell'elemento di lavoro durante l'elaborazione degli elementi definendo un WorkItem tipo per il Action campo:

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

Il modello di messaggio di log del delegato riceve i valori segnaposto dai tipi forniti. L'app di esempio definisce un delegato per l'aggiunta di un elemento di lavoro in cui il parametro dell'elemento è :WorkItem

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

Il metodo di estensione statico per la registrazione dell'elaborazione di un elemento di lavoro, PriorityItemProcessed, riceve il valore dell'argomento dell'elemento Action di lavoro e lo passa al delegato:

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

Nel metodo ExecuteAsync del servizio di lavoro, viene chiamato PriorityItemProcessed per registrare il messaggio:

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);
        }
    }
}

Esamina l'output della console dell'applicazione:

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

Ottimizzazioni sorvegliate a livello di log

È possibile ottimizzare le prestazioni controllando LogLevel con ILogger.IsEnabled(LogLevel) prima di richiamare il metodo corrispondente Log* . Quando la registrazione non è configurata per il parametro specificato LogLevel, ILogger.Log non viene chiamata. Inoltre, si evita il boxing dei tipi valore e l'allocazione di object[] per rappresentare i parametri.

Per altre informazioni, vedere:

Vedere anche