Log4j最佳實踐

本文是結合項目中使用Log4j總結的最佳實踐,非轉載。網上能夠找到的是這一篇《Log4j最佳實踐》。原本Log4j使用是很是簡單的,無需多介紹其用法,這只是在小型項目中;但在大型的項目中使用log4j不太同樣。大型項目很是依賴日誌,由於解決線上問題必須依靠log,依靠大量的日誌!線上出現問題每每不能重現,並且沒法調試,log是必須中的必須,解決線上問題全靠它。本文內容:html

大型項目中Log4j的使用注意點

在大型項目中使用Log4j要注意下面幾點:java

  • 不能由於寫log使得系統性能變慢(最好使用異步
  • log能易於定位問題,log要能體現什麼時候(精確到毫秒級)、在哪(包、機器、類、函數、行、文件等)、發生了什麼問題、以及嚴重性
  • log要易於自動、手工、半自動分析(如記錄文件太大則不能打開分析,寫數據庫等)
  • 能根據模塊/包來動態單獨配置log級別(FATAL – ERROR – WARNING – INFO – DEBUG - TRACE) 和單獨配置輸出文件等
  • 能夠用grep分析出獨立的行,儘可能不要分行
  • 有時調試線上問題還須要很是豐富的信息,例如:進入模塊和函數的入口參數信息、完成某項操做耗費的時間、SessionID、機器IP地址和端口號、版本號、try{}catch{}裏面的StackTrace信息。
  • 大型系統的日誌文件應該按期用gzip壓縮並移動到一個專門的檔案日誌服務器。應該天天晚上,或者每小時這樣作一次。
  • 不要隨便從網上覆制一個Log4j的配置文件,你必須深刻理解裏面的每個配置項表明的含義!
  • 若是你拼裝的動做比較耗資源,請用if ( log.isDebugEnabled() )
  • 千萬不要try{}catch{}了異常卻沒有記錄日誌
  • 不要僅記錄到數據庫,記錄文件更加可靠,由於記錄到數據庫可能發生網絡和數據庫異常,沒有記錄本地磁盤可靠。

例以下面這個啓動日誌包含了版本號、耗費時間、userID等等豐富的信息:git

http://images.cnblogs.com/cnblogs_com/zhengyun_ustc/255879/o_clipboard%20-013%20%E5%89%AF%E6%9C%AC.png

 

Log4j爲性能考慮的注意點

爲系統性能考慮,使用Log4j注意下列幾點:github

  • 避免輸出'%C', '%F', '%L' '%M' 等位置信息
  • 儘可能使用異步
  • 爲每一個模塊設置單獨的輸出文件
  • 每次調用前檢查if(logger.isDebugEnabled()){ logger.debug(……) }

a). 避免輸出'%C', '%F', '%L' '%M' 等位置信息

當配置文件中的配置項包含Location信息時候會很是昂貴,所以,須要避免'C', 'F', 'L' 'M' 等位置信息的記錄(參數配置項詳細說明)。sql

  • %C - 輸出類名
  • %F - 輸出文件名
  • %L - 輸出行號
  • %M - 輸出函數名

注意:當配置爲異步輸出的時候,以上位置信息可能會顯示爲問號?,由於是在另一個線程記錄的調用信息。此時,咱們可使用下面的方法來獲取類名和函數名:數據庫

StackTraceElement se = Thread.currentThread().getStackTrace()[2];
String msg = se.getClassName() + "-[" + se.getMethodName() + "] " + errorMessage;

apache

b). 使用異步(異步寫文件,異步寫數據庫)

Log4j異步寫可使用默認的appender:org.apache.log4j.AsyncAppender,配置文件log4j.xml樣例:api

<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">  
  
<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/' debug="false">  
  
   <appender name="DAILY_FILE" class="org.apache.log4j.DailyRollingFileAppender">  
    <layout class="org.apache.log4j.PatternLayout">  
      <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss,SSS} %5p %c %x - %m%n"/>  
    </layout>  
    <param name="File" value="log/log4j.log"/>  
    <param name="DatePattern" value="'.'yyyy-MM-dd"/>
  </appender>  
  
  <appender name="ASYNC_FILE" class="org.apache.log4j.AsyncAppender">  
    <param name="BufferSize" value="10000"/>  
    <param name="Blocking" value="false"/>  
    <appender-ref ref="DAILY_FILE"/>  
  </appender> 
  
  <appender name="DB_OUT" class="org.apache.log4j.jdbc.JDBCAppender">
      <param name="URL" value="jdbc:postgresql://192.168.1.34:5432/myDB" />
      <param name="Driver" value="org.postgresql.Driver"/>
      <param name="User" value="aaa"/>
      <param name="Password" value="bbb"/>
      <param name="Sql" value="INSERT INTO tracelog (ModuleName ,LoginID,UserName,Class, Method,createTime,LogLevel,MSG) values ('%c', '','','','','%d{yyyy-MM-dd HH:mm:ss,SSS}','%p','%m')"/>

   </appender>
  
  <appender name="ASYNC_DB" class="org.apache.log4j.AsyncAppender">  
    <param name="BufferSize" value="10000"/>  
    <param name="Blocking" value="false"/>  
    <appender-ref ref="DB_OUT"/>  
  </appender>  
    
  <root>  
    <level value="info"/>  
    <appender-ref ref="ASYNC_DB" />  
    <appender-ref ref="ASYNC_FILE" />
  </root>  
  
  <logger name="PACKAGE_1" additivity="false">  
    <level value="info"/>   
    <appender-ref ref="ASYNC_DB" />  
    <appender-ref ref="ASYNC_FILE" />
  </logger>  

  <logger name="PACKAGE_2" additivity="false">  
    <level value="info"/>   
    <appender-ref ref="ASYNC_DB" />  
    <appender-ref ref="ASYNC_FILE" />
  </logger>  
  
  
