日志记录工具

在软件开发、测试以及生产环境中,日志记录是非常重要的,采用合适的日志记录工具、设计合理的日志输出格式,有利于帮助开发人员明确程序的执行流程以及错误的快速定位和修正。日志记录作为软件开发周期中的重要组成部分,本文介绍几种常用的日志记录工具: 

  • log4net
  • NLog
  • log4j
  • SLF4J

其中,log4net 和 NLog 主要用于 C#/.net 平台,log4j 和 SLF4J 主要用于 Java 开发。

log4net

参考Apache log4net 官网

免费的开源日志记录组件,log4net 库是基于 Apache log4j 框架在 Microsoft .NET 平台的实现。

关于 log4net 的基本介绍,可以参见:log4net Tutorial - CodeProject

特点

  •  Distinct Goals: speed and flexibility
  •  多种路径输出:日志信息输出到:[1]. 控制台;[2]. 文件;[3]. 数据库;
  •  支持多种 .Net 框架,支持多种日志信息等级,支持多种格式输出
  •  XML Configuration + Dynamic Configuration

基本使用

log4net.dll 引用 + 配置文件 log4net.xml

目前,最新版本的 .dll 文件是:log4net 2.0.8

本部分主要研究 log4net 的配置文件:log4net.xml

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4Net" />
  </configSections>
   
  <log4net>
    <root>
      <level value="ALL" />
      <appender-ref ref="RollingLogFileAppender" />
	  <appender-ref ref="ColoredLogConsoleAppender" />
    </root>
	
	<appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender,log4net">
	  <param name="File" value="Log\" />
	  <param name= "AppendToFile" value= "true" />
	  <param name= "DatePattern" value= "yyyyMMdd".log"" />
	  <param name= "RollingStyle" value= "Date" />
	  <param name="StaticLogFileName" value="false" />
	  <lockingModel type="log4net.Appender.FileAppender.MinimalLock" />
	  <layout type="log4net.Layout.XMLLayout,log4net">
	    <param name="Header" value="" />
        <param name="Footer" value="" />
		<param name="ConversionPattern" value="[%d] [%t] %-5p %c-(%line)  %m%n" />
	  </layout>
	  <filter type="log4net.Filter.LevelRangeFilter">
         <param name="LevelMin" value="ALL" />
         <param name="LevelMax" value="OFF" />
      </filter>
	</appender>
	
	<appender name="ColoredLogConsoleAppender" type="log4net.Appender.ColoredConsoleAppender,log4net">	  
      <mapping>
        <level value="INFO" />
        <foreColor value="Grey" />
      </mapping>
	  <mapping>
        <level value="WARN" />
        <foreColor value="Green" />
      </mapping>
	  <mapping>
        <level value="ERROR" />
        <foreColor value="Red, HighIntensity" />
      </mapping>
	  <layout type="log4net.Layout.PatternLayout,log4net">
         <param name="ConversionPattern" value="[%d] [%t] %-5p %c-(%line) %m%n" />
      </layout>
	</appender>
  </log4net>
</configuration>

首先对 log4net 配置文件的格式作说明,以下两种方法是等价的:

<param name="keyName" value="valueName" />
<keyName value="valueName" />

关于控制日志文件编码格式,在appender中加如下配置即可

<!--不加utf-8编码格式,中文字符将显示成乱码-->
<param name="Encoding" value="utf-8" /> 

下面对 .xml 文件中几个重要标签作说明:

[0.1].RollingFileAppender

RollingFileAppender 基于 FileAppender,FileAppender 和 RollingFileAppender 都是用来将日志写入到文本文件中,RollingFileAppender 提供更多的控制选项

[0.2].ColoredConsoleAppender

ColoredConsoleAppender 基于 ConsoleAppender,ConsoleAppender 和 ColoredConsoleAppender 都是用来将日志写入到控制台,ColoredConsoleAppender 提供更多的控制选项 

[1].level

日志信息控制级别,由低到高 ALL|DEBUG|INFO|WARN|ERROR|FATAL|OFF,默认 DEBUG,只记录 >=level 的日志信息

其中,ALL表示允许所有的日志请求,OFF表示拒绝所有的请求

[2].appender-ref

定义日志对象所使用的 Appender

[3].RollingStyle

日志文件滚动方式,支持 Date、Size、Composite 三种形式,具体地:

  •  Date:日期滚动,每天一个日志文件
  •  Size:文件大小滚动,匹配如下 2 个参数:
    •  MaximumFileSize:日志文件大小限制,支持 KB|MB|GB
    •  MaxSizeRollBackups:日志文件个数限制,默认 -1 表示无限制
  •  Composite:Date + Size 的合体版,适于日志量比较大的情况

[4].layout

日志信息输出格式,支持 XMLLayout、PatternLayout,具体地:

  •  PatternLayout:经典格式
  •  XMLLayout:XML格式

关于 Layout 的详细信息,参见下面的 深入理解 - Layout 部分。

[5].lockingModel

文件锁类型,允许多个进程可以写入同一个文件,RollingFileAppender 本身不是线程安全的,若在程序中没有进行线程安全限制,可在此处配置、确保写入安全。支持两种类型:

  •  ExclusiveLock:排它锁
  •  MinimalLock:最小锁定,以允许多个进程可以写入同一个文件

