自定義Appender類讓log4j.properties配置日誌文件根據時間和文件大小切分,限制最大日誌數自動刪除

一、單一根據時間或大小切分日誌文件

根據時間,這個相信大家都知道,主要是

log4j.appender.D = org.apache.log4j.DailyRollingFileAppender

就可以了,自然會根據日期來切分日誌文件.

根據大小也簡單

log4j.appender.debug=org.apache.log4j.RollingFileAppender
#設置日誌文件的大小
log4j.appender.debug.MaxFileSize=100M

但是如果同時想要根據時間和大小來分怎麼辦?

二、同時根據時間和大小切分

如果是使用logback,可以看我之前的博客,日誌框架logback的使用和配置詳解,裏面使用xml配置就可以實現。

如果是log4j的話,就需要自己實現appender類。

廢話不多說,代碼附上

這個類是根據DailyRollingFileAppender類照搬照抄修改的

Log4JDateAndSizeAppender

package com.zgd.base.core.log;

/**
 * Log4JDateAndSizeSplit
 * Log4JDateAndSizeSplit :log4j日誌支持按照日期和大小進行切割
 *
 * @author zgd
 * @date 2019/8/23 16:14
 */

import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Pattern;

import com.google.common.collect.Lists;
import org.apache.log4j.FileAppender;
import org.apache.log4j.Layout;
import org.apache.log4j.helpers.CountingQuietWriter;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.helpers.OptionConverter;
import org.apache.log4j.spi.LoggingEvent;

/**
 * ClassName: Log4JDateAndSizeSplit <br/>
 * Function: Log4j生成日誌類重寫,該類可以按照日期+指定大小分隔日誌. <br/>
 * date: 2016年3月31日 下午12:44:14 <br/>
 *
 * @author lujie
 */
public class Log4JDateAndSizeAppender extends FileAppender {

  /**
   * 可以根據分、時、半日、日、周、月的週期進行切分日誌
   */
  public static final int TOP_OF_TROUBLE = -1;

  public static final int TOP_OF_MINUTE = 0;

  public static final int TOP_OF_HOUR = 1;

  public static final int HALF_DAY = 2;

  public static final int TOP_OF_DAY = 3;

  public static final int TOP_OF_WEEK = 4;

  public static final int TOP_OF_MONTH = 5;

  /**
   * The default maximum file size is 10MB.
   */
  protected long maxFileSize = 10 * 1024 * 1024;

  /**
   * There is one backup file by default.
   */
  protected int maxBackupIndex = 1;

  /**
   * 最多保留日誌文件數
   */
  private int maxTotalFile = -1;

  /**
   * meaning daily rollover.
   */
  private String datePattern = "'.'yyyy-MM-dd";

  /**
   * "scheduledFilename" at the beginning of the next hour.
   */
  private String scheduledFilename;

  /**
   * The next time we estimate a rollover should occur.
   */
  private long nextCheck = System.currentTimeMillis() - 1;

  private Date now = new Date();

  private SimpleDateFormat sdf;

  private RollingCalendar rc = new RollingCalendar();

  int checkPeriod = TOP_OF_TROUBLE;

  // The gmtTimeZone is used only in computeCheckPeriod() method.
  private static final TimeZone GMT_TIME_ZONE = TimeZone.getTimeZone("GMT");

  /**
   * The default constructor does nothing.
   */
  public Log4JDateAndSizeAppender() {
  }

  /**
   * ouput destination for this appender.
   */
  public Log4JDateAndSizeAppender(Layout layout, String filename, String datePattern) throws IOException {
    super(layout, filename, true);
    this.datePattern = datePattern;
    activateOptions();
  }

  /**
   * being rolled over to backup files.
   *
   * @since 1.1
   */
  public long getMaximumFileSize() {
    return maxFileSize;
  }

