Лучшая практика обертки регистратора


91

Я хочу использовать в своем приложении nlogger, возможно, в будущем мне нужно будет изменить систему ведения журналов. Поэтому я хочу использовать фасад для вырубки.

Вы знаете какие-нибудь рекомендации по написанию существующих примеров? Или просто дайте мне ссылку на лучшую практику в этой области.




Ответы:


207

Раньше я использовал фасады журналирования, такие как Common.Logging (даже для того, чтобы скрыть мою собственную библиотеку CuttingEdge.Logging ), но в настоящее время я использую шаблон внедрения зависимостей, и это позволяет мне скрывать логгеры за моей собственной (простой) абстракцией, которая придерживается обоих зависимостей. Принцип инверсии и принцип разделения интерфейсов(ISP), потому что у него один член и потому что интерфейс определяется моим приложением; не внешняя библиотека. Сведите к минимуму знания основных частей вашего приложения о существовании внешних библиотек, тем лучше; даже если у вас нет намерения когда-либо заменять вашу библиотеку журналов. Жесткая зависимость от внешней библиотеки затрудняет тестирование вашего кода и усложняет ваше приложение с помощью API, который никогда не был разработан специально для вашего приложения.

Вот как часто выглядит абстракция в моих приложениях:

public interface ILogger
{
    void Log(LogEntry entry);
}

public enum LoggingEventType { Debug, Information, Warning, Error, Fatal };

// Immutable DTO that contains the log information.
public class LogEntry 
{
    public readonly LoggingEventType Severity;
    public readonly string Message;
    public readonly Exception Exception;

    public LogEntry(LoggingEventType severity, string message, Exception exception = null)
    {
        if (message == null) throw new ArgumentNullException("message");
        if (message == string.Empty) throw new ArgumentException("empty", "message");

        this.Severity = severity;
        this.Message = message;
        this.Exception = exception;
    }
}

При желании эта абстракция может быть расширена с помощью некоторых простых методов расширения (позволяя интерфейсу оставаться узким и придерживаться ISP). Это значительно упрощает код для потребителей этого интерфейса:

public static class LoggerExtensions
{
    public static void Log(this ILogger logger, string message) {
        logger.Log(new LogEntry(LoggingEventType.Information, message));
    }

    public static void Log(this ILogger logger, Exception exception) {
        logger.Log(new LogEntry(LoggingEventType.Error, exception.Message, exception));
    }

    // More methods here.
}

Поскольку интерфейс содержит только один метод, вы можете легко создать ILoggerреализацию , что прокси для log4net , к Serilog , Microsoft.Extensions.Logging , NLog или любой другой библиотеке лесозаготовительной и настроить DI контейнер , чтобы ввести его в классах , которые имеют ILoggerв своем конструктор.

Обратите внимание, что наличие статических методов расширения поверх интерфейса с одним методом сильно отличается от наличия интерфейса с множеством членов. Методы расширения - это просто вспомогательные методы, которые создают LogEntryсообщение и передают его через единственный метод ILoggerинтерфейса. Методы расширения становятся частью кода потребителя; не часть абстракции. Это не только позволяет методам расширения развиваться без необходимости изменения абстракции, методов расширения иLogEntryконструктор всегда выполняется, когда используется абстракция регистратора, даже если этот регистратор заглушен / имитируется. Это дает больше уверенности в правильности вызовов регистратора при запуске в комплекте тестов. Однокомпонентный интерфейс также значительно упрощает тестирование; Наличие абстракции с множеством членов затрудняет создание реализаций (таких как макеты, адаптеры и декораторы).

Когда вы это делаете, вряд ли когда-нибудь понадобится какая-то статическая абстракция, которую могут предложить фасады журналирования (или любая другая библиотека).


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

2
@ user1829319 Это просто пример. Я уверен, что вы можете предложить реализацию на основе этого ответа, которая соответствует вашим конкретным потребностям.
Стивен

2
Я до сих пор не понимаю ... в чем преимущество того, что 5 методов Logger являются расширениями ILogger и не являются членами ILogger?
Элизабет,

3
@Elisabeth Преимущество состоит в том, что вы можете адаптировать интерфейс фасада к ЛЮБОЙ структуре ведения журналов, просто реализовав одну функцию: «ILogger :: Log». Методы расширения гарантируют, что у нас есть доступ к «удобным» API (таким как «LogError», «LogWarning» и т. Д.), Независимо от того, какую структуру вы решите использовать. Это окольный способ добавить функциональность общего «базового класса», несмотря на работу с интерфейсом C #.
BTownTKD

2
Мне нужно еще раз подчеркнуть причину, по которой это здорово. Преобразование вашего кода из DotNetFramework в DotNetCore. В проектах, где я это делал, мне нужно было написать только один новый бетон. Те, где я этого не делал .... гааааааааааааааааааааааааааааааааааааааааааа! Я рада, что нашла "обратный путь".
granadaCoder


8

На данный момент лучше всего использовать пакет Microsoft.Extensions.Logging ( как указал Джулиан ). С этим можно использовать большинство фреймворков журналирования.

