log4net原理解析

2019-07-15 16:04:41 浏览数 (1)

在任何项目中使用log4net,首先需要在web.config(app.config)文件中配置log4net相关信息。一般情况下,如下:

代码语言:javascript复制
<configSections>
  <!-- 定义log4net的section -->
  <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
</configSections>
<!-- 默认的Repository -->
<log4net>
  <!-- 默认的Root Logger -->
  <root> 
  <appender-ref ref="ERRORAppender" />
  <appender-ref ref="DEBUGAppender" />
  </root>
  <!-- 定义新Logger,自动继承Root Logger,并多了一个INFOAppender -->
  <logger name="Log4NetDemo.MyLogic"> 
  <appender-ref ref="INFOAppender" />
  </logger>
  <!-- 定义Appender -->
  <appender name="DEBUGAppender" type="log4net.Appender.RollingFileAppender">
  ...
  </appender>  
  <appender name="ERRORAppender" type="log4net.Appender.RollingFileAppender">
  ...
  </appender>
  <appender name="INFOAppender" type="log4net.Appender.RollingFileAppender">
  ...
  </appender>
</log4net>

一般而言,一个AppDomain需要配置一个log4net的section,它对应着一个repository,同一个AppDomain下所有程序集都可以使用这个repository。Repository可以说成基于一个log4net配置节点创建的顶级容器,它根据log4net配置节点的指示创建其他所有对象(Logger/Appender/Filter/Layout等等)并保有它们的实例,随时为你所用。下面的代码会根据配置信息来初始化一个Repository,一般会在程序启动的时候率先完成调用:

代码语言:javascript复制
log4net.Config.XmlConfigurator.Configure();

配置好log4net的信息,并调用XmlConfigurator.Configure()之后,就可以开始记日记了:

代码语言:javascript复制
log4net.LogManager.GetLogger().Info("Hello world!");

执行上面的代码,会经历log4net的完整的pipeline,如下图:

先看看这些执行的步骤,从整体上有一个认识,下面会进行具体分析,按照执行的顺序层层打通。

1. LogManager在调用GetLogger()时,会先确定repository,然后得到一个ILogger,最后通过WrapLogger封装得到一个ILog。大致代码为:

代码语言:javascript复制
ILogger logger = RepositorySelector.GetRepository(repositoryAssembly).GetLogger(name);
ILog log = WrapLogger(logger);

为什么需要ILogger和ILog两个接口?

ILogger是底层接口,api设计的更加通用,调用需要传递大量参数。ILog是建立在ILogger之上的高层接口,api设计的更加具体,调用api更加方便。所以ILogger是给ILog调用的,ILog是给用户调用的。我们用代码分层的角度看一下它们的关系:

底层的ILogger,已经提供了默认的实现,它们的OO关系图:

高层的ILog,基于对ILogger做了封装,也有自己的默认实现,它的OO关系图:

2. 在配置文件中logger(或root)节点是可以配置level信息的,level可以设置为:All,Debug,Info,Warn,Error,Fatal,Off里面的一种,如果希望关闭日志功能可以设置为Off,如果设置为Error可以记录Error和Fatal级别日志,如果设置为Warn可以记录Warn,Error和Fatal级别日志,以此类推。验证level的代码如下:

代码语言:javascript复制
bool IsEnabledFor(Level level)
{
  if (level != null)
  {
    if (m_hierarchy.IsDisabled(level))
    {
      return false;
    }
    return level >= this.EffectiveLevel;
  }
  return false;
}

在配置文件中配置如下:

代码语言:javascript复制
<log4net>
  <!-- 默认的Root Logger -->
  <root>
  <!-- level:All,Debug,Info,Warn,Error,Fatal,Off -->
  <!-- 如果不需要记录日志设置为Off -->
  <!-- 如果要记录Error和Fatal设置为Error -->
  <!-- 如果要记录Warn,Error和Fatal设置为Warn,以此类推 -->
  <level value="All" />
  <!-- 引用Appender -->
  <appender-ref ref="ERRORAppender" />
  <appender-ref ref="DEBUGAppender" />
  </root>
  ...
</log4net>

3. 如果验证level通过之后,会初始化一个LoggingEvent对象。这个对象包含了你所关心的信息,之后的步骤就都针对LoggingEvent对象来处理了。LoggingEvent对象里信息丰富,包含:时间、代码位置、Logger名、Domain、线程名、用户名、自定义属性信息、Message、异常、上下文等等。 我们看一下LoggingEvent类图:

