easylogging++的那些事(四)源码分析(六)VERBOSE日志信息的管理

在上一篇我们介绍了 日志格式配置方式
在 easylogging++的 功能介绍 中我们提过,easylogging++日志分两种:用户日志 和 syslog 日志,而用户日志又可分为 普通日志(分层日志) 和 verbose 日志。
在前面我们介绍过 VERBOSE 日志宏 的实现,今天我们来看看 VERBOSE 日志相关的信息是如何管理的。

VERBOSE 日志是什么?

    前面我们提到的记录日志的方式大部分时候都是 分层级日志,根据不同的日志级别(Trace, Debug, Fatal, Error, Warning, Info),统一对所有文件进行日志输出控制,可以单独控制某一个级别是否能进行日志输出,也可以禁用所有级别的日志输出,但也仅此而已了。
    我们 没办法针对某一类型的文件进行日志输出,也就是说无法更细粒度控制日志输出的文件范围,而 VERBOSE 日志正是为了解决这个问题

    VERBOSE 日志提供了两个属性来来进行细粒度的控制:
    1) 控制日志输出作用的文件范围
    2) VLevel 级别:类似于分层日志当中的日志级别
       不同于分层日志当中的日志级别,VLevel 的取值范围为 0-9

    VERBOSE 日志使用模块(我这里称之为 module)的机制来将两者有机结合起来,也就是说我们可以给模块指定文件范围和对应的 VLevelmodule 的格式如下:

<module name>=<verbose level>

    其中的 module name 就是源文件匹配规则, 支持部分正则表达式规则(支持其中的 ?* 通配符) verboser level 是进行 VERBOSE 日志输出时,针对符合这种匹配规则能够使用的最大的 VLevel 级别
    而 VERBOSE 日志可以同时设定多个这样的模块,这就是 VModuleVModule 格式如下:

<module1>,<module2>,<module3>... #各个模块之间用英文逗号","隔开

    举个例子:

main=5,parse*=6

    这里给 VModule 设置了 2 个模块,分别为:
    1) 模块 1:main=5 表示文件匹配规则为 "main" 字符串,而对应的最大 VLevel5
    2) 模块 2:parse*=6 表示文件匹配规则为 "parse*" 字符串,而对应的最大 VLevel6

    如果用一句话总结什么是 VERBOSE 日志,那就是 VERBOSE 日志是模块日志

VERBOSE 日志的实现

    在 easylogging++的 总体设计 中我们曾提过:VRegistry 类是 VERBOSE 日志相关配置的管理类(保存基准 VLevel 以及 vModule 相关信息),今天我们就来详细分析一下这个类的实现。

VRegistry 类的成员变量

base::type::VerboseLevel m_level;                                    // 当VMODULE未指定时(m_modules为空)时,用作基准的VLevel判断是否能写VERBOSE日志
base::type::EnumType *m_pFlags;                                      // 日志库的全局的logging flag(通过构造函数的参数传递进来)
std::unordered_map<std::string, base::type::VerboseLevel> m_modules; // 保存VMODULE经过调整后的的模块信息,key-文件匹配规则,value-VLevel

VRegistry 类的成员函数

构造函数

VRegistry::VRegistry(base::type::VerboseLevel level, base::type::EnumType *pFlags) : m_level(level), m_pFlags(pFlags)
{
}

设置基准的 VLevel

/// @brief Sets verbose level. Accepted range is 0-9
void VRegistry::setLevel(base::type::VerboseLevel level)
{
    base::threading::ScopedLock scopedLock(lock());
    if (level > 9)
        m_level = base::consts::kMaxVerboseLevel;
    else
        m_level = level;
}

获取基准的 VLevel

inline base::type::VerboseLevel level(void) const
{
    return m_level;
}

清空模块信息

inline void clearModules(void)
{
    base::threading::ScopedLock scopedLock(lock());
    m_modules.clear();
}

配置模块信息

