在任何项目中使用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,谢谢观看!