建议增加如下配置,避免第二次记录时,日志文件已经打开的异常

<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />

其它,需要注意的几个问题

// 如果是网站项目,须在项目中的 AssemblyInfo.cs 文件的最后一行中添加
[assembly: log4net.Config.DOMConfigurator(ConfigFile = "Web.config", Watch = true)] 

在 log4net 中,最常用的是 RollingFileAppender,各种常用的不同配置参见:Log4net中的RollingFileAppender解析

下面是 log4net 的基本调用方法:读取 log4net.xml 文件,日志信息记录。

using log4net;

namespace log4net_sqh
{
    public class Program
    {
        static void Main(string[] args)
        {
            string assemblyFilePath = Assembly.GetExecutingAssembly().Location;
            string assemblyDirPath = Path.GetDirectoryName(assemblyFilePath);
            string configFilePath = assemblyDirPath + " \\log4net.xml";
            log4net.Config.XmlConfigurator.ConfigureAndWatch(new FileInfo(configFilePath));

            ILog Logger = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

            Logger.Fatal("fatal, 严重错误");
            Logger.Error("error, 错误");
            Logger.Warn("warn, 警告");
            Logger.Info("info, 信息");
            Logger.Debug("debug, 调试");           
			
            Console.Read();
        }
    }
}

相关内容可参见:C#使用Log4Net记录日志

其中,MethodBase.GetCurrentMethod().DeclaringType 用于获取声明该成员的类。ConfigureAndWatch 方法读取配置文件 log4net.xml,相比 Configure 方法,当配置文件有变化时会重新加载。不同等级的日志信息记录支持多种重载方法:

void Info(object message);
void Info(object message, Exception exception);
void InfoFormat(string format, params object[] args);

下面提供如下链接,可以参考:

除了单独使用 log4net.xml 文件作为 log4net 的配置文件外,亦可直接使用项目自带的 app.config(CS程序)或 web.config(BS程序) 文件,只要将 <log4net> 结点放在 <configuration> 结点中,但 <log4net> 结点必须在 <configSections> 结点的下面,否则不会记日志。

直接用配置文件 app.Config 或 web.Config 配置应用系统的运行参数,比单独做一个 .xml 配置文件,简洁方便:

  • 提供常见的 connectionStrings 和 appSettings,给出 数据库连接 和 常见的键/值表的定义访问方法
  • 提供自定义段configSections,可以自行定义段元素,eg. log4net、assembly 等

下面给出一个 app.config 文件的模板代码:

使用并读取 app.config 文件的配置信息,项目应注意:

[1]. 添加引用程序集 System.configuration
[2]. 名称空间添加 using System.Configuration; 

关于加载 log4net.config 配置文件,可以采用不同的方法,相关信息参考:log4net 配置文件配置方法

(1)Configuration Attributes

该方式通过添加 Assembly Attribute,在项目 AssemblyInfo.cs 文件中添加如下代码,指示程序从 app.config 中读取配置

[assembly: log4net.Config.XmlConfigurator(Watch=true)]

(2)appSettings

该方式在配置文件中添加如下配置

<appSettings>
  <add key="log4net.Config" value="log4net.config"/>
  <add key="log4net.Config.Watch" value="True"/>
</appSettings>

(3)Configuration Files

该方式可以通过如下两种方法实现

  • Using the .NET System.Configuration API
  • Reading the file contents directly

推荐第一种方式,在程序入口使用如下代码

log4net.Config.XmlConfigurator.Configure(new FileInfo("app.config")); 或 
log4net.Config.XmlConfigurator.ConfigureAndWatch(new FileInfo("app.config"));

第二种方式就是前面说的单独使用一个 log4net.xml 文件作为配置文件的方法

Only one log4net element can be specified in the xml file, but it may/can be located anywhere in the XML hierarchy. 

这三种方法,Configuration Files 拥有绝对控制权,appSettings 次之,assembly-level attributes 最弱。

一个小小的扩展知识

app.config - exe.config - vshost.exe.config

  • app.config是自定义的配置文件,vshost.exe.config和exe.config是程序运行时自动创建的内容跟app.config一样的文件
  • vshost.exe.config是程序运行时使用的配置文件,exe.config是程序运行后会复制到vshost.exe.config
  • 从app.config复制到exe.config再复制到vshost.exe.config
  • 对 .config 文件的读写操作,实际是针对 exe.config,该文件必不可少

有关 app.config 文件配置问题,可参考:关于自定义配置结点

关于log4net写数据库,具体参见:https://www.cnblogs.com/yonghuacui/p/6179196.html

深入理解

最新版本的源码文件:log4net-2.0.8-src.zip

log4net 有四个主要组件,分别是:

  • Logger(记录器)
  • Appender(附着器)
  • Layout(布局)
  • Repository(库)

下面分别具体介绍各个组件:

1Logger

应用程序交互的主要组件,产生日志消息,日志消息经过 Layout 的格式化处理才会输出。

Log4net 框架定义一个 ILog 接口,所有的 logger 类(包括自定义的logger类)都必须实现这个接口。