// 参数modules的格式为“<module1>,<module2>,<module3>...”
void VRegistry::setModules(const char *modules)
{
    base::threading::ScopedLock scopedLock(lock());

    auto addSuffix = [](std::stringstream &ss, const char *sfx, const char *prev)
    {
        // 文件匹配规则如果是prev后缀,则去掉prev后缀
        if (prev != nullptr && base::utils::Str::endsWith(ss.str(), std::string(prev)))
        {
            std::string chr(ss.str().substr(0, ss.str().size() - strlen(prev)));
            ss.str(std::string(""));
            ss << chr;
        }
        // 文件匹配规则如果是sfx后缀,则去掉sfx后缀
        if (base::utils::Str::endsWith(ss.str(), std::string(sfx)))
        {
            std::string chr(ss.str().substr(0, ss.str().size() - strlen(sfx)));
            ss.str(std::string(""));
            ss << chr;
        }
        // 文件匹配规则再次添加sfx后缀
        ss << sfx;
    };

    // 上面的addSuffix这个lambda表达式的功能代码有些重复
    // 无非就是针对两个后缀依次判断了,prev是一定清除的,而sfx是需要保留的,完全可以实现的更精简点:
    /*
    auto addSuffix = [](std::stringstream& ss, const char* sfx, const char* prev) {
        std::string chr = ss.str();
        //文件匹配规则如果是prev后缀,则去掉prev后缀
        if (prev != nullptr && base::utils::Str::endsWith(chr, std::string(prev))) {
            chr = chr.substr(0, chr.size() - strlen(prev));
        }
    //调整后的文件匹配规则如果不是sfx后缀,则添加sfx后缀
    if (!base::utils::Str::endsWith(chr, std::string(sfx))) {
        chr += sfx;
    }
    ss.str(chr);
    };
    */

    auto insert = [&](std::stringstream &ss, base::type::VerboseLevel level)
    {
        // 没有禁用添加文件扩展名
        if (!base::utils::hasFlag(LoggingFlag::DisableVModulesExtensions, *m_pFlags))
        {
            // 原始文件匹配文件匹配规添加.h后缀
            addSuffix(ss, ".h", nullptr);
            m_modules.insert(std::make_pair(ss.str(), level));
            // 原始文件匹配规添加.c后缀
            addSuffix(ss, ".c", ".h");
            m_modules.insert(std::make_pair(ss.str(), level));
            // 原始文件匹配规添加.cpp后缀
            addSuffix(ss, ".cpp", ".c");
            m_modules.insert(std::make_pair(ss.str(), level));
            // 原始文件匹配规添加.cc后缀
            addSuffix(ss, ".cc", ".cpp");
            m_modules.insert(std::make_pair(ss.str(), level));
            // 原始文件匹配规添加.cxx后缀
            addSuffix(ss, ".cxx", ".cc");
            m_modules.insert(std::make_pair(ss.str(), level));
            // 原始文件匹配规添加.-inl.h后缀
            addSuffix(ss, ".-inl.h", ".cxx");
            m_modules.insert(std::make_pair(ss.str(), level));
            // 原始文件匹配规添加.hxx后缀
            addSuffix(ss, ".hxx", ".-inl.h");
            m_modules.insert(std::make_pair(ss.str(), level));
            // 原始文件匹配规添加.hpp后缀
            addSuffix(ss, ".hpp", ".hxx");
            m_modules.insert(std::make_pair(ss.str(), level));
            // 原始文件匹配规添加.hh后缀
            addSuffix(ss, ".hh", ".hpp");
        }
        m_modules.insert(std::make_pair(ss.str(), level));
    };

    // 下面的这段代码就是对VModule字符串的一个解析,对照着VModule的格式(<module1>,<module2>,<module3>...)看就很清楚了。
    bool isMod = true;
    bool isLevel = false;
    std::stringstream ss;
    int level = -1;
    for (; *modules; ++modules)
    {
        switch (*modules)
        {
        case '=':
            isLevel = true;
            isMod = false;
            break;
        case ',':
            isLevel = false;
            isMod = true;
            if (!ss.str().empty() && level != -1)
            {
                insert(ss, static_cast<base::type::VerboseLevel>(level));
                ss.str(std::string(""));
                level = -1;
            }
            break;
        default:
            if (isMod)
            {
                ss << *modules;
            }
            else if (isLevel)
            {
                if (isdigit(*modules))
                {
                    level = static_cast<base::type::VerboseLevel>(*modules) - 48;
                }
            }
            break;
        }
    }
    if (!ss.str().empty() && level != -1)
    {
        insert(ss, static_cast<base::type::VerboseLevel>(level));
    }
}

    针对上面这个配置过程的结果,我再举个例子说明一下:
    如:vmodule 的格式是: "*main*=3,*base*=4"
    配置之后 m_modules 当中存放的键值对如下:

{"*base*.h", 4}
{"*main*.h", 3}
{"*base*.hpp", 4}
{"*main*.hpp", 3}
{"*base*.hxx", 4}
{"*main*.hxx", 3}
{"*base*.cxx", 4}
{"*main*.cxx", 3}
{"*base*.cpp", 4}
{"*main*.cpp", 3}
{"*base*.c", 4}
{"*main*.c", 3}
{"*base*.-inl.h", 4}
{"*main*.-inl.h", 3}
{"*base*.cc", 4}
{"*main*.cc", 3}
{"*base*.hh", 4}
{"*main*.hh", 3}

    顺序不一定是上面的顺序,但一定是这些键值对。

