Создание собственного провайдера логгирования в ASP.NET Core

ASP.NET Core предоставляет удобную абстракцию для ведения логов. Благодаря этому работа с логами устроена одинаково, независимо от того, какой провайдер используется в приложении.

Провайдеры в свою очередь обеспечивают инфраструктуру для хранения логов в том или ином хранилище. Базовые библиотеки уже имеют набор стандартных провайдеров для наиболее популярных хранилищ. Но что если требуется отправлять логи в собственное, нестандартное хранилище? Для этого можно реализовать собственный провайдер.

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

Логгер

Для начала нам требуется реализовать логгер. Логгер — это объект, который непосредственно реализует логику сохранения в конкретный тип хранилища. Он в курсе всех деталей и тонкостей именно этого хранилища.

Для реализации собственного логгера, следует создать объект, реализующий интерфейс ILogger. Этот интерфейс имеет три метода:

  • BeginScope() — позволяет определить группу сообщений. Если логгер поддерживает scope, то здесь следует вернуть IDisposable.
  • IsEnabled() — возвращает информацию о том, доступен ли данный логгер для заданного уровня логгирования.
  • Log() — метод, непосредственно реализующий логику сохранения записи в лог.

Первая реализация логгера может выглядеть так:

public class SmsLogger : ILogger
{
  public SmsLogger()
  {
  }

  public IDisposable BeginScope<TState>(TState state)
  {
    return null;
  }

  public bool IsEnabled(LogLevel logLevel)
  {
    return true;
  }

  public void Log<TState>(LogLevel logLevel, EventId eventId,
    TState state, Exception exception, Func<TState, Exception, string> formatter)
  {
    if (IsEnabled(logLevel))
    {
      var msg = formatter(state, exception);

      // send sms here
    }
  }
}

В этой реализации отсутствует фильтрация, поэтому IsEnabled() всегда возвращает true. Также отправка SMS не поддерживает scope, поэтому BeginScope() возвращает null.

Внутри метода Log() следует обратиться к внешнему сервису для отправки сообщения. Здесь можно использовать любой сторонний сервис и просто отправлять ему HTTP-запрос всякий раз, когда требуется логгирование.

Провайдер для логгера

Чтобы подключить логгер к приложению требуется провайдер. Провайдер — это объект, который при необходимости создает логгер. Также провайдер занимается освобождением ресурсов. Для реализации провайдера нужно создать объект, реализующий интерфейс ILoggerProvider:

public class SmsLoggerProvider : ILoggerProvider
{
  public SmsLoggerProvider()
  {
  }

  public ILogger CreateLogger(string category)
  {
    return new SmsLogger();
  }

  public void Dispose()
  {
  }
}

Подключаем провайдер к приложению

Теперь чтобы подключить провайдер к приложению, достаточно добавить этот провайдер в коллекцию провайдеров:

public class Startup
{
  public void ConfigureServices(IServiceCollection services)
  {
  }

  public void Configure(IApplicationBuilder app, IHostingEnvironment env,
    ILoggerFactory loggerFactory)
  {
    loggerFactory.AddProvider(new SmsLoggerProvider());
  }
}

Можно добавить немного синтаксического сахара и сделать метод-расширение для добавления провайдера:

public static class SmsLoggerProviderExtensions
{
  public static ILoggerFactory AddSms(this ILoggerFactory loggerFactory)
  {
    loggerFactory?.AddProvider(new SmsLoggerProvider());
    return loggerFactory;
  }
}

Тогда код добавления провайдера можно переписать следующим образом:

public class Startup
{
  public void ConfigureServices(IServiceCollection services)
  {
  }

  public void Configure(IApplicationBuilder app, IHostingEnvironment env,
    ILoggerFactory loggerFactory)
  {
    loggerFactory.AddSms();
  }
}

Поддержка фильтров

Фильтры позволяют настроить какие именно записи будут попадать в лог для данного провайдера. Для нашего примера это особенно актуально — было бы глупо отправлять через SMS информацию о всех HTTP-запросах. Поэтому давайте добавим поддержку фильтров для нашего провайдера.

В первую очередь добавим поддержку фильтров в реализацию логгера:

public class SmsLogger : ILogger
{
  private readonly string _category;

  private readonly Func<string, LogLevel, bool> _filter;

  public SmsLogger(string category, Func<string, LogLevel, bool> filter)
  {
    _category = category;
    _filter = filter;
  }

  public IDisposable BeginScope<TState>(TState state)
  {
    return null;
  }

  public bool IsEnabled(LogLevel logLevel)
  {
    return _filter == null || _filter(_category, logLevel);
  }

  public void Log<TState>(LogLevel logLevel, EventId eventId,
    TState state, Exception exception, Func<TState, Exception, string> formatter)
  {
    if (IsEnabled(logLevel))
    {
      var msg = formatter(state, exception);
      // send sms here
    }
  }
}

Как видно, код поменялся не очень существенно: конструктор теперь получает информацию о категории и функцию-фильтр. Эти данные используются в методе IsEnabled().

Для задания фильтра немного изменим реализацию провайдера:

public class SmsLoggerProvider : ILoggerProvider
{
  private readonly Func<string, LogLevel, bool> _filter;

  public SmsLoggerProvider(Func<string, LogLevel, bool> filter)
  {
    _filter = filter;
  }

  public ILogger CreateLogger(string category)
  {
    return new SmsLogger(category, _filter);
  }

  public void Dispose()
  {
  }
}

И метода-расшрения для подключения провайдера:

public static class SmsLoggerProviderExtensions
{
  public static ILoggerFactory AddSms(this ILoggerFactory loggerFactory,
    Func<string, LogLevel, bool> filter = null)
  {
    loggerFactory?.AddProvider(new SmsLoggerProvider(filter));
    return loggerFactory;
  }
}

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

public class Startup
{
  public void ConfigureServices(IServiceCollection services)
  {
  }

  public void Configure(IApplicationBuilder app, IHostingEnvironment env,
    ILoggerFactory loggerFactory)
  {
    loggerFactory.AddSms((category, level) =>
    {
      if (category.Contains("MyApp") && level == LogLevel.Critical)
        return true;

      return false;
    });
  }
}