public interface ILog
{
   // 基本方法
   void Debug(object message);
   void Info(object message);
   void Warn(object message);
   void Error(object message);
   void Fatal(object message);
 
   // 以上的每一个方法都有多个重载的方法,用来支持异常处理及格式化处理
   void Debug(object message, Exception ex);
   void DebugFormat(...);
        ...

   //属性用来检查Logger的日志级别
   bool isDebugEnabled;
        ...
}

Log4net 框架定义一个 LogManager 类,管理所有的 logger 对象。该类的 GetLogger() 静态方法,用我们提供的名字创建 Logger 对象或检索已经存在的 Logger 对象

log4net.ILog log = log4net.LogManager.GetLogger("logger-name");

方法 GetLogger() 的入参可以使用自定义的日志名字 "logger-name",也可以使用如下参数作为入参

System.Reflection.MethodBase.GetCurrentMethod().DeclaringType

在应用程序中可以创建多个 Logger,每个实例化的 Logger 对象都被 log4net 框架作为命名实体(named entity)来分别维护。 

2Appender

定义输出介质,log4net 支持多种形式的日志消息输出。

一个 Appender 对象缺省地将所有的日志消息传递到输出流,可以使用 Appender Filters 按照不同的标准过滤日志事件。

3Layout

用于控制 Appender 的输出格式,可以是线性的或XML,但是一个 Appender 只能有一个 Layout。log4net 支持多种 layout:

  • PatternLayout:经典格式,最常用
  • SimpleLayout:简单格式,只输出日志级别与消息内容
  • XMLLayout:XML格式
  • RawTimeStampLayout:可格式化时间,常用于输出到数据库

关于 layout 结点的配置说明:

 %d:datetime,当前语句的日期时间,%d{HH:mm:ss,fff} 可以只显示时间
 %p:priority,日志级别
 %m:message,输出的日志消息
 %n:newline,换行
 %t:threadid,当前语句所在线程ID
 %c:class,当前日志对象的名称
 %L:输出语句所在的行号
 %F:输出语句所在的文件名
 %-数字:表示该项的最小长度,如果不够,则用空格填充

4Repository

负责日志对象组织结构的维护。

扩展应用

在学习 log4net 的最后,实现 log4net 输出 xml 格式的日志:

  • 使用 log4net 自带的 XmlLayout
  • 自定义 XmlLayout

使用自定义 XmlLayout 格式,务必如下:

  • write a class deriving from XmlLayoutBase,override the FormatXml method
  • instruct your appender with yourXmlLayout

首先给出配置文件 app.config,基本同上,layout 使用自定义的 XmlLayout

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/>
  </configSections>

  <log4net>
	<root>
      <level value="ALL" />
      <appender-ref ref="RollingLogFileAppender" />
    </root>
  
    <appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender" >
      <param name="File" value="log\" />
      <param name="AppendToFile" value="true" />
      <param name="RollingStyle" value="Date" />
	  <param name="datePattern" value="yyyyMMdd'.txt'" />
      <param name="staticLogFileName" value="false" />
      <layout type="MyNamespace.MyXmlLayout">
	    <param name="Header" value="<?xml version="1.0" encoding="utf-8"?>
<Root>
" />
        <param name="Footer" value="</Root>
" />
      </layout>
    </appender>    
  </log4net>
  
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
  </startup>
</configuration>

其中,&#13;&#10; 表示 \r\n 换行。

然后,自定义类 MyXmlLayout 继承自 XmlLayoutBase,重写 FormatXml 方法:

namespace MyNamespace {
	public class MyXmlLayout : XmlLayoutBase
	{
		protected override void FormatXml(XmlWriter writer, LoggingEvent loggingEvent)
		{
			// 自定义 XML 文档格式
			... ...
		}
	}
}

该方法中可以自定义显示的 xml 文件内容。其中,类 XmlLayoutBase 定义如下

public abstract class XmlLayoutBase : LayoutSkeleton
{
	protected XmlLayoutBase();

	public override void ActivateOptions() { }
	public override void Format(TextWriter writer, LoggingEvent loggingEvent);
	{
		XmlTextWriter xmlTextWriter = new XmlTextWriter(new ProtectCloseTextWriter(writer));
		xmlTextWriter.Formatting = Formatting.None;
		xmlTextWriter.Namespaces = false;
		this.FormatXml(xmlTextWriter, loggingEvent);
		xmlTextWriter.WriteWhitespace(SystemInfo.NewLine); // 换行
		xmlTextWriter.Close(); // 关闭流
	}
	
	protected abstract void FormatXml(XmlWriter writer, LoggingEvent loggingEvent);
}

除此之外,主函数调用方式保持不变。

至此,xml 格式日志文件基本可正常输出。但是该文件可能存在缩进、中文格式、xml读取的问题,需做如下修正:

[1]. 缩进 XML

在 log4net 的源码 src\Layout\XMLLayoutbase.cs 中,在 Format 方法中添加如下代码

// 注意是使用 XmlTextWriter,而不是 XmlWriter
xmlTextWriter.Formatting = Formatting.Indented;
xmlTextWriter.Indentation = 2;

