Sherlock的程序人生

log4cxx配置日期回滚策略中增加MaxFileSize属性

1、背景

C++ 项目,使用 log4cxx 日志库,需要按照日期生成日志目录,目录下存放日志文件,且日志文件不能过大,过大需要分片,例如,文件目录可以是:

.
|-- log
|---|-- 2022-10-02
|---|---|-- log.log
|---|---|-- log.log.1
|---|---|-- log.log.2
|---|---|-- log.log.3
|---|-- 2022-10-03
|---|---|-- log.log
|---|---|-- log.log.1
|---|---|-- log.log.2

log4cxx 库针对该需要有两个问题:

  1. DailyRollingFileAppender 只能针对文件进行回滚,效果是在文件名后增加日期后缀,不能实现日期创建目录;
  2. RollingFileAppender 支持文件过大分片(MaxFileSize 属性),但是 DailyRollingFileAppender 不支持;

针对问题一,已有解决方案,见文章:按照日期回滚不创建新目录的BUG

问题二,log4cxx 库确实不支持,只能自定义实现了

2、实现方式

先使用配置文件如下:

log4j.rootLogger=INFO,R

log4j.appender.R=org.apache.log4j.DailyRollingFileAppender
log4j.appender.R.ImmediateFlush=true
log4j.appender.R.Append=true
log4j.appender.R.MaxFileSize=1MB
log4j.appender.R.DatePattern='${LOG_HOMR_DIR}/${LOG_DIR}/'yyyy-MM-dd'/log.log'
log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern=%d{yyyy-MM-dd hh:mm:ss.SSS} %-5p [%c] %m %n

正常情况下,MaxFileSize 因为不是 DailyRollingFileAppender 的属性,所以不会生效,我们需要进行如下修改

2.1、DailyRollingFileAppender新增MaxFileSize属性

配置中的属性直接对应 appender 的属性,所以添加属性的最外层应该是 appender

DailyRollingFileAppender 类是配置文件中指定的 appender,所以属性先加在这里

代码文件 dailyrollingfileappender.h,增加属性和getter/setter

namespace log4cxx {
...

  class LOG4CXX_EXPORT DailyRollingFileAppender : public log4cxx::rolling::RollingFileAppenderSkeleton {
  DECLARE_LOG4CXX_OBJECT(DailyRollingFileAppender)
  BEGIN_LOG4CXX_CAST_MAP()
          LOG4CXX_CAST_ENTRY(DailyRollingFileAppender)
          LOG4CXX_CAST_ENTRY_CHAIN(FileAppender)
  END_LOG4CXX_CAST_MAP()

  /**
     The date pattern used to initiate rollover.
  */
  LogString datePattern;
  int maxFileSize;//新增属性,用于控制单个文件大小

public:
  DailyRollingFileAppender();

  //属性的getter/setter
  void setMaxFileSize( const LogString & value);
  long getMaxFileSize() const;

...
};
LOG4CXX_PTR_DEF(DailyRollingFileAppender);
}
...

dailyrollingfileappender.cpp 实现属性的获取和设置

DailyRollingFileAppender::DailyRollingFileAppender(
  const LayoutPtr& layout,
  const LogString& filename,
  const LogString& datePattern1)
  : datePattern(datePattern1), maxFileSize(0) {//构造函数进行maxFileSize属性初始化
    setLayout(layout);
    setFile(filename);
    Pool p;
    activateOptions(p);
}
//getter/setter
void DailyRollingFileAppender::setMaxFileSize( const LogString & value) {
    //这里调用log4cxx的接口,进行单位转换
    maxFileSize = OptionConverter::toFileSize(value, maxFileSize + 1);
}
long DailyRollingFileAppender::getMaxFileSize() const {
    return maxFileSize;
}
//activateOptions方法作用是激活配置,即将配置塞到policy中
void DailyRollingFileAppender::activateOptions(log4cxx::helpers::Pool& pool) {
  TimeBasedRollingPolicyPtr policy = new TimeBasedRollingPolicy();
  LogString pattern(getFile());
  bool inLiteral = false;
  bool inPattern = false;

  for (size_t i = 0; i < datePattern.length(); i++) {
    if (datePattern[i] == 0x27 /* '\'' */) {
      inLiteral = !inLiteral;

      if (inLiteral && inPattern) {
        pattern.append(1, (logchar) 0x7D /* '}' */);
        inPattern = false;
      }
    } else {
      if (!inLiteral && !inPattern) {
        const logchar dbrace[] = { 0x25, 0x64, 0x7B, 0 }; // "%d{"
        pattern.append(dbrace);
        inPattern = true;
      }

      pattern.append(1, datePattern[i]);
    }
  }

  if (inPattern) {
    pattern.append(1, (logchar) 0x7D /* '}' */);
  }

  policy->setFileNamePattern(pattern);
  policy->setMaxFileSize(maxFileSize);//新增,将maxFileSize属性塞给policy
  policy->activateOptions(pool);
  setTriggeringPolicy(policy);
  setRollingPolicy(policy);

  RollingFileAppenderSkeleton::activateOptions(pool);
}

2.2、TimeBasedRollingPolicy策略新增maxFileSize的判断

appender 把 maxFileSize 属性塞给了 policy,那么 policy 需要新增对应接口来接收,并做策略的判断

同时 policy 只是策略,roll 的具体操作在 action 中,policy 根据需要创建 action,然后由 action 来执行,具体见下面代码

DailyRollingFileAppender 只使用了 TimeBasedRollingPolicy ,所以我们只需要看 TimeBasedRollingPolicy 的代码

timebasedrollingpolicy.h 修改如下:

namespace log4cxx {
    namespace rolling {
        class LOG4CXX_EXPORT TimeBasedRollingPolicy : public RollingPolicyBase,
             public TriggeringPolicy {
          DECLARE_LOG4CXX_OBJECT(TimeBasedRollingPolicy)
          BEGIN_LOG4CXX_CAST_MAP()
                  LOG4CXX_CAST_ENTRY(TimeBasedRollingPolicy)
                  LOG4CXX_CAST_ENTRY_CHAIN(RollingPolicyBase)
                  LOG4CXX_CAST_ENTRY_CHAIN(TriggeringPolicy)
          END_LOG4CXX_CAST_MAP()

        private:
            //新增以下三个属性
        	size_t maxFileSize;//单个文件的大小上限
        	bool fileSizeChanged;//当前记录的文件是否已经超出上限
        	int backupIndex;//rollover的index,即需要给备份文件增加的后缀
        public:
            TimeBasedRollingPolicy();

			//maxFileSize属性的getter/setter
        	void setMaxFileSize(size_t size);
        	size_t getMaxFileSize() const;
        };
        LOG4CXX_PTR_DEF(TimeBasedRollingPolicy);
    }
}

timebasedrollingpolicy.cpp 修改如下:

构造函数进行属性初始化,index从1开始,maxFileSize为0表示没有大小限制

TimeBasedRollingPolicy::TimeBasedRollingPolicy()
    : maxFileSize(0), fileSizeChanged(false), backupIndex(1) {
}

新增 maxFileSize 属性的 getter/setter

void TimeBasedRollingPolicy::setMaxFileSize(size_t size) {
    maxFileSize = size;
}

size_t TimeBasedRollingPolicy::getMaxFileSize() const {
    return maxFileSize;
}

isTriggeringEvent() 方法用于判断是否需要进行 rollover,修改如下:

lastFileName 变量存放备份的日志的文件的绝对路径,不需要备份,则它应该和当前记录的日志文件一直,若我们需要备份,则该变量就是备份的文件

这里先检查日期是否变化,如果日志变化了,则无需判断文件大小

bool TimeBasedRollingPolicy::isTriggeringEvent(
  Appender* /* appender */,
  const log4cxx::spi::LoggingEventPtr& /* event */,
  const LogString& /* filename */,
  size_t fileLength)  {
    if (apr_time_now() > nextCheck) {
        return true;
    }
    //maxFileSize <= 0 则不检查大小,当前日志文件超出限额,则需要rollover
    if (maxFileSize > 0 && fileLength >= maxFileSize) {
        fileSizeChanged = true;//置true,表示需要分片
        //备份的文件增加后缀index,同时index加一
        lastFileName = lastFileName + "." + std::to_string(backupIndex++);
        return true;
    }
    return false;
}

rollover() 方法是日志回滚的具体实现,修改如下:

RolloverDescriptionPtr TimeBasedRollingPolicy::rollover(
   const LogString& currentActiveFile,
   Pool& pool) {
  apr_time_t n = apr_time_now();
  nextCheck = ((n / APR_USEC_PER_SEC) + 1) * APR_USEC_PER_SEC;

  LogString buf;
  ObjectPtr obj(new Date(n));
  formatFileName(obj, buf, pool);

  LogString newFileName(buf);

  //
  //  if file names haven't changed, no rollover
  //
  //新增fileSizeChanged的判断,日志变化或者文件超限都需要执行rollover
  if (newFileName == lastFileName && !fileSizeChanged) {
      RolloverDescriptionPtr desc;
      return desc;
  }
  if (fileSizeChanged) {
      //如果是文件超限,表示不是日期变化的roll,重置flag
      fileSizeChanged = false;
  } else {
    //日期变化,则备份的文件后缀index重新从1开始
    backupIndex = 1;
  }

  ActionPtr renameAction;
  ActionPtr compressAction;
  LogString lastBaseName(
    lastFileName.substr(0, lastFileName.length() - suffixLength));
  LogString nextActiveFile(
    newFileName.substr(0, newFileName.length() - suffixLength));

  //
  //   if currentActiveFile is not lastBaseName then
  //        active file name is not following file pattern
  //        and requires a rename plus maintaining the same name
  if (currentActiveFile != lastBaseName) {
    //创建rename action,用于将当前的日志文件重命名,实现日志的备份
    renameAction =
      new FileRenameAction(
        File().setPath(currentActiveFile), File().setPath(lastBaseName), true);
    nextActiveFile = currentActiveFile;
  }

  if (suffixLength == 3) {
    compressAction =
      new GZCompressAction(
        File().setPath(lastBaseName), File().setPath(lastFileName), true);
  }

  if (suffixLength == 4) {
    compressAction =
      new ZipCompressAction(
        File().setPath(lastBaseName), File().setPath(lastFileName), true);
  }

  lastFileName = newFileName;

  return new RolloverDescription(
    nextActiveFile, false, renameAction, compressAction);
}

这里我们只需要创建 FileRenameAction,告诉他需要 rename 的文件和新的文件即可

3、总结

本次修改的修改综合来说只有以下几步:

  1. appender 解析属性;
  2. appender 在配置active的函数中将属性交给policy;
  3. policy 负责策略,需要判断是否要进行 roll,并构造 roll 需要的action;
  4. action 最后执行,action有多种,rename 是其中一个;

由此可见,log4cxx 的框架层次分明,结构合理,便于修改和拓展,设计是相当优秀的,值得深入学习

posted @ 2023-02-08 21:15  sherlock_lin  阅读(447)  评论(0编辑  收藏  举报