easylogging++的那些事(四)源码分析(四)日志格式配置管理类

在上一篇我们分析了 logger 类的 类 printf 接口,今天我们开始介绍日志格式的配置管理类。

easylogging++对于日志格式的管理主要是通过两个类来实现的: Configurations 类和 TypedConfigurations 类。
在 easylogging++的 总体设计 中简单介绍过这两个类的用途:

Configurations 类主要用于以字符串形式保存配置项的值 。每个 Configurations 类保存了单个 Logger 实例对应各个日志级别的配置项的字符串形式的值, 比如是否启用配置项是字符串形式的 "true" 或者 "false"

TypedConfigurations 类主要用于以配置项在程序运行时的实际类型保存配置项的值 。每个 TypedConfigurations 类保存了单个 Logger 实例对应各个日志级别的配置项的程序运行时的实际的类型的值,比如是否启用配置项是 bool 型的 true 或者 false

Configurations 类

    先说下 Configurations 类这个类的继承关系:
    Configurations 类继承自 base::utils::RegistryWithPred <Configuration, Configuration::Predicate>
    底层容器为 std::vector <Configuration*>,其中 Configuration 是指定日志级别(el::Level)的指定配置项(el::ConfigurationType)的工具类,后面会详细介绍。
    从底层容器可以看出,Configurations 是用来管理 Configuration 的。

    是否线程安全: 线程安全。

    迭代器类型说明:

//实际iterator就是std::vector<Configuration*>::iterator
typedef typename RegistryWithPred<Configuration, Configuration::Predicate>::iterator iterator; 
//实际const_iterator就是std::vector<Configuration*>::const_iterator 
typedef typename RegistryWithPred<Configuration, Configuration::Predicate>::const_iterator const_iterator; 

    base::utils::RegistryWithPred 会在后面介绍 easylogging++的设计理念时专门进行介绍。

成员变量

bool m_isFromFile;               // 当从文件中成功解析时为true,其他为false
std::string m_configurationFile; // 配置文件的名称

成员函数

析构函数

virtual ~Configurations(void)
{
}

默认构造函数

/// @brief Default constructor with empty repository
Configurations::Configurations(void) : m_configurationFile(std::string()), m_isFromFile(false)
{
}

从配置文件中加载配置

/// @brief Constructor used to set configurations using configuration file.
/// @param configurationFile Full path to configuration file
/// @param useDefaultsForRemaining Lets you set the remaining configurations to default.
/// @param base If provided, this configuration will be based off existing repository that this argument is > pointing to.
/// @see parseFromFile(const std::string&, Configurations* base)
/// @see setRemainingToDefault()
Configurations::Configurations(const std::string &configurationFile, bool useDefaultsForRemaining, Configurations *base)
    : m_configurationFile(configurationFile), m_isFromFile(false)
{
    parseFromFile(configurationFile, base);
    if (useDefaultsForRemaining)
    {
        // 当部分配置项不存在时,设置对应配置项的默认值
        setRemainingToDefault();
    }
}

解析配置文件

/// @brief Parses configuration from file.
/// @param configurationFile Full path to configuration file
/// @param base Configurations to base new configuration repository off. This value is used when you want to use
///        existing Configurations to base all the values and then set rest of configuration via configuration file.
/// @return True if successfully parsed, false otherwise. You may define 'ELPP_DEBUG_ASSERT_FAILURE' to make sure you
///         do not proceed without successful parse.
bool Configurations::parseFromFile(const std::string &configurationFile, Configurations *base = nullptr)
{
    // We initial assertion with true because if we have assertion disabled, we want to pass this
    // check and if assertion is enabled we will have values re-assigned any way.
    bool assertionPassed = true;
    ELPP_ASSERT((assertionPassed = base::utils::File::pathExists(configurationFile.c_str(), true)) == true,
                "Configuration file [" << configurationFile << "] does not exist!");
    if (!assertionPassed)
    {
        return false;
    }
    bool success = Parser::parseFromFile(configurationFile, this, base);
    m_isFromFile = success;
    return success;
}

    实际的解析动作委托给了 Parser::parseFromFile 接口完成。Parser::parseFromFile 接口在后面会进行详细介绍,这里就不多说了。

解析字符串形式的配置

/// @brief Parse configurations from configuration string.
///
/// @detail This configuration string has same syntax as configuration file contents. Make sure all the > necessary
/// new line characters are provided.
/// @param base Configurations to base new configuration repository off. This value is used when you want to > use
///        existing Configurations to base all the values and then set rest of configuration via configuration > text.
/// @return True if successfully parsed, false otherwise. You may define 'ELPP_DEBUG_ASSERT_FAILURE' to make > sure you
///         do not proceed without successful parse.
bool Configurations::parseFromText(const std::string &configurationsString, Configurations *base = nullptr)
{
    bool success = Parser::parseFromText(configurationsString, this, base);
    if (success)
    {
        m_isFromFile = false;
    }
    return success;
}

    实际的解析动作委托给了 Parser::parseFromText 接口完成。Parser::parseFromText 接口在后面会进行详细介绍,这里就不多说了。

设置指定日志级别的指定配置项

/// @brief Sets value of configuration for specified level.
///
/// @detail Any existing configuration for specified level will be replaced. Also note that configuration types
/// ConfigurationType::SubsecondPrecision and ConfigurationType::PerformanceTracking will be ignored if not set for
/// Level::Global because these configurations are not dependant on level.
/// @param level Level to set configuration for (el::Level).
/// @param configurationType Type of configuration (el::ConfigurationType)
/// @param value A string based value. Regardless of what the data type of configuration is, it will always be string
/// from users' point of view. This is then parsed later to be used internally.
/// @see Configuration::setValue(const std::string& value)
/// @see el::Level
/// @see el::ConfigurationType
void Configurations::set(Level level, ConfigurationType configurationType, const std::string &value)
{
    base::threading::ScopedLock scopedLock(lock());
    unsafeSet(level, configurationType, value); // This is not unsafe anymore as we have locked mutex
    if (level == Level::Global)
    {
        unsafeSetGlobally(configurationType, value, false); // Again this is not unsafe either
    }
}

    unsafeSetGlobally 接口后面会进行详细介绍。

