Записать имя пользователя с помощью log4net

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

HttpContext context = HttpContext.Current;
if (context != null && context.User != null && context.User.Identity.IsAuthenticated)
{
    MDC.Set("user", HttpContext.Current.User.Identity.Name);
}

Код выглядит нормально, за исключением событий, у которых нет связанного с ними пользовательского контекста (т.е. пользователя на нашей общедоступной веб-странице). В этом случае захват log4net, похоже, когда-нибудь записывает последнюю зарегистрированную учетную запись пользователя (плохой) и когда-то записывает нуль (хороший). Кто-нибудь получил эту функцию для надежной работы во всех случаях? Я считаю, что увидел заметку о том, что MDC больше не является рекомендуемой функцией для использования, но я не смог найти альтернативы, которые рекомендуются.

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

Ответы

Ответ 1

Если информация, доступная в HttpContext, достаточна, то есть, если код примера, который вы опубликовали, дает правильный ответ (за исключением проблемы с MDC), и вы просто просто не будете писать:

HttpContext context = HttpContext.Current; 
if (context != null && context.User != null && context.User.Identity.IsAuthenticated)
{     
  MDC.Set("user", HttpContext.Current.User.Identity.Name); 
} 

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

См. этот вопрос для примера того, как написать собственный шаблон PatternLayoutConverter:

Пользовательское свойство log4net PatternLayoutConverter (с индексом)

Используя пример в этой ссылке, вы можете сделать что-то вроде этого:

namespace Log4NetTest
{
  class HttpContextUserPatternConverter : PatternLayoutConverter
  {
    protected override void Convert(System.IO.TextWriter writer, LoggingEvent loggingEvent)
    {
      string name = "";
      HttpContext context = HttpContext.Current;
      if (context != null && context.User != null && context.User.Identity.IsAuthenticated)
      {
        name = context.User.Identity.Name;
      }
      writer.Write(name);
    }
  }
}

Вы должны настроить это в log4net примерно так:

  //Log HttpContext.Current.User.Identity.Name
  <layout type="log4net.Layout.PatternLayout">
    <param name="ConversionPattern" value="%d [%t] %-5p [User = %HTTPUser] %m%n"/>
    <converter>
      <name value="HTTPUser" />
      <type value="Log4NetTest.HttpContextUserPatternConverter" />
    </converter>
  </layout>

Кроме того, вы можете создавать другие конвертеры шаблонов, которые используют параметр Option (см. пример по ссылке выше), чтобы вытащить определенный элемент из коллекций HttpContext.Current.Items или HttpContext.Current.Session.

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

namespace Log4NetTest
{
  class HttpContextSessionPatternConverter : PatternLayoutConverter
  {
    protected override void Convert(System.IO.TextWriter writer, LoggingEvent loggingEvent)
    {
      //Use the value in Option as a key into HttpContext.Current.Session
      string setting = "";

      HttpContext context = HttpContext.Current;
      if (context != null)
      {
        object sessionItem;
        sessionItem = context.Session[Option];
        if (sessionItem != null)
        {
          setting = sessionItem.ToString();
        }
        writer.Write(setting);
      }
    }
  }
}


namespace Log4NetTest
{
  class HttpContextItemPatternConverter : PatternLayoutConverter
  {
    protected override void Convert(System.IO.TextWriter writer, LoggingEvent loggingEvent)
    {
      //Use the value in Option as a key into HttpContext.Current.Session
      string setting = "";

      HttpContext context = HttpContext.Current;
      if (context != null)
      {
        object item;
        item = context.Items[Option];
        if (item != null)
        {
          setting = item.ToString();
        }
        writer.Write(setting);
      }
    }
  }
}

Вы также можете найти эти ссылки полезными:

http://piers7.blogspot.com/2005/12/log4net-context-problems-with-aspnet.html

Здесь блоггер предлагает другое решение для регистрации значений из HttpContext, чем то, что я предложил. Прочтите сообщение в блоге, чтобы увидеть его описание проблемы и его решение. Чтобы суммировать решение, он хранит объект в GlobalDiagnosticContext (более современное имя для MDC). Когда log4net регистрирует значение объекта, он использует ToString(). Реализация его объекта извлекает значение из HttpContext:

Итак, вы можете сделать что-то вроде этого:

public class HttpContextUserNameProvider
{
  public override string ToString()
  {
    HttpContext context = HttpContext.Current;  
    if (context != null && context.User != null && context.User.Identity.IsAuthenticated)
    {
      return context.Identity.Name;
    }
    return "";
  }
}

Вы можете поместить экземпляр этого объекта в GlobalDiagnosticContext (MDC) в начале вашей программы, и он всегда будет возвращать правильное значение, так как он обращается к HttpContext.Current.

MDC.Set("user", new HttpContextUserNameProvider());

Это кажется намного проще, чем я предлагал!

Для полноты, если кто-то хочет знать, как сделать то же самое в NLog, NLog, похоже, делает всю/всю информацию HttpContext доступной через его "aspnet- *" LayoutRenderers:

https://github.com/nlog/nlog/wiki/Layout-Renderers

Ответ 2

В соответствии с Log4Net официальными документами API, MDC устарел:

Кроме того, MDC.Set принимает значения только как значения, поэтому последнее решение из @wageoghe не может работать (тот, который использует HttpContextUserNameProvider)

Моим решением было использовать HttpContextUserNameProvider с log4net.GlobalContext, также предложенным в официальных документах API:

  • Добавьте это сразу после инициализации log4net (например, в Global.Application_Start)

    log4net.GlobalContext.Properties["user"] = new HttpContextUserNameProvider();
    
  • Добавьте этот класс

    public class HttpContextUserNameProvider
    {
        public override string ToString()
        {
            HttpContext context = HttpContext.Current;
            if (context != null && context.User != null && context.User.Identity.IsAuthenticated)
            {
                return context.User.Identity.Name;
            }
            return "";
        }
    }
    
  • Измените конфигурацию log4net, добавив значение свойства "пользователь", например:

    <layout type="log4net.Layout.PatternLayout" value="%property{user}"/>
    

Ответ 3

Начиная с версии Log4Net 1.2.11 теперь вы можете просто использовать шаблон appender для получения авторизованного пользователя через запрос ASP.NET, например

%aspnet-request{AUTH_USER}

Ответ 4

Это чистая спекуляция, но это очень похоже на проблему, связанную с общими потоками запросов, то есть потоками ThreadPool. Когда вы устанавливаете значение MDC, оно связано с текущим потоком, и этот поток будет возвращен в ThreadPool в конце запроса, а затем повторно использован для последующих запросов. Если значение не перезаписывается, вы можете увидеть старые значения в новых запросах.

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

Ответ 5

Есть два разных способа сделать то, что вы хотите %identity и %username.

Эти могут быть использованы в вашем шаблоне appender.

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

См. это сообщение: Log4Net не может найти свойство% username, когда я назову файл в своем приложении

Ответ 6

Я использовал решение wageoghe и Gian Marco Gherardi, но вместо того, чтобы GlobalContext установил контекст потока непосредственно перед записью сообщения:

ThreadContext.Properties["user"] = new HttpContextUserNameProvider();