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


Логирование в C# и .NET

.NET поддерживает высокую производительность, структурированное ведение журнала через ILogger API для мониторинга поведения приложений и диагностики проблем. Журналы можно записать в разные места назначения, настроив разных поставщиков ведения журнала. Основные поставщики ведения журналов являются встроенными и есть множество сторонних поставщиков, доступных также.

Начало работы

В первом примере показаны основные сведения, но он подходит только для тривиального консольного приложения. В этом примере консольного приложения используются следующие пакеты NuGet:

В следующем разделе вы узнаете, как улучшить код, учитывая масштаб, производительность, конфигурацию и типичные шаблоны программирования.

using Microsoft.Extensions.Logging;

using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole());
ILogger logger = factory.CreateLogger("Program");
logger.LogInformation("Hello World! Logging is {Description}.", "fun");

Предшествующий пример:

  • Создает объект ILoggerFactory. ILoggerFactory хранит всю конфигурацию, определяющую, куда отправляются сообщения журнала. В этом случае вы настраиваете поставщик консольного ведения журнала так, чтобы сообщения журнала записывались в консоль.
  • Создает категорию ILogger с именем Program. Категория — это , связанная с каждым сообщением, зарегистрированным объектом . Он используется для группирования сообщений журнала из одного класса (или категории) вместе при поиске или фильтрации журналов.
  • Вызывает LogInformation для регистрации сообщения на уровне Information. Уровень журнала указывает серьезность зарегистрированного события и используется для фильтрации менее важных сообщений журнала. Запись журнала также включает шаблон сообщения"Hello World! Logging is {Description}." и пару Description = fun "ключ-значение". Имя ключа (или заполнитель) происходит из слова внутри фигурных скобок в шаблоне, а значение исходит из аргумента метода, оставшегося.

Этот файл проекта для этого примера включает два пакета NuGet:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.4" />
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.4" />
  </ItemGroup>

</Project>

Совет

Весь пример исходного кода для ведения журнала доступен для загрузки в Обозревателе примеров. Дополнительные сведения см. в разделе Обзор примеров кода: ведение журнала в .NET.

Ведение журнала в нетривиальном приложении

Существует несколько изменений, которые следует внести в предыдущий пример при входе в менее тривиальный сценарий:

  • Если ваше приложение использует внедрение зависимостей (DI) или хост, такой как WebApplication ASP.NET или Generic Host, то следует использовать ILoggerFactory и ILogger объекты из их соответствующих DI контейнеров, а не создавать их напрямую. Дополнительные сведения см. в разделе «Интеграция с DI и hosts».

  • Ведение журнала на основе генерации исходного кода во время компиляции обычно является лучшей альтернативой ILogger методам расширения, таким как LogInformation. Генерация источника логирования обеспечивает более высокую производительность, более сильную типизацию и избегает разброса констант по всем методам. Компромисс заключается в том, что использование этого метода требует немного большего кода.

using Microsoft.Extensions.Logging;

internal partial class Program
{
    static void Main(string[] args)
    {
        using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole());
        ILogger logger = factory.CreateLogger("Program");
        LogStartupMessage(logger, "fun");
    }

    [LoggerMessage(Level = LogLevel.Information, Message = "Hello World! Logging is {Description}.")]
    static partial void LogStartupMessage(ILogger logger, string description);
}
  • Рекомендуется для названий категорий журнала использовать полное квалифицированное имя класса, создающего сообщение журнала. Это помогает связать сообщения журнала с кодом, который создал их, и обеспечивает хороший уровень управления при фильтрации журналов. CreateLogger принимает Type, чтобы упростить процесс именования.
using Microsoft.Extensions.Logging;

internal class Program
{
    static void Main(string[] args)
    {
        using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole());
        ILogger logger = factory.CreateLogger<Program>();
        logger.LogInformation("Hello World! Logging is {Description}.", "fun");
    }
}
  • Если вы не используете журналы консоли в качестве единственного рабочего решения для мониторинга, добавьте поставщиков журналов, которые вы планируете использовать. Например, можно использовать OpenTelemetry для отправки журналов по протоколу OTLP (протокол OpenTelemetry):