通过 Configuration 对象来设置指定日志级别的指定配置项

/// @brief Sets single configuration based on other single configuration.
/// @see set(Level level, ConfigurationType configurationType, const std::string& value)
void Configurations::set(Configuration *conf)
{
    if (conf == nullptr)
    {
        return;
    }
    // 设置指定日志级别的指定配置项
    set(conf->level(), conf->configurationType(), conf->value());
}

设置基准配置

/// @brief Sets configuration based-off an existing configurations.
/// @param base Pointer to existing configurations.
void Configurations::setFromBase(Configurations *base)
{
    if (base == nullptr || base == this)
    {
        return;
    }
    base::threading::ScopedLock scopedLock(base->lock());
    for (Configuration *&conf : base->list())
    {
        // 通过Configuration对象来设置指定日志级别的指定配置项
        set(conf);
    }
}

设置除 Global 级别外的所有配置项

    实际写日志时并没有 Global 这样一个级别,这里实际就是设置了所有的日志级别。

/// @brief Sets configuration for all levels.
/// @param configurationType Type of configuration
/// @param value String based value
/// @see Configurations::set(Level level, ConfigurationType configurationType, const std::string& value)
inline void setGlobally(ConfigurationType configurationType, const std::string &value)
{
    setGlobally(configurationType, value, false);
}

/// @brief Sets configurations for all levels including Level::Global if includeGlobalLevel is true
/// @see Configurations::setGlobally(ConfigurationType configurationType, const std::string& value)
void Configurations::setGlobally(ConfigurationType configurationType, const std::string &value, bool includeGlobalLevel)
{
    if (includeGlobalLevel)
    {
        set(Level::Global, configurationType, value);
    }
    base::type::EnumType lIndex = LevelHelper::kMinValid;
    LevelHelper::forEachLevel(&lIndex, [&](void) -> bool
                              {
                                  set(LevelHelper::castFromInt(lIndex), configurationType, value);
                                  return false; // Do not break lambda function yet as we need to set all levels regardless
                              });
}

    LevelHelper::forEachLevel 接口在 CLOG其他相关类 中已经介绍过了:从指定日志级别开始遍历,对于每个级别执行一些操作(fn)

设置默认的配置信息

/// @brief Sets configurations to "factory based" configurations.
void Configurations::setToDefault(void)
{
    setGlobally(ConfigurationType::Enabled, std::string("true"), true);
    setGlobally(ConfigurationType::Filename, std::string(base::consts::kDefaultLogFile), true);
    // 是否写日志到文件
#if defined(ELPP_NO_LOG_TO_FILE)
    setGlobally(ConfigurationType::ToFile, std::string("false"), true);
#else
    setGlobally(ConfigurationType::ToFile, std::string("true"), true);
#endif // defined(ELPP_NO_LOG_TO_FILE)
    setGlobally(ConfigurationType::ToStandardOutput, std::string("true"), true);
    setGlobally(ConfigurationType::SubsecondPrecision, std::string("3"), true);
    setGlobally(ConfigurationType::PerformanceTracking, std::string("true"), true);
    setGlobally(ConfigurationType::MaxLogFileSize, std::string("0"), true);
    setGlobally(ConfigurationType::LogFlushThreshold, std::string("0"), true);

    setGlobally(ConfigurationType::Format, std::string("%datetime %level [%logger] %msg"), true);
    set(Level::Debug, ConfigurationType::Format,
        std::string("%datetime %level [%logger] [%user@%host] [%func] [%loc] %msg"));
    // INFO and WARNING are set to default by Level::Global
    set(Level::Error, ConfigurationType::Format, std::string("%datetime %level [%logger] %msg"));
    set(Level::Fatal, ConfigurationType::Format, std::string("%datetime %level [%logger] %msg"));
    set(Level::Verbose, ConfigurationType::Format, std::string("%datetime %level-%vlevel [%logger] %msg"));
    set(Level::Trace, ConfigurationType::Format, std::string("%datetime %level [%logger] [%func] [%loc] %msg"));
}

当部分配置项不存在时,设置对应配置项的默认值

/// @brief Lets you set the remaining configurations to default.
///
/// @detail By remaining, it means that the level/type a configuration does not exist for.
/// This function is useful when you want to minimize chances of failures, e.g, if you have a configuration file that sets
/// configuration for all the configurations except for Enabled or not, we use this so that ENABLED is set to default i.e,
/// true. If you dont do this explicitly (either by calling this function or by using second param in Constructor
/// and try to access a value, an error is thrown
void Configurations::setRemainingToDefault(void)
{
    base::threading::ScopedLock scopedLock(lock());
    // 是否写日志到文件
#if defined(ELPP_NO_LOG_TO_FILE)
    unsafeSetIfNotExist(Level::Global, ConfigurationType::Enabled, std::string("false"));
#else
    unsafeSetIfNotExist(Level::Global, ConfigurationType::Enabled, std::string("true"));
#endif // defined(ELPP_NO_LOG_TO_FILE)
    unsafeSetIfNotExist(Level::Global, ConfigurationType::Filename, std::string(base::consts::kDefaultLogFile));
    unsafeSetIfNotExist(Level::Global, ConfigurationType::ToStandardOutput, std::string("true"));
    unsafeSetIfNotExist(Level::Global, ConfigurationType::SubsecondPrecision, std::string("3"));
    unsafeSetIfNotExist(Level::Global, ConfigurationType::PerformanceTracking, std::string("true"));
    unsafeSetIfNotExist(Level::Global, ConfigurationType::MaxLogFileSize, std::string("0"));
    unsafeSetIfNotExist(Level::Global, ConfigurationType::Format, std::string("%datetime %level [%logger] %msg"));
    unsafeSetIfNotExist(Level::Debug, ConfigurationType::Format, std::string("%datetime %level [%logger] [%user@%host] [%func] [%loc] %msg"));
    // INFO and WARNING are set to default by Level::Global
    unsafeSetIfNotExist(Level::Error, ConfigurationType::Format, std::string("%datetime %level [%logger] %msg"));
    unsafeSetIfNotExist(Level::Fatal, ConfigurationType::Format, std::string("%datetime %level [%logger] %msg"));
    unsafeSetIfNotExist(Level::Verbose, ConfigurationType::Format, std::string("%datetime %level-%vlevel [%logger] %msg"));
    unsafeSetIfNotExist(Level::Trace, ConfigurationType::Format, std::string("%datetime %level [%logger] [%func] [%loc] %msg"));
}

