本文是结合项目中使用Log4j总结的最佳实践,非转载。网上可以找到的是这一篇《Log4j最佳实践》。本来Log4j使用是非常简单的,无需多介绍其用法,这只是在小型项目中;但在大型的项目中使用log4j不太一样。大型项目非常依赖日志,因为解决线上问题必须依靠log,依靠大量的日志!线上出现问题往往不能重现,而且无法调试,log是必须中的必须,解决线上问题全靠它。本文内容:

大型项目中Log4j的使用注意点

在大型项目中使用Log4j要注意下面几点:

  • 不能因为写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等等丰富的信息:

Log4j为性能考虑的注意点

为系统性能考虑,使用Log4j注意下列几点:

  • 避免输出'%C', '%F', '%L' '%M' 等位置信息
  • 尽量使用异步
  • 为每个模块设置单独的输出文件
  • 每次调用前检查if(logger.isDebugEnabled()){ logger.debug(……) }

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

当配置文件中的配置项包含Location信息时候会非常昂贵,因此,需要避免'C', 'F', 'L' 'M' 等位置信息的记录(参数配置项详细说明)。

  • %C - 输出类名
  • %F - 输出文件名
  • %L - 输出行号
  • %M - 输出函数名

注意:当配置为异步输出的时候,以上位置信息可能会显示为问号?,因为是在另外一个线程记录的调用信息。此时,我们可以使用下面的方法来获取类名和函数名:

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

b). 使用异步(异步写文件,异步写数据库)

Log4j异步写可以使用默认的appender:org.apache.log4j.AsyncAppender,配置文件log4j.xml样例:

<?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设大一点。最好的解决办法:1、自己实现消息队列和自定义的AsyncAppender; 2. 等log4j 2.0 成熟发布。

(注:log4j 2 由于采用了LMAX Disruptor,性能超过原来AsyncAppender几个数量级,支持每秒并发写入1800万条日志)

Log4j最佳实践的更多相关文章

  1. paip.log4j 日志系统 参数以及最佳实践

    paip.log4j 日志系统 参数以及最佳实践   %d{yyyy-MM-dd HH:mm:ss} [thrd:%t] %5p   loger:%c   (%C.%M.%L)  - %m%n 201 ...

  2. Atitit.log日志技术的最佳实践attilax总结

    Atitit.log日志技术的最佳实践attilax总结 1. 日志的意义与作用1 1.1. 日志系统是一种不可或缺的单元测试,跟踪调试工具1 2. 俩种实现[1]日志系统作为一种服务进程存在 [2] ...

  3. java 读取文件最佳实践

    1.  前言 Java应用中很常见的一个问题,如何读取jar/war包内和所在路径的配置文件,不同的人根据不同的实践总结出了不同的方案,但其他人应用却会因为环境等的差异发现各种问题,本文则从原理上解释 ...

  4. 转载--JAVA读取文件最佳实践

    1.  前言 Java应用中很常见的一个问题,如何读取jar/war包内和所在路径的配置文件,不同的人根据不同的实践总结出了不同的方案,但其他人应用却会因为环境等的差异发现各种问题,本文则从原理上解释 ...

  5. atitit. 日志系统的原则and设计and最佳实践(1)-----原理理论总结.

    atitit. 日志系统的原则and设计and最佳实践总结. 1. 日志系统是一种不可或缺的单元测试,跟踪调试工具 1 2. 日志系统框架通常应当包括如下基本特性 1 1. 所输出的日志拥有自己的分类 ...

  6. Java 日志管理最佳实践

    转:http://blog.jobbole.com/51155/ 日志记录是应用程序运行中必不可少的一部分.具有良好格式和完备信息的日志记录可以在程序出现问题时帮助开发人员迅速地定位错误的根源.对于开 ...

  7. 13. ZooKeeper最佳实践

    以下列举了运行和管理ZooKeeper ensemble的一些最佳实践: ZooKeeper数据目录包含快照和事务日志文件.如果autopurge选项未启用,定期清理目录是一个好习惯.另外,管理员可能 ...

  8. 最重要的 Java EE 最佳实践

    參考:IBM WebSphere 开发人员技术期刊: 最重要的 Java EE 最佳实践 IBM WebSphere 开发人员技术期刊: 最重要的 Java EE 最佳实践 2004 年 IBM® W ...

  9. mybatis 3.x源码深度解析与最佳实践(最完整原创)

    mybatis 3.x源码深度解析与最佳实践 1 环境准备 1.1 mybatis介绍以及框架源码的学习目标 1.2 本系列源码解析的方式 1.3 环境搭建 1.4 从Hello World开始 2 ...

随机推荐

  1. asp.net微信支付发起页面jsapi.aspx

    jsapi.aspx 后台核心代码 //创建支付应答对象 RequestHandler packageReqHandler = new RequestHandler(Context); //初始化 p ...

  2. 关于TM影像各波段组合的简介

    321:真彩色合成,即3.2.1波段分别赋予红.绿.蓝色,则获得自然彩色合成图像,图像的色彩与原地区或景物的实际色彩一致,适合于非遥感应用专业人员使用. 432:标准假彩色合成,即4.3.2波段分别赋 ...

  3. 查看Redis集群所有节点内存工具

    指定集群中任意一个节点,查看集群中所有节点当前已用物理内存.配置的最大物理内存和系统物理内存. ​ ​源码(可从https://github.com/eyjian/redis-tools下载): #! ...

  4. MFC载入BMP图片

    两步 hBitmap = (HBITMAP)LoadImage(NULL,fullPathName,IMAGE_BITMAP,120,120,LR_LOADFROMFILE);//载入图片 m_pic ...

  5. (转)类(class)和结构(struct)的区别是什么?它们对性能有影响吗?.NET BCL里有哪些是类(结构),为什么它们不是结构(类)?在自定义类型时,您如何选择是类还是结构?

    转自:http://blog.csdn.net/lingxyd_0/article/details/8695747 类(class)和结构(struct)的区别是什么?它们对性能有影响吗?.NET B ...

  6. Installing Apache Hadoop Single Node

    转载请注明出处:http://www.cnblogs.com/wubdut/p/4681286.html platform: Ubuntu 14.04 LTS hadoop 1.2.1 1. inst ...

  7. VS2010与VS2013中的多字节编码与Unicode编码问题

    1. 多字节字符与单字节字符 char与wchar_t 我们知道C++基本数据类型中表示字符的有两种:char.wchar_t.  char叫多字节字符,一个char占一个字节,之所以叫多字节字符是因 ...

  8. AngularJS 指令中的require

    require参数可以被设置为字符串或数组,字符串代表另外一个指令的名字.require会将控制器注入到其值所指定的指令中,并作为当前指令的链接函数的第四个参数.字符串或数组元素的值是会在当前指令的作 ...

  9. Crontab有关

    AIX下面 0,2,4,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,58 * * * * /usr/s ...

  10. WPF实战案例-打印

    在前段时间做了一下打印,因为需要支持的格式比较多,所以wpf能打印的有限分享一下几种格式的打印(.xls .xlsx .doc .docx .png .jpg .bmp .pdf) 首先为了保证exc ...