在LOG4J中把日志写入远程数据库

LOG4J强大的功能让我爱不释手,为了不用跑到机房看日志,我打算把日志写入远程数据库,这样操作起来就方便了,同时又可以按时间、按关键词搜索,一举两得。

LOG4J 提花了一个JDBCAppender的远程数据库输出方案,使用也很简单,在配置文件里写好驱动名、URL及远程数据库的登陆账号、密码,再加一个布局&SQL语句全部搞定,真当是方便极了。在我本机上测试没有问题,但欢天喜地移植到服务器上问题就来了,发现每次数据库连接特别慢,严重影响了程序的正常运行,这还得了,赶紧恢复到原来的状态,思索如何改进。

了解它的原理之后,问题的关键部分就很清楚了,JDBCAppender在向远程数据库写日志时,用的是短连接,虽然定义了一个BufferSize,但好像不起什么作用,这就相当于每次写日志都要重新建立一次数据连接,而建连接往往最耗时间的啦,能不能把我原来写的数据库连接池和LOG4J和JDBCAppender结合起来使用呢?

大的方向应该是没有问题,也搜索了一下别人实现的数据库连接池,但总觉得不是很满意,还是用自己的连接池放心。如何扩展原来的JDBCAppender、把数据库连接池传递进去,一开始没有搞明白,想了老半天,后来终于整明白了。我只需要继承原来的JDBCAppender,把getConnection()和 closeConnection()两个方法重写即可,其它的都不用变,示例如下:
package com.gftech.log4j;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;

import org.apache.log4j.jdbc.JDBCAppender;

import com.gftech.common.GFConn;
import com.gftech.common.GFDB;

public class JDBCExtAppender extends JDBCAppender ...{
    protected String driver;

    public static GFDB gfdb;

    private ArrayList<GFConn> tempList;

    public JDBCExtAppender() ...{
        super();
        tempList = new ArrayList<GFConn>();
    }

    /** *//**
     * Override this to return the connection to a pool, or to clean up the
     * resource.
     *
     * The default behavior holds a single connection open until the appender is
     * closed (typically when garbage collected).
     */
    protected void closeConnection(Connection con) ...{
        if (con != null && tempList != null) ...{
            for (int i = 0; i < tempList.size(); i++) ...{
                GFConn gfconn = tempList.get(i);
                if (con==gfconn.getConn()) ...{
                    gfconn.close();
                    tempList.remove(i);
//                    System.err.println("remove conn:"+con);
                    break;
                }
            }
        }
    }

    /** *//**
     * Override this to link with your connection pooling system.
     *
     * By default this creates a single connection which is held open until the
     * object is garbage collected.
     */
    protected Connection getConnection() throws SQLException ...{
        if (gfdb == null)
            gfdb = new GFDB("log4jDB", driver, databaseURL, databaseUser, databasePassword);

        if (gfdb != null) ...{
            GFConn gfconn = gfdb.getConn();
            if (gfconn != null) ...{
                connection = gfconn.getConn();
                tempList.add(gfconn);
            }
        }

        return connection;
    }

    public void setDriver(String driverClass) ...{
        driver = driverClass;

    }

}

虽然用到了数据库连接池,但我在实际测试中发现,在多线程应用程序中,很快就提示使用的连接无效,但该连接明明在刚开始还可以用,难道连接被主动关闭掉了? 既然用的是数据库连接池,我当然希望所有的连接在连接池内部进行动态管理,外面不需要干涉,这个现象说明JDBCAppender里面一定是显式关闭了连接.分析出原因后,很容易找到JDBCAppender里面有个close()方法,里面显式关闭了连接:
/** *//**
   * Closes the appender, flushing the buffer first then closing the default
   * connection if it is open.
   */
  public void close()
  ...{
    flushBuffer();

    try ...{
      if (connection != null && !connection.isClosed())
          connection.close();
    } catch (SQLException e) ...{
        errorHandler.error("Error closing connection", e, ErrorCode.GENERIC_FAILURE);
    }
    this.closed = true;
  }

为了保证连接不被关闭,让连接池自己管理,我们需要重写该方法,去掉连接关闭的代码,修改如下:
public void close() ...{
        flushBuffer();

        this.closed = true;
    }
   

再一次测试,发现连接已不会再断开,达到预期效果。同时又发现另外一个问题,就是堆栈信息无法写入到数据库当中,而ConsoleAppender、 FileAppender、SMTPAppender都可以显示异常时的详细堆栈信息,为何JDBCAppender不可以呢?通过查看源代码得知,其它 Appender在最后生成%m对应的信息时都做了一下判断,如果异常被忽略了,则把异常的堆栈信息重新添加进去,而JDBCAppender并没有做这一步,这是导致它与众不同的原因。搞清楚问题所在之后,我们重写getLogStatement()方法并且JDBCAppender里面也建议重写该方法,自己对信息做一下处理并为每句添加一个回车换行使信息看起来更清晰,如下所示:
public String getLogStatement(LoggingEvent event)...{
        StringBuffer sbuf=new StringBuffer();
        sbuf.append(layout.format(event));
        if (layout.ignoresThrowable()) ...{
            sbuf.delete(sbuf.length()-2,sbuf.length() );
            String[] s = event.getThrowableStrRep();
            if (s != null) ...{
                for (int j = 0; j < s.length; j++) ...{
                    sbuf.append("\r\n ");
                    sbuf.append(s[j]);
                }
            }
            sbuf.append("')");
        }
       
        return sbuf.toString() ;
    }



在配置文件中做如下设置:
#A5 send log info to remote mysql database
log4j.appender.A5 = com.gftech.log4j.JDBCExtAppender
log4j.appender.A5.Driver = com.mysql.jdbc.Driver
log4j.appender.A5.URL = jdbc:mysql://192.168.10.1:3306/log
log4j.appender.A5.User = root
log4j.appender.A5.Password = plus
log4j.appender.A5.layout = org.apache.log4j.PatternLayout
log4j.appender.A5.sql = INSERT INTO app_log(machine,occur_date,thread_name,cat,level,info) values('DP','%d{yyyy-MM-dd HH:mm:ss}','%t','%c','%p','%m')

指定LOG4J用自定义的扩展JDBC Appender,这样一来大大减少数据库连接建立的次数,提示程序的执行效率

说明:

也可用开源的一些数据库连接池技术,原理都大同小异,同样是修改这两个处理数据库连接的方法可以了

你可能感兴趣的:(sql,log4j,mysql,jdbc,配置管理)