判断某个文件在某个 Verbose Level 下是否允许进行 VERBOSE 日志输出

bool VRegistry::allowed(base::type::VerboseLevel vlevel, const char *file)
{
    base::threading::ScopedLock scopedLock(lock());
    if (m_modules.empty() || file == nullptr)
    {
        // 没有设置VMODULE或者文件名为空,则根据基准VLevel来判断
        return vlevel <= m_level;
    }
    else
    {

        char baseFilename[base::consts::kSourceFilenameMaxLength] = "";
        // 将文件名调整为指定长度大小以内的文件名
        base::utils::File::buildBaseFilename(file, baseFilename);
        // 然后再依次和模块规则集(m_modules)里面每条规则作匹配,
        std::unordered_map<std::string, base::type::VerboseLevel>::iterator it = m_modules.begin();
        for (; it != m_modules.end(); ++it)
        {
            // base::utils::Str::wildCardMatch支持通配符(*和?)
            if (base::utils::Str::wildCardMatch(baseFilename, it->first.c_str()))
            {
                return vlevel <= it->second;
            }
        }
        // 设置了VMODULE,但模块规则集(m_modules)里面的规则都匹配不上当前文件时,如果设置了AllowVerboseIfModuleNotSpecified,也允许在对应的Verbose Level下进行VERBOSE日志输出
        if (base::utils::hasFlag(LoggingFlag::AllowVerboseIfModuleNotSpecified, *m_pFlags))
        {
            return true;
        }

        // 其他情况则不允许在对应的Verbose Level下进行VERBOSE日志输出
        return false;
    }
}

获取模块信息

inline const std::unordered_map<std::string, base::type::VerboseLevel> &modules(void) const
{
    return m_modules;
}

是否启用了 VMODULE

/// @brief Whether or not vModules enabled
inline bool vModulesEnabled(void)
{
    return !base::utils::hasFlag(LoggingFlag::DisableVModules, *m_pFlags);
}

通过命令行参数设置 VMODULE

// commandLineArgs是命令行参数解析器,命令行参数解析的结果已经保存在里面了
void VRegistry::setFromArgs(const base::utils::CommandLineArgs *commandLineArgs)
{
    if (commandLineArgs->hasParam("-v") || commandLineArgs->hasParam("--verbose") ||
        commandLineArgs->hasParam("-V") || commandLineArgs->hasParam("--VERBOSE"))
    {
        setLevel(base::consts::kMaxVerboseLevel);
    }
    else if (commandLineArgs->hasParamWithValue("--v"))
    {
        setLevel(static_cast<base::type::VerboseLevel>(atoi(commandLineArgs->getParamValue("--v"))));
    }
    else if (commandLineArgs->hasParamWithValue("--V"))
    {
        setLevel(static_cast<base::type::VerboseLevel>(atoi(commandLineArgs->getParamValue("--V"))));
    }
    else if ((commandLineArgs->hasParamWithValue("-vmodule")) && vModulesEnabled())
    {
        setModules(commandLineArgs->getParamValue("-vmodule"));
    }
    else if (commandLineArgs->hasParamWithValue("-VMODULE") && vModulesEnabled())
    {
        setModules(commandLineArgs->getParamValue("-VMODULE"));
    }
}

    CommandLineArgs 类在 日志格式配置方式 中已经详细介绍过了,这里就不多说了。
    vModulesEnabled 接口在前面也介绍过了。

对外提供的 VERBOSE 日志配置的接口

    VERBOSE 日志配置相关的接口主要是由 el::loggers 工具类提供的。相关接口如下:

void Loggers::setVerboseLevel(base::type::VerboseLevel level)
{
    ELPP->vRegistry()->setLevel(level);
}

base::type::VerboseLevel Loggers::verboseLevel(void)
{
    return ELPP->vRegistry()->level();
}

void Loggers::setVModules(const char *modules)
{
    if (ELPP->vRegistry()->vModulesEnabled())
    {
        ELPP->vRegistry()->setModules(modules);
    }
}

void Loggers::clearVModules(void)
{
    ELPP->vRegistry()->clearModules();
}

    ELPP 宏在 偶尔日志宏 中已经介绍过了,是 easylogging++的全局管理类。ELPP->vRegistry() 是全局管理类的 VRegistry 对象成员,这里就是对 VRegistry 类相关接口做了简单的包装而已。

至此,VERBOSE 日志信息的管理就介绍完了。下一篇我们开始介绍性能跟踪的实现。

posted @ 2022-12-05 13:47  节奏自由  阅读(91)  评论(0编辑  收藏  举报