</log4j:configuration>

上面的配置文件包含異步寫文件和異步寫入postgreSQL數據庫的配置,默認是root,也有各個Package的配置。用的時候能夠寫一個logUtil的類來初始化這個log4j.xml:服務器

package com.ibm;

import org.apache.log4j.Logger;
import org.apache.log4j.xml.DOMConfigurator;

public class LogUtil implements ILogUtil {

	public static LogUtil getInstance() {
		if (instance == null)
			instance = new LogUtil();
		return instance;
	}

	@Override
	public void init()  {
		//PropertyConfigurator.configure("conf/log4j.properties");
		DOMConfigurator.configure("conf/log4j.xml");
	}

	@Override
	public void close() {
	}

	@Override
	public void logError(String errorMessage) {
		logger.error(errorMessage.replace("'", "''"));
	}

	@Override
	public void logWarn(String warnMessage) {
		logger.warn(warnMessage.replace("'", "''"));
	}

	@Override
	public void logInfo(String infoMessage) {
		logger.info(infoMessage.replace("'", "''"));
	}

	@Override
	public void logDebug(String debugMessage) {
		logger.debug(debugMessage.replace("'", "''"));
	}

	@Override
	public void logFatal(String fatalMessage) {
		logger.fatal(fatalMessage.replace("'", "''"));
	}

	private LogUtil() {
	}

	private static Logger getPackageLogger(String packageName){
		if(packageName.equals(PackageName.PACKAGE_1.toString()))
			return Logger.getLogger(PackageName.PACKAGE_1.toString());
		else if(packageName.equals(PackageName.PACKAGE_2.toString()))
			return Logger.getLogger(PackageName.PACKAGE_2.toString());
		else 
			return Logger.getRootLogger();
	}

	@Override
	public void logError(String packageName, String errorMessage) {
		getPackageLogger(packageName).error(errorMessage.replace("'", "''"));
	}

	@Override
	public void logError(String packageName, String errorMessage,
			Throwable exception) {
		getPackageLogger(packageName).error(errorMessage.replace("'", "''"), exception);
	}

	@Override
	public void logWarn(String packageName, String warnMessage) {
		getPackageLogger(packageName).warn(warnMessage.replace("'", "''"));
	}

	@Override
	public void logWarn(String packageName, String warnMessage,
			Throwable exception) {
		getPackageLogger(packageName).warn(warnMessage.replace("'", "''"), exception);
	}

	@Override
	public void logInfo(String packageName, String infoMessage) {
		getPackageLogger(packageName).info(infoMessage.replace("'", "''"));
	}

	@Override
	public void logInfo(String packageName, String infoMessage,
			Throwable exception) {
		getPackageLogger(packageName).info(infoMessage.replace("'", "''"), exception);
	}

	@Override
	public void logDebug(String packageName, String debugMessage) {
		getPackageLogger(packageName).debug(debugMessage.replace("'", "''"));
	}

	@Override
	public void logDebug(String packageName, String debugMessage,
			Throwable exception) {
		getPackageLogger(packageName).debug(debugMessage.replace("'", "''"), exception);
	}

	@Override
	public void logFatal(String packageName, String fatalMessage) {
		getPackageLogger(packageName).fatal(fatalMessage.replace("'", "''"));
	}

	@Override
	public void logFatal(String packageName, String fatalMessage,
			Throwable exception) {
		getPackageLogger(packageName).fatal(fatalMessage.replace("'", "''"), exception);
	}

	private static Logger logger = Logger.getRootLogger();
	private static LogUtil instance;
}

具體各個Package能夠調用:網絡

LogUtil.getInstance().logError("PACKAGE_1", "error message....", e);

注意:寫數據庫的時候配置文件log4j.xml裏面有一菊SQL,這個SQL在寫的message包含單引號或雙引號的時候會爆異常,因此須要把單引號或雙引號轉義爲兩個單引號;咱們本身的log能夠控制,若是是例如Tomcat/jBoss寫的log的message包含單引號或雙引號的時候會寫數據庫異常,具體作法能夠自定義JDBCAppender,參考這一片文章。自定義字段可使用MDC和%X,參考這一片文章。)

上面的配置文件已經根據各個Package配置單獨的log輸出,能夠配置爲寫某個文件,或單獨寫數據庫,或是組合,均可以靈活根據本身的須要配置。