为了成功编译,还需要修改 src\AssemblyInfo.cs 文件,添加 false 关闭 log4net 的强签名

#if fasle && STRONG && (CLI_1_0 || NET_1_0 || NET_1_1 || NETCF_1_0 || SSCLI)
[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile(@"..\..\..\log4net.snk")]
#endif

将生成的 log4net.dll 文件替换项目中的 .dll 文件即可。

参考修改 log4net,生成缩进 XML

[2]. 中文格式

在 log4net 的源码 src\Util\Transform.cs 中,更新如下代码

private static Regex INVALIDCHARS = 
    new Regex(@"[^\x09\x0A\x0D\x20-\xFF\u00FF-\u07FF\uE000-\uFFFD\u4e00-\u9fa5]",RegexOptions.Compiled); 

[3]. XML 读取小问题

在 log4net 的源码 src\Layout\XMLLayout.cs 中,方法 ActivateOptions 修正如下

将 ":" 全部改成 "_" 

参考修改 log4net,支持中文格式

[4]. <Root> 结点问题

目前生成的 xml 文件,根结点只有 <Root>,没有 </Root>

至于怎么解决,有待于进一步研究。

现阶段,在使用的时候要特别处理一下,为 xml 文档添加 </Root>

NLog

免费的 .NET 开源日志框架,NLog 允许自定义从跟踪消息的来源(source)到记录跟踪信息的目标(target)的规则。

关于 NLog 的详细信息,参考官网:NLogCodeProject - Introduction to NLog

性能特点

  • Easy-to-configure:configuration file or programmatically
  • Templatable:layout renders
  • Extensible:write custom targets or pass custom values
  • 字符串延迟加载
  • 支持异步日志记录

NLog 与 log4net 相比,具体细节可参见:日志框架对比 NLog VS Log4net两者比较 - 逗比版

配置文件

NLog 同 log4net 一样,但是比 log4net 更简单,NLog 使用路由表(routing table)进行配置,log4net 使用层次性的 appender 配置,同样支持两种形式的配置方式

  • 单独的 NLog.Config 配置文件,直接以 <nlog> 作为根结点
  • App.config/Web.config 文件中添加 NLog 的配置结点

不管使用哪一种配置方式,配置内容和使用方法是一样的。配置信息结点包括两部分

  • targets:输出目标,支持 File、Mail、Console、DataBase等,每个 target 代表一个输出目标,包括2个属性
    • name:输出模板名称,在 <rules> 中使用
    • type:输出类型,NLog can dynamically write to one of multiple targets for each log message
  • rules:路由规则,将日志和输出目标匹配起来
    • name:日志记录者的名字 (允许使用通配符*)
    • minlevel/maxlevel:日志范围的最低/高级别
    • level/levels:单一日志级别/一系列日志级别,逗号分隔
    • writeTo:规则匹配时日志信息应该被写入的一系列目标,由逗号分隔

第一种方式,特别注意不要包含中文!

第二种方式,特别注意在 App.config/Web.config 中必须保证 <configSections> 结点存在并且是根结点 <configuration> 的第一个结点,否则程序会报错:配置系统未能初始化,文件结构形式如下

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="nlog" type="NLog.Config.ConfigSectionHandler, nLog"/>
  </configSections>
  
  <nlog>
    <targets>
      <target />
    </targets>

    <rules>
      <logger />
    </rules>  
  </nlog>
</configuration>

基本使用

NLog.dll 引用 + 配置文件

目前 NLog 的源码最新版本是:NLog 4.4.10 - Source

1添加引用

添加对 NLog 的引用,有 2 种方法

  • 通过 Nuget,添加 NLog 和 NLog Configuration 两个文件
  • 工具-Nuget程序包管理器-程序包管理器控制台,输入 Install-Package NLog 和 Install-Package NLog.config 

或者直接在程序中添加 NLog.dll 引用,并 using NLog; 

2配置结点 <nlog>

配置文件 <nlog> 结点支持多个路由规则和输出目标,非常灵活,下面直接给出结点配置信息

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="nlog" type="NLog.Config.ConfigSectionHandler, nLog"/>
  </configSections>
  
  <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	    autoReload="true" 
      throwExceptions="false">
    <targets>
      <!-- 控制台 -->
      <target name="myLogConsole" xsi:type="ColoredConsole"
              layout="${date:format=HH\:mm\:ss}> ${level:padding=-5} ${message}"/>
      <!-- 文件(每天一个日志文件) -->
      <target name="myRollingLogFile" xsi:type="File"
              fileName="${basedir}/Log/${date:format=yyyyMMdd}.txt" keepFileOpen="false"
              layout="${date:format=HH\:mm\:ss} ${level:uppercase=true:padding=-5} ${message}" />
    </targets>

    <rules>
      <logger name="*" minlevel="Debug" writeTo="myLogConsole" />
      <logger name="*" minlevel="Info" WriteTo="myRollingLogFile" />
    </rules>  
  </nlog>
</configuration>

其中,autoReload="true" 表示自动再配置,无需重启程序、更新配置信息。

特别提醒,在 源码文件\examples\targets\Configuration File 下有各种各样的 NLog 配置文件可以参考。

关于对配置文件的扩展使用