  /**
   * being rolled over to backup files.
   *
   * <p>
   * JavaBeans {@link java.beans.Introspector Introspector}.
   *
   * @see #setMaxFileSize(String)
   */
  public void setMaximumFileSize(long maxFileSize) {
    this.maxFileSize = maxFileSize;
  }

  /**
   * being rolled over to backup files.
   *
   * <p>
   * the value "10KB" will be interpreted as 10240.
   */
  public void setMaxFileSize(String value) {
    maxFileSize = OptionConverter.toFileSize(value, maxFileSize + 1);
  }

  /**
   * Returns the value of the <b>MaxBackupIndex</b> option.
   */
  public int getMaxBackupIndex() {
    return maxBackupIndex;
  }

  /**
   * Set the maximum number of backup files to keep around.
   *
   * <p>
   */
  public void setMaxBackupIndex(int maxBackups) {
    this.maxBackupIndex = maxBackups;
  }

  /**
   * 按日期劃分,日期格式
   */
  public void setDatePattern(String pattern) {
    datePattern = pattern;
  }


  /**
   * Returns the value of the <b>DatePattern</b> option.
   */
  public String getDatePattern() {
    return datePattern;
  }

  public int getMaxTotalFile() {
    return maxTotalFile;
  }

  public void setMaxTotalFile(int maxTotalFile) {
    this.maxTotalFile = maxTotalFile;
  }

  /**
   * 小數點+索引的後綴匹配
   */
  private static final Pattern suffixCompile = Pattern.compile("^.*\\.\\d$");

  @Override
  public void activateOptions() {
    super.activateOptions();
    if (datePattern != null && fileName != null) {
      now.setTime(System.currentTimeMillis());
      sdf = new SimpleDateFormat(datePattern);
      int type = computeCheckPeriod();
      printPeriodicity(type);
      rc.setType(type);
      File file = new File(fileName);
      scheduledFilename = fileName + sdf.format(new Date(file.lastModified()));

    } else {
      LogLog.error("Either File or DatePattern options are not set for appender [" + name + "].");
    }
  }

  private void printPeriodicity(int type) {
    switch (type) {
      case TOP_OF_MINUTE:
        LogLog.debug("Appender [" + name + "] to be rolled every minute.");
        break;
      case TOP_OF_HOUR:
        LogLog.debug("Appender [" + name + "] to be rolled on top of every hour.");
        break;
      case HALF_DAY:
        LogLog.debug("Appender [" + name + "] to be rolled at midday and midnight.");
        break;
      case TOP_OF_DAY:
        LogLog.debug("Appender [" + name + "] to be rolled at midnight.");
        break;
      case TOP_OF_WEEK:
        LogLog.debug("Appender [" + name + "] to be rolled at start of week.");
        break;
      case TOP_OF_MONTH:
        LogLog.debug("Appender [" + name + "] to be rolled at start of every month.");
        break;
      default:
        LogLog.warn("Unknown periodicity for appender [" + name + "].");
    }
  }

  // This method computes the roll over period by looping over the
  // periods, starting with the shortest, and stopping when the r0 is
  // different from from r1, where r0 is the epoch formatted according
  // the datePattern (supplied by the user) and r1 is the
  // epoch+nextMillis(i) formatted according to datePattern. All date
  // formatting is done in GMT and not local format because the test
  // logic is based on comparisons relative to 1970-01-01 00:00:00
  // GMT (the epoch).

