目录
在上一篇我们介绍了 easylogging++的 异步日志 的实现。在结尾的时候,我们提到 easylogging++的默认实现不符合真实项目的需求,因此日志回滚的功能我们就需要根据实际的项目需求定制一下。今天我们就来看看如何定制日志回滚以满足真实项目的需求。
日志回滚接口
在 日志格式配置管理类 中我们介绍
TypedConfigurations
类的时候我们详细分析了 easylogging++提供的实现日志回滚的接口:bool TypedConfigurations::unsafeValidateFileRolling(Level level, const PreRollOutCallback &preRollOutCallback) { base::type::fstream_t *fs = unsafeGetConfigByRef(level, &m_fileStreamMap, "fileStream").get(); if (fs == nullptr) { return true; } std::size_t maxLogFileSize = unsafeGetConfigByVal(level, &m_maxLogFileSizeMap, "maxLogFileSize"); std::size_t currFileSize = base::utils::File::getSizeOfFile(fs); // 判断当前日志文件大小是否达到设定的阈值 if (maxLogFileSize != 0 && currFileSize >= maxLogFileSize) { std::string fname = unsafeGetConfigByRef(level, &m_filenameMap, "filename"); ELPP_INTERNAL_INFO(1, "Truncating log file [" << fname << "] as a result of configurations for level [" << LevelHelper::convertToString(level) << "]"); fs->close(); // 执行日志滚动回调 preRollOutCallback(fname.c_str(), currFileSize); fs->open(fname, std::fstream::out | std::fstream::trunc); return true; } return false; }
但这个接口即使我们实现了提供了日志回滚对应的回调函数
PreRollOutCallback
,也没办法满足日常对于日志滚动的需要。我们通过这个回调函数类型的定义就明白了:typedef std::function<void(const char*, std::size_t)> PreRollOutCallback;
PreRollOutCallback
是void(const char*, std::size_t)
这种函数签名的可调用对象,也就是说,可调用对象的参数只能是第一个为const char*
(文件名),第二个参数只能是std::size_t
(文件大小)。很显然,我们是没办法根据这两个参数获取日志文件名的配置项的值(只能通过日志级别来获取),然后动态生成符合日志文件名配置项格式的日志文件名。日常项目中的日志回滚一般是当日志文件达到一定的条件后,比如文件大小达到阈值或者超过了一定的时间,比如 24 小时,这时候我们会重新生成一个新的日志文件,原日志文件一般保持不变。
但上面这个接口的默认实现实现的效果却是清空需要当前的日志文件,显然不符合我们真实项目的需求。秉着对扩展开放对修改关闭的原则,我们尽量少改动源码。这里,我们只基于文件大小达到阈值(easylogging++的默认日志滚动条件)这个条件实现日志回滚。
扩展后的日志回滚接口
增加创建新文件的 LoggingFlag(CreateNewLogFile)
这里我们增加一个用于日志滚动时创建新文件的
LoggingFlag
(CreateNewLogFile
), 枚举类型LoggingFlag
增加一个值( 这样可以兼容库本身的代码 ):/// @brief Flags used while writing logs. This flags are set by user enum class LoggingFlag : base::type::EnumType { /// @brief Makes sure we have new line for each container log entry NewLineForContainer = 1, /// @brief Makes sure if -vmodule is used and does not specifies a module, then verbose /// logging is allowed via that module. AllowVerboseIfModuleNotSpecified = 2, /// @brief When handling crashes by default, detailed crash reason will be logged as well LogDetailedCrashReason = 4, /// @brief Allows to disable application abortion when logged using FATAL level DisableApplicationAbortOnFatalLog = 8, /// @brief Flushes log with every log-entry (performance sensitive) - Disabled by default ImmediateFlush = 16, /// @brief Enables strict file rolling StrictLogFileSizeCheck = 32, /// @brief Make terminal output colorful for supported terminals ColoredTerminalOutput = 64, /// @brief Supports use of multiple logging in same macro, e.g, CLOG(INFO, "default", "network") MultiLoggerSupport = 128, /// @brief Disables comparing performance tracker's checkpoints DisablePerformanceTrackingCheckpointComparison = 256, /// @brief Disable VModules DisableVModules = 512, /// @brief Disable VModules extensions DisableVModulesExtensions = 1024, /// @brief Enables hierarchical logging HierarchicalLogging = 2048, /// @brief Creates logger automatically when not available CreateLoggerAutomatically = 4096, /// @brief Adds spaces b/w logs that separated by left-shift operator AutoSpacing = 8192, /// @brief Preserves time format and does not convert it to sec, hour etc (performance tracking only) FixedTimeFormat = 16384, // @brief Ignore SIGINT or crash IgnoreSigInt = 32768, // @brief When file rolling, if the size of the log file reaches the threshold, a new log file is created CreateNewLogFile = 65536, };
改动后的实现
bool TypedConfigurations::unsafeValidateFileRolling(Level level, const PreRollOutCallback &preRollOutCallback) { base::type::fstream_t *fs = unsafeGetConfigByRef(level, &m_fileStreamMap, "fileStream").get(); if (fs == nullptr) { return true; } std::size_t maxLogFileSize = unsafeGetConfigByVal(level, &m_maxLogFileSizeMap, "maxLogFileSize"); std::size_t currFileSize = base::utils::File::getSizeOfFile(fs); if (maxLogFileSize != 0 && currFileSize >= maxLogFileSize) { std::string fname = unsafeGetConfigByRef(level, &m_filenameMap, "filename"); ELPP_INTERNAL_INFO(1, "Truncating log file [" << fname << "] as a result of configurations for level [" << LevelHelper::convertToString(level) << "]"); fs->close(); preRollOutCallback(fname.c_str(), currFileSize); // 日志滚动时创建新文件 if (ELPP->hasFlag(LoggingFlag::CreateNewLogFile)) { // 1、获取对应日志记录器的Configurations对象 Configurations *tempConfigurations = const_cast<Configurations *>(configurations()); if (!tempConfigurations) { ELPP_INTERNAL_ERROR("Configurations is NULL, please re-check your configurations for level[" << LevelHelper::convertToString(level) << "]", false); } // 2、获取对应日志记录器的对应日志级别的日志文件名配置项的字符串值(配置文件中的FILENAME配置项的值,比如FILENAME配置项为FILENAME="log/default-%datetime{%Y%M%d%H%m%s%g}.log"), // 那FilenameConfValue值为"log/default-%logger-%level-%datetime{%Y%M%d%H%m%s%g}.log" // 之所以这样做,是为了能灵活根据配置项动态调整生成的新的日志文件名称,不是写死的固定格式的日志文件名称,这样更通用 const std::string &FilenameConfValue = tempConfigurations->get(level, el::ConfigurationType::Filename)->value(); // 下面的do while循环是为了保证新生成的日志文件名和当前的日志文件名称不同。这里的不足是这里只能支持含时间格式(配置项中含有%datetime)的日志文件名称,比如"log/default-%datetime{%Y%M%d%H%m%s%g}.log" const std::string &FilenameConfValue = tempConfigurations->get(level, el::ConfigurationType::Filename)->value(); std::string resolvedFilename; do { // 调整文件名配置项值中的日期格式部分为实际当前时间,其中的"/"替换为"-" resolvedFilename = resolveFilename(FilenameConfValue); if (resolvedFilename.empty()) { std::cerr << "resolveFilename failed! resolvedFilename is empty! please re-check your configurations for level [" << LevelHelper::convertToString(level) << "]"; } } while (resolvedFilename.empty() || (fname == resolvedFilename)); fname = resolvedFilename; // 基于新生成的日志文件名重新打开,相当于对应日志记录器的当前日志级别新创建了一个日志文件,以后写日志文件都写到这个新创建的文件里面 fs->open(fname, std::fstream::out | std::fstream::trunc); if (fs->is_open()) { fs->flush(); // 日志文件名更新了,文件流对象还是同一个文件流对象,只不过打开的文件变了,这时候需要调整日志记录器对应的日志级别的日志文件相关的映射关系 // 这里的映射关系主要是有三个: // m_filenameMap:日志级别和日志文件名的映射关系(日志文件名改变了,需要调整) // m_logStreamsReference:日志文件名和文件流的映射关系(日志文件名改变了,需要调整) // m_fileStreamMap:日志级别和文件流的映射关系(文件流对象并未改变,不需要调整) setValue(level, fname, &m_filenameMap); m_logStreamsReference->insert(std::make_pair(fname, base::FileStreamPtr(m_fileStreamMap.at(level)))); } else { ELPP_INTERNAL_ERROR("Bad file [" << fname << "]", true); } } else { // 源码当中只是简单的清空日志文件后重新打开 fs->open(fname, std::fstream::out | std::fstream::trunc); } return true; } return false; }
实际使用日志滚动时,需要在程序入口处增加两个
LoggingFlag
:
1) 开启日志滚动的StrictLogFileSizeCheck
2) 日志滚动时,当文件大小达到阈值时创建新文件的CreateNewLogFile
程序入口如下:
#include "easylogging++.h" INITIALIZE_EASYLOGGINGPP int main(int argc, char *argv[]) { el::Loggers::addFlag(el::LoggingFlag::StrictLogFileSizeCheck); el::Loggers::addFlag(el::LoggingFlag::CreateNewLogFile); return 0; }
至此,日志滚动的修改就完成了。
到目前为止,我们已经将 easylogging++的主要功能的实现基本都分析完了。从下一篇开始,我们将前面未分析过的类或者接口一一介绍。