当指定日志级别的配置项不存在时,设置配置项

/// @brief Unsafely sets configuration if does not already exist
void Configurations::unsafeSetIfNotExist(Level level, ConfigurationType configurationType, const std::string &value)
{
    Configuration *conf = RegistryWithPred<Configuration, Configuration::Predicate>::get(level, configurationType);
    if (conf == nullptr)
    {
        unsafeSet(level, configurationType, value);
    }
}

/// @brief Thread unsafe set
void Configurations::unsafeSet(Level level, ConfigurationType configurationType, const std::string &value)
{
    Configuration *conf = RegistryWithPred<Configuration, Configuration::Predicate>::get(level, configurationType);
    if (conf == nullptr)
    {
        // 不存在则像Configurations实例中添加新的配置项
        registerNew(new Configuration(level, configurationType, value));
    }
    else
    {
        // 存在则更新配置项的值
        conf->setValue(value);
    }
    if (level == Level::Global)
    {
        // 值为Level::Global时,设置其他所有的日志级别
        unsafeSetGlobally(configurationType, value, false);
    }
}

/// @brief Sets configurations (Unsafely) for all levels including Level::Global if includeGlobalLevel is true
/// @see Configurations::setGlobally(ConfigurationType configurationType, const std::string& value)
void Configurations::unsafeSetGlobally(ConfigurationType configurationType, const std::string &value, bool includeGlobalLevel)
{
    if (includeGlobalLevel)
    {
        unsafeSet(Level::Global, configurationType, value);
    }
    base::type::EnumType lIndex = LevelHelper::kMinValid;
    LevelHelper::forEachLevel(&lIndex, [&](void) -> bool
                              {
                                  unsafeSet(LevelHelper::castFromInt(lIndex), configurationType, value);
                                  return false; // Do not break lambda function yet as we need to set all levels regardless
                              });
}

    LevelHelper::forEachLevel 接口在 CLOG其他相关类 中已经介绍过了:从指定日志级别开始遍历,对于每个级别执行一些操作(fn)

清除所有配置项

/// @brief Clears repository so that all the configurations are unset
inline void clear(void)
{
    base::threading::ScopedLock scopedLock(lock());
    unregisterAll();
}

    unregisterAll 接口在后面文章中会进行详细说明。

查询是否含有指定的配置项

    查询到第一个这样的配置项时停止查询(不同日志级别可以设置同样的配置项)

/// @brief Determines whether or not specified configuration type exists in the repository.
///
/// @detail Returns as soon as first level is found.
/// @param configurationType Type of configuration to check existence for.
bool Configurations::hasConfiguration(ConfigurationType configurationType)
{
    base::type::EnumType lIndex = LevelHelper::kMinValid;
    bool result = false;
    LevelHelper::forEachLevel(&lIndex, [&](void) -> bool
                              {
    if (hasConfiguration(LevelHelper::castFromInt(lIndex), configurationType)) {
      result = true;
    }
    return result; });
    return result;
}

    LevelHelper::forEachLevel 接口在 CLOG其他相关类 中已经介绍过了:从指定日志级别开始遍历,对于每个级别执行一些操作(fn)

查询是否存在指定日志级别的指定配置项

/// @brief Determines whether or not specified configuration type exists for specified level
/// @param level Level to check
/// @param configurationType Type of configuration to check existence for.
bool Configurations::hasConfiguration(Level level, ConfigurationType configurationType)
{
    base::threading::ScopedLock scopedLock(lock());
#if ELPP_COMPILER_INTEL
    // We cant specify template types here, Intel C++ throws compilation error
    // "error: type name is not allowed"
    return RegistryWithPred::get(level, configurationType) != nullptr;
#else
    return RegistryWithPred<Configuration, Configuration::Predicate>::get(level, configurationType) != nullptr;
#endif // ELPP_COMPILER_INTEL
}

获取指定日志级别的指定配置项

inline Configuration *get(Level level, ConfigurationType configurationType)
{
    base::threading::ScopedLock scopedLock(lock());
    return RegistryWithPred<Configuration, Configuration::Predicate>::get(level, configurationType);
}

获取配置文件的名称

/// @brief Gets configuration file used in parsing this configurations.
///
/// @detail If this repository was set manually or by text this returns empty string.
inline const std::string &configurationFile(void) const
{
    return m_configurationFile;
}

    上面 unsafeSetsetGloballyunsafeSetGlobally 这三个接口都是用来设置配置项,其中代码有些重复,不要被名称误导,最终是否线程安全,要看接口实现是否加锁了

内部类 Configurations::Parser

    真正执行解析配置的工具类

从配置文件解析配置

