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 库针对该需要有两个问题:
- DailyRollingFileAppender 只能针对文件进行回滚,效果是在文件名后增加日期后缀,不能实现日期创建目录;
- 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、总结
本次修改的修改综合来说只有以下几步:
- appender 解析属性;
- appender 在配置active的函数中将属性交给policy;
- policy 负责策略,需要判断是否要进行 roll,并构造 roll 需要的action;
- action 最后执行,action有多种,rename 是其中一个;
由此可见,log4cxx 的框架层次分明,结构合理,便于修改和拓展,设计是相当优秀的,值得深入学习