using Microsoft.Extensions.Logging;
using OpenTelemetry.Logs;

using ILoggerFactory factory = LoggerFactory.Create(builder =>
{
    builder.AddOpenTelemetry(logging =>
    {
        logging.AddOtlpExporter();
    });
});
ILogger logger = factory.CreateLogger("Program");
logger.LogInformation("Hello World! Logging is {Description}.", "fun");

Интеграция с узлами и внедрением зависимостей

Если ваше приложение использует внедрение зависимостей (DI) или хост, такой как WebApplication или Generic Host от ASP.NET, следует использовать ILoggerFactory и ILogger объекты из контейнера DI, а не создавать их напрямую.

Получите ILogger из DI

В этом примере получается объект ILogger в размещенном приложении с помощью ASP.NET Minimal APIs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<ExampleHandler>();

var app = builder.Build();

var handler = app.Services.GetRequiredService<ExampleHandler>();
app.MapGet("/", handler.HandleRequest);

app.Run();

partial class ExampleHandler(ILogger<ExampleHandler> logger)
{
    public string HandleRequest()
    {
        LogHandleRequest(logger);
        return "Hello World";
    }

    [LoggerMessage(LogLevel.Information, "ExampleHandler.HandleRequest was called")]
    public static partial void LogHandleRequest(ILogger logger);
}

Предшествующий пример:

  • Создана одноэлементная служба ExampleHandler, которая сопоставляет входящие веб-запросы для запуска функции ExampleHandler.HandleRequest.
  • Строка 12 задает основной конструктор для ExampleHandler, добавленный в C# 12. Использование более старого конструктора C# будет работать так же хорошо, но немного более многословно.
  • Конструктор определяет параметр типа ILogger<ExampleHandler>. ILogger<TCategoryName> является производным от ILogger и указывает, какая категория ILogger имеет объект. Контейнер DI находит элемент ILogger с правильной категорией и предоставляет его в качестве аргумента конструктора. Если ILogger с этой категорией еще не существует, контейнер DI автоматически создает его из ILoggerFactory поставщика услуг.
  • Параметр logger, полученный в конструкторе, использовался для ведения журнала в функции HandleRequest.

ILoggerFactory, предоставленный сервером

Построитель узлов инициализирует конфигурацию по умолчанию, а затем добавляет настроенный ILoggerFactory объект в контейнер DI узла при создании узла. Перед сборкой узла можно настроить конфигурацию ведения журнала с помощью HostApplicationBuilder.LoggingWebApplicationBuilder.Loggingили аналогичных API на других узлах. Хосты также используют настройки для ведения журнала из стандартных источников конфигурации, таких как appsettings.json и переменные среды. Дополнительные сведения см. в статье Конфигурация в .NET.

Этот пример расширяет предыдущий, чтобы настроить ILoggerFactory, предоставленный WebApplicationBuilder. Он добавляет OpenTelemetry в качестве поставщика ведения журнала, передавающего журналы по протоколу OTLP (протокол OpenTelemetry):

var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddOpenTelemetry(logging => logging.AddOtlpExporter());
builder.Services.AddSingleton<ExampleHandler>();
var app = builder.Build();

Создание ILoggerFactory с помощью DI

Если вы используете контейнер DI без узла, используйте AddLogging для настройки и добавления ILoggerFactory в контейнер.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

// Add services to the container including logging
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddConsole());
services.AddSingleton<ExampleService>();
IServiceProvider serviceProvider = services.BuildServiceProvider();

// Get the ExampleService object from the container
ExampleService service = serviceProvider.GetRequiredService<ExampleService>();

// Do some pretend work
service.DoSomeWork(10, 20);

class ExampleService(ILogger<ExampleService> logger)
{
    public void DoSomeWork(int x, int y)
    {
        logger.LogInformation("DoSomeWork was called. x={X}, y={Y}", x, y);
    }
}

Предшествующий пример:

  • Создан контейнер службы DI, содержащий ILoggerFactory, настроенный для записи в консоль
  • Добавлен синглтон ExampleService в контейнер
  • Создал экземпляр ExampleService с использованием контейнера DI, который также автоматически создал ILogger<ExampleService> для использования в качестве аргумента конструктора.
  • Вызывался ExampleService.DoSomeWork, который был использован ILogger<ExampleService> для записи сообщения в консоль.

