Appender of Log4j [3]

4.FileAppender

WriterAppender另一个子类为FileAppender,顾名思义,FileAppender用于将LoggingEvent输出到文件中(ConsoleAppender仅为标准输出、标准错误),扩展了用户可以输出日志的范围。FileAppender在WriterAppender基础上,增加的可配置属性有:

  • File: 日志输出所在的文件名
  • Append: 文件写出模式,默认值为true表示在原有文件后追加内容,为false表示流建立时先清空truncate文件原有内容
  • BufferedIO: 文件流写出是否使用缓冲,true表示使用,默认值为false即不使用缓冲
  • BufferSize: 如果文件流写出使用缓冲,缓冲区内存大小,默认8k



注意,log4j properties中属性名的选择需要参考Appender的setXXX方法,配置的属性名为XXX。举例如配置Appender属性时,属性名为File

log4j.appender.writer.File=/tmp/log.log
/**
* The name of the log file. 
*/
protected String fileName = null;
public void setFile(String file) {
  // Trim spaces from both ends. The users probably does not want
  // trailing spaces in file names.
  String val = file.trim();
  fileName = val;
}

FileAppender的主要逻辑在打开文件获取写出流上,类似ConsoleAppender,不需要重写父类WriterAppender的subAppend方法。打开文件流相关逻辑为:

  1. 根据是否需要缓冲设置ImmediateFlush,缓冲和ImmediateFlush是互斥功能。
  2. 尝试根据文件名以及写出append模式打开文件输出流FileOutputStream。
  3. 若第二步打开流出现异常,判断异常类型,如果是文件不存在FileNotFoundException,则尝试重新创建文件。会确认父目录的存在性并重新打开文件流FileOutputStream,如果仍然出现异常则再抛错。
  4. 根据得到的FileOutputStream进一步构造QuietWriter,构造完成QuietWriter后,后续日志逻辑主要集中在父类WriterAppender实现中。
  5. 文件流打开的异常虽然抛出到FileAppender之外,但并不会阻塞应用的正常执行,即log4j会消化本身的异常。
public synchronized 
  void setFile(String fileName, boolean append, 
               boolean bufferedIO, int bufferSize) throws IOException {
    LogLog.debug("setFile called: "+fileName+", "+append);
    // It does not make sense to have immediate flush and bufferedIO.
    if(bufferedIO) {
      setImmediateFlush(false);
    }
    reset();
    FileOutputStream ostream = null;
    try {
          //   attempt to create file
          ostream = new FileOutputStream(fileName, append);
    } catch(FileNotFoundException ex) {
          //   if parent directory does not exist then
          //      attempt to create it and try to create file
          //      see bug 9150
          String parentName = new File(fileName).getParent();
          if (parentName != null) {
             File parentDir = new File(parentName);
             if(!parentDir.exists() && parentDir.mkdirs()) {
                ostream = new FileOutputStream(fileName, append);
             } else {
                throw ex;
             }
          } else {
             throw ex;
          }
    }
    Writer fw = createWriter(ostream);
    if(bufferedIO) {
      fw = new BufferedWriter(fw, bufferSize);
    }
    this.setQWForFiles(fw);
    this.fileName = fileName;
    this.fileAppend = append;
    this.bufferedIO = bufferedIO;
    this.bufferSize = bufferSize;
    writeHeader();
    LogLog.debug("setFile ended");
  }

demo log4j config:

log4j.rootLogger=INFO,writer
#指定Appender为FileAppender
log4j.appender.writer=org.apache.log4j.FileAppender
#FileAppender日志输出文件名
log4j.appender.writer.File=/tmp/log.log
#文件写出模式,true为文件追加模式
log4j.appender.writer.Append=true
#true表示文件流使用装饰器,增加缓冲模式
log4j.appender.writer.BufferedIO=true
#缓冲区大小
log4j.appender.writer.BufferSize=10
#流编码
log4j.appender.writer.Encoding=utf8
#Appender关联日志级别
log4j.appender.writer.Threshold=debug
#关联的Layout
log4j.appender.writer.layout=org.apache.log4j.PatternLayout
#layout关联的转换字符串
log4j.appender.writer.layout.ConversionPattern=%t %m%n
#appender关联的名字
log4j.appender.writer.name=writerdemo