从上图中可以看到,LoggingEvent类中定义了RenderedMessage属性,这个属性的返回值会最后输出在日志里。定义大概如下:

代码语言:javascript复制
get
{
  string m_data.Message = m_repository.RendererMap.FindAndRender(m_message);
  return m_data.Message;
}

这里就涉及到了对象的Render逻辑,我们看一下ILog里面最简单的接口定义:

代码语言:javascript复制
void Info(object message);

这里Info方法参数类型是object,我们在调用Info方法进行日志记录的时候,可以传递任意的类型,当传递的是string类型,会原样记录,当传递的是其他类型的时候,比如是一些自定义的类型对象,我们可以控制这些对象的Render逻辑的。自定义的Render需要实现log4net.ObjectRenderer.IObjectRenderer接口,然后在配置文件里面指定自定义的Render以及服务的类型。调用XmlConfigurator.Configure()之后,这些配置信息会在Repository里初始化好。在配置文件中配置自定义Render如下:

代码语言:javascript复制
<renderer renderingClass="Log4NetDemo.CustomExcpetionRenderer" renderedClass="Log4NetDemo.MyException" />

自定义的CustomExcpetionRenderer定义如下:

代码语言:javascript复制
public class CustomExcpetionRenderer : IObjectRenderer
{
    public void RenderObject(RendererMap rendererMap, object obj, TextWriter writer)
    {
        MyException myException = obj as MyException;
        writer.WriteLine(string.Format("Transaction ID  :  {0}", myException.TransID));
        writer.WriteLine(string.Format("Username        :  {0}", myException.Username));

        Exception exception = obj as Exception;
        while (exception != null)
        {
            WriteException(exception, writer);
            exception = exception.InnerException;
        }
    }
    private void WriteException(Exception ex, TextWriter writer)
    {
        writer.WriteLine(string.Format("Type: {0}", ex.GetType().FullName));
        writer.WriteLine(string.Format("Message: {0}", ex.Message));
        writer.WriteLine(string.Format("Source: {0}", ex.Source));
        writer.WriteLine(string.Format("TargetSite: {0}", ex.TargetSite));
        WriteExceptionData(ex, writer);
        writer.WriteLine(string.Format("StackTrace: {0}", ex.StackTrace));
    }
    private void WriteExceptionData(Exception ex, TextWriter writer)
    {
        foreach (DictionaryEntry entry in ex.Data)
        {
            writer.WriteLine(string.Format("{0}: {1}", entry.Key, entry.Value));
        }
    }
}

自定义的MyException如下:

代码语言:javascript复制
public class MyException : ApplicationException
{
    public string TransID;
    public string Username;
    public MyException(string message) : base(message)
    {
    }
}

通过配置文件初始化自定义Render的代码大致如下:

代码语言:javascript复制
void ParseRenderer(XmlElement element)
{
  string renderingClassName = element.GetAttribute(RENDERING_TYPE_ATTR);
  string renderedClassName = element.GetAttribute(RENDERED_TYPE_ATTR);
  IObjectRenderer renderer = (IObjectRenderer)OptionConverter.InstantiateByClassName(renderingClassName, typeof(IObjectRenderer), null);
  m_hierarchy.RendererMap.Put(SystemInfo.GetTypeFromString(renderedClassName, true, true), renderer);
}

最终所有的自定义Render和它服务的类型之间的映射关系保存在m_hierarchy.RendererMap里面。RendererMap.FindAndRender的方法定义如下:

代码语言:javascript复制
public string FindAndRender(object obj)
{
  // Optimisation for strings
  string strData = obj as String;
  if (strData != null)
  {
    return strData;
  }

  StringWriter stringWriter = new StringWriter(System.Globalization.CultureInfo.InvariantCulture);
  FindAndRender(obj, stringWriter);
  return stringWriter.ToString();
}

4. 初始化完成LoggingEvent对象之后,Logger传递LoggingEvent对象给Appenders,并委托Appenders来处理接下来的步骤。代码大致如下:

代码语言:javascript复制
var loggingEvent = new LoggingEvent(...);
CallAppenders(loggingEvent);

Logger里面的Appenders是如何管理的?我们看上面的ILogger OO关系图,Logger类通过实现IAppenderAttachable接口来对Appenders进行管理,AppenderAttachedImpl是具体实现IAppenderAttachable接口的类,Logger类通过调用AppenderAttachedImpl的方法最终实现管理多个Appenders。在AppenderAttachedImpl内部所有的Appenders最后会保存在AppenderCollection对象里面。看一下它们之间的OO图:

