Generazione dell'origine di registrazione in fase di compilazione

.NET 6 introduce il tipo LoggerMessageAttribute. Questo attributo fa parte dello spazio dei nomi Microsoft.Extensions.Logging e, se usato, genera API di registrazione con prestazioni elevate. Il supporto per la registrazione di generazione dell'origine progettato per offrire una soluzione di registrazione altamente utilizzabile e ad alte prestazioni per le applicazioni .NET moderne. Il codice sorgente generato automaticamente si basa sull'interfaccia ILogger insieme alle funzionalità LoggerMessage.Define.

Il generatore di origine viene attivato quando LoggerMessageAttribute viene usato nei metodi di registrazione partial. Quando viene attivato, è in grado di generare automaticamente l'implementazione dei metodi partial di decorazione, oppure di produrre una diagnostica in fase di compilazione con suggerimenti sull'utilizzo appropriato. La soluzione di registrazione in fase di compilazione è in genere molto più veloce in fase di esecuzione rispetto agli approcci di registrazione esistenti. Questo risultato viene raggiunto eliminando il più possibile la conversione boxing, le allocazioni temporanee e le copie.

Utilizzo di base

Per usare LoggerMessageAttribute, la classe e il metodo di utilizzo devono essere partial. Il generatore di codice viene attivato in fase di compilazione e genera un'implementazione del metodo partial.

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

Nell'esempio precedente il metodo di registrazione è static e il livello di log viene specificato nella definizione dell'attributo. Quando si usa l'attributo in un contesto statico, l'istanza di ILogger è necessaria come parametro oppure modificare la definizione per usare la parola chiave this per definire il metodo come metodo di estensione.

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

È possibile scegliere di usare l'attributo anche in un contesto non statico. Si consideri l'esempio seguente in cui il metodo di registrazione viene dichiarato come metodo di istanza. In questo contesto, il metodo di registrazione ottiene il logger accedendo a un campo ILogger nella classe contenitore.

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

In alcuni casi, il livello di log deve essere dinamico anziché essere integrato in modo statico nel codice. A tale scopo, è possibile omettere il livello di log dall'attributo e richiederlo invece come parametro al metodo di registrazione.

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

È possibile omettere il messaggio di registrazione e String.Empty verrà fornito per il messaggio. Lo stato conterrà gli argomenti, formattati come coppie chiave-valore.

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

Si consideri l'output di registrazione di esempio quando si usa il formattatore JsonConsole.

{
  "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}."
  }
}

Vincoli del metodo di log

Quando si usa LoggerMessageAttribute nei metodi di registrazione, è necessario seguire alcuni vincoli:

  • I metodi di registrazione devono essere partial e restituire void.
  • I nomi dei metodi di registrazione non devono iniziare con un carattere di sottolineatura.
  • I nomi dei parametri dei metodi di registrazione non devono iniziare con un carattere di sottolineatura.
  • I metodi di registrazione non possono essere definiti in un tipo annidato.
  • I metodi di registrazione non possono essere generici.
  • Se un metodo di registrazione è static, l'istanza di ILogger è obbligatoria come parametro.

Il modello di generazione del codice dipende dalla compilazione del codice con un compilatore C# moderno, versione 9 o successiva. Il compilatore C# 9.0 è diventato disponibile con .NET 5. Per eseguire l'aggiornamento a un compilatore C# moderno, modificare il file di progetto in modo che sia destinato a C# 9.0.

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

Per altre informazioni, vedere controllo delle versioni del linguaggio C#.

Anatomia del metodo di log

La firma ILogger.Log accetta il LogLevel e facoltativamente un Exception, come illustrato di seguito.

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

Come regola generale, la prima istanza di ILogger, LogLevele Exception vengono trattate appositamente nella firma del metodo di log del generatore di origine. Le istanze successive vengono considerate come parametri normali per il modello di messaggio:

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

Importante

Gli avvisi generati forniscono informazioni dettagliate sull'utilizzo corretto di LoggerMessageAttribute. Nell'esempio precedente, WarningLogMethod segnala un DiagnosticSeverity.Warning di SYSLIB0025.

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

Supporto del nome del modello senza distinzione tra maiuscole e minuscole

Il generatore esegue un confronto senza distinzione tra gli elementi nel modello di messaggio e i nomi degli argomenti nel messaggio di log. Ciò significa che quando ILogger enumera lo stato, l'argomento viene prelevato dal modello di messaggio, il che può rendere i log più gradevoli da utilizzare:

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

Si consideri l'output di registrazione di esempio quando si usa il formattatore JsonConsole:

{
  "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}!"
  }
}

Ordine dei parametri indeterminato

Non esistono vincoli sull'ordinamento dei parametri del metodo di log. Uno sviluppatore può definire ILogger come ultimo parametro, anche se può sembrare un po' imbarazzante.

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

Suggerimento

L'ordine dei parametri in un metodo di log non deve necessariamente corrispondere all'ordine dei segnaposto del modello. I nomi segnaposto nel modello devono invece corrispondere ai parametri. Si consideri l’output JsonConsole seguente e l'ordine degli errori.

{
  "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}"
  }
}

Altri esempi di registrazione

Gli esempi seguenti illustrano come recuperare il nome dell'evento, impostare il livello di log in modo dinamico e formattare i parametri di registrazione. I metodi di registrazione sono:

  • LogWithCustomEventName: recuperare il nome dell'evento tramite l’attributo LoggerMessage.
  • LogWithDynamicLogLevel: impostare il livello di log in modo dinamico per consentire l'impostazione del livello di log in base all'input di configurazione.
  • UsingFormatSpecifier: usare gli identificatori di formato per formattare i parametri di registrazione.
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);
    }
}

Si consideri l'output di registrazione di esempio quando si usa il formattatore SimpleConsole:

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

Si consideri l'output di registrazione di esempio quando si usa il formattatore JsonConsole:

{
  "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}"
  }
}

Riepilogo

Con l'avvento dei generatori di origine C#, la scrittura di API di registrazione ad alte prestazioni è molto più semplice. L'uso dell'approccio del generatore di origine presenta diversi vantaggi principali:

  • Consente di mantenere la struttura di registrazione e di abilitare la sintassi di formato esatta richiesta dai modelli di messaggio.
  • Consente di specificare nomi alternativi per i segnaposto del modello e di utilizzare gli identificatori di formato.
  • Consente il passaggio di tutti i dati originali così com'è, senza alcuna complicazione sulla modalità di archiviazione prima che venga eseguita un'operazione (oltre alla creazione di un string).
  • Fornisce la diagnostica specifica della registrazione e genera avvisi per gli ID evento duplicati.

Inoltre, esistono vantaggi rispetto all'uso manuale di LoggerMessage.Define:

  • Sintassi più breve e semplice: uso di attributi dichiarativi anziché di codifica boilerplate.
  • Esperienza di sviluppo guidata: il generatore fornisce avvisi per aiutare gli sviluppatori a fare quello che serve.
  • Supporto per un numero arbitrario di parametri di registrazione. LoggerMessage.Define ne supporta un massimo di sei.
  • Supporto per il livello di log dinamico. Questo non è possibile solo con LoggerMessage.Define.

Vedi anche