Ведение журнала ClassName и MethodName с использованием log4net для проекта .NET - PullRequest
9 голосов
/ 22 сентября 2010

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

  1. То, что log4net использует внутренне выбрасывающее исключение для генерации фрейма стека, и это становится дорогостоящим, если вы используете это, как правило, для всей регистрации.
  2. Головокружение. Там много литературы. Я попробовал кучу всего этого и не получил что-то полезное.

Если вы меня на секунду потешите, я бы хотел сбросить.

Я создал такой класс в своем проекте

public static class Log {
    private static Dictionary<Type, ILog> _loggers = new Dictionary<Type, ILog>();
    private static bool _logInitialized = false;
    private static object _lock = new object();

    public static string SerializeException(Exception e) {
        return SerializeException(e, string.Empty);
    }

    private static string SerializeException(Exception e, string exceptionMessage) {
        if (e == null) return string.Empty;

        exceptionMessage = string.Format(
            "{0}{1}{2}\n{3}",
            exceptionMessage,
            (exceptionMessage == string.Empty) ? string.Empty : "\n\n",
            e.Message,
            e.StackTrace);

        if (e.InnerException != null)
            exceptionMessage = SerializeException(e.InnerException, exceptionMessage);

        return exceptionMessage;
    }

    private static ILog getLogger(Type source) {
        lock (_lock) {
            if (_loggers.ContainsKey(source)) {
                return _loggers[source];
            }

            ILog logger = log4net.LogManager.GetLogger(source);
            _loggers.Add(source, logger);
            return logger;
        }
    }

    public static void Debug(object source, object message) {
        Debug(source.GetType(), message);
    }

    public static void Debug(Type source, object message) {
        getLogger(source).Debug(message);
    }

    public static void Info(object source, object message) {
        Info(source.GetType(), message);
    }

    public static void Info(Type source, object message) {
        getLogger(source).Info(message);
    }

...

    private static void initialize() {
        XmlConfigurator.Configure(); 
    }

    public static void EnsureInitialized() {
        if (!_logInitialized) {
            initialize();
            _logInitialized = true;
        }
    }
}

(Если этот код выглядит знакомым, то это потому, что он заимствован из примеров!)

В любом случае, во всем моем проекте я использую такие строки для входа:

        Log.Info(typeof(Program).Name, "System Start");

Ну, этот вид работ. Самое главное, я получаю имя класса, но не имя метода. Меньше, что важно, я загрязняю свой код этим мусором типа "typeof". Если я скопирую и вставлю фрагмент кода между файлами и т. Д., Структура логов будет лежать!

Я попытался поиграть с PatternLayout (% C {1}. {M}), но это не сработало (все, что он сделал, это записал «Log.Info» в журнал - потому что все идет через журнал. Х статических методов!). Кроме того, это должно быть медленно.

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

Заранее признателен за любую помощь.

Ответы [ 4 ]

9 голосов
/ 23 сентября 2010

log4net (и NLog) оба предоставляют метод ведения журнала, который позволяет «обернуть» их регистраторы и все же получить правильную информацию о сайте вызова. По сути, регистратору log4net (или NLog) нужно сообщить типу, который образует «границу» между кодом регистрации и кодом приложения. Я думаю, что они называют это «тип регистратора» или что-то подобное. Когда библиотеки получают информацию о сайте вызова, они перемещаются вверх по стеку вызовов до тех пор, пока MethodBase.DeclaringType не станет равным (или, возможно, AssignableFrom) «типу регистратора». Следующий кадр стека будет вызывать код приложения.