[1]. 根结点 <nlog> 配置属性 internalLogLevel 和 internalLogFile

用于查看 NLog 输出日志时的内部信息,比如配置文件信息错误等,不过会有效率影响。

[2]. File Archival Based on Time or Size

Log files can be automatically archived by moving them to another location after reaching certain size or time.

// 按时间滚动归档
<targets>
	<target name="onTimeFile" xsi:type="File"
		layout="${longdate} ${level:padding=-5} ${message}" 
		fileName="${basedir}/Log/logfile.txt" 
		archiveFileName="${basedir}/Archives/log.{#}.txt"
		archiveEvery="Day"
		archiveNumbering="Rolling"
		maxArchiveFiles="30"
		concurrentWrites="true"
		keepFileOpen="false"
		encoding="iso-8859-2" />
</targets>    

// 按大小滚动归档
<targets>
	<target name="file" xsi:type="File"
		layout="${longdate} ${level:padding=-5} ${message}" 
		fileName="${basedir}/Log/logfile.txt" 
		archiveFileName="${basedir}/Archives/log.{#####}.txt"
		archiveAboveSize="102400" // unit:Byte
		archiveNumbering="Sequence"
		concurrentWrites="true"
		keepFileOpen="false"
		encoding="iso-8859-2" />
</targets>

Archive Numbering 的解释以及其余配置信息可参见:File target Configuration