Настроить ведение журнала

Конфигурация ведения журнала устанавливается в коде или через внешние источники, такие как файлы конфигурации и переменные среды. Использование внешней конфигурации полезно, если это возможно, так как его можно изменить, не перестроив приложение. Однако некоторые задачи, такие как настройка поставщиков ведения журнала, можно настроить только из кода.

Настройка ведения журнала без кода

Для приложений, использующих хост, конфигурация ведения журнала обычно предоставляется в разделе "Logging" appsettings.{Environment} файлов. Для приложений, которые не используют узел, внешние источники конфигурации явно задаются или конфигурируются в коде.

Шаблоны рабочей службы .NET создают следующий файл appsettings.Development.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

В предыдущем объекте JSON:

  • Указаны категории уровней журнала "Default", "Microsoft" и "Microsoft.Hosting.Lifetime".
  • Значение "Default" применяется ко всем категориям, которые не указаны отдельно, тем самым устанавливая все значения по умолчанию для всех категорий как "Information". Это поведение можно переопределить, указав значение для категории.
  • Категория "Microsoft" применяется ко всем категориям, начинающимся с "Microsoft".
  • Записи категории "Microsoft" регистрируются на уровне журнала Warning и выше.
  • Категория "Microsoft.Hosting.Lifetime" более конкретна, чем категория "Microsoft", поэтому категория "Microsoft.Hosting.Lifetime" ведёт логирование на уровне "Information" и выше.
  • Поскольку конкретный поставщик журналов не указан, LogLevel применяется ко всем включенным поставщикам, за исключением Windows EventLog.

Свойство Logging может включать как свойства LogLevel, так и свойства поставщика журналов. Свойство LogLevel указывает минимальный уровень для записи в журнал для выбранных категорий. В приведенном выше коде JSON заданы уровни ведения журнала Information и Warning. LogLevel определяет уровень серьезности записей журнала и варьируется в диапазоне от 0 до 6:

Trace = 0, Debug = 1, Information = 2, Warning = 3, Error = 4, Critical = 5 и None = 6.

Если задан LogLevel, логирование будет вестись для сообщений с указанного уровня и выше. В предыдущем JSON ведется журнал для категории Default и более высоких уровней Information. Например, в журнал записываются сообщения Information, Warning, Error и Critical. Если LogLevel не задан, по умолчанию устанавливается уровень журналирования Information. Дополнительную информацию см. в разделе Уровни журналов.

Свойство поставщика может указывать свойство LogLevel. Свойство LogLevel указывает уровни ведения журналов для этого поставщика и переопределяет общие параметры ведения журналов, не связанные с поставщиком. Рассмотрите следующий файл appsettings.json:

{
    "Logging": {
        "LogLevel": {
            "Default": "Error",
            "Microsoft": "Warning"
        },
        "Debug": {
            "LogLevel": {
                "Default": "Information",
                "Microsoft.Hosting": "Trace"
            }
        },
        "EventSource": {
            "LogLevel": {
                "Default": "Warning"
            }
        }
    }
}

Параметры в Logging.{ProviderName}.LogLevel переопределяют параметры в Logging.LogLevel. В приведенном выше JSON для поставщика Debug установлен уровень ведения журнала по умолчанию Information.

Logging:Debug:LogLevel:Default:Information

Приведенный выше параметр задает уровень ведения журнала Information для всех категорий Logging:Debug:, за исключением Microsoft.Hosting. Если задана конкретная категория, она переопределяет категорию по умолчанию. В приведенном выше коде JSON категории Logging:Debug:LogLevel"Microsoft.Hosting" и "Default" переопределяют параметры в Logging:LogLevel.

Минимальный уровень ведения журнала можно указать для:

  • Определенные поставщики: например Logging:EventSource:LogLevel:Default:Information
  • Конкретные категории: например, Logging:LogLevel:Microsoft:Warning
  • Все поставщики и все категории: Logging:LogLevel:Default:Warning