  private int computeCheckPeriod() {
    RollingCalendar rollingCalendar = new RollingCalendar(GMT_TIME_ZONE, Locale.ENGLISH);
    // 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);
        // do all date
        simpleDateFormat.setTimeZone(GMT_TIME_ZONE);
        // formatting in GMT
        String r0 = simpleDateFormat.format(epoch);
        rollingCalendar.setType(i);
        Date next = new Date(rollingCalendar.getNextCheckMillis(epoch));
        String r1 = simpleDateFormat.format(next);
        if (!r0.equals(r1)) {
          return i;
        }
      }
    }
    // Deliberately head for trouble...
    return TOP_OF_TROUBLE;
  }

  /**
   * Implements the usual roll over behaviour.
   *
   * <p>
   * If <code>MaxBackupIndex</code> is positive, then files {
   *
   * <p>
   * If <code>MaxBackupIndex</code> is equal to zero, then the
   * <code>File</code> is truncated with no backup files created.
   */
  private// synchronization not necessary since doAppend is alreasy synched
  void sizeRollOver() {
    File target;
    File file;

    LogLog.debug("rolling over count=" + ((CountingQuietWriter) qw).getCount());
    LogLog.debug("maxBackupIndex=" + maxBackupIndex);

    String datedFilename = fileName + sdf.format(now);

    if (maxBackupIndex > 0) {
      // Delete the oldest file, to keep Windows happy.
      file = new File(datedFilename + '.' + maxBackupIndex);
      if (file.exists()) {
        file.delete();
      }

      // Map {(maxBackupIndex - 1), ..., 2, 1} to {maxBackupIndex, ..., 3,
      // 2}
      for (int i = maxBackupIndex - 1; i >= 1; i--) {
        file = new File(datedFilename + "." + i);
        if (file.exists()) {
          target = new File(datedFilename + '.' + (i + 1));
          LogLog.debug("Renaming file " + file + " to " + target);
          file.renameTo(target);
        }
      }

      // Rename fileName to datedFilename.1
      target = new File(datedFilename + "." + 1);

      this.closeFile(); // keep windows happy.

      file = new File(fileName);
      LogLog.debug("Renaming file " + file + " to " + target);
      file.renameTo(target);
    } else if (maxBackupIndex < 0) {
      // infinite number of files
      // find the max backup index
      for (int i = 1; i < Integer.MAX_VALUE; i++) {
        target = new File(datedFilename + "." + i);
        if (!target.exists()) { // Rename fileName to datedFilename.i
          this.closeFile();
          file = new File(fileName);
          file.renameTo(target);
          LogLog.debug("Renaming file " + file + " to " + target);
          break;
        }
      }
    }
    LogLog.debug("start delete old logs...");
    deleteObsoleteFiles();
    try {
      // This will also close the file. This is OK since multiple
      // close operations are safe.
      this.setFile(fileName, false, bufferedIO, bufferSize);
    } catch (IOException e) {
      LogLog.error("setFile(" + fileName + ", false) call failed.", e);
    }
    scheduledFilename = datedFilename;
  }

  @Override
  public synchronized void setFile(String fileName, boolean append, boolean bufferedIO, int bufferSize) throws IOException {
    super.setFile(fileName, append, this.bufferedIO, this.bufferSize);
    if (append) {
      File f = new File(fileName);
      ((CountingQuietWriter) qw).setCount(f.length());
    }
  }

  @Override
  protected void setQWForFiles(Writer writer) {
    this.qw = new CountingQuietWriter(writer, errorHandler);
  }

  /**
   * Rollover the current file to a new file.
   */
  void timeRollOver() throws IOException {

    /* Compute filename, but only if datePattern is specified */
    if (datePattern == null) {
      errorHandler.error("Missing DatePattern option in rollOver().");
      return;
    }

    String datedFilename = fileName + sdf.format(now);
    // It is too early to roll over because we are still within the
    // bounds of the current interval. Rollover will occur once the
    // next interval is reached.
    if (scheduledFilename.equals(datedFilename)) {
      return;
    }

    // close current file, and rename it to datedFilename
    this.closeFile();

    File target = new File(scheduledFilename);
    if (target.exists()) {
      target.delete();
    }

    File file = new File(fileName);
    boolean result = file.renameTo(target);
    if (result) {
      LogLog.debug(fileName + " -> " + scheduledFilename);
    } else {
      LogLog.error("Failed to rename [" + fileName + "] to [" + scheduledFilename + "].");
    }

    try {
      // This will also close the file. This is OK since multiple
      // close operations are safe.
      super.setFile(fileName, false, this.bufferedIO, this.bufferSize);
    } catch (IOException e) {
      errorHandler.error("setFile(" + fileName + ", false) call failed.");
    }
    scheduledFilename = datedFilename;
  }

  /**
   * <p>
   * rollover.
   */
  @Override
  protected void subAppend(LoggingEvent event) {
    long n = System.currentTimeMillis();

    if (n >= nextCheck) {
      now.setTime(n);
      nextCheck = rc.getNextCheckMillis(now);
      try {
        timeRollOver();
      } catch (IOException ioe) {
        LogLog.error("rollOver() failed.", ioe);
      }
    } else if ((fileName != null) && ((CountingQuietWriter) qw).getCount() >= maxFileSize) {
      sizeRollOver();
    }

    super.subAppend(event);

  }

  /**
   * 刪除超出指定數量的日誌
   */
  private void deleteObsoleteFiles() {
    if (maxTotalFile > 0) {
      File file = new File(fileName);
      File dir = file.getParentFile();

      File[] files = dir.listFiles((dir1, name) -> {
        String realFileName = file.getName();
        if (!name.startsWith(realFileName)) {
          return false;
        }
        if (suffixCompile.matcher(name).matches()) {
          name = name.substring(0, name.lastIndexOf("."));
        }
        String f = name.replaceFirst(realFileName, "");
        try {
          sdf.parse(f);
          return true;
        } catch (ParseException ignored) {
        }
        return false;
      });
      if (files == null || files.length == 0) {
        return;
      }
      //判斷是否超出最大文件數
      if (files.length > maxTotalFile) {
        List<File> tlt = Arrays.asList(files);
        ArrayList<File> list = new ArrayList<>(tlt);

        list.sort((o1, o2) -> (int) (o1.lastModified() - o2.lastModified()));
        Iterator<File> iterator = list.iterator();
        int i = 0;
        int size = list.size();
//        synchronized (this) {
          while (iterator.hasNext()) {
            File next = iterator.next();
            if (size - maxTotalFile > i) {
              LogLog.debug("delete old file :" + files[i].getName());
              next.delete();
              iterator.remove();
              i++;
            } else {
              return;
            }
//          }
        }
      }
    }
  }
}
RollingCalendar
package com.zgd.base.core.log;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;