Определение собственного интерфейса, как объяснено в ответе Стивена, подходит для простых случаев, но при этом упускается несколько вещей, которые я считаю важными:

  • Объекты структурированного ведения журнала и деструктуризации (обозначение @ в Serilog и NLog)
  • Отложенное построение / форматирование строки: поскольку он принимает строку, он должен оценивать / форматировать все при вызове, даже если в конце событие не будет зарегистрировано, потому что оно ниже порога (стоимость производительности, см. Предыдущий пункт)
  • Условные проверки, подобные IsEnabled(LogLevel)которым вы можете захотеть, еще раз по соображениям производительности

Вы, вероятно, сможете реализовать все это в своей собственной абстракции, но в этот момент вы будете изобретать колесо.


4

Обычно я предпочитаю создавать интерфейс вроде

public interface ILogger
{
 void LogInformation(string msg);
 void LogError(string error);
}

а во время выполнения я добавляю конкретный класс, который реализуется из этого интерфейса.


11
И не забывайте, LogWarningи LogCriticalметоды , и все их перегрузку. При этом вы нарушите принцип разделения интерфейса . Предпочитайте определять ILoggerинтерфейс одним Logметодом.
Стивен

2
Мне очень жаль, это не было моим намерением. Не нужно стыдиться. Я действительно часто вижу этот дизайн, потому что многие разработчики используют популярный log4net (который использует именно этот дизайн) в качестве примера. К сожалению, этот дизайн не очень хорош.
Стивен

2
Я предпочитаю это ответу @ Стивена. Он вводит зависимость LogEntryи, следовательно, зависимость от LoggingEventType. Однако ILoggerреализация LoggingEventTypes, вероятно case/switch, должна иметь дело с этим , что является запахом кода . Зачем скрывать LoggingEventTypesзависимость? Реализация в любом случае должна обрабатывать уровни ведения журнала , поэтому было бы лучше явно указать, что должна делать реализация, а не скрывать это за одним методом с общим аргументом.
DharmaTurtle,

1
В качестве крайнего примера представьте, что у ICommandкоторого есть, Handleкоторый принимает object. case/switchДля выполнения контракта интерфейса реализации должны иметь более возможные типы. Это не идеально. У вас нет абстракции, скрывающей зависимость, которую нужно обрабатывать в любом случае. Вместо этого имейте интерфейс, который ясно заявляет, что ожидается: «Я ожидаю, что все регистраторы будут обрабатывать предупреждения, ошибки, критические ошибки и т. Д.». Это предпочтительнее, чем "Я ожидаю, что все регистраторы будут обрабатывать сообщения, которые включают предупреждения, ошибки, критические ошибки и т. Д."
DharmaTurtle,

Я вроде как согласен и с @Steven, и с @DharmaTurtle. Кроме того, LoggingEventTypeих следует называть, LoggingEventLevelпоскольку типы являются классами и должны кодироваться как таковые в ООП. Для меня нет разницы между неиспользованием метода интерфейса и отказом от соответствующего enumзначения. Вместо этого используйте ErrorLoggger : ILogger, InformationLogger : ILoggerгде каждый регистратор определяет свой уровень. Затем DI необходимо ввести необходимые регистраторы, возможно, через ключ (перечисление), но этот ключ больше не является частью интерфейса. (Теперь вы ТВЕРДЫЙ).
Wouter

4

Прекрасное решение этой проблемы появилось в виде проекта LibLog .

LibLog - это абстракция журналирования со встроенной поддержкой основных журналов, включая Serilog, NLog, Log4net и Enterprise logger. Он устанавливается через диспетчер пакетов NuGet в целевую библиотеку как файл источника (.cs) вместо ссылки .dll. Такой подход позволяет включить абстракцию журналирования, не заставляя библиотеку принимать внешние зависимости. Это также позволяет автору библиотеки включать ведение журнала, не заставляя приложение-потребитель явно предоставлять средство ведения журнала библиотеке. LibLog использует отражение, чтобы выяснить, какой конкретный регистратор используется, и подключиться к нему без какого-либо явного кода подключения в проектах библиотеки.

Итак, LibLog - отличное решение для ведения журнала в библиотечных проектах. Просто создайте ссылку и настройте конкретный регистратор (Serilog для победы) в своем основном приложении или службе и добавьте LibLog в свои библиотеки!


Я использовал это, чтобы обойти проблему с изменением log4net ( yuck ) ( wiktorzychla.com/2012/03/pathetic-breaking-change-between.html ) Если вы получите это от nuget, он фактически создаст файл .cs в коде вместо добавления ссылок на предварительно скомпилированные библиотеки DLL. Файл .cs находится в пространстве имен вашего проекта. Поэтому, если у вас разные уровни (csprojs), у вас либо будет несколько версий, либо вам нужно будет объединить их в общий csproj. Вы поймете это, когда попытаетесь его использовать. Но, как я уже сказал, это была палочка-выручалочка с проблемой критического изменения log4net.
granadaCoder


Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.