Log4j日志存储到数据库——优化

1 篇文章 0 订阅
1 篇文章 0 订阅

年前没有什么心情开发正常的需求,最近一直在做保监会存量数据提取,很头大,打算研究点新东西换换心情。
我工作的公司服务器日志必须跟DBA要,一直麻烦人家不太好,而且他如果有事,过了很长时间才给我日志,会影响我问题排查的效率。我就想把日志存储到数据库中,这样十分完美。
最开始想自己写一套Appender来实现,后来Google一下,发现Log4j默认就提供JDBCAppender来实现日志存储数据库的功能。我参考了 log4j日志存储到数据库这篇博客,但是有个问题没有解决,所以才有我这篇博客的诞生,即我优化的之处。

1. 创建日志存储表

 create table XXX_LOG
(
  loglevel VARCHAR2(5) not null,--日志登记
  maketime VARCHAR2(25) not null,--打印时间
  location VARCHAR2(300) not null,--输出位置
  message  VARCHAR2(4000) not null--日志内容
);

由于输出位置我是用的是%l,即输出 类全名.函数名(文件名:行号),所以输出位置长度需要比较长。

2. log4j配置文件的管理

由于项目中用的是log4j.properties,我主要对properties格式的配置文件做说明。

#使用下面定义的appender.DB做完日志的输出位置
log4j.rootLogger=DB
#创建名字为DB的Appender,用来输出日志到数据库
log4j.appender.DB=org.apache.log4j.jdbc.JDBCAppender
#数据库的驱动
log4j.appender.DB.driver=oracle.jdbc.driver.OracleDriver
#数据库的路径
log4j.appender.DB.URL=jdbc:oracle:thin:@IP:1521:dbname
#数据库的用户名
log4j.appender.DB.user=xxx
#数据库的密码
log4j.appender.DB.password=xxx
#有多少条日志信息后才存入数据库,如果数量过小是会影响生产的效率
log4j.appender.DB.BufferSize=30
#插入数据库使用的SQL语句,用的跟PatternLayout是一样的格式化参数
log4j.appender.DB.sql=INSERT INTO Prip_Log VALUES('%p','%d','%l','%m')
#格式化参数使用的类,也是我跟我参考的那篇博客不一样的地方,下面具体说明
log4j.appender.DB.layout=xxx.DBPatternLayout

3. 待解决的问题说明

如果直接按照我参考博客干,会出一个问题,打印信息中包含”’”(单引号),系统会报“缺少逗号”的错误。为什么会出现这个问题能,如果你把相应内容直接替换到插入SQL中就会发现问题。例如下面的SQL:

insert into xxx_log (LOGLEVEL, MAKETIME, LOCATION, MESSAGE)
values ('INFO', '2017-01-23 09:13:54,092', 'xxx.Configs.<init>(Configs.java:37)', ''测试单引号'');

你会发现打印信息的单引号跟SQL中的字符串边界的单引号匹配了,“测试单引号”这句话就没有被单引号包裹,数据库就不认为它是字符串。才产生这个问题。
那么如何解决的这个问题呢,用转义字符,将单引号转义就行了,根据具体数据库有不同的转义字符,oracle是用单引号作为转义,例如如下改写后的SQL:

insert into xxx_log (LOGLEVEL, MAKETIME, LOCATION, MESSAGE)
values ('INFO', '2017-01-23 09:13:54,092', 'xxx.Configs.<init>(Configs.java:37)', '''测试单引号''');

三个单引号,第一个是字符串边界,第二个是转义字符,第三个才是要打印的单引号。
或者是MySql转义字符用的是”\”,如下SQL:

insert into xxx_log (LOGLEVEL, MAKETIME, LOCATION, MESSAGE)
values ('INFO', '2017-01-23 09:13:54,092', 'xxx.Configs.<init>(Configs.java:37)', '\'测试单引号\'');

4. 具体实施方案

上面只是说明了一下问题的原因,及解决思路,具体的实施方法没有说明。Log4j既然是个开源库,肯定会有各种的扩展方法,我就开始研究JDBCAppender的实现方式。想怎么样可以将日志内容中的“’”替换成“””。这个就得研究源代码了。
1. 方案1
先说一个不是十分完美的方案。我发现%m是获取org.apache.log4j.spi.LoggingEvent.getRenderedMessage(),我最开始的想法是写个子类继承LoggingEvent,重写getRenderedMessage方法,然后将子类对象植入log4j的过程中。

public class DBLoggingEvent extends LoggingEvent
{
    private LoggingEvent mEvent = null;

    public DBLoggingEvent(LoggingEvent tEvent)
    {
        super(tEvent.getFQNOfLoggerClass(), tEvent.getLogger(), tEvent.getLevel(), tEvent.getMessage(), null);
        mEvent = tEvent;
    }
    ……
    @Override
    public String getRenderedMessage()
    {
        // 将字符串中的“'”替换成“''”
        return mEvent.getRenderedMessage().replace("'", "''");
    }
}
public class DBPatternLayout extends PatternLayout
{
    @Override
    public String format(LoggingEvent event)
    {
        // 将新的LoggingEvent对象植入log4j的过程中
        DBLoggingEvent tEvent = new DBLoggingEvent(event);
        return super.format(tEvent);
    }
}

