日志模块

打印 上一主题 下一主题

主题 548|帖子 548|积分 1654

介绍

在学习了sylar的C++高性能分布式服务器框架后,想把自己在学习过程中的感想记录下来。当然主要缘故原由还是sylar的B站视频过于难以理解了,也是想加强一下自己对这个框架的理解。许多内容也是借鉴了其他大佬的博文,比如找人找不到北zhongluqiang
日志模块概述

日志模块的目标:

  • 用于格式化输出程序日志,方便从日志中定位程序运行过程中出现的问题。
  • 同时应该包罗文件名/行号,时间戳,线程/协程号,模块名称,日志级别等额外信息。
  • 在打印致命的日志时,还应该附加程序的栈回溯信息,以便于分析和排查问题。
从设计上看,一个完备的日志模块应该具备以下功能:

  • 区分不同的级别,比如常的DEBUG/INFO/WARN/ERROR等级别。
  • 区分不同的输出地。不同的日志可以输出到不同的位置,比如可以输出到尺度输出,输出到文件,输出到syslog,输出到网络上的日志服务器等,甚至同一条日志可以同时输出到多个输出地。
  • 区分不同的类别。日志可以分类并命名,一个程序的各个模块可以使用不同的名称来输出日志,这样可以很方便地判断出当前日志是哪个程序模块输出的。
  • 日志格式可灵活配置。可以按需指定每条日志是否包含文件名/行号、时间戳、线程/协程号、日志级别、启动时间等内容。
  • 可通过配置文件的方式配置以上功能。
日志模块设计

类似于log4cpp,日志模块拥有以下几个主要类:

  • class LogLevel:定义日志级别。并提供将日志级别与文本之间的互相转化
  • class Logger:日志器。定义日志级别,设置输出地,设置日志格式。
  • class LogEvent:记录日志事件。主要记录一下信息
  • class LogEventWarp:日志事件包装器。将logEvent打包,可以直接通过使用该类完成对日志的定义。
  • class LogFormatter:日志格式化。
  • class LogAppender:日志输出目标。有两个子类 class StdoutLogAppender 和 class FileLogAppender,可以分别输出到控制台和文件
  • class LoggerManager:日志管理器。单例模式
具体实现

日志级别 class LogLevel
  1. enum Level {
  2.     /// 致命情况,系统不可用
  3.     FATAL  = 0,
  4.     /// 高优先级情况,例如数据库系统崩溃
  5.     ALERT  = 100,
  6.     /// 严重错误,例如硬盘错误
  7.     CRIT   = 200,
  8.     /// 错误
  9.     ERROR  = 300,
  10.     /// 警告
  11.     WARN   = 400,
  12.     /// 正常但值得注意
  13.     NOTICE = 500,
  14.     /// 一般信息
  15.     INFO   = 600,
  16.     /// 调试信息
  17.     DEBUG  = 700,
  18.     /// 未设置
  19.     NOTSET = 800,
  20. };
复制代码
ToString(提供从日志级别 TO 文本的转换)

通过X宏(X Macros)将不同的级别放入switch case语句中。X宏的根本思想是将重复的代码片段定义为一个宏,然后在必要使用这些代码的地方多次调用这个宏。这样可以避免手动编写和维护大量重复代码。
  1. const char* LogLevel::ToString(LogLevel::Level level){
  2.     switch (level){
  3. #define XX(name) \
  4.         case LogLevel::name: \
  5.             return #name; \
  6.             break;
  7.         XX(DEBUG);
  8.         XX(INFO);
  9.         XX(WARN);
  10.         XX(ERROR);
  11.         XX(FATAL);
  12. #undef XX
  13.         default:
  14.             return "UNKNOW";
  15.         }
  16.         return "UNKNOW";
  17. }
复制代码
swtich (level):对传入的 level 参数举行 switch 分支判断。
//#define XX(name) ... #undef XX:定义了一个名为 XX 的宏,用于减少重复代码。宏 XX 接受一个参数 name,并生成对应的 case 语句。宏会睁开成:
  1. case LogLevel::DEBUG:
  2.     return "DEBUG";
  3.     break;
  4. case LogLevel::INFO:
  5.     return "INFO";
  6.     break;
  7. // 依次类推
复制代码
注1:常见的X宏使用场景:
枚举与字符串映射:如将枚举值转换为字符串。
多次声明相似的代码结构:如函数声明、结构体初始化等。
生成重复的测试代码:如生成一系列测试用例。

