1. 日志系统搭配配置系统使用

1.1 改动:LogManager:getLogger(name)函数

目的:当寻找的name:Logger不存在时,直接返回LogManager::Logger::ptr m_root默认的日志器(智能指针)。改动后要实现寻找不存在的日志器时候会自动创建。

  • 原来的写法: ```cpp Logger::ptr LogManager::getLogger(const std::string &name) { auto it = s_loggers.find(name);

    //注意:当指定一个name之后找不到对应的Logger 返回的是默认的日志器 return it == s_loggers.end() ? m_root : it->second;

}

  1. - **改动1:**
  2. ```cpp
  3. Logger::ptr LogManager::getLogger(const std::string &name)
  4. {
  5. //改动:
  6. auto it = s_loggers.find(name);
  7. if(it != s_loggers.end())
  8. return it->second;
  9. Logger::ptr logger(new Logger(name));
  10. //为其默认分配一个 输出到控制台的 日志输出器
  11. logger->addAppender(LogAppender::ptr(new StdoutLogAppender));
  12. s_loggers[name] = logger;
  13. return logger;
  14. }
  • 改动2: ```cpp class Logger { … private: … //默认日志器的智能指针 Logger::ptr default_root;

};

Logger::ptr LogManager::getLogger(const std::string &name) { //改动: auto it = s_loggers.find(name); if(it != s_loggers.end()) return it->second;

  1. Logger::ptr logger(new Logger(name));
  2. s_loggers[name] = logger;
  3. //借用LogManager中的默认日志器,为的是有LogAppender可以使用
  4. logger->default_root = m_root;
  5. return logger;

}

  1. 用意:改动2是注意到原本`LogManager`中就有一个创建好的默认日志器的实体,日志输出器也为其默认配备`StdoutLogAppender`控制台的输出。在没有特别指明需要将新创建的日志输出到特定的地方时候,可以考虑直接借用这个默认的日志器,避免每一次不确定输出到哪时,就盲目的创建一个日志输出器`LogAppender`浪费一部分内存。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/25460685/1638611551614-f8a1dc11-9fb1-48f0-be15-8cb5031019f6.png#clientId=ubdf48eab-67ee-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=242&id=u127d2e28&margin=%5Bobject%20Object%5D&name=image.png&originHeight=242&originWidth=841&originalType=binary&ratio=1&rotation=0&showTitle=false&size=29233&status=done&style=none&taskId=u88a6d9c3-a275-4acc-9929-a7f073815c8&title=&width=841)
  2. <a name="xzyKB"></a>
  3. ### 1.1.1 最终解决的问题:
  4. 假设:名为"system"Logger日志器不存在<br />` auto temp = KIT_LOG_NAME("system");`<br />`#define KIT_LOG_NAME(name) kit_server::LoggerMgr::GetInstance().getLogger(name)`
  5. **原本**:`getLogger("ststem")`--->得到的是 `LogManager::Logger::ptr m_root`一个默认的日志器<br />![](https://cdn.nlark.com/yuque/0/2021/jpeg/25460685/1638612814793-8d6b1d3e-c955-41e9-80d5-53f850cd6e10.jpeg)
  6. **改动**:`getLogger("ststem")`--->创建一个名为 "system"`Logger`返回出来。但是并没有给该Logger分配对应的日志输出器`LoggerAppender`,于是就是先借用原本默认的日志器`Logmanager::m_root`来完成日志输出。直到给`Logger("system")`重新定义了新的输出器,比如`FileLogAppender`输出到文件,就不会再借用默认日志器。<br />![](https://cdn.nlark.com/yuque/0/2021/jpeg/25460685/1638612943074-f076a63d-3f1a-452e-ad17-d522ff2b00f0.jpeg)
  7. <a name="AZ8IB"></a>
  8. ## 1.2 定义从.yaml接收配置项的自定义结构体,并进行偏特化
  9. **来自**`**Log.cpp**`**文件中**
  10. - **定义**`**LogDefine**`**和**`**LogAppenderDefine**`**两个自定义类型分别用于存储**`**Logger**`**的配置和**`**LogAppender**`**的配置。定义如下:**
  11. ```cpp
  12. struct LogAppenderDefine
  13. {
  14. //0:StdOut 1:File
  15. int type = -1;
  16. LogLevel::Level level = LogLevel::UNKNOW;
  17. std::string formatter;
  18. std::string file;
  19. bool operator==(const LogAppenderDefine& d) const
  20. {
  21. return type == d.type && level == d.level &&
  22. formatter == d.formatter && file == d.file;
  23. }
  24. };
  25. struct LogDefine
  26. {
  27. std::string name;
  28. LogLevel::Level level = LogLevel::UNKNOW;
  29. std::string formatter;
  30. std::vector<LogAppenderDefine> appenders;
  31. bool operator==(const LogDefine& d) const
  32. {
  33. return name == d.name && level == d.level &&
  34. formatter == d.formatter && appenders == d.appenders;
  35. }
  36. bool operator<(const LogDefine& d) const
  37. {
  38. return name < d.name;
  39. }
  40. };

注意:由于LogAppenderDefineLogDefine中是用vector存储,需要重载operator==用于比较;在LogDefine中重载operator<是因为:ConfigVar<std::set<LogDefine> >使用set容器作为存储容器红黑树有排序、去重。

  • 偏特化如下,方法和配置开发(二)中一样的,参考即可: ```cpp //自定义类型偏特化 string————->LogAppenderDefine 序列化 template<> class LexicalCast { public:

    LogAppenderDefine operator()(const std::string &val) {

    1. LogAppenderDefine p;
    2. YAML::Node node = YAML::Load(val);
    3. std::string str = node["type"].as<std::string>();
    4. if(str == "FileLogAppender")
    5. p.type = 1;
    6. else if(str == "StdoutLogAppender")
    7. p.type = 0;
    8. else
    9. p.type = -1;
  1. p.level = LogLevel::FromString(node["level"].as<std::string>());
  2. p.formatter = node["formatte"].as<std::string>();
  3. p.file = node["file"].as<std::string>();
  4. return p;
  5. }

};

//自定义类型偏特化 LogAppenderDefine————->string 反序列化 template<> class LexicalCast { public: std::string operator()(const LogAppenderDefine& p) { YAML::Node node; std::string str; switch(p.type) { case 0: str = “FileLogAppender”;break; case 1: str = “StdoutLogAppender”;break; }

  1. node["type"] = str;
  2. node["level"] = LogLevel::ToString(p.level);
  3. node["formatter"] = p.formatter;
  4. node["file"] = p.file;
  5. std::stringstream ss;
  6. ss << node;
  7. return ss.str();
  8. }

};