Любые журналы с уровнем ниже минимального не записываются:

  • переданы поставщику
  • регистрируются или отображаются.

Чтобы отключить все журналы, укажите LogLevel.None. LogLevel.None имеет значение 6, то есть выше LogLevel.Critical (5).

Если поставщик поддерживает области журналов, IncludeScopes указывает, включены ли они. Дополнительные сведения см. в разделе области журналов .

Следующий файл appsettings.json содержит параметры для всех встроенных поставщиков:

{
    "Logging": {
        "LogLevel": {
            "Default": "Error",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Warning"
        },
        "Debug": {
            "LogLevel": {
                "Default": "Information"
            }
        },
        "Console": {
            "IncludeScopes": true,
            "LogLevel": {
                "Microsoft.Extensions.Hosting": "Warning",
                "Default": "Information"
            }
        },
        "EventSource": {
            "LogLevel": {
                "Microsoft": "Information"
            }
        },
        "EventLog": {
            "LogLevel": {
                "Microsoft": "Information"
            }
        },
        "AzureAppServicesFile": {
            "IncludeScopes": true,
            "LogLevel": {
                "Default": "Warning"
            }
        },
        "AzureAppServicesBlob": {
            "IncludeScopes": true,
            "LogLevel": {
                "Microsoft": "Information"
            }
        },
        "ApplicationInsights": {
            "LogLevel": {
                "Default": "Information"
            }
        }
    }
}

В предыдущем примере:

  • Категории и уровни не являются предлагаемыми значениями. Этот пример представлен с целью продемонстрировать все поставщики по умолчанию.
  • Параметры в Logging.{ProviderName}.LogLevel переопределяют параметры в Logging.LogLevel. Например, уровень в Debug.LogLevel.Default переопределяет уровень в LogLevel.Default.
  • Для каждого поставщика используется псевдоним. Каждый поставщик определяет псевдоним, используемый в конфигурации вместо полного имени типа. Псевдонимы встроенных поставщиков:
    • Console
    • Debug
    • EventSource
    • EventLog
    • AzureAppServicesFile
    • AzureAppServicesBlob
    • ApplicationInsights

Настройка уровня ведения журнала с помощью командной строки, переменных среды и других способов конфигурации

Уровень ведения журнала может задаваться любыми поставщиками конфигурации. Например, вы можете создать хранимую переменную среды с именем Logging:LogLevel:Microsoft и значением Information.

Создайте хранимую переменную среды и присвойте ей значение уровня ведения журнала.

:: Assigns the env var to the value
setx "Logging__LogLevel__Microsoft" "Information" /M

В новом экземпляре командной строки считайте переменную среды.

:: Prints the env var value
echo %Logging__LogLevel__Microsoft%

Указанный выше параметр среды сохраняется в среде. Чтобы проверить параметры при работе с приложением, созданным на базе шаблонов рабочей службы .NET, выполните команду dotnet run в каталоге проекта после назначения переменной среды.

dotnet run

Совет

После настройки переменной среды перезапустите интегрированную среду разработки, чтобы гарантировать доступность добавленных переменных среды.

В Службе приложений Azure выберите Новый параметр приложения на странице Параметры > Конфигурация. Параметры приложения Службы приложений Azure:

  • Шифруются, когда они неактивны, и передаются по зашифрованному каналу.
  • Предоставляются в качестве переменных среды.

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

Настройка ведения журнала с помощью кода

Чтобы настроить логирование в коде, используйте API ILoggingBuilder. Доступ к ней можно получить из разных мест:

В этом примере показано задание поставщика ведения журнала для консоли и нескольких фильтров.

using Microsoft.Extensions.Logging;

using var loggerFactory = LoggerFactory.Create(static builder =>
{
    builder
        .AddFilter("Microsoft", LogLevel.Warning)
        .AddFilter("System", LogLevel.Warning)
        .AddFilter("LoggingConsoleApp.Program", LogLevel.Debug)
        .AddConsole();
});

ILogger logger = loggerFactory.CreateLogger<Program>();
logger.LogDebug("Hello {Target}", "Everyone");