/// @brief Parses configuration from file.
/// @param configurationFile Full path to configuration file
/// @param sender Sender configurations pointer. Usually 'this' is used from calling class
/// @param base Configurations to base new configuration repository off. This value is used when you want to use
///        existing Configurations to base all the values and then set rest of configuration via configuration file.
/// @return True if successfully parsed, false otherwise. You may define '_STOP_ON_FIRSTELPP_ASSERTION' to make sure you
///         do not proceed without successful parse.
bool Configurations::Parser::parseFromFile(const std::string &configurationFile, Configurations *sender, Configurations *base = nullptr)
{
    //设置基准配置
    sender->setFromBase(base);
    //打开配置文件
    std::ifstream fileStream_(configurationFile.c_str(), std::ifstream::in);
    ELPP_ASSERT(fileStream_.is_open(), "Unable to open configuration file [" << configurationFile << "] for parsing.");
    bool parsedSuccessfully = false;
    //一行配置
    std::string line = std::string();
    Level currLevel = Level::Unknown;
    std::string currConfigStr = std::string();
    std::string currLevelStr = std::string();
    while (fileStream_.good())
    {
        //读取一行配置
        std::getline(fileStream_, line);
        //解析一行配置
        parsedSuccessfully = parseLine(&line, &currConfigStr, &currLevelStr, &currLevel, sender);
        ELPP_ASSERT(parsedSuccessfully, "Unable to parse configuration line: " << line);
    }
    return parsedSuccessfully;
}

从配置字符串中解析配置

/// @brief Parse configurations from configuration string.
///
/// @detail This configuration string has same syntax as configuration file contents. Make sure all the necessary
/// new line characters are provided. You may define '_STOP_ON_FIRSTELPP_ASSERTION' to make sure you
/// do not proceed without successful parse (This is recommended)
/// @param configurationsString the configuration in plain text format
/// @param sender Sender configurations pointer. Usually 'this' is used from calling class
/// @param base Configurations to base new configuration repository off. This value is used when you want to use
///        existing Configurations to base all the values and then set rest of configuration via configuration text.
/// @return True if successfully parsed, false otherwise.
bool Configurations::Parser::parseFromText(const std::string &configurationsString, Configurations *sender, Configurations *base = nullptr)
{
    //设置基准配置
    sender->setFromBase(base);
    bool parsedSuccessfully = false;
    //通过配置字符串来初始化流对象
    std::stringstream ss(configurationsString);
    std::string line = std::string();
    Level currLevel = Level::Unknown;
    std::string currConfigStr = std::string();
    std::string currLevelStr = std::string();
    //每次读取一行
    while (std::getline(ss, line))
    {
        //解析一行
        parsedSuccessfully = parseLine(&line, &currConfigStr, &currLevelStr, &currLevel, sender);
        ELPP_ASSERT(parsedSuccessfully, "Unable to parse configuration line: " << line);
    }
    return parsedSuccessfully;
}

忽略一行配置当中的注释

static const char *kConfigurationComment = "##";

void Configurations::Parser::ignoreComments(std::string *line)
{
    std::size_t foundAt = 0;
    // 查找第一个双引号
    std::size_t quotesStart = line->find("\"");
    std::size_t quotesEnd = std::string::npos;
    if (quotesStart != std::string::npos)
    {
        // 查找第二个双引号
        quotesEnd = line->find("\"", quotesStart + 1);
        while (quotesEnd != std::string::npos && line->at(quotesEnd - 1) == '\\')
        {
            // 第二个双引号前面是'\',则继续查找,这里感觉有点怪异,应该是直接跳过第二个双引号就行了,为啥还要多跳过一个字符呢?
            //  Do not erase slash yet - we will erase it in parseLine(..) while loop
            quotesEnd = line->find("\"", quotesEnd + 2);
        }
    }
    // 上面循环结束时,quotesEnd等于std::string::npos或者是指向前面不带'\'的双引号(配置项值得结束的位置的那个双引号)
    //  查找注释标志(##)
    if ((foundAt = line->find(base::consts::kConfigurationComment)) != std::string::npos)
    {
        // 找到的第一个注释标志是在配置项值内部,则需要从配置项值得结束的位置后面的部分开始查找注释标志
        if (foundAt < quotesEnd)
        {
            foundAt = line->find(base::consts::kConfigurationComment, quotesEnd + 1);
        }
        // 去掉注释
        *line = line->substr(0, foundAt);
    }
}

判断当前这一行配置是否是日志级别项

static const char *kConfigurationLevel = "*";

bool Configurations::Parser::isLevel(const std::string &line)
{
    // 当前行是否以*开头
    return base::utils::Str::startsWith(line, std::string(base::consts::kConfigurationLevel));
}

判断当前这一行配置是否是注释

static const char *kConfigurationComment = "##";

bool Configurations::Parser::isComment(const std::string &line)
{
    // 当前行是否以##开头
    return base::utils::Str::startsWith(line, std::string(base::consts::kConfigurationComment));
}

判断当前这一行配置是否是配置项

// 当前行里面是否有'=',配置项如:"TO_FILE =  true ##" 
bool Configurations::Parser::isConfig(const std::string &line)
{
    std::size_t assignment = line.find('=');
    return line != "" &&
           ((line[0] >= 'A' && line[0] <= 'Z') || (line[0] >= 'a' && line[0] <= 'z')) &&
           (assignment != std::string::npos) &&
           (line.size() > assignment);
}

解析配置文件或者配置字符串当中的一行