//自定义类型偏特化 string————->LogDefine 序列化 template< > class LexicalCast { public:

  1. LogDefine operator()(const std::string &val)
  2. {
  3. LogDefine d;
  4. YAML::Node node = YAML::Load(val);
  5. d.name = node["name"].as<std::string>();
  6. d.level = LogLevel::FromString(node["level"].as<std::string>());
  7. d.formatter = node["formatter"].as<std::string>();
  8. auto t = node["appender"][0];
  9. for(size_t i = 0;i < node["appender"].size();++i)
  10. {
  11. std::stringstream ss;
  12. ss << node["appender"][i];
  13. d.appenders.push_back(LexicalCast<std::string, LogAppenderDefine>()(ss.str()));
  14. }
  15. return d;
  16. }

};

//自定义类型偏特化 LogDefine————->string 反序列化 template<> class LexicalCast { public: std::string operator()(const LogDefine& p) { YAML::Node node;

  1. node["name"] = p.name;
  2. node["level"] = LogLevel::ToString(p.level);
  3. node["formatter"] = p.formatter;
  4. for(size_t i = 0;i < p.appenders.size();++i)
  5. {
  6. node["appender"].push_back(LexicalCast<LogAppenderDefine, std::string>()(p.appenders[i]));
  7. }
  8. std::stringstream ss;
  9. ss << node;
  10. return ss.str();
  11. }

};

  1. <a name="EdZVN"></a>
  2. ### 1.2.1 BUG:错误的类型转换,简单将枚举类型当做int型处理
  3. (注意:模板错误提示非常隐晦,很难找出真正错误的地方,要谨慎)<br />原因:虽然`LogLevel::Level`作为枚举类型,内部元素是int型。但是在进行`.yaml`序列化和反序列化的过程中,错误的将其作为int型处理,实际上应该是`string`和`int`之间的相互转换。
  4. <a name="mtOix"></a>
  5. #### 1.2.1.1 问题代码:
  6. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/25460685/1638636785223-7418ae84-ea61-46bf-af25-7a9ad94c1f07.png#clientId=u87126adf-0861-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=422&id=u2993ea70&margin=%5Bobject%20Object%5D&name=image.png&originHeight=422&originWidth=806&originalType=binary&ratio=1&rotation=0&showTitle=false&size=51494&status=done&style=none&taskId=ub3637d0c-2700-4ae1-97df-ac383f69f56&title=&width=806)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/25460685/1638636831836-69ac4eb0-9cc3-4df5-af27-34c0ef660612.png#clientId=u87126adf-0861-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=435&id=ueb51f061&margin=%5Bobject%20Object%5D&name=image.png&originHeight=435&originWidth=749&originalType=binary&ratio=1&rotation=0&showTitle=false&size=47280&status=done&style=none&taskId=u4ffa5f39-b659-456e-81ca-0c08b2c45b2&title=&width=749)<br />显然,从序列化代码过程能够看到:从`.yaml`文件读入的应该是一个字符串`string`但是却直接转化为`LogLevel::Level`明显不对,`yaml-cpp`内部不会支持这种转换。
  7. <a name="UnizR"></a>
  8. #### 1.2.1.2 修改代码:(多加一层`LogLevel::Level`与`string`之间的相互转换)
  9. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/25460685/1638637343904-d1731927-0982-4f3f-8ef3-5eaced41b92c.png#clientId=u87126adf-0861-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=462&id=u3970a392&margin=%5Bobject%20Object%5D&name=image.png&originHeight=462&originWidth=796&originalType=binary&ratio=1&rotation=0&showTitle=false&size=38277&status=done&style=none&taskId=u9da5c86d-1fd9-4b93-9891-609616a4d2c&title=&width=796)
  10. - **小技巧:使用宏替换简化代码量:**
  11. ```cpp
  12. const char* LogLevel::ToString(LogLevel::Level level)
  13. {
  14. switch(level)
  15. {
  16. #define XX(name)\
  17. case LogLevel::name:\
  18. return #name;\
  19. break;
  20. XX(DEBUG);
  21. XX(INFO);
  22. XX(WARN);
  23. XX(FATAL);
  24. XX(ERROR);
  25. #undef XX //用完一个宏XX 希望下面的代码不再使用到它
  26. default:
  27. return "UNKNOW";
  28. }
  29. return "UNKNOW";
  30. }
  31. LogLevel::Level LogLevel::FromString(const std::string& val)
  32. {
  33. #define XX(name)\
  34. if(#name == val)\
  35. {\
  36. return LogLevel::name;\
  37. }
  38. XX(DEBUG);
  39. XX(INFO);
  40. XX(WARN);
  41. XX(FATAL);
  42. XX(ERROR);
  43. #undef XX
  44. return LogLevel::UNKNOW;
  45. }

1.2.1.3 编译结果:通过

image.png

1.2.2 BUG:配置系统+日志系统联调时,出现段错误

  • 使用gdb调试,现象如下:

image.png

  • 使用**where**命令进一步定位出错位置,现象如下:

image.png

  • 其他提示地点出现在库函数文件中,红框标识出来的是自己写的代码的问题地点,点击追踪后,跳到了一个不知道为啥出现的地方:Config.h:494

image.png

1.2.2.1 问题解决:静态变量初始化的问题:未初始化却被先调用

冷知识:静态变量的构建在程序进入到main()之前,并且静态变量构建的顺序是不确定的,因此造成段错误的原因很可能就是需要使用的静态变量m_datas还未被构建,就先被Config::LookUp()函数所使用造成的段错误问题。

  • 原本代码:(静态变量Config::m_datas初始化在config.cpp中)

image.png

  • 修改后代码:将其改为返回静态变引用的函数,每次请求即使还没初始化,也会在函数里初始化后再返回,杜绝了未初始化先调用的情形

image.png

1.3 约定日志系统的配置,利用静态全局变量初始化

1.3.1 约定针对日志系统配置的配置项ConfigVar<std::set<LogDefine> >::ptr

image.png

1.3.2 定义全局静态变量static LogIniter __log_init (核心)

LogIniter结构体的构造函数中完成初始化工作:
① 利用之前的配置系统事件机制,定义一个回调函数(lambda表达式):key值=0x123456。一旦发生
与”logs”有关的修改,就会触发该函数的调用。

② 回调函数中针对三种情况作出修改:新增日志器、对原有日志器修改、删除原有日志器

③”删除”日志器,并非真的将其内存空间立马释放,而是将其日志级别设置为足够大,且清空日志器
中所有的输出器,让其不再能够被输出则认为完成”删除”。目的:防止频繁的创建和销毁,将原有资
源保留一段时间。

1.3.2.1 改动 viod addListener(uint64_t key, on_change_cb cb)—>uint64_t addListener(on_change_cb cb)

在函数内部定义一个static静态变量,采用累加的方式来给回调函数赋key值。避免了原本手动传入key值会有重复传入而导致回调函数间的相互覆盖。

  1. kit_server::ConfigVar<std::set<LogDefine> >::ptr g_log_defines =
  2. kit_server::Config::LookUp("logs", std::set<LogDefine>(), "logs configs");
  3. //冷知识:全局变量在main函数之前构建
  4. struct LogIniter
  5. {
  6. LogIniter()
  7. {
  8. //设置好 key值="logs"的配置项的触发事件,一旦从.yaml文件读到"logs"相关配置就会进入到该lambda函数中来
  9. g_log_defines->addListener([](const std::set<LogDefine> &old_value, const std::set<LogDefine> &new_value){
  10. /* 1.有新增日志
  11. * 2.有旧日志修改
  12. * 3.有旧日志删除*/
  13. KIT_LOG_INFO(KIT_LOG_ROOT()) << "logger configs changed!";
  14. for(auto &x : new_value)
  15. {
  16. auto it = old_value.find(x);
  17. Logger::ptr logger;
  18. if(it == old_value.end()) //新增日志器
  19. {
  20. //新创建一个日志器并以.yaml中读取的名字命名
  21. //logger.reset(new kit_server::Logger(x.name));
  22. logger = KIT_LOG_NAME(x.name);
  23. }
  24. else //修改日志器 旧的日志器存在 新的日志器也存在
  25. {
  26. //判断一下内容是否发生变化
  27. if(!(x == *it)) //有变化
  28. {
  29. logger = KIT_LOG_NAME(x.name);
  30. }
  31. else //无变化
  32. continue;
  33. }
  34. //设定日志级别
  35. logger->setLevel(x.level);
  36. //设定日志格式
  37. if(x.formatter.size())
  38. {
  39. logger->setFormatter(x.formatter);
  40. }
  41. //清空原有的日志输出器 重新根据.yaml文件设置输出器
  42. logger->clearAppender();
  43. for(auto &k : x.appenders)
  44. {
  45. LogAppender::ptr p;
  46. if(k.type == 0) //输出到控制台
  47. {
  48. p.reset(new StdoutLogAppender);
  49. }
  50. else if(k.type == 1) //输出到文件
  51. {
  52. p.reset(new FileLogAppender(k.file));
  53. }
  54. //设置输出器级别
  55. p->setLevel(k.level);
  56. //设置输出器格式
  57. if(k.formatter.size())
  58. {
  59. //只有在文件中手动设置过的才展示
  60. //从Logger继承过来的不展示
  61. LogFormatter::ptr f(new LogFormatter(k.formatter));
  62. //如果配置的formatter不合法也不能进行初始化 那么就会继续使用Logger的formatter
  63. if(!f->isError())
  64. {
  65. p->setIsFormatter();
  66. p->setFormatter(f);
  67. }
  68. else
  69. std::cout << "[ERROR]: " << "yaml Logger name= "
  70. << logger->getName() << " include LogAppender name= " <<
  71. k.name << " formatter= " << k.formatter << " is invaild" << std::endl;
  72. }
  73. //添加到日志器的输出队列中 会将Logger的formatter也默认给LogAppender
  74. logger->addAppender(p);
  75. }
  76. }
  77. //老日志器存在而新日志器没有
  78. //删除不是真的将空间释放,打一个标记或者使用别的方法让其不再写日志
  79. //不真正删除的理由:防止添加回来又要重新创建,开销大
  80. for(auto &x : old_value)
  81. {
  82. auto it = new_value.find(x);
  83. if(it == new_value.end())
  84. {
  85. auto t_logger = KIT_LOG_NAME(x.name);
  86. t_logger->setLevel((LogLevel::Level)100);
  87. t_logger->clearAppender();
  88. }
  89. }
  90. });
  91. }
  92. };
  93. static LogIniter __log_init;

1.3.3 小改动:希望LogAppender在没有进行配置时不输出没配置的配置信息

现象:当我们在.yaml文件配置StdoutLogAppender这一个输出器时,只写了相关的type,没有写levelformatter等信息,但是打印时候依旧打印了。

目的:希望不是我们自己配置的formatter不要出现在打印中,在我们没有手动配置时候,LogAppender会默认使用Logger的一些配置,以实现正常输出。

  • 改动代码:

在类LogAppender中添加一个标志位LogAppender::is_set_formatter,用于标识当前的
LogFormatter::ptr指向的内容是否是通过文件配置完成的。如果是,则可以打印;否则不打印。

  • LogAppender类中:

image.png

  • **LogIniter()**构造函数中:

image.png

1.4 改动:日志输出文件路径使用相对路径即可

原因:输出绝对路径比较暴露一部分隐私,这个和CMake的设置有关系。CMake默认输出文件绝对路径。

参考解决办法:
https://blog.csdn.net/yindongjie1221/article/details/90614261?opsrequestmisc=%257B%2522request%255Fid%2522%253A%2522163888096216780357219678%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=163888096216780357219678&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-1-90614261.pc_search_result_cache&utm_term=cmake3.16+__FILE&spm=1018.2226.3001.4187

2.配置系统考虑线程安全

实现目标:保证线程安全前提下,尽量对减少性能损失,采用读写锁RWMutex作为配置系统的安全保障。因为对于系统的配置项来说,大多数时候是”约定”即不改动,改动的时候偏少。因此符合,读多写少的场景。