Generowanie źródła rejestrowania w czasie kompilacji

Platforma .NET 6 wprowadza LoggerMessageAttribute typ. Ten atrybut jest częścią Microsoft.Extensions.Logging przestrzeni nazw, a gdy jest używany, generuje wydajne interfejsy API rejestrowania. Obsługa rejestrowania generacji źródłowej została zaprojektowana w celu zapewnienia wysoce użytecznego i wysoce wydajnego rozwiązania do rejestrowania dla nowoczesnych aplikacji platformy .NET. Wygenerowany automatycznie kod źródłowy opiera się na interfejsie w połączeniu ILogger z funkcjami LoggerMessage.Define .

Generator źródła jest wyzwalany, gdy LoggerMessageAttribute jest używany w partial metodach rejestrowania. Po wyzwoleniu może automatycznie wygenerować implementację partial metod, które są dekorowane, lub utworzyć diagnostykę czasu kompilacji z wskazówkami dotyczącymi odpowiedniego użycia. Rozwiązanie do rejestrowania w czasie kompilacji jest zwykle znacznie szybsze w czasie wykonywania niż istniejące podejścia rejestrowania. Pozwala to wyeliminować boxing, tymczasowe alokacje i kopie do maksymalnego możliwego zakresu.

Podstawowy sposób użycia

Aby użyć LoggerMessageAttributeklasy , klasa i metoda zużywania muszą mieć wartość partial. Generator kodu jest wyzwalany w czasie kompilacji i generuje implementację partial metody.

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

W poprzednim przykładzie metoda rejestrowania jest static i poziom dziennika jest określony w definicji atrybutu. W przypadku używania atrybutu w kontekście ILogger statycznym wystąpienie jest wymagane jako parametr lub zmodyfikuj definicję, aby użyć this słowa kluczowego , aby zdefiniować metodę jako metodę rozszerzenia.

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

Możesz również użyć atrybutu w kontekście niestacjonanym. Rozważmy następujący przykład, w którym metoda rejestrowania jest zadeklarowana jako metoda wystąpienia. W tym kontekście metoda rejestrowania pobiera rejestrator przez uzyskanie ILogger dostępu do pola w klasie zawierającej.

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

Czasami poziom dziennika musi być dynamiczny, a nie statycznie wbudowany w kod. Można to zrobić, pomijając poziom dziennika z atrybutu i zamiast tego wymagając go jako parametru do metody rejestrowania.

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

Możesz pominąć komunikat rejestrowania i String.Empty zostanie podany dla komunikatu. Stan będzie zawierać argumenty sformatowane jako pary klucz-wartość.

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

Rozważmy przykładowe dane wyjściowe rejestrowania JsonConsole podczas korzystania z formatera.

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

Ograniczenia metody dziennika

W przypadku korzystania z metod rejestrowania LoggerMessageAttribute należy przestrzegać pewnych ograniczeń:

  • Metody rejestrowania muszą być partial i zwrócić wartość void.
  • Nazwy metod rejestrowania nie mogą rozpoczynać się od podkreślenia.
  • Nazwy parametrów metod rejestrowania nie mogą rozpoczynać się od podkreślenia.
  • Metody rejestrowania mogą nie być zdefiniowane w typie zagnieżdżonym.
  • Metody rejestrowania nie mogą być ogólne.
  • Jeśli metoda rejestrowania to static, ILogger wystąpienie jest wymagane jako parametr.

Model generowania kodu zależy od kompilowanego kodu przy użyciu nowoczesnego kompilatora języka C#, wersji 9 lub nowszej. Kompilator języka C# 9.0 stał się dostępny na platformie .NET 5. Aby przeprowadzić uaktualnienie do nowoczesnego kompilatora języka C#, przeprowadź edycję pliku projektu, aby był przeznaczony dla języka C# 9.0.

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

Aby uzyskać więcej informacji, zobacz Przechowywanie wersji języka C#.

Anatomia metody dziennika

Podpis ILogger.Log akceptuje LogLevel element i opcjonalnie Exceptionelement , jak pokazano poniżej.

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