[3]. Layout:格式化输出(CSV/XML

Layouts provide a way to format the contents of the logs as it is written to a file. There are 2 main kinds of layouts:

  • simple layout - just a string,which is composed of Layout Renderers
  • structural layouts - which can output XML, CSV, and other complex formats

首先,CSV 格式的结点配置

<target name="myCsv" xsi:type="File" fileName="${basedir}/file.csv">
  <layout xsi:type="CSVLayout">
     <column name="Time" layout="${longdate}" /> 
     <column name="Level" layout="${level}"/>
     <column name="Message" layout="${message}" />
  </layout>
</target>

下面给出 XML 格式的结点配置

结点如何配置,暂时不明确
仅有的信息可参见:
[1]. Configuring NLog to log exceptions in an XML output?

在程序中调用方法:

namespace NLogSqh 
{
    public class Program
    {
	
        public static NLog.Logger logger = null;		
        public static void Main(string[] args)
        {
            logger = NLog.LogManager.GetCurrentClassLogger();

            logger.Trace("Trace");
            logger.Debug("Debug");
            logger.Info("Info");
            logger.Warn("Warn");
            logger.Error("Error");
            logger.Fatal("Fatal");                                 
        }
    }
}

参考NLog 学习系列NLog 文章系列

log4j

log for java(log4j)是 Apache 的开源项目功能强大的日志组件,Java 编写的可靠、快速和灵活的日志框架(API),主要应用于 Java 项目。

一个精心编写的日志代码提供快速的调试、维护方便、应用程序的运行时信息结构化存储。

目前最新的 Generation 是 Apache log4j 2,最新的 Release 是 Log4j 2.8.2,相关代码:

在编译前,最好应正确设置 PATHCLASSPATH ,项目中最好在 /src/ 目录下存放 log4j.properties 文件。

性能特点

  • 线程安全
  • 参数配置化
  • 多种日志等级,多种输出介质

log4j 包括三个主要组件

  • loggers:记录日志信息
  • appenders:日志记录方式、输出介质等信息
  • layouts:日志信息格式化

下面是 log4j 组件虚拟图

Log4j Architecture

输出介质:支持控制台、文件、数据库等

  • ConsoleAppender:控制台
  • FileAppender:文件
  • RollingFileAppender:文件大小滚动生成日志,达到指定尺寸时生成一个新的日志文件
  • DailyRollingFileAppender:时间滚动生成日志文件
  • WriterAppender将日志信息以流格式发送到任意指定的地方,eg. 数据库

基本使用

log4j 的很多信息可以参考 log4net,此部分略过,往事不要再提。

Log4j 支持两种配置文件格式,一种是XML格式的文件,一种是Java特性文件(键=值)

log4j 通过 log4j.properties 文件(key-value形式)保存属性配置。默认情况下,日志管理在 CLASSPATH 查找一个名为 log4j.properties 的文件。基本格式如下:

# Define the root logger with appender file
log4j.rootLogger = DEBUG, FILE

# Define the file appender
log4j.appender.FILE = org.apache.log4j.FileAppender
log4j.appender.FILE.File = ${log}/log.out
log4j.appender.FILE.Append = true
log4j.appender.FILE.Threshold= INFO

# Define the layout for file appender 
log4j.appender.FILE.layout = org.apache.log4j.PatternLayout 
log4j.appender.FILE.layout.conversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n

可以将上述格式转换为 xml 形式,两者等同

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration>

	<appender name="FILE" class="org.apache.log4j.FileAppender">
	   <param name="file" value="${log}/log.out" />
	   <param name="append" value="true" />
	   <layout class="org.apache.log4j.PatternLayout">
	      <param name="conversionPattern" value="%-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n" />
	   </layout>
	</appender>
	
	<logger name="log4j.rootLogger" additivity="false">
	   <level value="ALL"/>
	   <appender-ref ref="FILE"/>
	</logger>

</log4j:configuration>

在 Java 程序中调用如下:

import org.apache.log4j.Logger;
import java.io.*;
import java.sql.SQLException;
import java.util.*;

public class log4jExample
{
   public static void main(String[] args) throws IOException,SQLException
   {     
       PropertyConfigurator.configure("log4j.properties"); // 入参为配置文件的路径
       Logger logger = Logger.getLogger( log4jExample.class.getName() );
       logger.debug("DEBUG");
       logger.info("INFO");
   }
}

关于配置文件有几个需要注意的问题:

[1]. log4j.properties 文件中根节点配置格式

log4j.rootLogger = [ level ] , appenderName1, appenderName2, …

[2]. 关于 appender 结点属性配置

// 为不同的 appender 配置日志输出级别
log4j.appender.AppenderName.Threshold = DEBUG
// 所有的消息都会被立即输出,默认 true
log4j.appender.AppenderName.ImmediateFlush = true 

[3]. 对于项目中 log4j.xml 和 log4j.properties 不在 CLASSPATH 路径的,在项目程序中需要直接加载两个文件

DOMConfigurator.configure(log4j_xml_Path);  
PropertyConfigurator.configure(log4j_properties_Path);

除此上述基本配置外,可以扩展 FileAppender,支持 Date 和 Size 滚动日志,详情信息参见:log4j 日志配置讲解

1Date:按日期滚动,支持多种 DatePattern

# Define the file appender
log4j.appender.FILE=org.apache.log4j.DailyRollingFileAppender

# Set the DatePattern
log4j.appender.FILE.DatePattern='.' yyyy-MM-dd'.log'

2Size:按文件大小滚动

# Define the file appender
log4j.appender.FILE=org.apache.log4j.RollingFileAppender

# Set the maximum file size before rollover
log4j.appender.FILE.MaxFileSize=10MB
# Set the the backup index
log4j.appender.FILE.MaxBackupIndex=10

关于 log4j 的详细信息,参考官网:log4j 官网

SLF4J

Commons Logging 通用日志接口,用户可以自由选择第三方日志组件作为具体实现。通过动态查找机制,在程序运行时自动找出真正使用的日志框架。代码依赖的是 common-logging 而非具体的日志组件, 避免与具体的日志方案直接耦合,必要时可更改日志实现的第三方组件。

import org.apache.commons.logging.Log;  
import org.apache.commons.logging.LogFactory;  
      
public class A {  
    private static Log logger = LogFactory.getLog(this.getClass());  
}  

Commons Logging + log4j 作为Java日志的经典组合,但是 Commons Logging 存在

  • ClassLoader 问题
  • 为了避免多余的字符串拼接,必须 isDebugEnabled() 判断逻辑

的问题,log4j 团队开发出日志门面框架SLF4J。

Simple Logging Facade for Java(SLF4J,简单日志门面),不是具体的日志解决方案,用于服务各种日志系统,为不同的日志框架提供统一的日志接口,在编译时绑定相应的日志框架(静态绑定)。关于 SLF4J 的详细信息,参见:SLF4J 官网

类似 JDBC,但是比 JDBC 更简单,无需加载驱动、只需添加特定的包。

面向 Java,推荐 SLF4J,而不是 log4j

参见:为什么使用SLF4J而不是Log4J来做Java日志为什么要使用SLF4J而不是Log4J

性能特点

  • 独立于各种日志系统
  • Parameterized Logging,提供基于占位符(place holder)的日志记录方法,可读性高
  • 日志信息(String)延迟构建,减少 String 池过多消耗堆内存

基本使用

SLF4J 只是一个日志外壳,需要在项目中加入 slf4j-jdk14.jar、slf4j-log4j12.jar 或 logback.jar,将日志调用转发到实际的日志框架。SLF4J 是一个抽象层( Abstraction Layer),使你的代码独立于任意一个特定的日志API,具体地:

SLF4J 应用 Facade(门面)设计模式,SLF4J 所提供的核心 API 是提供一些对外的接口以及一个 LoggerFactory 工厂类

SLF4J 提供统一的日志记录接口,只要按照其提供的方法记录即可,最终日志格式、日志级别、输出方式等由具体的日志系统的配置来实现,因此在应用中可以灵活切换到不同的日志系统,用户使用自己希望的日志系统 loging APIs。

以 log4j 为例,SLF4J 的绑定操作实现如下

  • slf4j-api 作为日志接入的接口,编译时 slf4j-api 的 public final class LoggerFactor 类中 private final static void bind() 方法会寻找并绑定具体的日志实现类,主要通过调用 StaticLoggerBinder.getSingleton() 方法
  • slf4j-log4j12 是链接 slf4j-api 和 log4j 的中间适配器,该类实现 slf4j-api 中 LoggerFactoryBinder 接口,从而在编译时绑定 slf4j-log4j12 的 getSingleton() 方法
  • log4j 是具体的日志系统,通过 slf4j-log4j12 初始化 log4j

其中,SLF4J 的核心是 slf4j api(slf4j-api.jar包),该 .jar 包只提供日志记录接口。有关对 SLF4J 基本认识,参见:初识 SLF4J

SLF4J 不依赖于任何特殊的 class loader 机制。实际上,SLF4J 与已有日志实现的绑定是在编译时静态执行的,具体绑定工作是通过一个 .jar包 实现的,使用时只要把相应的jar包(只有一个)放到类路径上即可,SLF4J 通过 .jar包 来告知使用哪种日志系统实现。

在 Java 程序中调用如下,通过工厂类 LoggerFactory 创建日志对象

import org.apache.SLF4J.Logger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TestSlf4j 
{
    private static final Logger logger = LoggerFactory.getLogger(TestSlf4j.class);

    public static void main(String[] args) 
	{
        logger.info("Hello {}", "SLF4J");
    }
} 

注意,在项目中 Maven 依赖应如下设置

<!-- slf4j -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.12</version>
</dependency>

<!-- slf4j-log4j -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.12</version>
</dependency>

<!-- log4j -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

具体实现过程讲解参加:SLF4J+log4j 原理实现和源码分析SLF4J 源码剖析

深入研究

目前 Latest STABLE version 是 slf4j-1.7.25.zip;其中,SLF4J 的核心包为

// 核心包
slf4j-api-1.7.25.jar
// 核心包源码
slf4j-api-1.7.25-sources.jar

学习 SLF4J 的源码,可以从以下三个包开始

slf4j-api,slf4j-log4j12

SLF4J 有五个主要组件,分别是:

  • ILoggerFactory:工厂接口
  • LoggerFactory:工厂类
  • LoggerFactoryBinder:日志框架绑定接口
  • Logger:日志接口
  • StaticLoggerBinder:日志框架绑定实现类

此处先给出整体的流程:LoggerFactory 调用 getILoggerFactory() 方法,该方法会初始化 LoggerFactory,即通过在 bind() 方法中加载 classpath 中的 StaticLoggerBinder 类,并根据加载结果设置当前 LoggerFactory 的初始化状态,从而在 getILoggerFactory() 方法中通过当前 LoggerFactory 的状态判断返回的 ILoggerFactory 实例。下面分别介绍各个相关组件:

1ILoggerFactory

该接口仅提供一个 getLogger() 方法,用于获取日志实例。

public interface ILoggerFactory {
    // 所有日志框架均通过该方法获取Logger实例
    public Logger getLogger(String name);
}

所有的日志框架均需要实现该接口。

2LoggerFactory

负责查找系统里日志的实现,提供 getLogger() 创建日志实例。

public static Logger getLogger(String name) {
	ILoggerFactory iLoggerFactory = getILoggerFactory();
	return iLoggerFactory.getLogger(name);
}

该类的 getLogger() 方法通过调用该类的 getILoggerFactory() 方法获取日志工厂接口的实例,再调用接口提供的 getLogger() 方法获取具体的日志实例。首先看下该类的 getILoggerFactory() 方法

public static ILoggerFactory getILoggerFactory() 
{
    if (INITIALIZATION_STATE == UNINITIALIZED) {
        synchronized (LoggerFactory.class) {
            if (INITIALIZATION_STATE == UNINITIALIZED) {
                INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                performInitialization();  // 初始化
            }
        }
    }
    switch (INITIALIZATION_STATE) {
    case SUCCESSFUL_INITIALIZATION:
        return StaticLoggerBinder.getSingleton().getLoggerFactory();
    case NOP_FALLBACK_INITIALIZATION:
        ... ...
}

第一步工厂初始化,第二步调用 StaticLoggerBinder.getSingleton().getLoggerFactory() 返回日志工厂接口的实例,具体查看下面的5)StaticLoggerBinder 部分。

此处看下 performInitialization() 方法,用于 绑定日志框架+校验日志框架版本号,代码如下

private final static void performInitialization() 
{
    bind();  // 绑定日志框架
    if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
        versionSanityCheck();  // 校验日志框架版本号
    }
}

