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ć LoggerMessageAttribute
klasy , 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);
}
Począwszy od platformy .NET 9, metoda rejestrowania może dodatkowo pobrać rejestrator z podstawowego ILogger
parametru konstruktora w klasie zawierającej.
public partial class InstanceLoggingExample(ILogger logger)
{
[LoggerMessage(
EventId = 0,
Level = LogLevel.Critical,
Message = "Could not open socket to `{HostName}`")]
public partial void CouldNotOpenSocket(string hostName);
}
Jeśli istnieje zarówno ILogger
pole, jak i podstawowy parametr konstruktora, metoda rejestrowania pobierze rejestrator z pola.
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
, LogLevel
i 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.Warning
SYSLIB0025
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
.