demo 日志输出(标准输出):

fileappender

FileAppender的子类们

FileAppender主要在WriterAppender上增加了文件流相关的功能,每个Appender仅关联一个日志输出文件。可对日志输出文件做进一步的功能增强,主要体现为日志文件轮转,即根据需要将日志信息输出到不同的日志文件中,log4j提供了两种具体实现,分别为

  • DailyRollingFileAppender
  • RollingFileAppender
  • ExternallyRolledFileAppender

5.RollingFileAppender

RollingFileAppender继承自FileAppender、在FileAppender的基础上增加了日志文件大小超过指定阈值后的日志轮转写功能。RollingFileAppender在FileAppender基础上新支持下面配置属性:

  • MaxBackupIndex 日志轮转时备份日志文件数量,不包括当前正在写的文件,默认值是1。如果设置为0,当前日志文件达到MaximumFileSize大小后,当前写的日志文件会被truncated。
  • MaximumFileSize 单个日志文件最大大小,当日志文件达到此指定大小后,会触发日志轮转写新的日志文件。需要配置为long类型的数字,即子节数量,范围在0 - 2^63。
  • MaxFileSize 单个日志文件最大大小,当日志文件达到此指定大小后,会触发日志轮转写新的日志文件。支持单位语法,如10kb,1mb,10gb等。具体是调用OptionConverter.toFileSize完成到long类型数字的转换,单位不区分大小写。

RollingFileAppender重写了继承的subAppender方法,在当前日志文件写出子节数超过阈值时,触发一次轮转。轮转后,当前RollingFileAppender关联的输出流指向一个新的文件。size >= maxFileSize && size >= nextRollover判断中:size >= maxFileSize表示日志文件要达到轮转大小;size >= nextRollover是用于若轮转失败下次轮转的时机是否满足,避免一次轮转失败导致后续每次LoggingEvent日志输出都触发轮转,影响效率。

protected void subAppend(LoggingEvent event) {
  super.subAppend(event);
  if(fileName != null && qw != null) {
    long size = ((CountingQuietWriter) qw).getCount();
    if (size >= maxFileSize && size >= nextRollover) {
      rollOver();
    }
  }
}

RollingFileAppender提供的轮转rollOver策略为:

  1. 删除最老的File.MaxBackupIndex,将File.1...File.MaxBackupIndex -1 重命名到File.2...File.MaxBackupIndex,文件右移一位。
  2. 当前Appender使用的文件File被重命名到File.1,不再更新此文件内容。
  3. MaxBackupIndex如果设置为0,当前日志文件达到MaximumFileSize大小后,当前写的日志文件会被truncated。
  4. 如果MaxBackupIndex为负数,则不进行日志轮转逻辑,相当于FileAppender。默认轮转策略时,MaxBackupIndex为1。