代码如下:

Logger的AddAppender代码:

代码语言:javascript复制
if (m_appenderAttachedImpl == null) 
{
  m_appenderAttachedImpl = new log4net.Util.AppenderAttachedImpl();
}
m_appenderAttachedImpl.AddAppender(newAppender);

AppenderAttachedImpl的AddAppender代码:

代码语言:javascript复制
if (m_appenderList == null) 
{
  m_appenderList = new AppenderCollection(1);
}
if (!m_appenderList.Contains(newAppender))
{
  m_appenderList.Add(newAppender);
}

通过上面的代码实现了给Logger添加多个Appenders,但是具体到每一个Logger加载哪些Appenders,这些信息是配置在配置文件中的,<log4net>节点里面可以配置多个appenders,并给不同的name进行标识,然后在每一个logger(root是一个特殊的logger)中引用自己需要的appenders,就像文章开头配置的那样。配置好这些信息比如RollingFileAppender:

代码语言:javascript复制
<appender name="DEBUGAppender" type="log4net.Appender.RollingFileAppender">
  <!-- 文件路径 -->
  <file value="logs\DEBUG\" />
  <datePattern value="yyyy\yyyyMM\yyyyMMdd'.log'" />
  <appendToFile value="true" />
  <rollingStyle value="Composite" />
  <staticLogFileName value="false"/>
  <maxSizeRollBackups value="100" />
  <maximumFileSize value="10MB" />
  <!-- 定义layout -->
  <layout type="log4net.Layout.PatternLayout">
    <conversionPattern value="�te %logger %user %message%newline" />
  </layout>
  <!-- 定义filter -->
  <filter type="log4net.Filter.LevelRangeFilter">
    <param name="LevelMin" value="DEBUG"/>
    <param name="LevelMax" value="DEBUG"/>
  </filter>
</appender>

执行XmlConfigurator.Configure()时,会对Loggers分别进行解析,根据自己配置的appender-ref信息加载并引用对应的appenders。代码大致如下:

代码语言:javascript复制
void ParseLogger(XmlElement loggerElement)
{
  string loggerName = loggerElement.GetAttribute(NAME_ATTR);
  Logger log = m_hierarchy.GetLogger(loggerName) as Logger;
  ParseChildrenOfLoggerElement(loggerElement, log);
}

void ParseChildrenOfLoggerElement(XmlElement catElement, Logger log) 
{
  log.RemoveAllAppenders();
  foreach (XmlNode currentNode in catElement.ChildNodes)
  {

    XmlElement currentElement = (XmlElement) currentNode;
    if (currentElement.LocalName == APPENDER_REF_TAG)
    {
      IAppender appender = FindAppenderByReference(currentElement);
      if (appender != null)
      {
        log.AddAppender(appender);
      }
    } 
  }
}

IAppender FindAppenderByReference(XmlElement appenderRef) 
{  
  string appenderName = appenderRef.GetAttribute(REF_ATTR);
  // Find the element with that id
  XmlElement element = null;

  if (appenderName != null && appenderName.Length > 0)
  {
    foreach (XmlElement curAppenderElement in appenderRef.OwnerDocument.GetElementsByTagName(APPENDER_TAG))
    {
      if (curAppenderElement.GetAttribute("name") == appenderName)
      {
        element = curAppenderElement;
        break;
      }
    }
  }

  if (element != null) 
  {
    appender = ParseAppender(element);
    if (appender != null)
    {
      return appender;
    }
  } 
  return null;
}

IAppender ParseAppender(XmlElement appenderElement) 
{
  string appenderName = appenderElement.GetAttribute(NAME_ATTR);
  string typeName = appenderElement.GetAttribute(TYPE_ATTR);
  
  IAppender appender = (IAppender)Activator.CreateInstance(SystemInfo.GetTypeFromString(typeName, true, true));
  appender.Name = appenderName;
  
  return appender;
}

最后我们在看一下已经实现好了的复杂的Appender的OO模型图:

6. 程序运行的pipeline进行到Appender之后,会调用里面的DoAppend(LoggingEvent loggingEvent)方法,在这个方法内部有一个Filter逻辑,是否真的会记录日志,取决于Filter是否通过,IFilter接口是一个链式Filter,定义如下:

代码语言:javascript复制
interface IFilter
{
  FilterDecision Decide(LoggingEvent loggingEvent);
  IFilter Next { get; set; }
}