bool Configurations::Parser::parseLine(std::string *line, std::string *currConfigStr, std::string *currLevelStr, Level *currLevel, Configurations *conf)
{
    ConfigurationType currConfig = ConfigurationType::Unknown;
    std::string currValue = std::string();
    // 去掉当前行头尾的空白字符
    *line = base::utils::Str::trim(*line);
    // 全是注释直接返回
    if (isComment(*line))
        return true;
    // 去掉行当中的注释
    ignoreComments(line);
    // 再次去掉行当中头尾的空白字符
    *line = base::utils::Str::trim(*line);
    // 无有效内容直接返回
    if (line->empty())
    {
        // Comment ignored
        return true;
    }
    // 是否日志级别项,	日志级别项例子:"*GLOBAL:"
    if (isLevel(*line))
    {
        if (line->size() <= 2)
        {
            // 长度不够,无效直接返回
            return true;
        }
        // 跳过开头的'*',长度去掉开头的'*'和结尾的':'
        *currLevelStr = line->substr(1, line->size() - 2);
        *currLevelStr = base::utils::Str::toUpper(*currLevelStr);
        *currLevelStr = base::utils::Str::trim(*currLevelStr);
        // 字符串形式的日志级别项值转化为Level类型的值
        *currLevel = LevelHelper::convertFromString(currLevelStr->c_str());
        return true;
    }
    // 是否配置项如:"TO_FILE =  true"
    if (isConfig(*line))
    {
        // 找到'='
        std::size_t assignment = line->find('=');
        // 获取配置项名称('='之前的部分)
        *currConfigStr = line->substr(0, assignment);
        *currConfigStr = base::utils::Str::toUpper(*currConfigStr);
        // 配置项名称去掉头尾的空白字符
        *currConfigStr = base::utils::Str::trim(*currConfigStr);
        // 将字符串形式的配置项名称转化为ConfigurationType类型的值
        currConfig = ConfigurationTypeHelper::convertFromString(currConfigStr->c_str());
        // 获取配置项值('='后面的部分)
        currValue = line->substr(assignment + 1);
        // 配置项值去掉头尾的空白字符
        currValue = base::utils::Str::trim(currValue);
        //检查是否字符串形式的值(以双引号开头)
        std::size_t quotesStart = currValue.find("\"", 0);
        std::size_t quotesEnd = std::string::npos;
        if (quotesStart != std::string::npos)
        {
            //是字符串形式的值则查找结束的双引号
            quotesEnd = currValue.find("\"", quotesStart + 1);
            //双引号前面是'\'
            while (quotesEnd != std::string::npos && currValue.at(quotesEnd - 1) == '\\')
            {
                //移除双引号前面的'\'
                currValue = currValue.erase(quotesEnd - 1, 1);
                //查找下一个双引号,这里其实不必改变quotesEnd的值,因为上面已经将双引号前面的'\'给去掉了,这样'\'后面的字符都会往前面移动一个位置,现在的quotesEnd实际已经指向的是双引号后面的字符了。
                quotesEnd = currValue.find("\"", quotesEnd + 2);
            }
        }
        if (quotesStart != std::string::npos && quotesEnd != std::string::npos)
        {
            // Quote provided - check and strip if valid
            ELPP_ASSERT((quotesStart < quotesEnd), "Configuration error - No ending quote found in ["
                                                       << currConfigStr << "]");
            ELPP_ASSERT((quotesStart + 1 != quotesEnd), "Empty configuration value for [" << currConfigStr << "]");
            if ((quotesStart != quotesEnd) && (quotesStart + 1 != quotesEnd))
            {
                // Explicit check in case if assertion is disabled
                //去掉字符串形式的值两边的双引号,这里其实不算严谨,有可能双引号里面的内容两边还有空白字符,更好的做法是在做一次去除空白字符的动作
                currValue = currValue.substr(quotesStart + 1, quotesEnd - 1);
            }
        }
    }
    ELPP_ASSERT(*currLevel != Level::Unknown, "Unrecognized severity level [" << *currLevelStr << "]");
    ELPP_ASSERT(currConfig != ConfigurationType::Unknown, "Unrecognized configuration [" << *currConfigStr << "]");
    if (*currLevel == Level::Unknown || currConfig == ConfigurationType::Unknown)
    {
        return false; // unrecognizable level or config
    }
    conf->set(*currLevel, currConfig, currValue);
    return true;
}

TypedConfigurations 类

    是否线程安全: 线程安全。

成员变量

Configurations *m_configurations;                                            // TypedConfigurations对应的Configurations类实例
std::unordered_map<Level, bool> m_enabledMap;                                // 记录器日志级别和其是否启用配置项映射关系
std::unordered_map<Level, bool> m_toFileMap;                                 // 记录器日志级别和其是否写文件配置项映射容器
std::unordered_map<Level, std::string> m_filenameMap;                        // 记录器日志级别和其日志文件名配置项映射关系
std::unordered_map<Level, bool> m_toStandardOutputMap;                       // 记录器日志级别和其是否输出终端配置项的映射关系
std::unordered_map<Level, base::LogFormat> m_logFormatMap;                   // 记录器日志级别和其是否启用配置项的映射关系
std::unordered_map<Level, base::SubsecondPrecision> m_subsecondPrecisionMap; // 记录器日志级别和其子秒精度配置项的映射关系
std::unordered_map<Level, bool> m_performanceTrackingMap;                    // 记录器日志级别和其是否启用性能跟踪配置项的映射关系
std::unordered_map<Level, base::FileStreamPtr> m_fileStreamMap;              // 记录器日志级别和其对应的日志文件流的映射关系
std::unordered_map<Level, std::size_t> m_maxLogFileSizeMap;                  // 记录器日志级别和其日志文件大小的配置项映射关系
std::unordered_map<Level, std::size_t> m_logFlushThresholdMap;               // 记录器日志级别和其日志刷新对应的日志条数阈值配置项的映射关系
LogStreamsReferenceMapPtr m_logStreamsReference = nullptr;                   // 日志文件名和日志文件流的映射关系

成员函数

获取指定配置项的接口

bool TypedConfigurations::enabled(Level level)
{
    return getConfigByVal<bool>(level, &m_enabledMap, "enabled");
}

bool TypedConfigurations::toFile(Level level)
{
    return getConfigByVal<bool>(level, &m_toFileMap, "toFile");
}

const std::string &TypedConfigurations::filename(Level level)
{
    return getConfigByRef<std::string>(level, &m_filenameMap, "filename");
}

