Ведение журнала создания исходного кода во время компиляции
На платформе .NET 6 появился тип LoggerMessageAttribute
. Этот атрибут является частью пространства имен Microsoft.Extensions.Logging
. Он создает исходный код для высокопроизводительных API-интерфейсов ведения журналов. Поддержка ведения журналов создания исходного кода предназначена для создания широко используемого и высокопроизводительного решения по ведению журналов для современных приложений .NET. Автоматически создаваемый исходный код полагается на интерфейс ILogger в сочетании с возможностями LoggerMessage.Define.
Генератор исходного кода активируется при использовании LoggerMessageAttribute
с методами ведения журналов partial
. При активации генератор может автоматически создавать реализацию методов partial
, к которым он применяется, или создавать данные диагностики во время компиляции с указаниями о правильном использовании. Решение по ведению журналов времени компиляции обычно работает значительно быстрее во время выполнения, чем при существующих подходах к ведению журналов. Это достигается за счет максимально возможного исключения упаковки, временных выделений и копирования.
Базовое использование
Чтобы использовать LoggerMessageAttribute
, необходимо указать partial
в качестве метода и класса использования. Генератор кода активируется во время компиляции и создает реализацию метода 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);
}
В предыдущем примере используется метод ведения журнала static
, а уровень ведения журнала указывается в определении атрибута. При использовании атрибута в статическом контексте ILogger
экземпляр требуется в качестве параметра или измените определение, чтобы использовать this
ключевое слово для определения метода расширения.
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);
}
Вы можете также использовать атрибут в контексте, не являющемся статическим. Рассмотрим следующий пример, где метод ведения журнала объявляется в качестве метода экземпляра. В этом контексте метод ведения журнала получает средство ведения журнала, обращаясь к полю ILogger
в содержащем его классе.
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);
}
Начиная с .NET 9 метод ведения журнала может дополнительно получить средство ведения журнала из основного ILogger
параметра конструктора в содержающем классе.
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);
}
Если поле и основной ILogger
параметр конструктора имеются, метод ведения журнала получит средство ведения журнала из поля.
Иногда необходимо, чтобы уровень ведения журнала был динамическим, а не статически встроенным в код. Это можно обеспечить, опустив уровень ведения журнала в атрибуте и вместо этого требуя его в качестве параметра для метода ведения журнала.
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);
}
Сообщение ведения журнала можно опустить, указав вместо него String.Empty. Состояние будет содержать аргументы, отформатированные в виде пар "ключ — значение".
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);
}
Рассмотрим пример выходных данных ведения журнала при использовании форматировщика 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}."
}
}
Ограничения метода ведения журнала
При использовании LoggerMessageAttribute
методов ведения журнала следует соблюдать некоторые ограничения:
- Методы ведения журнала должны быть
partial
и возвращатьсяvoid
. - Имена методов ведения журналов не должны начинаться с символа подчеркивания.
- Имена параметров методов ведения журналов не должны начинаться с символа подчеркивания.
- Методы ведения журнала не должны определяться во вложенном типе.
- Методы ведения журналов не могут быть универсальными.
- Если используется
static
метод ведения журнала,ILogger
экземпляр требуется в качестве параметра.
Модель создания кода зависит от кода, компилируемого с помощью современного компилятора C# версии 9 или более поздней. Компилятор C# 9.0 стал доступен на платформе .NET 5. Чтобы обновить версию компилятора C# до современной, измените файл проекта, указав C# 9.0 в качестве целевого компилятора.
<PropertyGroup>
<LangVersion>9.0</LangVersion>
</PropertyGroup>
Дополнительные сведения см. в статье Управление версиями языка C#.
Структура метода ведения журнала
Сигнатура ILogger.Log принимает LogLevel и дополнительно Exception, как показано ниже.
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);
}
Как правило, первый экземпляр ILogger
, LogLevel
и Exception
обрабатывается специально в сигнатуре метода ведения журнала в генераторе исходного кода. Последующие экземпляры обрабатываются как обычные параметры шаблона сообщения.
// 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);
Внимание
Порождаемые предупреждения предоставляют подробные сведения о правильном использовании LoggerMessageAttribute
. В предыдущем примере WarningLogMethod
сообщит о DiagnosticSeverity.Warning
SYSLIB0025
.
Don't include a template for `ex` in the logging message since it is implicitly taken care of.
Поддержка имен шаблонов без учета регистра
Генератор выполняет нечувствительное сравнение элементов в шаблоне сообщения и именах аргументов в сообщении журнала. Это означает, что при ILogger
перечислении состояния аргумент выбирается шаблоном сообщения, что может сделать журналы более удобными для использования:
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");
}
}
Рассмотрим пример выходных данных ведения журнала при использовании форматировщика 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}!"
}
}
Неопределенный порядок параметров
В отношении порядка параметров для методов ведения журналов нет ограничений. Разработчик может определить ILogger
в качестве последнего параметра, хотя это может быть немного неудобным.
[LoggerMessage(
EventId = 110,
Level = LogLevel.Debug,
Message = "M1 {Ex3} {Ex2}")]
static partial void LogMethod(
Exception ex,
Exception ex2,
Exception ex3,
ILogger logger);
Совет
Порядок параметров в методе ведения журнала может не соответствовать порядку заполнителей в шаблоне. Но имена заполнителей в шаблоне должны соответствовать параметрам. Рассмотрим следующие выходные данные JsonConsole
и порядок ошибок.
{
"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}"
}
}
Дополнительные примеры ведения журналов
В следующих примерах показано, как получить имя события, задать динамически уровень журнала и форматировать параметры ведения журнала. Методы ведения журнала:
LogWithCustomEventName
— как получить имя события с помощью атрибутаLoggerMessage
;LogWithDynamicLogLevel
— как задать динамический уровень ведения журналов, чтобы разрешить установку уровня ведения журнала на основе входных данных конфигурации;UsingFormatSpecifier
— как использовать описатели формата, чтобы форматировать параметры ведения журнала.
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);
}
}
Рассмотрим пример выходных данных ведения журнала при использовании форматировщика 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
Рассмотрим пример выходных данных ведения журнала при использовании форматировщика 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}"
}
}
Итоги
Появление генераторов исходного кода C# значительно упростило написание высокопроизводительных API-интерфейсов ведения журналов. Использование генератора исходного кода предоставляет несколько ключевых преимуществ:
- позволяет сохранять структуру ведения журналов и обеспечивает точный синтаксис формата, необходимый для шаблонов сообщений;
- позволяет указывать альтернативные имена для заполнителей в шаблоне и использовать описатели формата;
- Позволяет передавать все исходные данные как есть, без каких-либо осложнений вокруг того, как он хранится, прежде чем что-то сделано с ним (кроме создания
string
). - Предоставляет определенные для ведения журнала диагностика и выдает предупреждения для повторяющихся идентификаторов событий.
Кроме того, применение LoggerMessage.Define вручную предоставляет следующие преимущества:
- более короткий и простой синтаксис — декларативное использование атрибутов вместо стандартного кода;
- интерактивное взаимодействие с разработчиками — генератор выводит предупреждения, помогающие выполнить нужное действие;
- поддержка произвольного числа параметров ведения журналов.
LoggerMessage.Define
поддерживает не более шести; - поддержка динамического уровня ведения журнала. Сейчас она невозможна при использовании только
LoggerMessage.Define
.