В предыдущем примере AddFilter используется для настройки уровня журнала, который включен для различных категорий. AddConsole используется для добавления поставщика ведения консольного журнала. По умолчанию журналы с Debug серьезностью не включены, но так как конфигурация изменяет фильтры, на консоли отображается отладочное сообщение "Hello Everyone".

Применение правил фильтрации

При создании объекта ILogger<TCategoryName> объект ILoggerFactory выбирает одно правило для каждого поставщика, которое будет применено к этому средству ведения журналов. Все сообщения, записываемые с помощью экземпляра ILogger, фильтруются на основе выбранных правил. Самое подробное правило для каждой пары поставщика и категории выбирается из списка доступных правил.

При создании ILogger для данной категории для каждого поставщика используется приведенный далее алгоритм:

  • Выберите все правила, которые соответствуют поставщику или его псевдониму. Если соответствие не найдено, выберите все правила с пустым поставщиком.
  • В результатах предыдущего шага выберите правила с самым длинным соответствующим префиксом категории. Если совпадений не найдено, выберите все правила без категории.
  • Если выбрано несколько правил, примите последнее.
  • Если правила не выбраны, укажите минимальный уровень ведения журнала с помощью LoggingBuilderExtensions.SetMinimumLevel(ILoggingBuilder, LogLevel).

Категория журналирования

При создании объекта ILogger указывается категория. Эта категория включена в каждое сообщение журнала, созданное этим экземпляром ILogger. Строка категории является произвольной, но соглашение заключается в том, чтобы использовать полное имя класса. Например, категория может называться "Example.DefaultService", если служба в приложении определяется как следующий объект:

namespace Example
{
    public class DefaultService : IService
    {
        private readonly ILogger<DefaultService> _logger;

        public DefaultService(ILogger<DefaultService> logger) =>
            _logger = logger;

        // ...
    }
}

Если требуется дальнейшая классификация, соглашение заключается в том, чтобы использовать иерархическое имя путем добавления подкатегории к полному имени класса и явного указания категории с помощью LoggerFactory.CreateLogger:

namespace Example
{
    public class DefaultService : IService
    {
        private readonly ILogger _logger;

        public DefaultService(ILoggerFactory loggerFactory) =>
            _logger = loggerFactory.CreateLogger("Example.DefaultService.CustomCategory");

        // ...
    }
}

Вызов CreateLogger с фиксированным именем может быть полезным при использовании в нескольких классах или типах, чтобы события можно было упорядочить по категориям.

Использование ILogger<T> эквивалентно вызову CreateLogger с полностью квалифицированным именем типа T.

Уровень логирования

В следующей таблице перечислены значения LogLevel, используемый для удобства метод расширения Log{LogLevel}, а также приводятся сведения о предполагаемом применении:

LogLevel Значение Метод Описание
Трассировка 0 LogTrace Содержит наиболее подробные сообщения. Эти сообщения могут содержать конфиденциальные данные приложения. Эти сообщения по умолчанию отключены, и их никогда не следует включать в рабочей среде.
Отлаживать 1 LogDebug Используется для отладки и разработки. В рабочей среде следует использовать с осторожностью из-за высокого объема.
Информация 2 LogInformation Отслеживание общего потока работы приложения. Может использоваться в долгосрочных целях.
Предупреждения 3 LogWarning Для нестандартных или непредвиденных событий. Обычно содержит ошибки или условия, которые не приводят к сбою приложения.
Ошибка 4 LogError Для ошибок и исключений, которые не могут быть обработаны. Эти сообщения указывают на сбой текущих операции или запроса, а не на ошибку уровня приложения.
Критически 5 LogCritical Для сбоев, которые требуют неотложного внимания. Примеры: потеря данных, нехватка места на диске.
Не допускается 6 Указывает, что не следует записывать сообщения.

В приведенной выше таблице значения LogLevel приведены в порядке от самого низкого к самому высокому уровню серьезности.

Первый параметр метода Log, LogLevel, указывает на степень серьезности лога. Вместо вызова Log(LogLevel, ...) большинство разработчиков вызывают методы расширения Log{LogLevel}. Метод расширения Log{LogLevel} вызывает метод Log и указывает LogLevel. Например, следующие два вызова ведения журнала функционально эквивалентны и позволяют получить одинаковые журналы:

