Génération de la source de journalisation au moment de la compilation

.NET 6 introduit le type LoggerMessageAttribute. Cet attribut fait partie de l’espace de noms Microsoft.Extensions.Logging et, lorsqu’il est utilisé, il génère des API de journalisation performantes. La prise en charge de la journalisation de génération de la source est conçue pour fournir une solution de journalisation hautement utilisable et hautement performante pour les applications .NET modernes. Le code source généré automatiquement s’appuie sur l’interface ILogger conjointement avec les fonctionnalités LoggerMessage.Define.

Le générateur source est déclenché lorsque LoggerMessageAttribute est utilisé sur les méthodes de journalisation partial. Lorsqu’il est déclenché, il peut soit générer automatiquement l’implémentation des méthodes partial qu’il décore, soit produire des diagnostics au moment de la compilation avec des conseils sur une utilisation appropriée. La solution de journalisation au moment de la compilation est en général considérablement plus rapide au moment de l’exécution que les approches de journalisation existantes. Pour ce faire, il élimine le boxing, les allocations temporaires et les copies dans la mesure du possible.

Utilisation de base

Pour utiliser LoggerMessageAttribute, la classe et la méthode consommateurs doivent être partial. Le générateur de code est déclenché au moment de la compilation et génère une implémentation de la méthode 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);
}

Dans l’exemple précédent, la méthode de journalisation est static et le niveau du journal est spécifié dans la définition d’attribut. Lorsque vous utilisez l’attribut dans un contexte statique, l’instance ILogger est requise en tant que paramètre ou modifiez la définition pour utiliser le mot clé this pour définir la méthode en tant que méthode d’extension.

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

Vous pouvez également choisir d’utiliser l’attribut dans un contexte non statique. Prenons l’exemple suivant où la méthode de journalisation est déclarée en tant que méthode d’instance. Dans ce contexte, la méthode de journalisation obtient l’enregistreur d’événements en accédant à un champ ILogger dans la classe contenante.

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

Parfois, le niveau du journal doit être dynamique plutôt que statiquement consigné dans le code. Pour ce faire, vous pouvez omettre le niveau du journal de l’attribut et l’exiger en tant que paramètre pour la méthode de journalisation.

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

Vous pouvez omettre le message de journalisation et String.Empty sera fourni pour le message. L’état contient les arguments, mis en forme sous forme de paires clé-valeur.

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

Considérez l’exemple de sortie de journalisation lorsque vous utilisez le formateur 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}."
  }
}

Contraintes de méthode du journal

Lors de l’utilisation de LoggerMessageAttribute sur les méthodes de journalisation, certaines contraintes doivent être respectées :

  • Les méthodes de journalisation doivent être partial et retourner void.
  • Les noms de méthode de journalisation ne doivent pas commencer par un trait de soulignement.
  • Les noms de paramètres des méthodes de journalisation ne doivent pas commencer par un trait de soulignement.
  • Les méthodes de journalisation peuvent ne pas être définies dans un type imbriqué.
  • Les méthodes de journalisation ne peuvent pas être génériques.
  • Si une méthode de journalisation est static, l’instance ILogger est requise en tant que paramètre.

Le modèle de génération de code dépend du code compilé avec un compilateur C# moderne, version 9 ou ultérieure. Le compilateur C# 9.0 est devenu disponible avec .NET 5. Pour effectuer une mise à niveau vers un compilateur C# moderne, modifiez votre fichier projet pour cibler C# 9.0.

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

Pour plus d’informations, consultez Contrôle de version du langage C#.

Anatomie de la méthode du journal

La signature ILogger.Log accepte LogLevel et éventuellement une Exception, comme indiqué ci-dessous.

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

En règle générale, la première instance de ILogger, LogLevelet Exception sont traitées spécialement dans la signature de la méthode du journal du générateur source. Les instances suivantes sont traitées comme des paramètres normaux pour le modèle de message :

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

Important

Les avertissements émis fournissent des détails sur l’utilisation correcte de LoggerMessageAttribute. Dans l’exemple précédent, WarningLogMethod signale un DiagnosticSeverity.Warning de SYSLIB0025.

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

Prise en charge des noms de modèle sans respect de la casse

Le générateur effectue une comparaison qui ne respecte pas la casse entre les éléments du modèle de message et les noms des arguments dans le message de journal. Cela signifie que lorsque ILogger énumère l’état, l’argument est récupéré par le modèle de message, ce qui peut rendre les journaux plus agréables à consommer :

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

Tenez compte de l’exemple de sortie de journalisation lorsque vous utilisez le formateur 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}!"
  }
}

Ordre des paramètres indéterminés

Il n’existe aucune contrainte sur l’ordre des paramètres de méthode du journal. Un développeur peut définir ILogger comme dernier paramètre, même s’il peut sembler un peu gênant.

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

Conseil

L’ordre des paramètres d’une méthode du journal n’est pas obligatoire pour correspondre à l’ordre des espaces réservés du modèle. Au lieu de cela, les noms d’espace réservé dans le modèle sont censés correspondre aux paramètres. Tenez compte de la sortie JsonConsole suivante et de l’ordre des erreurs.

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

Exemples de journalisation supplémentaires

Les exemples suivants montrent comment récupérer le nom de l’événement, définir dynamiquement le niveau de journalisation et mettre en forme les paramètres de journalisation. Les méthodes de journalisation sont les suivantes :

  • LogWithCustomEventName : Récupérer le nom de l’événement via l’attribut LoggerMessage.
  • LogWithDynamicLogLevel : définir le niveau de journal dynamiquement pour permettre au niveau du journal d’être défini en fonction de l’entrée de configuration.
  • UsingFormatSpecifier : utiliser des spécificateurs de format pour mettre en forme les paramètres de journalisation.
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);
    }
}

Tenez compte de l’exemple de sortie de journalisation lorsque vous utilisez le formateur 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

Tenez compte de l’exemple de sortie de journalisation lorsque vous utilisez le formateur 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}"
  }
}

Résumé

Avec l’avènement des générateurs de sources C#, l’écriture d’API de journalisation hautement performantes est beaucoup plus facile. L’utilisation de l’approche du générateur source présente plusieurs avantages clés :

  • Permet de conserver la structure de journalisation et d’activer la syntaxe de format exacte requise par les modèles de message.
  • Permet de fournir d’autres noms pour les espaces réservés de modèle et d’utiliser des spécificateurs de format.
  • Permet la transmission de toutes les données d’origine telles quelles, sans aucune complication quant à la façon dont elles sont stockées avant que quelque chose ne soit effectué avec elles (autre que la création de string).
  • Fournit des diagnostics spécifiques à la journalisation et émet des avertissements pour les ID d’événement en double.

En outre, il existe des avantages par rapport à l’utilisation manuelle de LoggerMessage.Define :

  • Syntaxe plus courte et plus simple : utilisation déclarative des attributs au lieu de coder des éléments réutilisables.
  • Expérience des développeurs guidée : le générateur envoie des avertissements pour aider les développeurs à faire la bonne chose.
  • Prise en charge d’un nombre arbitraire de paramètres de journalisation. LoggerMessage.Define prend en charge un maximum de six.
  • Prise en charge du niveau du journal dynamique. Ce n’est pas possible avec LoggerMessage.Define seul.

Voir aussi