Включая ведение журнала как часть моей модели домена

#c# #logging #nlog #error-logging

#c# #ведение журнала #nlog #ошибка-ведение журнала

Вопрос:

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

Итак, моя модель домена включает LogMessage класс:

 public sealed class LogMessage
{
    public string Message { get; }
    public DateTime TimestampUtc { get; }
    public LogLevel Level { get; }
}

public enum LogLevel
{
    Fatal = 5,
    Error = 4,
    Warn = 3,
    Info = 2,
    Debug = 1,
    Trace = 0
}
  

У меня также есть Result класс, который имеет свойство collection LogMessages . Конечные пользователи могут сохранять результаты в файлах моего приложения и открывать их из них.

 public class Result
{
    public bool Succeeded {get; set;}
    public string StatusMessage {get; set;}
    public IList<LogMessage> LogMessages {get; set;}
}
  

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

 public interface ILogger
{
    void Debug(string message);
    void Error(string message);
    void Fatal(string message);
    void Info(string message);
    void Log(LogLevel level, string message);
    void Trace(string message);
    void Warn(string message);
}
  

Я предоставляю экземпляр ILogger подключаемым модулям, в которые выполняется запись Result.LogMessages .

 public interface IPlugIn
{
    Output DoSomeThing(Input in, ILogger logger);
}
  

Я, очевидно, также хочу иметь возможность входить в систему из моего собственного внутреннего кода и в конечном итоге хочу Result.LogMessages содержать смесь моих внутренних сообщений журнала и сообщений журнала из подключаемых модулей. Таким образом, конечный пользователь, у которого возникли проблемы, может отправить мне файл результатов, который будет содержать журналы отладки как из моего внутреннего кода, так и из любых используемых плагинов.

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

 public class LogResultTarget : NLog.Targets.Target
{
    public static Result CurrentTargetResult { get; set; }

    protected override void Write(NLog.LogEventInfo logEvent)
    {
        if (CurrentTargetResult != null)
        {
            //Convert NLog logEvent to LogMessage
            LogLevel level = (LogLevel)Enum.Parse(typeof(LogLevel), logEvent.Level.Name);
            LogMessage lm = new LogMessage(logEvent.TimeStamp.ToUniversalTime(), level, logEvent.Message);
            CurrentTargetResult.LogMessages.Add(lm);
        }
    }

    protected override void Write(NLog.Common.AsyncLogEventInfo logEvent)
    {
        Write(logEvent.LogEvent);
    }
}
  

Этот класс пересылает сообщение в Result присвоенное статическому LogResultTarget.CurrentTargetResult свойству. Мой внутренний код регистрируется в регистраторах NLog, и у меня есть реализация, ILogger которая также регистрируется в NLog.Logger .

Это работает, но кажется действительно хрупким. Если CurrentTargetResult установлено неправильно или не возвращено значение null, я могу в конечном итоге сохранить сообщения журнала с результатами, к которым они не применимы. Кроме того, поскольку существует только один статический CurrentTargetResult , я никак не мог поддерживать обработку нескольких результатов одновременно.

Есть ли другой / лучший способ, которым я мог бы подойти к этому? Или то, что я пытаюсь сделать, в корне неверно?

Комментарии:

1. Я не до конца понимаю CurrentTargetResult свойство, оно не используется в вашей текущей цели? (Только проверка null?) Как именно выглядит Result тип?

2. Похоже, у меня была опечатка в моем примере кода, где у меня было «Target» вместо «CurrentTargetResult». Я также добавил пример класса «Результат». Помогает ли это?

3. Да, добавлен ответ 🙂

Ответ №1:

Я думаю, что ваш подход правильный, но вы могли бы сэкономить усилия, используя библиотеку, которая уже выполняет эту абстракцию за вас. Общая библиотека ведения журнала — это то, что вам нужно.

Ваш доменный код будет зависеть только от ILogger интерфейса обычного ведения журнала. Только когда ваш домен используется средой выполнения, например, Web API, вы затем настраиваете, какого поставщика ведения журнала вы собираетесь использовать.

Существует ряд готовых поставщиков, доступных в виде отдельных пакетов nuget:

Обычный.Ведение журнала предоставляет адаптеры, которые поддерживают все следующие популярные цели / платформы ведения журнала в .NET:

  • Log4Net (версии v1.2.9 — v1.2.15)
  • NLog (v1.0 — v4.4.1)
  • SeriLog (версия v1.5.14)
  • Блок приложения для ведения журнала корпоративной библиотеки Microsoft (версии v3.1 — v6.0)
  • Microsoft AppInsights (2.4.0)
  • Отслеживание событий Microsoft для Windows (ETW)
  • Войдите в стандартный вывод
  • Войдите в систему для ОТЛАДКИ

Я использую это в течение ряда лет, и было здорово, что код вашего домена / библиотеки можно повторно использовать в другом контексте, но при этом не обязательно иметь фиксированную зависимость от платформы ведения журнала (я перешел с корпоративных библиотек на log4net и, наконец, на NLog … это было легко).

Ответ №2:

На мой взгляд, static CurrentTargetResult действительно немного хрупок, но общий подход хорош.

Я предлагаю следующие изменения:

  1. нестатизируйте CurrentTargetResult и всегда делайте его инициализированным,

    Что-то вроде этого:

     public class LogResultTarget : NLog.Targets.Target
    {
        public Result CurrentTargetResult { get; } = new Result();
    
        protected override void Write(NLog.LogEventInfo logEvent)
        {
            //Convert NLog logEvent to LogMessage
            LogLevel level = (LogLevel)Enum.Parse(typeof(LogLevel), logEvent.Level.Name);
            LogMessage lm = new LogMessage(logEvent.TimeStamp.ToUniversalTime(), level, logEvent.Message);
            CurrentTargetResult.LogMessages.Add(lm);
        }
    
        protected override void Write(NLog.Common.AsyncLogEventInfo logEvent)
        {
            Write(logEvent.LogEvent);
        }
    }
      
  2. Всегда инициализируйте LogMessages:

     public class Result
    {
        public bool Succeeded {get; set;}
        public string StatusMessage {get; set;}
        public IList<LogMessage> LogMessages {get; set;} = new List<LogMessage>();
    }
      
  3. Извлекайте сообщения — при необходимости — с помощью следующих вызовов:

     // find target by name
    var logResultTarget1 = LogManager.Configuration.FindTargetByName<LogResultTarget>("target1");
    var results = logResultTarget1.CurrentTargetResu<
    
    // or multiple targets
    var logResultTargets = LogManager.Configuration.AllTargets.OfType<LogResultTarget>();
    var allResults = logResultTargets.Select(t => t.CurrentTargetResult);
      

PS: Вы также могли бы перезаписать InitializeTarget в LogResultTarget для инициализации цели