// 不需要增加synchronized关键字,rollOver的调用方已经有同步
public void rollOver() {
  File target;
  File file;
  if (qw != null) {
    long size = ((CountingQuietWriter) qw).getCount();
    LogLog.debug("rolling over count=" + size);
    // 如果log4j.debug、log4j.configDebug系统属性为false(不区分大小写),则LogLog.debug函数调用不输出消息。默认为false。具体见ErrorHandler和LogLog相关的介绍
    // 如果本次轮转失败,下次轮转的时机是当前日志文件又增加了maxFileSize子节大小,避免一次轮转失败导致后续每次LoggnigEvent日志输出都触发轮转,影响效率
    nextRollover = size + maxFileSize;
  }
  LogLog.debug("maxBackupIndex="+maxBackupIndex);

  boolean renameSucceeded = true;
  // 如果maxBackups为负数,不进行日志轮转逻辑,整体行为类似于FileAppender
  if(maxBackupIndex > 0) {
    // 删除最老的一个日志文件,如果maxBackupIndex为10,则当前最老日志文件是后缀为.10的那一个,删除最老日志文件后,方便后续文件循环重命名右移一位.
    file = new File(fileName + '.' + maxBackupIndex);
    if (file.exists())
      renameSucceeded = file.delete();
      // 文件循环mv一次:Map {(maxBackupIndex - 1), ..., 2, 1} -> {maxBackupIndex, ..., 3, 2}
      for (int i = maxBackupIndex - 1; i >= 1 && renameSucceeded; i--) {
        file = new File(fileName + "." + i);
        if (file.exists()) {
          target = new File(fileName + '.' + (i + 1));
          LogLog.debug("Renaming file " + file + " to " + target);
          renameSucceeded = file.renameTo(target);
        }
      }
      if(renameSucceeded) {
        // 如果上面重命名OK,则将当前写出日志重命名到:fileName -> fileName.1
        target = new File(fileName + "." + 1);
        this.closeFile(); // keep windows happy.
        file = new File(fileName);
        LogLog.debug("Renaming file " + file + " to " + target);
        renameSucceeded = file.renameTo(target);
        //   if file rename failed, reopen file with append = true
        if (!renameSucceeded) {
          try {
            this.setFile(fileName, true, bufferedIO, bufferSize);
          } catch(IOException e) {
            if (e instanceof InterruptedIOException) {
              Thread.currentThread().interrupt();
            }
            LogLog.error("setFile("+fileName+", true) call failed.", e);
          }
        }
      }  
    }
    // 如果文件名轮转成功
    if (renameSucceeded) {
      try {
        this.setFile(fileName, false, bufferedIO, bufferSize);
        nextRollover = 0;
      } catch(IOException e) {
        if (e instanceof InterruptedIOException) {
          Thread.currentThread().interrupt();
        }
        LogLog.error("setFile("+fileName+", false) call failed.", e);
      }
   }
}

demo log4j config

log4j.rootLogger=INFO,writer
log4j.appender.writer=org.apache.log4j.RollingFileAppender
log4j.appender.writer.MaxBackupIndex=5
#RollingFileAppender每个日志文件的最大大小,超出时轮转
#log4j.appender.writer.MaximumFileSize=1024
#RollingFileAppender每个日志文件的最大大小,超出时轮转,同MaximumFileSize区别是这里的字符串支持kb、mb、gb单位
log4j.appender.writer.MaxFileSize=1kb
log4j.appender.writer.File=/tmp/log.log
log4j.appender.writer.Append=true
log4j.appender.writer.BufferedIO=true
log4j.appender.writer.BufferSize=10
log4j.appender.writer.Encoding=utf8
log4j.appender.writer.Threshold=debug
log4j.appender.writer.layout=org.apache.log4j.PatternLayout
log4j.appender.writer.layout.ConversionPattern=%t %m%n
log4j.appender.writer.name=writerdemo

输出日志文件列表为

RollingFileAppender

6.ExternallyRolledFileAppender

ExternallyRolledFileAppender继承自RollingFileAppender,在其功能基础上增加了强制轮转功能,即ExternallyRolledFileAppender会在指定端口Port的监听轮转请求,在收到客户端发送的RollOver消息字符串后,触发一次轮转并返回OK消息给客户端,此时日志文件的大小可能还未到达指定阈值大小。日志轮转具体操作过程和RollingFileAppender相对一致。
注意,ExternallyRolledFileAppender的守护线程仅仅是ServerSocket等待客户端连接,检测消息字符串是否为RollOver,没有做权限判断。在生产环境中,建议对客户端请求进行健全,避免非法的RollOver请求。org.apache.log4j.varia.Roller是log4j提供的实例工具,可用于向ExternallyRolledFileAppender发出RollOver请求。