FromString(提供从文本 To 日志级别的转换)

同样通过宏定义处理多种情况。转换时不针对大小写,DEBUG和debug都可以完成对应的转化
  1. LogLevel::Level LogLevel::FromString(const std::string &str) {
  2. #define XX(level, v)    \
  3.     if(str == #v) { \
  4.         return LogLevel::level; \
  5.     }
  6.     XX(DEBUG, debug);
  7.     XX(INFO, info);
  8.     XX(WARN, warn);
  9.     XX(ERROR, error);
  10.     XX(FATAL, fatal);
  11.     XX(DEBUG, DEBUG);
  12.     XX(INFO, INFO);
  13.     XX(WARN, WARN);
  14.     XX(ERROR, ERROR);
  15.     XX(FATAL, FATAL);
  16.     return LogLevel::UNKNOW;
  17. #undef XX
  18. }
复制代码
日志事件 class LogEvent

用于记录日志现场,比如该日志的级别,文件名/行号,日志消息,线程/协程号,所属日志器名称等。
成员变量
  1.     const char* m_file = nullptr;   //文件名
  2.     int32_t m_line = 0;             //行号
  3.     uint32_t m_elapse = 0;          //程序启动开始到现在的毫秒数
  4.     uint32_t m_thieadId = 0;        //线程id
  5.     uint32_t m_fiberId = 0;         //协程id
  6.     uint64_t m_time;                //时间戳
  7.     std::string m_threadName;       //线程名称
  8.     std::stringstream m_ss;         //日志内容流
  9.     std::shared_ptr<Logger> m_logger;   //日志器
  10.     LogLevel::Level m_level;        //日志等级
复制代码
成员函数
  1.      /**
  2.      * @brief 构造函数
  3.      *
  4.      * @param[in] logger 日志器
  5.      * @param[in] level 日志级别
  6.      * @param[in] file 文件名
  7.      * @param[in] line 文件行号
  8.      * @param[in] elapse 程序启动依赖的耗时(毫秒)
  9.      * @param[in] thread_id 线程id
  10.      * @param[in] fiber_id 协程id
  11.      * @param[in] time 日志时间
  12.      * @param[in] thread_name 线程名称
  13.      */
  14. LogEvent::LogEvent(std::shared_ptr<Logger> logger, LogLevel::Level level
  15.             , const char* file, int32_t line, uint32_t elapse
  16.             , uint32_t thread_id, uint32_t fiber_id, uint64_t time
  17.             , const std::string& thread_name)
  18.     :m_file(file)
  19.     ,m_line(line)
  20.     ,m_elapse(elapse)
  21.     ,m_thieadId(thread_id)
  22.     ,m_fiberId(fiber_id)
  23.     ,m_time(time)
  24.     ,m_threadName(thread_name)
  25.     ,m_logger(logger)
  26.     ,m_level(level) {
  27.     }
  28. void LogEvent::format(const char* fmt, ...) {
  29.     va_list al;                  //1)
  30.         va_start(al, fmt);        //2)
  31.         format(fmt, al);        //3)
  32.         va_end(al);                        //6)
  33. }
  34. void LogEvent::format(const char* fmt, va_list al){
  35.         char *buf = nullptr;
  36.     // len返回写入buf的长度
  37.         int len = vasprintf(&buf, fmt, al);        //4)
  38.         if(len != -1) {
  39.                 m_ss << std::string(buf, len);        //5)
  40.                 free(buf);
  41.         }
  42. }
复制代码
在此说一下使用日志的宏,这里定义了SYLAR_LOG_LEVEL宏,用来输出Level级别的LogEvent,并将LogEvent写入到Logger中。
  1. // 日志事件
  2. LogEvent::ptr m_event;
  3. // 构造函数
  4. LogEventWarp::LogEventWarp(LogEvent::ptr e)
  5.         :m_event(e){
  6. }
  7. // 析构函数
  8. LogEventWarp::~LogEventWarp() {
  9.         m_event->getLogger()->log(m_event->getLevel(), m_event);
  10. }
复制代码
日志输出

日志输出器 class LogAppender