bool TypedConfigurations::toStandardOutput(Level level)
{
    return getConfigByVal<bool>(level, &m_toStandardOutputMap, "toStandardOutput");
}

const base::LogFormat &TypedConfigurations::logFormat(Level level)
{
    return getConfigByRef<base::LogFormat>(level, &m_logFormatMap, "logFormat");
}

const base::SubsecondPrecision &TypedConfigurations::subsecondPrecision(Level level)
{
    return getConfigByRef<base::SubsecondPrecision>(level, &m_subsecondPrecisionMap, "subsecondPrecision");
}

const base::MillisecondsWidth &TypedConfigurations::millisecondsWidth(Level level)
{
    return getConfigByRef<base::MillisecondsWidth>(level, &m_subsecondPrecisionMap, "millisecondsWidth");
}

bool TypedConfigurations::performanceTracking(Level level)
{
    return getConfigByVal<bool>(level, &m_performanceTrackingMap, "performanceTracking");
}

base::type::fstream_t *TypedConfigurations::fileStream(Level level)
{
    return getConfigByRef<base::FileStreamPtr>(level, &m_fileStreamMap, "fileStream").get();
}

std::size_t TypedConfigurations::maxLogFileSize(Level level)
{
    return getConfigByVal<std::size_t>(level, &m_maxLogFileSizeMap, "maxLogFileSize");
}

std::size_t TypedConfigurations::logFlushThreshold(Level level)
{
    return getConfigByVal<std::size_t>(level, &m_logFlushThresholdMap, "logFlushThreshold");
}

    这部分代码不复杂,就不多说了。
    上面这些接口内部使用下面的通用获取配置项的接口进行配置项的获取:

通用获取配置项的接口

template <typename Conf_T>
inline Conf_T getConfigByVal(Level level, const std::unordered_map<Level, Conf_T> *confMap, const char *confName)
{
    base::threading::ScopedLock scopedLock(lock());
    return unsafeGetConfigByVal(level, confMap, confName); // This is not unsafe anymore - mutex locked in scope
}

template <typename Conf_T>
inline Conf_T &getConfigByRef(Level level, std::unordered_map<Level, Conf_T> *confMap, const char *confName)
{
    base::threading::ScopedLock scopedLock(lock());
    return unsafeGetConfigByRef(level, confMap, confName); // This is not unsafe anymore - mutex locked in scope
}

template <typename Conf_T>
Conf_T unsafeGetConfigByVal(Level level, const std::unordered_map<Level, Conf_T> *confMap, const char *confName)
{
    ELPP_UNUSED(confName);
    typename std::unordered_map<Level, Conf_T>::const_iterator it = confMap->find(level);
    if (it == confMap->end())
    {
        try
        {
            return confMap->at(Level::Global);
        }
        catch (...)
        {
            ELPP_INTERNAL_ERROR("Unable to get configuration [" << confName << "] for level ["
                                                                << LevelHelper::convertToString(level) << "]"
                                                                << std::endl
                                                                << "Please ensure you have properly configured logger.",
                                false);
            return Conf_T();
        }
    }
    return it->second;
}

template <typename Conf_T>
Conf_T &unsafeGetConfigByRef(Level level, std::unordered_map<Level, Conf_T> *confMap, const char *confName)
{
    ELPP_UNUSED(confName);
    typename std::unordered_map<Level, Conf_T>::iterator it = confMap->find(level);
    if (it == confMap->end())
    {
        try
        {
            return confMap->at(Level::Global);
        }
        catch (...)
        {
            ELPP_INTERNAL_ERROR("Unable to get configuration [" << confName << "] for level ["
                                                                << LevelHelper::convertToString(level) << "]"
                                                                << std::endl
                                                                << "Please ensure you have properly configured logger.",
                                false);
        }
    }
    return it->second;
}

    这部分代码也不复杂,就是从对应的容器中获取指定的元素。

通用设置配置项的接口

template <typename Conf_T>
void setValue(Level level, const Conf_T &value, std::unordered_map<Level, Conf_T> *confMap, bool includeGlobalLevel = true)
{
    // If map is empty and we are allowed to add into generic level (Level::Global), do it!
    if (confMap->empty() && includeGlobalLevel)
    {
        confMap->insert(std::make_pair(Level::Global, value));
        return;
    }
    // If same value exist in generic level already, dont add it to explicit level
    typename std::unordered_map<Level, Conf_T>::iterator it = confMap->find(Level::Global);
    if (it != confMap->end() && it->second == value)
    {
        return;
    }
    // Now make sure we dont double up values if we really need to add it to explicit level
    it = confMap->find(level);
    if (it == confMap->end())
    {
        // Value not found for level, add new
        confMap->insert(std::make_pair(level, value));
    }
    else
    {
        // Value found, just update value
        confMap->at(level) = value;
    }
}

    这部分代码也不复杂,注释已经很清楚了。

getULong 接口

    getULong 接口用于将配置项的字符串值转化为整型值

unsigned long TypedConfigurations::getULong(std::string confVal) {
  bool valid = true;
  base::utils::Str::trim(confVal);
  valid = !confVal.empty() && std::find_if(confVal.begin(), confVal.end(),
  [](char c) {
    return !base::utils::Str::isDigit(c);
  }) == confVal.end();
  if (!valid) {
    valid = false;
    ELPP_ASSERT(valid, "Configuration value not a valid integer [" << confVal << "]");
    return 0;
  }
  return atol(confVal.c_str());
}

resolveFilename 接口

    resolveFilename 接口用于调整日志文件名配置项值中的日期格式部分为实际当前时间,其中的 "/" 替换为 "-"

static const char *kDateTimeFormatSpecifierForFilename = "%datetime";
static const char kFormatSpecifierChar = '%';
static const char *kDefaultDateTimeFormatInFilename = "%Y-%M-%d_%H-%m";