demo log4j config

log4j.rootLogger=INFO,writer
log4j.appender.writer=org.apache.log4j.varia.ExternallyRolledFileAppender
#日志轮转监听端口
log4j.appender.writer.Port=8000
log4j.appender.writer.MaxBackupIndex=5
log4j.appender.writer.MaxFileSize=10GB
log4j.appender.writer.File=/tmp/log.log
log4j.appender.writer.Append=true
log4j.appender.writer.BufferedIO=true
log4j.appender.writer.BufferSize=10
log4j.appender.writer.Encoding=utf8
log4j.appender.writer.Threshold=debug
log4j.appender.writer.layout=org.apache.log4j.PatternLayout
log4j.appender.writer.layout.ConversionPattern=%t %m%n
log4j.appender.writer.name=writerdemo

日志效果

注意日志文件并没有达到大小阈值

ExternallyRolledFileAppender

7.DailyRollingFileAppender

DailyRollingFileAppender继承自FileAppender(支持FileAppender所有特性),增加了按照时间频率进行日志轮转的机制,可以配置DatePattern配置项设置日志轮转的时间频率。DatePattern配置项需要符合SimpleDateFormat格式,注意格式串中的常量需要单引号括住,如月份轮转'.'yyyy-MM

每次日志轮转时,DailyRollingFileAppender获取当前写出日志文件的最后修改时间,并使用配置的DatePattern格式化出时间字符串,此字符串会做为轮转备份日志文件名的后缀。如月份轮转'.'yyyy_MM_dd、发生轮转时时间为2015-04-10 00:01,用户配置File为luohw,则DailyRollingFileAppender会将当前写的日志文件luohw重命名到luohw.2015_04_10。后续DailyRollingFileAppender创建一个新的文件luohw,继续后续的日志记录。

注意,检测日志是否需要轮转是根据日志事件到来触发的,即应用调用产生一次LoggingEvent,DailyRollingFileAppender刷出日志消息时检测是否需要日志轮转,故DailyRollingFileAppender并不是严格的准时准点轮转的,会受到日志消息是否到来的影响,可能有一定的延迟。

DailyRollingFileAppender支持的轮转频率有:

  • 月轮转,每月的第一天发生日志轮转,DatePattern配置如'.'yyyy-MM
  • 周轮转,每周的第一天发生日志轮转,根据Locate地域不同,具体周几是星期第一天不一定,DatePattern配置如'.'yyyy-ww
  • 天轮转,每天的0点发生日志轮转,这是DailyRollingFileAppender默认轮转频率,DatePattern配置如'.'yyyy-MM-dd
  • 半天轮转,每天的12点和24点发生日志轮转,DatePattern配置如'.'yyyy-MM-dd-a
  • 小时轮转,每个小时的整点发生日志轮转,DatePattern配置如'.'yyyy-MM-dd-HH
  • 分钟轮转,每分钟的开始发生日志轮转,DatePattern配置如'.'yyyy-MM-dd-HH-mm
    注意DatePattern格式中,天和小时之间不需要冒号:

DailyRollingFileAppender在执行FileAppender逻辑前判断是否超过日志轮转时间点,nextCheck是下次日志轮转发生时间。如果符合轮转时间,则更新nextCheck下次轮转触发时间点,并进行rollOver函数调用,完成文件重命名等操作。

protected void subAppend(LoggingEvent event) {
  long n = System.currentTimeMillis();
  if (n >= nextCheck) {
    now.setTime(n);
    nextCheck = rc.getNextCheckMillis(now);
    try {
      rollOver();
    } catch(IOException ioe) {
      if (ioe instanceof InterruptedIOException) {
        Thread.currentThread().interrupt();
      }
      LogLog.error("rollOver() failed.", ioe);
    }
  }
  super.subAppend(event);
}