其中,bind() 方法是 SLF4J 最核心的代码,实现日志框架绑定,具体地

private final static void bind() {
    try {
        Set<URL> staticLoggerBinderPathSet = null;
        // skip check under android, see also
        // http://jira.qos.ch/browse/SLF4J-328
        if (!isAndroid()) {
            staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();  // 寻找可能的日志框架
            reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);  // 输出搜索到的所有StaticLoggerBinder日志
        }
		
        // the next line does the binding:实现绑定
        StaticLoggerBinder.getSingleton(); 
        INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
        reportActualBinding(staticLoggerBinderPathSet);  // 输出最终实际绑定的StaticLoggerBinder日志
        fixSubstituteLoggers();
        replayEvents();
        // release all resources in SUBST_FACTORY
        SUBST_FACTORY.clear();
    } 
	catch () {
        ... ...
    }
}

该方法会加载并绑定找见的第一个 StaticLoggerBinder 类,并调用该类的 getSingleton() 方法,完成日志框架的绑定。其中,

private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";

static Set<URL> findPossibleStaticLoggerBinderPathSet() 
{
	Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
	try 
	{
		ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
		
		Enumeration<URL> paths;
		if (loggerFactoryClassLoader == null) {
			paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
		} 
		else {
			paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
		}
		
		while (paths.hasMoreElements()) {
			URL path = paths.nextElement();
			staticLoggerBinderPathSet.add(path);
		}
	} 
	catch (IOException ioe) {
		Util.report("Error getting resources from path", ioe);
	}
	return staticLoggerBinderPathSet;
}