/**
 * ClassName:RollingCalendar <br/>
 * Date:     2016年3月31日 上午11:41:34 <br/>
 * @author   lujie
 * @version
 * @see
 */
class RollingCalendar extends GregorianCalendar
{
    private static final long serialVersionUID = -3560331770601814177L;

    private int type = Log4JDateAndSizeAppender.TOP_OF_TROUBLE;

    RollingCalendar()
    {
        super();
    }

    RollingCalendar(TimeZone tz, Locale locale)
    {
        super(tz, locale);
    }

    void setType(int type)
    {
        this.type = type;
    }

    public long getNextCheckMillis(Date now)
    {
        return getNextCheckDate(now).getTime();
    }

    public Date getNextCheckDate(Date now)
    {
        this.setTime(now);

        switch (type)
        {
        case Log4JDateAndSizeAppender.TOP_OF_MINUTE:
            this.set(Calendar.SECOND, 0);
            this.set(Calendar.MILLISECOND, 0);
            this.add(Calendar.MINUTE, 1);
            break;
        case Log4JDateAndSizeAppender.TOP_OF_HOUR:
            this.set(Calendar.MINUTE, 0);
            this.set(Calendar.SECOND, 0);
            this.set(Calendar.MILLISECOND, 0);
            this.add(Calendar.HOUR_OF_DAY, 1);
            break;
        case Log4JDateAndSizeAppender.HALF_DAY:
            this.set(Calendar.MINUTE, 0);
            this.set(Calendar.SECOND, 0);
            this.set(Calendar.MILLISECOND, 0);
            int hour = get(Calendar.HOUR_OF_DAY);
            if (hour < 12)
            {
                this.set(Calendar.HOUR_OF_DAY, 12);
            }
            else
            {
                this.set(Calendar.HOUR_OF_DAY, 0);
                this.add(Calendar.DAY_OF_MONTH, 1);
            }
            break;
        case Log4JDateAndSizeAppender.TOP_OF_DAY:
            this.set(Calendar.HOUR_OF_DAY, 0);
            this.set(Calendar.MINUTE, 0);
            this.set(Calendar.SECOND, 0);
            this.set(Calendar.MILLISECOND, 0);
            this.add(Calendar.DATE, 1);
            break;
        case Log4JDateAndSizeAppender.TOP_OF_WEEK:
            this.set(Calendar.DAY_OF_WEEK, getFirstDayOfWeek());
            this.set(Calendar.HOUR_OF_DAY, 0);
            this.set(Calendar.MINUTE, 0);
            this.set(Calendar.SECOND, 0);
            this.set(Calendar.MILLISECOND, 0);
            this.add(Calendar.WEEK_OF_YEAR, 1);
            break;
        case Log4JDateAndSizeAppender.TOP_OF_MONTH:
            this.set(Calendar.DATE, 1);
            this.set(Calendar.HOUR_OF_DAY, 0);
            this.set(Calendar.MINUTE, 0);
            this.set(Calendar.SECOND, 0);
            this.set(Calendar.MILLISECOND, 0);
            this.add(Calendar.MONTH, 1);
            break;
        default:
            throw new IllegalStateException("Unknown periodicity type.");
        }
        return getTime();
    }

}
配置
log4j.appender.R=com.zgd.base.core.log.Log4JDateAndSizeAppender
log4j.appender.R.Threshold=INFO
log4j.appender.R.ImmediateFlush=true
log4j.appender.R.File=./logs/info/new.log
# 可以根據日期格式,設置日誌切分週期爲分、時、半日、日、周、月,半日的時間格式爲hh
# 中間時間格式要用單引號隔開,注意冒號:對於文件名是非法字符,不要使用
log4j.appender.R.DatePattern='-'yyyy-MM-dd-HHmm'.log'
log4j.appender.R.Append=true
log4j.appender.R.MaxFileSize=10KB
#MaxBackupIndex=-1(not limit about file number)
# 單個日期週期類,保留的日誌數量
log4j.appender.R.MaxBackupIndex=-1
# 總保留文件數量,-1表示不限
log4j.appender.R.maxTotalFile=15
log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern= ${property.pattern} 

log4j.appender.R是這個自定義appender類的全路徑名
log4j.appender.R.DatePattern屬性,跟DailyRollingFileAppender配置一樣,可以根據日期最小格式可以設置日誌切分週期爲分、時、半日、日、周、月,半日的時間格式爲yyyy-MM-dd-a,比如上面的配置就是指的根據來切分,看下圖在這裏插入圖片描述

log4j.appender.R.MaxBackupIndex是單個日期週期類,保留的日誌數量,-1無限制
log4j.appender.R.maxTotalFile是總保留文件,超過則刪除最早修改日期的日誌文件,-1無限制
log4j.appender.R.MaxFileSize是切分大小,可以是KB,MB,GB,默認是10MB

演示一下,

  @org.junit.Test
  public void fun01() throws InterruptedException {
    log.trace("............trace........");
    log.debug("............debug........");
    long l = System.currentTimeMillis();
    while (true){
      Thread.sleep(RandomUtils.nextInt(100,150));
      log.info("............info........");
      log.warn("............warn........");
      log.error("............error........");
      if (System.currentTimeMillis() - l > 60000){
        System.exit(0);
      }
    }
  }
}

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章