public void LogDetails()
{
    var logMessage = "Details for log.";

    _logger.Log(LogLevel.Information, AppLogEvents.Details, logMessage);
    _logger.LogInformation(AppLogEvents.Details, logMessage);
}

AppLogEvents.Details содержит идентификатор события и неявным образом представляется постоянным значением Int32. AppLogEvents обозначает класс, который предоставляет разные именованные константы для идентификаторов и отображается в разделе Идентификатор события журнала.

Следующий код создает журналы Informationи Warning:

public async Task<T> GetAsync<T>(string id)
{
    _logger.LogInformation(AppLogEvents.Read, "Reading value for {Id}", id);

    var result = await _repository.GetAsync(id);
    if (result is null)
    {
        _logger.LogWarning(AppLogEvents.ReadNotFound, "GetAsync({Id}) not found", id);
    }

    return result;
}

В приведенном выше коде первый Log{LogLevel} параметр AppLogEvents.Read— это идентификатор события журнала. Второй параметр — это шаблон сообщения с заполнителями для значений аргументов, предоставляемых оставшимися параметрами метода. Параметры метода рассматриваются более подробно в разделе, посвященном шаблону сообщений далее в этой статье.

Настройте подходящий уровень ведения журнала и вызовите правильные методы Log{LogLevel}, чтобы управлять объемом данных журнала, записываемых на определенный носитель. Например:

  • В производстве
    • При ведении журнала на уровнях Trace или Debug создается большой объем подробных сообщений журнала. Чтобы контролировать затраты и не превышать лимиты объема хранилища данных, записывайте сообщения уровня Trace и Debug в хранилище данных с большим объемом и низкими затратами. Рассмотрите возможность ограничения уровней Trace и Debug конкретными категориями.
    • При ведении журналов от Warning до Critical должно создаваться мало сообщений.
      • При этом стоимость и ограничения хранения, как правило, не важны.
      • Меньший объем журналов позволяет выбирать среди большего количества вариантов хранения данных.
  • В разработке:
    • Задайте значение Warning.
    • Добавляйте сообщения Trace или Debug при устранении неполадок. Чтобы ограничить объем выходных данных, задавайте уровни Trace или Debug только для исследуемых категорий.

Приведенный ниже код JSON задает Logging:Console:LogLevel:Microsoft:Information.

{
    "Logging": {
        "LogLevel": {
            "Microsoft": "Warning"
        },
        "Console": {
            "LogLevel": {
                "Microsoft": "Information"
            }
        }
    }
}

Идентификатор события в журнале

В каждом журнале может быть задан идентификатор события, который представляет собой структуру с Id и необязательными свойствами Name, доступными только для чтения. В этом примере исходного кода для определения идентификаторов событий используется класс AppLogEvents.

using Microsoft.Extensions.Logging;

internal static class AppLogEvents
{
    internal static EventId Create = new(1000, "Created");
    internal static EventId Read = new(1001, "Read");
    internal static EventId Update = new(1002, "Updated");
    internal static EventId Delete = new(1003, "Deleted");

    // These are also valid EventId instances, as there's
    // an implicit conversion from int to an EventId
    internal const int Details = 3000;
    internal const int Error = 3001;

    internal static EventId ReadNotFound = 4000;
    internal static EventId UpdateNotFound = 4001;

    // ...
}

Совет

Для получения дополнительной информации о преобразовании int в EventId, см. оператор EventId.Implicit(Int32 в EventId).

Идентификатор события связывает набор событий. Например, все журналы, связанные с чтением значений из репозитория, могут иметь идентификатор 1001.

Поставщик ведения журналов может записывать идентификатор события в поле идентификатора, в сообщении журнала, или вообще не сохранять его. Поставщик Debug не отображает идентификаторы событий. Поставщик Console отображает идентификаторы событий в квадратных скобках после категории:

info: Example.DefaultService.GetAsync[1001]
      Reading value for a1b2c3
warn: Example.DefaultService.GetAsync[4000]
      GetAsync(a1b2c3) not found

Некоторые поставщики ведения журнала хранят идентификатор события в поле, что позволяет выполнять фильтрацию по идентификатору.