findPossibleStaticLoggerBinderPathSet() 方法会查找项目根路径下的 "org/slf4j/impl/StaticLoggerBinder.class" 文件。然后 reportMultipleBindingAmbiguity() 方法会报告文件查找的结果信息

private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
	if (binderPathSet.size() > 1) 
	{
		Util.report("Class path contains multiple SLF4J bindings.");
		for (URL path : binderPathSet) {
			Util.report("Found binding in [" + path + "]");
		}
		Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
	}
}

如果找到多个,则输出 "Class path contains multiple SLF4J bindings" 提示信息,表示有多个日志框架绑定了 SLF4J。

3Logger

该接口提供不同层级日志信息记录的各个方法。

public interface Logger {
	public void debug(String msg);
	public void debug(String format, Object... arguments);
	... ...
	info, warn, error
}

4LoggerFactoryBinder

日志框架绑定接口,仅且提供 2 个方法

public interface LoggerFactoryBinder {
    public ILoggerFactory getLoggerFactory();
    public String getLoggerFactoryClassStr();
}

每个支持 SLF4J 的日志框架必须存在一个实现该接口的 StaticLoggerBinder 类。

5StaticLoggerBinder

实现 LoggerFactoryBinder 接口,类 LoggerFactory 调用该类的 getSingleton() 和 getLoggerFactory() 方法返回具体日志框架的接口实例,以 slf4j-log4j12.jar 为例

// 单例模式
public class StaticLoggerBinder implements LoggerFactoryBinder 
{
    private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
	public static final StaticLoggerBinder getSingleton() {  // 静态方法
        return SINGLETON;
    }
	
	private static final String loggerFactoryClassStr = Log4jLoggerFactory.class.getName();
	public String getLoggerFactoryClassStr() {
        return loggerFactoryClassStr;
    }
		    
    private final ILoggerFactory iloggerFactory;
	public ILoggerFactory getLoggerFactory() {
        return iloggerFactory;
    }
    private StaticLoggerBinder() {
        iloggerFactory = new Log4jLoggerFactory();  // 日志框架log4j的工厂类
		... ...
    }
  
    public static String REQUESTED_API_VERSION = "1.6.99"; // !final
}

该类的构造函数中新建一个 log4j 的日志工厂接口实例,并通过 getLoggerFactory() 方法返回。再深入地,查看 log4j的工厂类 Log4jLoggerFactory

public class Log4jLoggerFactory implements ILoggerFactory 
{ 
    ConcurrentMap<String, Logger> loggerMap;  // key: name (String), value: a Log4jLoggerAdapter;
    public Log4jLoggerFactory() {
        loggerMap = new ConcurrentHashMap<String, Logger>();  // 线程安全的HashMap类
        org.apache.log4j.LogManager.getRootLogger();  // force log4j to initialize
    }

	// 实现接口 ILoggerFactory 的 getLogger() 方法
    public Logger getLogger(String name) {
        Logger slf4jLogger = loggerMap.get(name);
        ... ...
    }
}

其中,ConcurrentHashMap 用于缓存所有之前创建的 Log4jLoggerAdapter 类实例,日志框架 log4j 的工厂类实现了接口 ILoggerFactory,调用 org.apache.log4j.LogManager.getRootLogger() 完成对 log4j 的绑定,通过调用 getLogger() 方法返回一个 Log4jLoggerAdapter 类实例,这是因为 Log4j 的 Logger 接口与 slf4j 的 Logger 接口不完全相同,需要在 Log4j 的 Logger 对象上进行一层封装。该类维护一个 org.apache.log4j.Logger logger 对象,用于日志记录

public final class Log4jLoggerAdapter extends MarkerIgnoringBase 
                                      implements LocationAwareLogger, Serializable
{
    final transient org.apache.log4j.Logger logger;
	
    Log4jLoggerAdapter(org.apache.log4j.Logger logger) {
        this.logger = logger;
        this.name = logger.getName();
    }
}

其中,接口 LocationAwareLogger 继承了 Logger 接口,是对 Logger 接口的包装

public interface LocationAwareLogger extends Logger { ... }

具体详细信息参见:slf4j源码剖析 - xingoo

综上所述,要实现一个兼容 SLF4J 的日志系统,最基本需要三个类:

  • StaticLoggerBinder 类,实现 LoggerFactoryBinder 接口
  • XxxLoggerFactory 类,实现 ILoggerFactory 接口
  • XxxLogger 类,实现 Logger 接口

推荐几款文件操作工具

记录了日志,就需要打开,那么强劲的编辑器必不可少!

打开

  • Notepad++:大文件150M限制,其他挺好
  • UltraEdit:搜索不优雅
  • EditPlus:相对比较好
  • glogg:the fast, smart log explorer,但是中文支持貌似不佳

分割

 

posted @ 2017-03-29 20:40  万箭穿心,习惯就好。  阅读(1241)  评论(0编辑  收藏  举报