std::string TypedConfigurations::resolveFilename(const std::string &filename)
{
    std::string resultingFilename = filename;
    std::size_t dateIndex = std::string::npos;
    std::string dateTimeFormatSpecifierStr = std::string(base::consts::kDateTimeFormatSpecifierForFilename);
    if ((dateIndex = resultingFilename.find(dateTimeFormatSpecifierStr.c_str())) != std::string::npos)
    {
        //"%datetime"前面一个字符是'%'
        while (dateIndex > 0 && resultingFilename[dateIndex - 1] == base::consts::kFormatSpecifierChar)
        {
            // 则从"%datetime"当中的第二个字符('d')的位置查找下一个"%datetime"
            dateIndex = resultingFilename.find(dateTimeFormatSpecifierStr.c_str(), dateIndex + 1);
        }
        // 找到了前面一个字符不是'%'的"%datetime"子串
        if (dateIndex != std::string::npos)
        {
            // ptr指向日志文件名配置项字符串值当中"%datetime"子串的位置
            const char *ptr = resultingFilename.c_str() + dateIndex;
            // Goto end of specifier
            // ptr指向日志文件名配置项字符串值当中"%datetime"子串之后的位置,一般日志文件名配置项的值为:"log/default-%datetime{%Y%M%d%H%m%s%g}.log"这种形式
            ptr += dateTimeFormatSpecifierStr.size();
            std::string fmt;
            // 文件名配置项字符串值当中"%datetime"子串之后的第一个字符是'{'
            if ((resultingFilename.size() > dateIndex) && (ptr[0] == '{'))
            {
                // User has provided format for date/time
                // 跳过'{'
                ++ptr;
                // 统计包括'{'和'}'以及它们之间的所有字符的数量
                int count = 1; // Start by 1 in order to remove starting brace
                std::stringstream ss;
                for (; *ptr; ++ptr, ++count)
                {
                    if (*ptr == '}')
                    {
                        ++count; // In order to remove ending brace
                        break;
                    }
                    // 临时保存'{'和'}'中间的有效日期时间格式
                    ss << *ptr;
                }
                // 日志文件名配置项字符串值中删除'{'和'}'以及它们之间的所有字符
                resultingFilename.erase(dateIndex + dateTimeFormatSpecifierStr.size(), count);
                // 保存'{'和'}'中间的有效日期时间格式,如:"log/default-%datetime{%Y%M%d%H%m%s%g}.log"中的"%Y%M%d%H%m%s%g"
                fmt = ss.str();
            }
            else
            {
                // 日志文件名配置项字符串值无效或者文件名配置项字符串值当中"%datetime"子串之后的第一个字符不是'{',则使用默认配置("%Y-%M-%d_%H-%m")
                fmt = std::string(base::consts::kDefaultDateTimeFormatInFilename);
            }

            base::SubsecondPrecision ssPrec(3);
            // 获取3位的子秒精度获取当前时间的字符串表示形式
            std::string now = base::utils::DateTime::getDateTime(fmt.c_str(), &ssPrec);
            // 将当前时间的字符串表示形式中的"/"替换为"-"
            base::utils::Str::replaceAll(now, '/', '-'); // Replace path element since we are dealing with filename
            // 将调整后的日志文件名配置项字符串值当中的"%datetime"指示器替换为调整后的当前时间的字符串now
            base::utils::Str::replaceAll(resultingFilename, dateTimeFormatSpecifierStr, now);
        }
    }
    // 返回调整后的日志文件名配置项字符串值
    return resultingFilename;
}

获取 configurations 对象

const Configurations *configurations(void) const
{
    return m_configurations;
}

支持 Configurations 来构造

/// @brief Constructor to initialize (construct) the object off el::Configurations
/// @param configurations Configurations pointer/reference to base this typed configurations off.
/// @param logStreamsReference Use ELPP->registeredLoggers()->logStreamsReference()
TypedConfigurations::TypedConfigurations(Configurations *configurations, LogStreamsReferenceMapPtr logStreamsReference)
{
    m_configurations = configurations;
    m_logStreamsReference = logStreamsReference;
    build(m_configurations);
}

    构造函数最终都委托给了 build 接口来实现,build 接口会面会详细说明。

拷贝构造

TypedConfigurations::TypedConfigurations(const TypedConfigurations &other)
{
    this->m_configurations = other.m_configurations;
    this->m_logStreamsReference = other.m_logStreamsReference;
    build(m_configurations);
}

    构造函数最终都委托给了 build 接口来实现,build 接口会面会详细说明。

build 接口