Шаблон сообщения журнала логов

Каждый API ведения журнала использует шаблон сообщения. Шаблон сообщения может содержать заполнители, для которых предоставляются аргументы. Используйте для заполнителей имена, а не числа. Параметры, используемые для предоставления значений, определяются порядком заполнителей, а не их именами. В приведенном ниже коде имена параметров идут не по порядку в шаблоне сообщения:

string p1 = "param1";
string p2 = "param2";
_logger.LogInformation("Parameter values: {p2}, {p1}", p1, p2);

Приведенный выше код создает сообщение журнала со значениями параметров в определенном порядке:

Parameter values: param1, param2

Примечание.

Помните, что при использовании нескольких заполнителей в шаблоне сообщения они зависят от порядкового номера. Имена не используются для выравнивания аргументов с заполнителями.

Такой подход позволяет поставщикам ведения журнала реализовывать семантическое или структурированное ведение журналов. Сами аргументы передаются в систему ведения журналов, а не только в отформатированный шаблон сообщения. Это позволяет поставщикам ведения журнала хранить значения параметров как поля. Рассмотрим следующий метод логгера:

_logger.LogInformation("Getting item {Id} at {RunTime}", id, DateTime.Now);

Например, при ведении журнала в хранилище таблиц Azure:

  • каждая сущность таблицы Azure может иметь свойства ID и RunTime;
  • использование таблиц со свойствами позволяет упростить выполнение запросов к данным журнала. Например, с помощью запроса можно находить все журналы в пределах определенного диапазона RunTime, не анализируя время ожидания текстового сообщения.

Форматирование шаблона сообщения журнала

Шаблоны сообщений журнала поддерживают форматирование переменных заполнителей. Шаблоны свободно могут указать любой допустимый формат для указанного аргумента типа. Рассмотрим следующий Information шаблон сообщения журнала:

_logger.LogInformation("Logged on {PlaceHolderName:MMMM dd, yyyy}", DateTimeOffset.UtcNow);
// Logged on January 06, 2022

В предыдущем примере экземпляр DateTimeOffset является типом, соответствующим PlaceHolderName в шаблоне сообщения журнала. Это название может быть любым, так как оно основано на порядковых значениях. Формат MMMM dd, yyyy является допустимым для типа DateTimeOffset.

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

Примеры

В данных примерах показано, как отформатировать шаблон сообщения, используя синтаксис заполнителя {}. Кроме того, показан пример экранирования синтаксиса заполнителя {} с его выходными данными. Наконец, интерполяция строк с заполнителями шаблонов также показана:

logger.LogInformation("Number: {Number}", 1);               // Number: 1
logger.LogInformation("{{Number}}: {Number}", 3);           // {Number}: 3
logger.LogInformation($"{{{{Number}}}}: {{Number}}", 5);    // {Number}: 5

Совет

  • В большинстве случаев при логировании следует использовать форматирование шаблона сообщения журнала. Использование интерполяции строк может привести к проблемам с производительностью.
  • Правило анализа кода CA2254: шаблон должен быть статическим выражением , которое помогает предупредить вас о том, где сообщения журнала не используют надлежащее форматирование.

Запись исключений в журнал

Методы логгера имеют перегрузки, принимающие параметр исключения.

public void Test(string id)
{
    try
    {
        if (id is "none")
        {
            throw new Exception("Default Id detected.");
        }
    }
    catch (Exception ex)
    {
        _logger.LogWarning(
            AppLogEvents.Error, ex,
            "Failed to process iteration: {Id}", id);
    }
}

Принципы записи исключений в журнал зависят от конкретного поставщика.

Уровень ведения журнала по умолчанию

Если не задан уровень ведения журнала по умолчанию, то по умолчанию используется уровень Information.

Давайте рассмотрим следующее приложение службы рабочих процессов:

  • Создано на основе шаблонов .NET Worker.
  • файлы appsettings.json и appsettings.Development.json были удалены или переименованы.

В рамках приведенной выше конфигурации при переходе на страницу сведений о конфиденциальности или домашнюю страницу создается множество сообщений с уровнем Trace, Debug и Information, в имени категории которых используется Microsoft.