在log4j.properties的配置文件中使用自定义的解析类,即

 log4j.appender.DB.layout=xxx.DBPatternLayout

但是这个方案有三个问题,1、LoggingEvent 没有默认的无参构造函数,必须使用有参构造函数,如果之后构造函数发送变化了,这个类也得跟着改,有耦合问题。就算修改LoggingEvent 的源代码也会有切换log4j版本产生的耦合问题。2、每次都得多建一个对象,消耗系统资源。3、违反log4j的设计模式,即LoggingEvent 只是个javaBean,用来传输数据的,不要增加多余的业务处理。
2. 方案2
那我继续研究代码,发现log4j是用PatternParser来获取解析格式化参数的解释器的,那我写个子类继承PatternParser,增加对单引号的处理,更能符合log4j的体系,即我第二种的方案的思路。
DBPatternParser相当于PatternParser的代理,增加了对保存到数据库时,打印信息中有单引号这个情况的特殊处理,其他情况还是调用父类的实现,我自己感觉用if能简便一些,但是我看父类代码,感觉switch更好一些,为了保持对将来的扩展能力,最后还是模仿父类的架构。

public class DBPatternParser extends PatternParser
{
    public DBPatternParser(String pattern)
    {
        super(pattern);
    }
    @Override
    protected void finalizeConverter(char c)
    {
        switch (c)
        {
            case 'm':
                // 如果是打印信息,调用DBPatternConverter来替换一个单引号成两个单引号
                addConverter(new DBPatternConverter(formattingInfo, c));
                currentLiteral.setLength(0);
                break;
            default:
                // 其他情况调用父类的实现
                super.finalizeConverter(c);
                break;
        }
    }
    private static class DBPatternConverter extends PatternConverter
    {
        char type;
        DBPatternConverter(FormattingInfo formattingInfo, char type)
        {
            super(formattingInfo);
            this.type = type;
        }
        public String convert(LoggingEvent event)
        {
            switch (type)
            {
                case 'm':
                    return event.getRenderedMessage().replace("'", "''");
                default:
                    return null;
            }
        }
    }
}

将新写的PatternParser子类植入log4j的体系中

public class DBPatternLayout extends PatternLayout
{
    @Override
    protected PatternParser createPatternParser(String pattern)
    {
        return new DBPatternParser(pattern);
    }
}

在log4j.properties的配置文件中使用自定义的解析类,跟方案1一样

 log4j.appender.DB.layout=xxx.DBPatternLayout

在这谈一下对zhangchu_63的感谢,即我参考的那篇博客的作者

5.进一步优化

由于

#有多少条日志信息后才存入数据库,如果数量过小是会影响生产的效率
log4j.appender.DB.BufferSize=30

这个配置对性能的优化,往往会出现这个问题,如果这个你一直监控着系统,发现有问题了,马上去查看数据库的日志存储,发现最后的日志还没有刷新到数据库中,这个时候就比较尴尬了,我原先只能手动的多点几下,多输出点日志,然后让我想看到的日志刷新到数据库中。
那么有什么更好的方法去实现呢?还是得研究代码,我发现在JDBCAppender中调用flushBuffer方法来实现将缓存区中的日志刷新到数据库中的。

    if (buffer.size() >= bufferSize)
      flushBuffer();

这个问题就变成获取JDBCAppender,然后调用flushBuffer。
如何获取呢?从代码中发现Category类中有

  synchronized
  public
  Appender getAppender(String name) {
     if(aai == null || name == null)
      return null;

     return aai.getAppender(name);
  }

这个函数能实现我想要的功能。
理所当然的问题就变成了如何获取Category,然后调用getAppender。
我一看这个Category的子类,看着好面熟啊,Logger这个不就是我们输出日志的时候使用的类吗,如果我直接使用程序中获取的Logger能获取到Appender,我感觉不太可能啊,经过实际测试,也是不太可行的。那怎么办,继续看代码,终于我眼前一亮,我看到了如下的代码:

    Logger root = Logger.getRootLogger();
    root.addAppender(appender);

然后就有了如下类的诞生。由于Appender在log4j中是单例模式,那我也使用单例模式来获取它

public class FlushLogBuffer
{
    JDBCAppender mAppender = null;
    private static FlushLogBuffer BUFFER = null;
    private FlushLogBuffer()
    {
        Logger logger = Logger.getRootLogger();
        // 获取名字为DB的Appender
        mAppender = (JDBCAppender) logger.getAppender("DB");
    }
    public static FlushLogBuffer getInstance()
    {
        if (BUFFER == null)
        {
            synchronized (FlushLogBuffer.class)
            {
                if (BUFFER == null)
                {
                    BUFFER = new FlushLogBuffer();
                }
            }
        }
        return BUFFER;
    }
    public void flushBuffer()
    {
        mAppender.flushBuffer();
    }
}

只要写个页面,然后后台调用,就能将缓存区的日志刷新到数据库中

FlushLogBuffer tFlushLogBuffer = FlushLogBuffer.getInstance();
tFlushLogBuffer.flushBuffer();

至此,log4j日志存储到数据库的优化已经全部完毕了

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值