Ogólnie rzecz biorąc, pierwsze wystąpienie ILogger, LogLeveli Exception są traktowane specjalnie w podpisie metody dziennika generatora źródłowego. Kolejne wystąpienia są traktowane jak normalne parametry szablonu komunikatu:

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

Ważne

Emitowane ostrzeżenia zawierają szczegółowe informacje dotyczące poprawnego użycia obiektu LoggerMessageAttribute. W poprzednim przykładzie parametr WarningLogMethod będzie zgłaszał wartość .DiagnosticSeverity.WarningSYSLIB0025

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

Obsługa nazw szablonów bez uwzględniania wielkości liter

Generator wykonuje porównanie bez uwzględniania wielkości liter między elementami w szablonie komunikatu i nazwami argumentów w komunikacie dziennika. Oznacza to, że gdy ILogger wyliczy stan, argument jest pobierany przez szablon komunikatu, co może sprawić, że dzienniki będą lepiej używane:

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

Rozważ przykładowe dane wyjściowe rejestrowania podczas korzystania z JsonConsole formatera:

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

Nieokreślona kolejność parametrów

Nie ma żadnych ograniczeń dotyczących kolejności parametrów metody dziennika. Deweloper może zdefiniować ILogger parametr jako ostatni, chociaż może wydawać się nieco niezręczny.

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

Napiwek

Kolejność parametrów metody dziennika nie jest wymagana do odpowiadania kolejności symboli zastępczych szablonu. Zamiast tego nazwy symboli zastępczych w szablonie powinny być zgodne z parametrami. Rozważ następujące JsonConsole dane wyjściowe i kolejność błędów.

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

Dodatkowe przykłady rejestrowania

W poniższych przykładach pokazano, jak pobrać nazwę zdarzenia, dynamicznie ustawić poziom dziennika i sformatować parametry rejestrowania. Metody rejestrowania to:

  • LogWithCustomEventName: pobierz nazwę zdarzenia za pomocą LoggerMessage atrybutu.
  • LogWithDynamicLogLevel: ustaw poziom dziennika dynamicznie, aby zezwolić na ustawianie poziomu dziennika na podstawie danych wejściowych konfiguracji.
  • UsingFormatSpecifier: Użyj specyfikatorów formatu, aby sformatować parametry rejestrowania.
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);
    }
}

Rozważ przykładowe dane wyjściowe rejestrowania podczas korzystania z SimpleConsole formatera:

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

Rozważ przykładowe dane wyjściowe rejestrowania podczas korzystania z JsonConsole formatera:

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

Podsumowanie

Dzięki pojawieniu się generatorów źródeł języka C# pisanie wysoce wydajnych interfejsów API rejestrowania jest znacznie łatwiejsze. Korzystanie z podejścia generatora źródła ma kilka kluczowych korzyści:

  • Umożliwia zachowanie struktury rejestrowania i włączenie dokładnej składni formatu wymaganej przez szablony komunikatów.
  • Umożliwia podawanie alternatywnych nazw symboli zastępczych szablonu i używanie specyfikatorów formatu.
  • Umożliwia przekazywanie wszystkich oryginalnych danych w taki sposób, jak jest, bez żadnych komplikacji dotyczących sposobu ich przechowywania, zanim coś zostanie wykonane z nim (inne niż utworzenie obiektu string).
  • Udostępnia diagnostykę specyficzną dla rejestrowania i emituje ostrzeżenia dotyczące zduplikowanych identyfikatorów zdarzeń.

Ponadto istnieją korzyści wynikające z ręcznego używania polecenia LoggerMessage.Define:

  • Krótsza i prostsza składnia: Użycie atrybutów deklaratywnych, a nie kodowanie standardowy.
  • Środowisko deweloperskie z przewodnikiem: generator wyświetla ostrzeżenia ułatwiające deweloperom wykonywanie odpowiednich czynności.
  • Obsługa dowolnej liczby parametrów rejestrowania. LoggerMessage.Define obsługuje maksymalnie sześć.
  • Obsługa dynamicznego poziomu dziennika. Nie jest to możliwe tylko w przypadku LoggerMessage.Define .

Zobacz też