本文是結合項目中使用Log4j總結的最佳實踐,非轉載。網上能夠找到的是這一篇《Log4j最佳實踐》。原本Log4j使用是很是簡單的,無需多介紹其用法,這只是在小型項目中;但在大型的項目中使用log4j不太同樣。大型項目很是依賴日誌,由於解決線上問題必須依靠log,依靠大量的日誌!線上出現問題每每不能重現,並且沒法調試,log是必須中的必須,解決線上問題全靠它。本文內容:html
在大型項目中使用Log4j要注意下面幾點:java
例以下面這個啓動日誌包含了版本號、耗費時間、userID等等豐富的信息:git
爲系統性能考慮,使用Log4j注意下列幾點:github
當配置文件中的配置項包含Location信息時候會很是昂貴,所以,須要避免'C', 'F', 'L' 'M' 等位置信息的記錄(參數配置項詳細說明)。sql
(注意:當配置爲異步輸出的時候,以上位置信息可能會顯示爲問號?,由於是在另一個線程記錄的調用信息。此時,咱們可使用下面的方法來獲取類名和函數名:數據庫
StackTraceElement se = Thread.currentThread().getStackTrace()[2]; String msg = se.getClassName() + "-[" + se.getMethodName() + "] " + errorMessage;
)apache
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(); } }
若是你的日誌級別是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>
最後要注意的是,若是你把寫日誌這部分封裝到一個獨立的jar包模塊裏面(在基類或者靜態類裏面寫日誌),就會致使輸出的類名、函數名都是基類的類名和函數名,這將是重大的錯誤。由於下面的這行:
private static Logger log = Logger.getLogger( MyClass.class );
若是你得到的是基類的logger那就永遠是基類的logger。這一點須要注意.
若是你對Log4j基礎不熟悉,建議你學習一下什麼是log4j裏面的logger, root logger, appender, configuration、Additivity和layout.
除了AsyncAppender,你還可使用SocketAppender, JMSAppender...和其它各類log4j的appender。固然,除了log4j,你也能夠轉到slf4j, logBack.
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萬條日誌)