前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >log4net原理解析

log4net原理解析

作者头像
小蜜蜂
发布2019-07-15 16:04:41
1.5K0
发布2019-07-15 16:04:41
举报
文章被收录于专栏:明丰随笔明丰随笔

在任何项目中使用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="%date %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="%date %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等等。如:"%date %-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,谢谢观看!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-07-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 明丰随笔 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档