void TypedConfigurations::build(Configurations *configurations)
{
    base::threading::ScopedLock scopedLock(lock());
    auto getBool = [](std::string boolStr) -> bool { // Pass by value for trimming
        base::utils::Str::trim(boolStr);
        return (boolStr == "TRUE" || boolStr == "true" || boolStr == "1");
    };
    std::vector<Configuration *> withFileSizeLimit;
    // 建立各个配置项和对应日志级别的映射关系
    for (Configurations::const_iterator it = configurations->begin(); it != configurations->end(); ++it)
    {
        Configuration *conf = *it;
        // We cannot use switch on strong enums because Intel C++ dont support them yet
        if (conf->configurationType() == ConfigurationType::Enabled)
        {
            setValue(conf->level(), getBool(conf->value()), &m_enabledMap);
        }
        else if (conf->configurationType() == ConfigurationType::ToFile)
        {
            setValue(conf->level(), getBool(conf->value()), &m_toFileMap);
        }
        else if (conf->configurationType() == ConfigurationType::ToStandardOutput)
        {
            setValue(conf->level(), getBool(conf->value()), &m_toStandardOutputMap);
        }
        else if (conf->configurationType() == ConfigurationType::Filename)
        {
            // We do not yet configure filename but we will configure in another
            // loop. This is because if file cannot be created, we will force ToFile
            // to be false. Because configuring logger is not necessarily performance
            // sensitive operation, we can live with another loop; (by the way this loop
            // is not very heavy either)
        }
        else if (conf->configurationType() == ConfigurationType::Format)
        {
            setValue(conf->level(), base::LogFormat(conf->level(), base::type::string_t(conf->value().begin(), conf->value().end())), &m_logFormatMap);
        }
        else if (conf->configurationType() == ConfigurationType::SubsecondPrecision)
        {
            setValue(Level::Global,
                     base::SubsecondPrecision(static_cast<int>(getULong(conf->value()))), &m_subsecondPrecisionMap);
        }
        else if (conf->configurationType() == ConfigurationType::PerformanceTracking)
        {
            setValue(Level::Global, getBool(conf->value()), &m_performanceTrackingMap);
        }
        else if (conf->configurationType() == ConfigurationType::MaxLogFileSize)
        {
            auto v = getULong(conf->value());
            setValue(conf->level(), static_cast<std::size_t>(v), &m_maxLogFileSizeMap);
            if (v != 0)
            {
                withFileSizeLimit.push_back(conf);
            }
        }
        else if (conf->configurationType() == ConfigurationType::LogFlushThreshold)
        {
            setValue(conf->level(), static_cast<std::size_t>(getULong(conf->value())), &m_logFlushThresholdMap);
        }
    }
    // 对日志记录器的所有日志级别的日志文件配置项单独处理
    //  As mentioned earlier, we will now set filename configuration in separate loop to deal with non-existent files
    for (Configurations::const_iterator it = configurations->begin(); it != configurations->end(); ++it)
    {
        Configuration *conf = *it;
        if (conf->configurationType() == ConfigurationType::Filename)
        {
            // insertFile接口已经在前面详细介绍过了
            insertFile(conf->level(), conf->value());
        }
    }
    // 对所有设置了日志文件大小的日志级别,检查其是否要进行日志滚动
    for (std::vector<Configuration *>::iterator conf = withFileSizeLimit.begin();
         conf != withFileSizeLimit.end(); ++conf)
    {
        // This is not unsafe as mutex is locked in currect scope
        // unsafeValidateFileRolling接口已经在前面详细介绍过了
        unsafeValidateFileRolling((*conf)->level(), base::defaultPreRollOutCallback);
    }
}

insertFile 接口

    当日志文件存在时,建立日志级别和对应日志文件的映射关系。
    当日志文件不存在时,创建对应日志级别的日志文件,同时也建立映射关系。
    当无法创建日志文件时,强制禁用写文件配置项(将 TO_FILE 配置项设置为 false)。

void TypedConfigurations::insertFile(Level level, const std::string &fullFilename)
{
    // 调整文件名配置项值中的日期格式部分为实际当前时间,其中的"/"替换为"-"
    std::string resolvedFilename = resolveFilename(fullFilename);
    if (resolvedFilename.empty())
    {
        std::cerr << "Could not load empty file for logging, please re-check your configurations for level ["
                  << LevelHelper::convertToString(level) << "]";
    }
    // 获取文件的目录路径
    std::string filePath = base::utils::File::extractPathFromFilename(resolvedFilename, base::consts::kFilePathSeparator);
    if (filePath.size() < resolvedFilename.size())
    {
        base::utils::File::createPath(filePath);
    }
    auto create = [&](Level level)
    {
        // 获取对应日志文件名的文件流对象
        base::LogStreamsReferenceMap::iterator filestreamIter = m_logStreamsReference->find(resolvedFilename);
        base::type::fstream_t *fs = nullptr;
        if (filestreamIter == m_logStreamsReference->end())
        {
            // We need a completely new stream, nothing to share with
            // 不存在就创建新的文件流,并建立相应的映射关系
            fs = base::utils::File::newFileStream(resolvedFilename);
            m_filenameMap.insert(std::make_pair(level, resolvedFilename));
            m_fileStreamMap.insert(std::make_pair(level, base::FileStreamPtr(fs)));
            m_logStreamsReference->insert(std::make_pair(resolvedFilename, base::FileStreamPtr(m_fileStreamMap.at(level))));
        }
        else
        {
            // Woops! we have an existing one, share it!
            // 存在则复用文件流,只建立映射关系即可
            m_filenameMap.insert(std::make_pair(level, filestreamIter->first));
            m_fileStreamMap.insert(std::make_pair(level, base::FileStreamPtr(filestreamIter->second)));
            fs = filestreamIter->second.get();
        }

        if (fs == nullptr)
        {
            // We display bad file error from newFileStream()
            ELPP_INTERNAL_ERROR("Setting [TO_FILE] of ["
                                    << LevelHelper::convertToString(level) << "] to FALSE",
                                false);
            // 日志文件流最终没能获取到或者新建则强制禁用写文件配置项
            setValue(level, false, &m_toFileMap);
        }
    };
    // If we dont have file conf for any level, create it for Level::Global first
    // otherwise create for specified level
    create(m_filenameMap.empty() && m_fileStreamMap.empty() ? Level::Global : level);
}

validateFileRolling 接口

    validateFileRolling 接口用于日志滚动。

inline bool validateFileRolling(Level level, const PreRollOutCallback &preRollOutCallback)
{
    base::threading::ScopedLock scopedLock(lock());
    return unsafeValidateFileRolling(level, preRollOutCallback);
}

    内部委托给 unsafeValidateFileRolling 接口实现,仅仅只多了加锁动作。
    unsafeValidateFileRolling 接口的实现如下:

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;
}

至此,日志格式的配置管理类就介绍完了。

基于 Configurations 类和 TypedConfigurations 类,easylogging++提供了多种日志格式配置的方式,下一篇我们会一一介绍这些配置方式。

posted @ 2022-12-04 15:15  节奏自由  阅读(251)  评论(0编辑  收藏  举报