В следующем коде задается уровень ведения журнала по умолчанию в том случае, если этот уровень не определен в конфигурации:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Logging.SetMinimumLevel(LogLevel.Warning);

using IHost host = builder.Build();

await host.RunAsync();

Функция фильтрации

Функция фильтрации вызывается для всех поставщиков и категорий, которым в конфигурации и (или) коде не назначены правила:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Logging.AddFilter((provider, category, logLevel) =>
{
    return provider.Contains("ConsoleLoggerProvider")
        && (category.Contains("Example") || category.Contains("Microsoft"))
        && logLevel >= LogLevel.Information;
});

using IHost host = builder.Build();

await host.RunAsync();

Приведенный выше код отображает журналы консоли, если категория содержит Example или Microsoft и задан уровень ведения журнала Information или выше.

Области логирования

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

Область:

  • имеет тип IDisposable, который возвращается методом BeginScope;
  • Действует до момента его удаления.

Области поддерживаются следующими поставщиками:

Используйте область действия, заключив вызовы средства ведения журналов в блок using.

public async Task<T> GetAsync<T>(string id)
{
    T result;
    var transactionId = Guid.NewGuid().ToString();

    using (_logger.BeginScope(new List<KeyValuePair<string, object>>
        {
            new KeyValuePair<string, object>("TransactionId", transactionId),
        }))
    {
        _logger.LogInformation(
            AppLogEvents.Read, "Reading value for {Id}", id);

        var result = await _repository.GetAsync(id);
        if (result is null)
        {
            _logger.LogWarning(
                AppLogEvents.ReadNotFound, "GetAsync({Id}) not found", id);
        }
    }

    return result;
}

Следующий JSON предоставляет области действия для поставщика console:

{
    "Logging": {
        "Debug": {
            "LogLevel": {
                "Default": "Information"
            }
        },
        "Console": {
            "IncludeScopes": true,
            "LogLevel": {
                "Microsoft": "Warning",
                "Default": "Information"
            }
        },
        "LogLevel": {
            "Default": "Debug"
        }
    }
}

Следующий код предоставляет области для поставщика Console:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Logging.ClearProviders();
builder.Logging.AddConsole(options => options.IncludeScopes = true);

using IHost host = builder.Build();

await host.RunAsync();

Создание журналов в Main

Следующий код выполняет вход в Main, получая экземпляр ILogger из DI после инициализации хоста.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

using IHost host = Host.CreateApplicationBuilder(args).Build();

var logger = host.Services.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Host created.");

await host.RunAsync();

Предыдущий код основан на двух пакетах NuGet:

Его файл проекта будет выглядеть примерно так:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
    <PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
  </ItemGroup>

</Project>

Отсутствуют асинхронные методы ведения журнала

Скорость ведения журналов должна быть настолько высокой, чтобы затраты на производительность в сравнение с асинхронным кодом были незначительными. Если хранилище для логирования работает медленно, не записывайте в него напрямую. Сначала записывайте сообщения журнала в быстрое хранилище, а затем перемещайте их в медленное хранилище. Например, при записи журналов в SQL Server, не делайте этого непосредственно в методе Log, так как методы Log являются синхронными. Вместо этого синхронно добавьте сообщения журнала в очередь в памяти, и фоновый рабочий поток извлечет сообщения из очереди для выполнения асинхронных операций отправки данных на SQL Server.

Изменение уровней ведения журнала в работающем приложении

API ведения журнала не включает сценарий для изменения уровней журнала во время работы приложения. Однако некоторые поставщики конфигурации могут перезагружать конфигурацию, что немедленно влияет на конфигурацию ведения журнала. Например, поставщик конфигурации файлов по умолчанию перезагружает конфигурацию ведения журнала. Если конфигурация изменяется в коде во время выполнения приложения, приложение может вызвать IConfigurationRoot.Reload , чтобы обновить конфигурацию ведения журнала приложения.

Пакеты NuGet

Интерфейсы ILogger<TCategoryName> и их реализации ILoggerFactory включены в большинство SDK для .NET в качестве неявной ссылки на пакет. Они также явно доступны в следующих пакетах NuGet, если они не ссылаются неявно.

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

См. также