Filter的逻辑是链式Filter上多个Filter中的Decide方法,只要有一个Decide返回Accept就表示会记录日志,代码如下:

代码语言:javascript复制
bool FilterEvent(LoggingEvent loggingEvent)
{
  IFilter f = this.FilterHead;
  while(f != null) 
  {
    switch(f.Decide(loggingEvent)) 
    {
      case FilterDecision.Deny: 
        return false;  // Return without appending
      case FilterDecision.Accept:
        f = null;    // Break out of the loop
        break;
      case FilterDecision.Neutral:
        f = f.Next;    // Move to next filter
        break;
    }
  }
  return true;
}

在配置文件中,可以针对每一个appender配置自己的filter,如下:

代码语言:javascript复制
<appender name="INFOAppender" type="log4net.Appender.RollingFileAppender">
  ...
  <filter type="log4net.Filter.LevelRangeFilter">
    <param name="LevelMin" value="INFO"/>
    <param name="LevelMax" value="INFO"/>
  </filter>
</appender>

为了演示,这里仅仅配置了LevelRangeFilter这种类型的Filter,在log4net中已经定义好了多种类型:

DenyAllFilter 阻止所有的日志事件被记录

LevelMatchFilter 只有指定等级的日志事件才被记录

LevelRangeFilter 日志等级在指定范围内的事件才被记录

LoggerMatchFilter 与Logger名称匹配才记录

PropertyFilter 消息匹配指定的属性值时才被记录

StringMathFilter 消息匹配指定的字符串才被记录

再看一下这些定义Filters的OO关系图:

7. Appender询问完Filter之后,Filter说要记录日志,那就肯定要记录日志了。但是具体用什么排版?Appender会委托给Layout去处理。在配置文件中可以对Appender配置自己的Layout:

代码语言:javascript复制
<appender name="INFOAppender" type="log4net.Appender.RollingFileAppender">
  ...
  <layout type="log4net.Layout.PatternLayout">
    <conversionPattern value="�te %logger %user %message%newline" />
  </layout>
</appender>

在log4net的代码中,会对配置的Layout进行调用:

代码语言:javascript复制
void RenderLoggingEvent(TextWriter writer, LoggingEvent loggingEvent)
{
  if (m_layout == null) 
  {
    throw new InvalidOperationException("A layout must be set");
  }

  if (m_layout.IgnoresException) 
  {
    string exceptionStr = loggingEvent.GetExceptionString();
    if (exceptionStr != null && exceptionStr.Length > 0) 
    {
      // render the event and the exception
      m_layout.Format(writer, loggingEvent);
      writer.WriteLine(exceptionStr);
    }
    else 
    {
      // there is no exception to render
      m_layout.Format(writer, loggingEvent);
    }
  }
  else 
  {
    // The layout will render the exception
    m_layout.Format(writer, loggingEvent);
  }
}

关于Layout也有很多实现,我们可以看一下它们的OO图:

最最常用的是PatternLayout,它的功能最丰富,可以输出各式各样的信息,比如:newline,logger,date,exception,message,level,appdomain,username等等。如:"�te %-5level- %message" 表示要以此输出日志日期、级别(5个字母的宽度)、信息。在PatternLayout静态构造器中,有这些输出信息的全部定义:

代码语言:javascript复制
static PatternLayout()
{
  s_globalRulesRegistry = new Hashtable(45);

  s_globalRulesRegistry.Add("literal", typeof(LiteralPatternConverter));
  s_globalRulesRegistry.Add("newline", typeof(NewLinePatternConverter));
  s_globalRulesRegistry.Add("n", typeof(NewLinePatternConverter));

// .NET Compact Framework 1.0 has no support for ASP.NET
// SSCLI 1.0 has no support for ASP.NET
#if !NETCF && !SSCLI && !CLIENT_PROFILE && !NETSTANDARD1_3
  s_globalRulesRegistry.Add("aspnet-cache", typeof(AspNetCachePatternConverter));
  s_globalRulesRegistry.Add("aspnet-context", typeof(AspNetContextPatternConverter));
  s_globalRulesRegistry.Add("aspnet-request", typeof(AspNetRequestPatternConverter));
  s_globalRulesRegistry.Add("aspnet-session", typeof(AspNetSessionPatternConverter));
#endif

  s_globalRulesRegistry.Add("c", typeof(LoggerPatternConverter));
  s_globalRulesRegistry.Add("logger", typeof(LoggerPatternConverter));

  s_globalRulesRegistry.Add("C", typeof(TypeNamePatternConverter));
  s_globalRulesRegistry.Add("class", typeof(TypeNamePatternConverter));
  s_globalRulesRegistry.Add("type", typeof(TypeNamePatternConverter));

  s_globalRulesRegistry.Add("d", typeof(DatePatternConverter));
  s_globalRulesRegistry.Add("date", typeof(DatePatternConverter));

  s_globalRulesRegistry.Add("exception", typeof(ExceptionPatternConverter));

  s_globalRulesRegistry.Add("F", typeof(FileLocationPatternConverter));
  s_globalRulesRegistry.Add("file", typeof(FileLocationPatternConverter));

  s_globalRulesRegistry.Add("l", typeof(FullLocationPatternConverter));
  s_globalRulesRegistry.Add("location", typeof(FullLocationPatternConverter));

  s_globalRulesRegistry.Add("L", typeof(LineLocationPatternConverter));
  s_globalRulesRegistry.Add("line", typeof(LineLocationPatternConverter));

  s_globalRulesRegistry.Add("m", typeof(MessagePatternConverter));
  s_globalRulesRegistry.Add("message", typeof(MessagePatternConverter));

  s_globalRulesRegistry.Add("M", typeof(MethodLocationPatternConverter));
  s_globalRulesRegistry.Add("method", typeof(MethodLocationPatternConverter));

  s_globalRulesRegistry.Add("p", typeof(LevelPatternConverter));
  s_globalRulesRegistry.Add("level", typeof(LevelPatternConverter));

  s_globalRulesRegistry.Add("P", typeof(PropertyPatternConverter));
  s_globalRulesRegistry.Add("property", typeof(PropertyPatternConverter));
  s_globalRulesRegistry.Add("properties", typeof(PropertyPatternConverter));

  s_globalRulesRegistry.Add("r", typeof(RelativeTimePatternConverter));
  s_globalRulesRegistry.Add("timestamp", typeof(RelativeTimePatternConverter));
  
#if !(NETCF || NETSTANDARD1_3)
  s_globalRulesRegistry.Add("stacktrace", typeof(StackTracePatternConverter));
  s_globalRulesRegistry.Add("stacktracedetail", typeof(StackTraceDetailPatternConverter));
#endif

  s_globalRulesRegistry.Add("t", typeof(ThreadPatternConverter));
  s_globalRulesRegistry.Add("thread", typeof(ThreadPatternConverter));

  // For backwards compatibility the NDC patterns
  s_globalRulesRegistry.Add("x", typeof(NdcPatternConverter));
  s_globalRulesRegistry.Add("ndc", typeof(NdcPatternConverter));

  // For backwards compatibility the MDC patterns just do a property lookup
  s_globalRulesRegistry.Add("X", typeof(PropertyPatternConverter));
  s_globalRulesRegistry.Add("mdc", typeof(PropertyPatternConverter));

  s_globalRulesRegistry.Add("a", typeof(AppDomainPatternConverter));
  s_globalRulesRegistry.Add("appdomain", typeof(AppDomainPatternConverter));

  s_globalRulesRegistry.Add("u", typeof(IdentityPatternConverter));
  s_globalRulesRegistry.Add("identity", typeof(IdentityPatternConverter));

  s_globalRulesRegistry.Add("utcdate", typeof(UtcDatePatternConverter));
  s_globalRulesRegistry.Add("utcDate", typeof(UtcDatePatternConverter));
  s_globalRulesRegistry.Add("UtcDate", typeof(UtcDatePatternConverter));

  s_globalRulesRegistry.Add("w", typeof(UserNamePatternConverter));
  s_globalRulesRegistry.Add("username", typeof(UserNamePatternConverter));
}

在这里完成了所有的规则的注册,这里可以看到大量不同类型的PatternConverter,我们看一下它们的OO图:

8. 到这里,我们完成了log4net所有的pipeline,在这整个过程中,我们首先定义log4net的section,接着配置Logger,还可以配置自定义的Render,然后配置Appender,以及Appender的Filter和Layout。一切就绪,整个流程走完,相信我们接触到的Logger、Appender、Filter、Layout、Render都已不再陌生。log4net良好的实现了事件过滤、格式排版的高度扩展性和可配置性。最后,给出Repository、Appender、Filter、Layout、Render的关系简图:

下一片文章将主要写,如何在项目中运用log4net,谢谢观看!

0 人点赞