class LogAppender是抽象类,有两个子类,分别为StdoutLogAppender和FileLogAppender,分别实现控制台和文件的输出。两个类都重写纯虚函数log方法实现写入日志,重写纯虚函数toYamlString方法实现将日志转化为YAML格式的字符串
成员变量
  1. #define SYLAR_LOG_LEVEL(logger, level) \
  2.         if (logger->getLevel() <= level) \
  3.                 sylar::LogEventWarp(sylar::LogEvent::ptr (new sylar::LogEvent(logger, level, \
  4.                                 __FILE__, __LINE__, 0, sylar::GetThreadId(), \
  5.                         sylar::GetFiberId(), time(0), sylar::Thread::GetName()))).getSS()
  6. #define SYLAR_LOG_DEBUG(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::DEBUG)
复制代码
成员函数
  1. // 消息format
  2. class MessageFormatItem : public LogFormatter::FormatItem{
  3. public:
  4.         MessageFormatItem(const std::string& str = "") {}
  5.         void format(std::ostream& os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override {
  6.                 os << event->getContent();
  7.         }
  8. };
  9. // 日志级别format
  10. class LevelFormatItem : public LogFormatter::FormatItem{
  11. public:
  12.         LevelFormatItem(const std::string& str = "") {}
  13.         void format(std::ostream& os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override {
  14.                 os << LogLevel::ToString(level);
  15.         }
  16. };
  17. // 执行时间format
  18. class ElapseFormatItem : public LogFormatter::FormatItem{
  19. public:
  20.         ElapseFormatItem(const std::string& str = "") {}
  21.         void format(std::ostream& os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override {
  22.                 os << event->getElapse();
  23.         }
  24. };
  25. // 日志器名称format
  26. class NameFormatItem : public LogFormatter::FormatItem{
  27. public:
  28.         NameFormatItem(const std::string& str = "") {}
  29.         void format(std::ostream& os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override {
  30.                 os << event->getLogger()->getName();
  31.         }
  32. };
  33. // 线程id format
  34. class ThreadIdFormatItem : public LogFormatter::FormatItem{
  35. public:
  36.         ThreadIdFormatItem(const std::string& str = "") {}
  37.         void format(std::ostream& os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override {
  38.                 os << event->getThieadId();
  39.         }
  40. };
  41. // 协程id format
  42. class FiberIdFormatItem : public LogFormatter::FormatItem{
  43. public:
  44.         FiberIdFormatItem(const std::string& str = "") {}
  45.         void format(std::ostream& os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override {
  46.                 os << event->getFiberId();
  47.         }
  48. };
  49. // 线程名称format
  50. class ThreadNameFormatItem : public LogFormatter::FormatItem{
  51. public:
  52.         ThreadNameFormatItem(const std::string& str = "") {}
  53.         void format(std::ostream& os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override {
  54.                 os << event->getThreadName();
  55.         }
  56. };
  57. // 时间format
  58. class DateTimeFormatItem : public LogFormatter::FormatItem{
  59. public:
  60.         DateTimeFormatItem(const std::string& format = "%Y-%m-%d %H:%M:%S")
  61.                 :m_format(format) {
  62.                         if(m_format.empty()) {
  63.                                 m_format = "%Y-%m-%d %H:%M:%S";
  64.                         }
  65.                 }
  66.         void format(std::ostream& os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) override {
  67.                 struct tm tm;
  68.                 time_t time = event->getTime();        //创建event时默认给的 time(0) 当前时间戳
  69.                 localtime_r(&time, &tm);        //将给定时间戳转换为本地时间,并将结果存储在tm中
  70.                 char buf[64];
  71.                 strftime(buf, sizeof(buf), m_format.c_str(), &tm);        //将tm格式化为m_format格式,并存储到buf中
  72.                 os << buf;
  73.         }
  74. private:
  75.         std::string m_format;
  76. };
复制代码
class StdoutLogAppender(输出到控制台的Appender)
  1. // 成员变量
  2. // 日志格式模板
  3. std::string m_pattern;
  4. // 日志格式解析后格式
  5. std::vector<FormatItem::ptr> m_items;
  6. // 判断日志格式错误
  7. bool m_error = false;
  8. // 构造函数
  9. LogFormatter::LogFormatter(const std::string& pattern)
  10.         :m_pattern(pattern) {
  11.                 init();
  12. }
  13. // 将解析后的日志信息输出到流中
  14. std::string LogFormatter::format (std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event){
  15.         std::stringstream ss;
  16.         for(auto& i : m_items) {
  17.                 i->format(ss, logger, level, event);
  18.         }
  19.         return ss.str();
  20. }
  21. // init(解析格式)
  22. // 得到相应FormatItem放入m_items
  23. // 默认格式模板为:"%d{%Y-%m-%d %H:%M:%S}%T%t%T%N%T%F%T[%p]%T[%c]%T%f:%l%T%m%n"
  24. // e.g.Y-M-D H:M:S threadId threadName fiberId [Level] [logName] FILE:LINE message
  25. //%xxx %xxx{xxx} %%
  26. // m_pattern "%d{%Y-%m-%d %H:%M:%S}%T%t%T%N%T%F%T[%p]%T[%c]%T%f:%l%T%m%n"
  27. void LogFormatter::init(){
  28.         //string, format, type
  29.         std::vector<std::tuple<std::string, std::string, int>> vec;
  30.         std::string nstr;        // 存放 [ ] :
  31.         for(size_t i = 0; i < m_pattern.size(); ++i) {
  32.                 if (m_pattern[i] != '%')        //若解析的不是'%'
  33.                 {
  34.                         nstr.append(1, m_pattern[i]);        //在nstr后面添加一个该字符
  35.                         continue;
  36.                 }
  37.                 if((i + 1) < m_pattern.size()) {        //保证m_pattern不越界
  38.                         if (m_pattern[i + 1] == '%') {        //解析 "%%"
  39.                                 nstr.append(1, '%');                //在nstr后面加上%
  40.                                 continue;
  41.                         }
  42.         
  43.                                                                                                
  44.                 size_t n = i + 1;                //遇到'%'往下  (e.g.) n = 1, m_pattern[1] = 'd'
  45.                 int fmt_status = 0;                //状态1: 解析时间{%Y-%m-%d %H:%M:%S} 状态0:解析之后的
  46.                 size_t fmt_begin = 0;        //开始位置 为{
  47.                 std::string str;                //d T t N等格式
  48.                 std::string fmt;                //保存时间格式 %Y-%m-%d %H:%M:%S       
  49.                 while(n < m_pattern.size()){
  50.             // fmt_status != 0, m_attern[n]不是字母,m_pattern[n]不是'{', m_pattern[n]不是'}'
  51.             // (e.g.) %T%  (i -> %, n -> T, while循环 n -> % 此时解析完一个T, break
  52.             // (e.g.) 遇到 [ ] break,取出[%p]中的p
  53.                         if(!fmt_status && (!isalpha(m_pattern[n]) && m_pattern[n] != '{' //返回0表示该字符不是字母字符。
  54.                                         && m_pattern[n] != '}')) {
  55.                                 str = m_pattern.substr(i + 1, n - i - 1);
  56.                                 break;
  57.                         }
  58.                         if(fmt_status == 0){        //开始解析时间格式
  59.                                 if(m_pattern[n] == '{'){
  60.                                         str = m_pattern.substr(i + 1, n - i - 1);        //str = "d"
  61.                                         fmt_status = 1;       
  62.                                         fmt_begin = n;
  63.                                         ++n;
  64.                                         continue;
  65.                                 }
  66.                         } else if(fmt_status == 1) {        //结束解析时间格式
  67.                                 if(m_pattern[n] == '}') {
  68.                     // fmt = %Y-%m-%d %H:%M:%S
  69.                                         fmt = m_pattern.substr(fmt_begin + 1, n - fmt_begin - 1);
  70.                                         fmt_status = 0;
  71.                                         ++n;
  72.                                         break;                //解析时间结束break
  73.                                 }
  74.                         }
  75.                         ++n;
  76.                         if (n == m_pattern.size()) {        //最后一个字符
  77.                                 if (str.empty()) {
  78.                                         str = m_pattern.substr(i + 1);
  79.                                 }
  80.                         }
  81.                 }
  82.                 if(fmt_status == 0){
  83.                         if(!nstr.empty()){        // nstr: [ :
  84.                                 vec.push_back(std::make_tuple(nstr, std::string(), 0));        // 将[ ]放入, type为0
  85.                                 nstr.clear();
  86.                         }
  87.                         vec.push_back(std::make_tuple(str, fmt, 1));        //(e.g.) ("d", %Y-%m-%d %H:%M:%S, 1) type为1
  88.                         i = n - 1;         //跳过已解析的字符,让i指向当前处理的字符,下个for循环会++i处理下个字符
  89.                 } else if(fmt_status == 1) {
  90.                         std::cout << "Pattern parde error: " << m_pattern << " - " << m_pattern.substr(i) << std::endl;
  91.                         m_error = true;
  92.                         vec.push_back(std::make_tuple("<<pattern_error>>", fmt, 0));
  93.                 }
  94.         }
  95.         if(!nstr.empty()) {
  96.                 vec.push_back(std::make_tuple(nstr, "", 0));        //(e.g.) 最后一个字符为[ ] :
  97.         }
  98.        
  99.     // map类型为<string, cb>, string为相应的日志格式, cb返回相应的FormatItem智能指针
  100.         static std::map<std::string, std::function<FormatItem::ptr(const std::string& fmt)> > s_format_items = {
  101. #define XX(str, C) \
  102.                 {#str, [] (const std::string& fmt) { return FormatItem::ptr(new C(fmt)); }}
  103.                 XX(m, MessageFormatItem),           //m:消息
  104.         XX(p, LevelFormatItem),             //p:日志级别
  105.         XX(r, ElapseFormatItem),            //r:累计毫秒数
  106.         XX(c, NameFormatItem),              //c:日志名称
  107.         XX(t, ThreadIdFormatItem),          //t:线程id
  108.         XX(n, NewLineFormatItem),           //n:换行
  109.         XX(d, DateTimeFormatItem),          //d:时间
  110.         XX(f, FilenameFormatItem),          //f:文件名
  111.         XX(l, LineFormatItem),              //l:行号
  112.         XX(T, TabFormatItem),               //T:Tab
  113.         XX(F, FiberIdFormatItem),           //F:协程id
  114.                 XX(N, ThreadNameFormatItem),                //N:线程名称
  115. #undef XX
  116.         };
  117.         for (auto& i : vec){
  118.                 if (std::get<2>(i) == 0) {        //若type为0
  119.             //将解析出的FormatItem放到m_items中 [ ] :
  120.                         m_items.push_back(FormatItem::ptr(new StringFormatItem(std::get<0>(i))));
  121.                 } else {        //type为1
  122.                         auto it = s_format_items.find(std::get<0>(i));        //从map中找到相应的FormatItem
  123.                         if(it == s_format_items.end()) {        //若没有找到则用StringFormatItem显示错误信息 并设置错误标志位
  124.                                 m_items.push_back(FormatItem::ptr(new StringFormatItem("<<error_format %" + std::get<0>(i) + ">>")));
  125.                                 m_error = true;
  126.                         } else {        //返回相应格式的FormatItem,其中std::get<1>(i)作为cb的参数
  127.                                 m_items.push_back(it->second(std::get<1>(i)));
  128.                         }
  129.                 }
  130.         }
  131. }
复制代码
获得主日志器
  1. //日志级别
  2. LogLevel::Level m_level = LogLevel::DEBUG;
  3. //日志格式器
  4. LogFormatter::ptr m_formatter;
  5. // 互斥锁
  6. MutexType m_mutex;
  7. // 是否有formatter
  8. bool m_hasFormatter = false;
复制代码
获得相应名字的日志器
  1. // setFormatter(更改日志格式器)
  2. void LogAppender::setFormatter(LogFormatter::ptr val) {
  3.         MutexType::Lock lock(m_mutex);
  4.         m_formatter = val;
  5.         if (m_formatter) {
  6.                 m_hasFormatter = true;
  7.         } else {
  8.                 m_hasFormatter = false;
  9.         }
  10. }
  11. // getFormatter(获得日志格式器)
  12. LogFormatter::ptr LogAppender::getFormatter() {
  13.         MutexType::Lock lock(m_mutex);
  14.         return m_formatter;
  15. }
复制代码
总结

总结一下日志模块的工作流程:


  • 初始化LogFormatter,LogAppender, Logger。
  • 通过宏定义提供流式风格和格式化风格的日志接口。每次写日志时,通过宏主动生成对应的日志事件LogEvent,并且将日志事件和日志器Logger包装到一起,生成一个LogEventWrap对象。
  • 日志接口执行结束后,LogEventWrap对象析构,在析构函数里调用Logger的log方法将日志事件举行输出。
待增补与完善

目前来看,sylar日志模块已经实现了一个完备的日志框架,并且配合后面的配置模块,可用性很高,待增补与完善的地方主要存在于LogAppender,目前只提供了输出到终端与输出到文件两类LogAppender,但从现实项目来看,以下几种类型的LogAppender都黑白常有必要的:

  • Rolling File Appender,循环覆盖写文件
  • Rolling Memory Appender,循环覆盖写内存缓冲区
  • 支持日志文件按大小分片或是按日期分片
  • 支持网络日志服务器,比如syslog

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

泉缘泉

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表