DailyRollingFileAppender依赖内部类RollingCalendar计算下一次轮转时间点,RollingCalendar根据当前时间点和轮转频率计算下次检查时间点,具体是调用jdk内置的java.util.GregorianCalendar相关的时间加减API完成。关键点是根据配置文件中DatePattern解析出轮转频率,DailyRollingFileAppender的方法computeCheckPeriod解析轮转频率逻辑为:
对于时间Date epoch = new Date(0);按照高频率(分钟轮转)到低频率(月轮转)的顺序,分别尝试6种轮转频率:对于一种轮转频率,获取下次日志轮转时间点Date next = new Date(rollingCalendar.getNextCheckMillis(epoch));使用配置格式化串DatePattern分别序列化2个时间epoch 和 next,如果得到的字符串值不相同则表示用户配置DatePattern为当前尝试的轮转频率;否则尝试后面更粗力度的轮转。code如下:

int computeCheckPeriod() {
  RollingCalendar rollingCalendar = new RollingCalendar(gmtTimeZone, Locale.getDefault());
  // set sate to 1970-01-01 00:00:00 GMT
  Date epoch = new Date(0);
  if(datePattern != null) {
    for(int i = TOP_OF_MINUTE; i <= TOP_OF_MONTH; i++) {
      SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);
      simpleDateFormat.setTimeZone(gmtTimeZone); // do all date formatting in GMT
      String r0 = simpleDateFormat.format(epoch);
      rollingCalendar.setType(i);
      Date next = new Date(rollingCalendar.getNextCheckMillis(epoch));
      String r1 =  simpleDateFormat.format(next);
      //System.out.println("Type = "+i+", r0 = "+r0+", r1 = "+r1);
      if(r0 != null && r1 != null && !r0.equals(r1)) {
        return i;
      }
    }
  }
  return TOP_OF_TROUBLE; // Deliberately head for trouble...
}

举例子: 

用户配置'.'yyyy-MM-dd-HH,构造SimpleDateFormat simpleDateFormat = new SimpleDateFormat(datePattern);对于epoch (1970-01-01 00:00:00 GMT),尝试分钟轮转,下一次轮转时间next(1970-01-01 00:01:00 GMT),使用 simpleDateFormat序列化后字符串相同,尝试下一个频率。尝试小时轮转,epoch (1970-01-01 00:00:00 GMT),下一次轮转时间next(1970-01-01 00:01:00 GMT),此时字符串不同,则解析到用户的轮转频率。

demo log4j config

log4j.rootLogger=INFO,writer
log4j.appender.writer=org.apache.log4j.DailyRollingFileAppender
#DailyRollingFileAppender的日志轮转配置
log4j.appender.writer.DatePattern='.'yyyy_MM_dd_HH_mm
log4j.appender.writer.File=/tmp/log.log
log4j.appender.writer.Append=true
log4j.appender.writer.BufferedIO=true
log4j.appender.writer.BufferSize=10
log4j.appender.writer.Encoding=utf8
log4j.appender.writer.Threshold=debug
log4j.appender.writer.layout=org.apache.log4j.PatternLayout
log4j.appender.writer.layout.ConversionPattern=%t %m%n
log4j.appender.writer.name=writerdemo

输出日志文件列表为

DailyRollingFileAppender

标签: none
评论列表
  1. 站长写得真是非常好,我要让我的朋友也瞧一瞧这篇有趣的文章内容。
    [url=http://jwc.hbust.com.cn]ca88亚洲城[/url]

  2. 各种乱入,是怎么回事呢!沙发都被你们搞没了!

  3. 这里甚是热闹!头像可以显示了?以前总是XX
    ca88 http://rsc.hbust.com.cn

  4. 我来了,既然来了我就得说几句!只说几句而已!如果我不说几句!就对不起人了,既然我要说几句!那么肯定是要说话的~
    ca88亚洲城官网 http://xxgk.hbust.com.cn

  5. 你好,敢问你的图片放在哪里呢

    1. Trafalgar

      有的是外链,有的在服务机器上

添加新评论