(AsyncAppender中BufferSize/默認128的含義:the number of messages allowed in the event buffer before the calling thread is blocked (if blocking is true) or until messages are summarized and discarded.)

JDBCAppender存在沒有數據庫鏈接池的問題,能夠擴展一下JDBCAppender,引入第三方鏈接池例如C3P0:

package com.ibm.log4j.jdbcplus;

import org.apache.log4j.jdbc.JDBCAppender;
import org.apache.log4j.spi.LoggingEvent;
import java.sql.Connection;  
import java.sql.SQLException;  
import org.apache.log4j.spi.ErrorCode;  
import com.codestudio.sql.PoolMan;  

public class DBAppender extends JDBCAppender {
	
	/**經過 PoolMan 獲取數據庫鏈接對象的 jndiName 屬性*/  
    protected String jndiName;  
    /**數據庫鏈接對象*/  
    protected Connection connection = null;
    
    public DBAppender() {  
        super();  
    }  
    
    @Override  
    protected void closeConnection(Connection con) {  
        try {  
            if (connection != null && !connection.isClosed())  
                connection.close();  
        } catch (SQLException e) {  
            errorHandler.error("Error closing connection", e, ErrorCode.GENERIC_FAILURE);  
        }  
    }  
    
    @Override  
    protected Connection getConnection() throws SQLException {  
    	
        try {  
            //經過 PoolMan 獲取數據庫鏈接對象(http://nchc.dl.sourceforge.net/project/poolman/PoolMan/poolman-2.1-b1/poolman-2.1-b1.zip)  
            Class.forName("com.codestudio.sql.PoolMan");  
            connection= PoolMan.connect("jdbc:poolman://" + getJndiName());  
        } catch (Exception e) {  
            System.out.println(e.getMessage());  
        }  
        return connection;  
    }  
    /** 
     * @return the jndiName 
     */  
    public String getJndiName() {  
        return jndiName;  
    }  
    /** 
     * @param jndiName the jndiName to set 
     */  
    public void setJndiName(String jndiName) {  
        this.jndiName = jndiName;  
    }  

	@Override
	public void append(LoggingEvent event) {
		if (event.getMessage() != null)
			event.getMessage().toString().replace("'", "''");
//		if (event.getThrowableInformation() != null)
//			event.getThrowableInformation().toString().replace("'", "''");
		buffer.add(event);
		
		if (buffer.size() >= bufferSize)
			flushBuffer();
	}
}

把ERROR信息輸出到單獨的文件

若是你的日誌級別是INFO,想把ERROR log輸出到單獨的文件,能夠這樣配置:

<appender name="ERROR_FILE">
   <param name="Threshold" value="ERROR"/>
</appender>

<appender name="GENERAL">
   <param name="Threshold" value="INFO"/>
</appender>

<logger name="com.acme">
  <level value="INFO"/>
  <appender-ref ref="ERROR_FILE"/>
  <appender-ref ref="GENERAL"/>
</logger>

在基類寫log4j日誌要注意的問題

最後要注意的是,若是你把寫日誌這部分封裝到一個獨立的jar包模塊裏面(在基類或者靜態類裏面寫日誌),就會致使輸出的類名、函數名都是基類的類名和函數名,這將是重大的錯誤。由於下面的這行:

private static Logger log = Logger.getLogger( MyClass.class );

若是你得到的是基類的logger那就永遠是基類的logger。這一點須要注意.

Log4j基礎知識

若是你對Log4j基礎不熟悉,建議你學習一下什麼是log4j裏面的logger, root logger, appender, configurationAdditivitylayout.

SocketAppender / JMSAppender

除了AsyncAppender,你還可使用SocketAppender, JMSAppender...和其它各類log4j的appender。固然,除了log4j,你也能夠轉到slf4j, logBack.

Log4j的AsyncAppender存在的嚴重問題

Log4j的異步appender也就是AsyncAppender存在性能問題(如今Log4j 2.0 RC提供了一種新的異步寫log的機制(基於disruptor)來試圖解決問題),問題是什麼呢?異步寫log有一個buffer的設置,也就是當隊列中多少個日誌的時候就flush到文件或數據庫,當配置爲blocking=true的時候,當你的應用寫日誌很快,log4j的緩衝隊列將很快充滿,當它批量flush到磁盤文件的時候,你的磁盤寫入速度很慢,會發生什麼狀況?是的,隊列阻塞,寫不進去了,整個log4j阻塞了,始終等待隊列寫入磁盤/DB,整個異步線程死了變成同步的了?而當配置爲blocking=false的時候,不會阻塞但會扔出異常並丟棄消息。你是但願log4j死掉,仍是但願後續消息被丟棄?都是問題。

固然,一個辦法是把緩衝bufferSize設大一點。最好的解決辦法:一、本身實現消息隊列和自定義的AsyncAppender; 2. 等log4j 2.0 成熟發佈。

(注:log4j 2 因爲採用了LMAX Disruptor,性能超過原來AsyncAppender幾個數量級,支持每秒併發寫入1800萬條日誌)

相關文章
相關標籤/搜索