Поделиться через


Ведение журнала создания исходного кода во время компиляции

Логирование, создаваемое исходным кодом, предназначено для обеспечения высокопроизводительного и удобного решения для логирования в современных .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, а уровень ведения журнала указывается в определении атрибута. При использовании LoggerMessageAttribute в статическом контексте ILogger экземпляр должен передаваться в качестве аргумента. Или добавьте модификатор в thisILogger параметр, чтобы определить метод как метод расширения.

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 метод логирования может дополнительно получать логгер из параметра основного конструктора в содержащем классе.

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

Свойство Message

Свойство Message on LoggerMessageAttribute является необязательным. Если вы опустите это, String.Empty будет использоваться для сообщения. Однако если метод ведения журнала имеет параметры без соответствующих заполнителей шаблона, компилятор выдает предупреждение SYSLIB1015 . Эти параметры хранятся в состоянии журнала, но не отображаются в отформатированных выходных данных журнала. Только структурированные поставщики ведения журнала, которые перечисляют состояние журнала, делают его доступным.

Ограничения метода ведения журнала

Методы ведения журнала, декорированные при помощи LoggerMessageAttribute, должны соответствовать следующим требованиям:

  • Методы ведения журнала должны быть partial и возвращать void.
  • Имена методов ведения журналов не должны начинаться с символа подчеркивания.
  • Имена параметров методов ведения журналов не должны начинаться с символа подчеркивания.
  • Методы ведения журнала поддерживают параметры универсального типа, но анти-ограничение C# 13 allows ref struct не поддерживается.
  • Параметры метода ведения журнала не могут использовать модификаторы params, scoped, или out, и не могут быть типами ref struct.
  • Если используется staticметод ведения журнала, ILogger экземпляр требуется в качестве параметра.

Модель создания кода зависит от компиляции кода с помощью современного компилятора C#, т. е. версии 9 или более поздней. Сведения об изменении языковой версии см. в разделе " Управление версиями на языке 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.WarningSYSLIB0025.

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

Изменение конфиденциальной информации в журналах

При ведении журнала конфиденциальных данных важно предотвратить случайное воздействие. Даже при использовании методов ведения журнала во время компиляции ведение журнала необработанных конфиденциальных значений может привести к утечкам данных и проблемам соответствия требованиям.

Библиотека Microsoft.Extensions.Telemetry предоставляет расширенные возможности ведения журнала и обогащения телеметрии для приложений .NET. Он расширяет конвейер ведения журнала, чтобы автоматически применять редактирование к классифицированным данным при записи журналов. Это позволяет применять политики защиты данных во всем приложении, интегрируя редактирование в рабочий процесс ведения журнала. Он создан для приложений, требующих сложных данных телеметрии и ведения журнала.

Чтобы включить редактирование, используйте библиотеку Microsoft.Extensions.Compliance.Redaction . Эта библиотека предоставляет редакторы — компоненты, которые преобразуют конфиденциальные данные (например, стирая, маскируя или хэшируя их), чтобы данные могли безопасно выводиться. Редакторы выбираются на основе классификации данных, которая позволяет пометить данные в соответствии с их конфиденциальностью (например, персональные, частные или общедоступные).

Чтобы использовать редактирование с методами ведения журнала, созданными источником, необходимо:

  1. Классификация конфиденциальных данных с помощью системы классификации данных.
  2. Зарегистрируйте и настройте редакторы для каждой классификации в вашем контейнере внедрения зависимостей.
  3. Включите редактирование в конвейере ведения журнала.
  4. Проверьте логи, чтобы убедиться, что конфиденциальные данные не раскрыты.

Например, если у вас есть сообщение журнала с параметром, который считается частным:

[LoggerMessage(0, LogLevel.Information, "User SSN: {SSN}")]
public static partial void LogPrivateInformation(
    this ILogger logger,
    [MyTaxonomyClassifications.Private] string SSN);

Вам потребуется установить такой же параметр:

using Microsoft.Extensions.Telemetry;
using Microsoft.Extensions.Compliance.Redaction;

var services = new ServiceCollection();
services.AddLogging(builder =>
{
    // Enable redaction.
    builder.EnableRedaction();
});

services.AddRedaction(builder =>
{
    // configure redactors for your data classifications
    builder.SetRedactor<StarRedactor>(MyTaxonomyClassifications.Private);
});

public void TestLogging()
{
    LogPrivateInformation("MySSN");
}

Выходные данные должны иметь следующий вид:

User SSN: *****

Этот подход гарантирует, что регистрируются только редактированные данные, даже если используется API ведения журнала во время компиляции. Для разных типов данных или классификаций можно использовать разные редакторы, а также централизованно обновлять логику редактирования.

Для получения дополнительной информации о том, как классифицировать данные, см. раздел Классификация данных в .NET. Дополнительные сведения о редактировании и редакторах см. в разделе Data redaction в .NET.

Сводка

С появлением генераторов источников C# создание высокопроизводительных API-интерфейсов ведения журнала упрощается. Использование генератора исходного кода предоставляет несколько ключевых преимуществ:

  • позволяет сохранять структуру ведения журналов и обеспечивает точный синтаксис формата, необходимый для шаблонов сообщений;
  • позволяет указывать альтернативные имена для заполнителей в шаблоне и использовать спецификаторы формата.
  • Позволяет передавать все исходные данные как есть, без каких-либо осложнений вокруг того, как они хранятся, пока что-то не будет с ними сделано (кроме создания string).
  • Предоставляет диагностику, специфичную для логирования, и выдает предупреждения о дублирующихся идентификаторах событий.

Кроме того, применение LoggerMessage.Define вручную предоставляет следующие преимущества:

  • более короткий и простой синтаксис — декларативное использование атрибутов вместо стандартного кода;
  • интерактивное взаимодействие с разработчиками — генератор выводит предупреждения, помогающие выполнить нужное действие;
  • поддержка произвольного числа параметров ведения журналов. LoggerMessage.Define поддерживает не более шести;
  • поддержка динамического уровня ведения журнала. Это невозможно сделать в одиночку с LoggerMessage.Define.

См. также