Вот пример того, как войти через NLog из оболочки (log4net был бы похож - посмотрите в документации по log4net интерфейс ILogger (не ILog):

  LogEventInfo logEvent = new LogEventInfo(level, _logger.Name, null, "{0}", new object[] { message }, exception);

  _logger.Log(declaringType, logEvent);

Где declaringType - это переменная-член, которая была установлена ​​примерно так:

  private readonly static Type declaringType = typeof(AbstractLogger);

И "AbstractLogger" - это тип вашей оболочки журнала. В вашем случае это будет выглядеть примерно так:

  private readonly static Type declaringType = typeof(Log);

Если NLog необходимо получить информацию о сайте вызова (из-за операторов сайта вызова в макете), он будет перемещаться вверх по стеку до тех пор, пока MethodBase.DeclaringType для текущего кадра не станет равным (или AssignableFrom) declaringType. Следующим кадром в стеке будет фактический сайт вызова.

Вот некоторый код, который будет работать для регистрации с «обернутым» log4net logger. Он использует интерфейс log4net ILogger и передает тип «упаковочного» регистратора для сохранения информации о сайте вызова. Вам не нужно заполнять класс / структуру события этим методом:

  _logger.Log(declaringType, level, message, exception);

Опять же, "declaringType" - это тип вашей оболочки. _logger - это log4net logger, Level - это значение log4net.LogLevel, message - это сообщение, исключение - это исключение (если есть, ноль в противном случае).

Что касается загрязнения ваших сайтов вызовов с помощью Typeof (что угодно), я думаю, вы застряли с этим, если хотите использовать один статический объект "Журнал". В качестве альтернативы, внутри методов ведения журнала объекта «Журнал» вы можете получить вызывающий метод, подобный принятому ответу в этом посте

Как найти метод, вызвавший текущий метод?

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

Взяв все это вместе, вы бы написали свой метод Debug примерно так (опять же, это с точки зрения NLog, потому что это то, что я имею перед собой):

public static void Debug(object message)
{
  MethodBase mb = GetCallingMethod();
  Type t = mb.DeclaringType;
  LogEventInfo logEvent = new LogEventInfo(LogLevel.Debug, t.Name, null, "{0}", new object [] message, null);
  ILogger logger = getLogger(t) As ILogger;
  logger.Log(declaringType, logEvent)
}

Обратите внимание, что вы, вероятно, не найдете здесь много людей в StackOverflow, которые бы рекомендовали написать функцию-оболочку журналирования, подобную этой (которая явно получает вызывающий метод для любого вызова журнала). Не могу сказать, что я бы тоже порекомендовал это, но он более или менее отвечает на вопрос, который вы задали. Если вы хотите использовать статический объект «Журнал», то вам придется либо явно передавать тип на каждом сайте вызовов журналирования (чтобы получить правильный регистратор классов), либо вам придется добавить код внутри вызова журналирования для навигации составить и выяснить эту информацию для себя. Я не думаю, что какой-либо из этих вариантов особенно привлекателен.

Теперь, сказав все это, вы можете рассмотреть возможность использования log4net или NLog напрямую, а не добавлять этот сложный (и не обязательно надежный) код для получения информации о сайте вызова. Как указывает Мэтью, NLog предоставляет простой способ получить регистратор для текущего класса. Чтобы получить регистратор для текущего класса с использованием log4net, вы должны сделать это в каждом классе:

private static readonly log4net.ILog log = log4net.LogManager.GetLogger( 
        System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); 

против этого с NLog:

  private static readonly NLog.logger log = NLog.LogManager.GetCurrentClassLogger();

Это довольно распространенное использование.

Если вы не хотите зависеть от конкретной реализации ведения журнала, вы можете использовать одну из доступных абстракций ведения журнала, например Common.Logging (NET) или Simple Logging Facade (SLF) ) .

Даже если вы не используете одну из этих абстракций, загрузите исходный код Common.Logging и посмотрите на абстракцию для log4net.Он точно покажет, как обернуть регистратор log4net таким образом, чтобы информация о месте вызова была сохранена (и была доступна операторам макета).

3 голосов
/ 29 августа 2015

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

public static void InfoWithCallerInfo(this ILog logger, 
    object message, Exception e = null, [CallerMemberName] string memberName = "",
    [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
{
    if (!logger.IsInfoEnabled)
        return;
    if (e == null)
        logger.Info(string.Format("{0}:{1}:{2} {3}", sourceFilePath, 
            memberName, sourceLineNumber, message));
    else
        logger.Info(string.Format("{0}:{1}:{2} {3}", sourceFilePath, 
            memberName, sourceLineNumber, message), e);
}

Примечания:

  • Это функция-обертка, которую я написал для ILog :: Info, она вам понадобится и для других уровней ведения журнала.
  • Для этого требуется Информация о вызывающем абоненте , которая доступна только начиная с .Net 4.5. Положительным моментом является то, что эти переменные заменяются во время компиляции строковыми литералами, поэтому все это очень эффективно.
  • Для простоты я оставил параметр sourceFilePath как есть, вы, вероятно, предпочли бы отформатировать его (обрезать большую часть / весь путь)
3 голосов
/ 23 сентября 2010

Я предпочитаю шаблон, подобный следующему, который работает с Log4Net и подобными API:

class MyClass
{
    private static readonly ILog logger = LogManager.GetLogger(typeof(MyClass));

    void SomeMethod(...)
    {
        logger.Info("some message");

        ...

        if (logger.IsInfoEnabled)
        {
            logger.Info(... something that is expensive to generate ...);
        }
    }

}

Некоторые замечания:

  • С этим шаблоном вы оцениваете typeof(MyClass) только один раз - по сравнению с вашим образцом, в котором вы вызываете object.GetType () при каждом вызове журнала, независимо от того, включен ли соответствующий уровень журнала. Ничего страшного, но в целом желательно, чтобы журналы имели минимальные издержки.

  • Вам все еще нужно использовать typeof и убедиться, что вы не получили неправильное имя класса при использовании копирования / вставки. Я предпочитаю жить с этим, потому что альтернатива (например, NLog's LogManager.GetCurrentClassLogger, как описано в ответе Мэтью Феррейры) требует получения StackFrame, который снижает производительность и требует, чтобы вызывающий код имел разрешение UnmanagedCode. Кроме того, я думаю, что было бы неплохо, если бы C # предоставлял некоторый синтаксис во время компиляции для ссылки на текущий класс - что-то вроде C ++ _ class _ macro.

  • Я бы отказался от любой попытки получить имя текущего метода по трем причинам. (1) Существенные накладные расходы производительности, и регистрация должна быть быстрой. (2) Встраивание означает, что вы не можете получить метод, который, как вы думаете, вы получаете. (3) Это накладывает требование на разрешение UnmanagedCode.

1 голос
/ 22 сентября 2010

Я знаю, что у вас уже есть код, который зависит от log4net, но задумывались ли вы о другой структуре ведения журналов, которая может лучше соответствовать вашим требованиям? Я лично использую NLog для своих собственных приложений. Это позволяет код как это:

class Stuff
{
    private static readonly Logger logger = LogManager.GetCurrentClassLogger();

    // ...

    void DoStuff()
    {
        logger.Info("blah blah");
    }
}

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

...