日志模块

1️⃣ 日志的作用

  • 在服务器的运行过程中,日志可以记录服务器运行过程中的各种事件,所以日志是非常有作用的。
    • 记录服务器的启动和关闭
    • 记录客户端连接/断开
    • 记录HTTP 请求的接收与响应
    • 记录协程调度执行情况
    • 记录异常或错误信息(如断网、文件打不开等)
  • 当运行过程中当出现异常行为(比如响应延迟、崩溃、死锁等)时,开发者可以通过日志快速定位问题所在:
    • 快速定位问题发生的代码逻辑
    • 分析错误发生的上下文(线程 ID、时间戳、调用路径等)
    • 重现问题流程
  • sylar 服务器框架中,使用了类似 Log4cpp 的结构,支持多级日志输出,支持 流式日志风格 写日志和 格式化风格 写日志,支持日志格式自定义,日志级别,多日志分离等等功能

2️⃣ 日志级别

  • 整体框架:
    • sylar 框架中主要使用 logLevel 表示日志级别

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      class LogLevel {
      public:
      /**
      * @brief 日志级别枚举
      */
      enum Level {
      /// 未知级别
      UNKNOW = 0,
      /// DEBUG 级别
      DEBUG = 1,
      /// INFO 级别
      INFO = 2,
      /// WARN 级别
      WARN = 3,
      /// ERROR 级别
      ERROR = 4,
      /// FATAL 级别
      FATAL = 5
      };

      /**
      * @brief 将日志级别转成文本输出
      * @param[in] level 日志级别
      */
      static const char* ToString(LogLevel::Level level);

      /**
      * @brief 将文本转换成日志级别
      * @param[in] str 日志级别文本
      */
      static LogLevel::Level FromString(const std::string& str);
      };
  • 转换函数
    • 为了方便在代码里使用,sylarlogLevel 中提供了两个方法,用于将 Level 转换成字符串,或者将字符串转换成 Level

3️⃣ 日志事件

  • 整体框架
    • sylar 中主要使用 LogEvent 表示一个日志事件

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      class LogEvent {
      public:
      typedef std::shared_ptr<LogEvent> ptr;
      /**
      * @brief 构造函数
      * @param[in] logger 日志器
      * @param[in] level 日志级别
      * @param[in] file 文件名
      * @param[in] line 文件行号
      * @param[in] elapse 程序启动依赖的耗时(毫秒)
      * @param[in] thread_id 线程id
      * @param[in] fiber_id 协程id
      * @param[in] time 日志事件(秒)
      * @param[in] thread_name 线程名称
      */
      LogEvent(std::shared_ptr<Logger> logger, LogLevel::Level level
      ,const char* file, int32_t line, uint32_t elapse
      ,uint32_t thread_id, uint32_t fiber_id, uint64_t time
      ,const std::string& thread_name);

      /**
      * @brief 返回文件名
      */
      const char* getFile() const { return m_file;}

      /**
      * @brief 返回行号
      */
      int32_t getLine() const { return m_line;}

      /**
      * @brief 返回耗时
      */
      uint32_t getElapse() const { return m_elapse;}

      /**
      * @brief 返回线程ID
      */
      uint32_t getThreadId() const { return m_threadId;}

      /**
      * @brief 返回协程ID
      */
      uint32_t getFiberId() const { return m_fiberId;}

      /**
      * @brief 返回时间
      */
      uint64_t getTime() const { return m_time;}

      /**
      * @brief 返回线程名称
      */
      const std::string& getThreadName() const { return m_threadName;}

      /**
      * @brief 返回日志内容
      */
      std::string getContent() const { return m_ss.str();}

      /**
      * @brief 返回日志器
      */
      std::shared_ptr<Logger> getLogger() const { return m_logger;}

      /**
      * @brief 返回日志级别
      */
      LogLevel::Level getLevel() const { return m_level;}

      /**
      * @brief 返回日志内容字符串流
      */
      std::stringstream& getSS() { return m_ss;}

      /**
      * @brief 格式化写入日志内容
      */
      void format(const char* fmt, ...);

      /**
      * @brief 格式化写入日志内容
      */
      void format(const char* fmt, va_list al);
      private:
      /// 文件名
      const char* m_file = nullptr;
      /// 行号
      int32_t m_line = 0;
      /// 程序启动开始到现在的毫秒数
      uint32_t m_elapse = 0;
      /// 线程ID
      uint32_t m_threadId = 0;
      /// 协程ID
      uint32_t m_fiberId = 0;
      /// 时间戳
      uint64_t m_time = 0;
      /// 线程名称
      std::string m_threadName;
      /// 日志内容流
      std::stringstream m_ss;
      /// 日志器
      std::shared_ptr<Logger> m_logger;
      /// 日志等级
      LogLevel::Level m_level;
      };
  • 重要函数:
    • LogEvent 中使用 get 方法用来提供对外的接口,返回对应的私有成员,例如:

      1
      LogLevel::Level getLevel() const { return m_level; } 
    • LogEvent 使用 format 方法来写入日志内容

      • 作用:printf 风格的可变参数格式化成字符串,再写入 LogEvent 内部的字符串流 m_ss 中。
      • 结构:
        • 第一个函数面向 外部调用,负责接收普通的 可变参数
        • 第二个函数面向 内部复用,负责接收已经整理好的 va_list,并真正完成格式化工作
      • 解析:
        • 第一层接口:
          • va_list al:专门指向函数参数的可变参数;
          • va_start(al, fmt):初始化 al,让它指向可变参数的起始位置;
          • format(fmt, al):将准备好的参数列表 al 传给另一个重载函数处理;
          • va_end(al);:结束对可变参数列表的使用,做清理工作。
        • 第二层接口:
          • vasprintf(&buf, fmt, al):根据格式串 fmt 和参数列表 al,动态分配一块足够大的内存,把格式化后的字符串写进去,并把地址赋给 buf
          • m_ss << std::string(buf, len):将格式化的日志写入到 整体日志
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      void LogEvent::format(const char* fmt, ...) {
      va_list al;
      va_start(al, fmt);
      format(fmt, al);
      va_end(al);
      }

      void LogEvent::format(const char* fmt, va_list al) {
      char* buf = nullptr;
      int len = vasprintf(&buf, fmt, al);
      if(len != -1) {
      m_ss << std::string(buf, len);
      free(buf);
      }
      }

4️⃣ 日志格式化器

  • LogFormatter 作用

    • sylar 框架中,LogFormatter 是日志系统的 格式化核心 模块。它负责将 日志事件LogEvent)格式化为 字符串,使其能够以我们设定的格式输出到日志文件、控制台或其他日志输出目标。
  • 为什么需要 LogFormatter

    • 直接输出 LogEvent 结构体是不直观的,比如
      • INFO 2025-06-11 13:45:12 thread1 [main] - something happened
    • LogFormatter 就是用来定义和控制这个 “格式模板” 的,比如你希望日志长这样:
      • 2025-06-11 13:37:42 [0ms] 8127 Scheduler_0 99955 [INFO] [system] /home/gch/sylar/src/middleware/middleware.cpp:18
      • 这就是为什么需要使用 LogFormatter 来格式化。
  • 默认格式

    • 默认格式是%%d{%%Y-%%m-%%d %%H:%%M:%%S}%%T%%t%%T%%N%%T%%F%%T[%%p]%%T[%%c]%%T%%f:%%l%%T%%m%%n
    • 默认格式描述: 年-月-日 时:分:秒 [累计运行毫秒数] \\t 线程id \\t 线程名称 \\t 协程id \\t [日志级别] \\t [日志器名称] \\t 文件名:行号 \\t 日志消息 换行符
  • 整体框架

    • sylar 框架中主要使用 LogFormatter 表示一个日志格式化器。

    logFormatter

  • 解析

    • 使用 日志格式化器 时,其会调用函数 init()解析给定日志格式模板
    • 之后将调用函数 format 用来格式化日志,这就会遍历 日志格式化器 的成员 m_items
    • 由于 m_items 存放的是 FormateItem::ptr,而 FormateItem::ptr 是基类指针,所以可以用基类指针来动态调用虚函数,执行的都是派生类对象的方法,所以派生类对象都会执行自己的 format 方法。这就可以将每条 LogEvent不同部分 交给对应的 FormateItem::ptr 来进行格式化输出。
  • 主要成员

    1
    2
    3
    std::string m_pattern;                  // 日志模板格式
    std::vector<FormateItem::ptr> m_items; // 用于存放不同的日志处理类
    bool m_error = false; // 用于判断日志模板格式化的时候是否出错
  • 重要函数

    • init()

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      void LogFormatter::init()
      {
      /**
      * @brief
      * 简单的状态机判断,提取pattern中的常规字符和模式字符
      * 解析的过程就是从头到尾遍历,根据状态标志决定当前字符是常规字符还是模式字符
      * 一共有两种状态,即正在解析常规字符和正在解析模板转义字符
      */

      // 按顺序存储从m_pattern解析到的patterns
      std::vector<std::pair<int,std::string>> patterns;
      // 判断解析过程是否出错
      bool error = false;
      // 临时存储常规字符串
      std::string temp;
      // 存储日期格式的字符串%d{}
      std::string date;
      // 判断是否在解析常规字符串
      bool parse_normal = true;
      // 从m_pattern的起始字符串开始
      size_t i = 0;
      while (i < m_pattern.size())
      {
      std::string c = std::string(1, m_pattern[i]);
      // 首先判断是否是%
      if (c == "%")
      {
      if (parse_normal) // 这表示前面一个字符不是%,仍然是普通字符
      {
      patterns.push_back(std::make_pair(0, temp));
      temp.clear();
      parse_normal = false;
      ++i;
      continue;
      }
      else
      {
      // 这说明%是转义字符,直接将其添加到patterns
      patterns.push_back(std::make_pair(1,"%"));
      parse_normal = true;
      ++i;
      continue;
      }
      }
      else // 这表示当前字符不是%
      {
      if (parse_normal) // 这表示前面一个字符不是%,仍然是普通字符
      {
      temp += c;
      ++i;
      continue;
      }

      else
      {
      // 这表示是模板字符
      patterns.push_back(std::make_pair(1,c));
      parse_normal = true;

      if (c != "d") // 这说明不是日期格式
      {
      ++i;
      continue;
      }
      else
      {
      // 这表明是日期格式
      ++i;
      if (i < m_pattern.size() && m_pattern[i] != '{')
      {
      // 说明格式错误
      continue;
      }
      else
      {
      while (i < m_pattern.size() && m_pattern[i] != '}')
      {
      date.push_back(m_pattern[i]);
      ++i;
      }
      }
      if (m_pattern[i] != '}')
      {
      // %d后面的大括号没有闭合,直接报错
      std::cout << "[ERROR] LogFormatter::init() " << "pattern: [" << m_pattern << "] '{' not closed" << std::endl;
      error = true;
      break;
      }
      ++i;
      continue;
      }
      }
      }
      }

      // 判断是否出错
      if (error)
      {
      m_error = error;
      return;
      }
      // 循环结束完,需要将剩余的字符串也添加到patterns中
      if (!temp.empty())
      {
      patterns.push_back(std::make_pair(0,temp));
      temp.clear();
      }

      // 定义具体的日志处理类,根据日志模板来调用对应的日志处理函数
      static std::map<std::string, std::function<FormateItem::ptr(const std::string&)>> s_format_item =
      {
      {"m", CreateFormateItem<MessageFormatItem>},
      {"p", CreateFormateItem<LevelFormatItem>},
      {"c", CreateFormateItem<LoggerNameFormatItem>},
      {"r", CreateFormateItem<ElapseFormatItem>},
      {"f", CreateFormateItem<FileNameFormatItem>},
      {"l", CreateFormateItem<LineFormatItem>},
      {"t", CreateFormateItem<ThreadIdFormatItem>},
      {"F", CreateFormateItem<FiberIdFormatItem>},
      {"N", CreateFormateItem<ThreadNameFormatItem>},
      {"%", CreateFormateItem<PercentSignFormatItem>},
      {"T", CreateFormateItem<TabFormatItem>},
      {"n", CreateFormateItem<NewLineFormatItem>}
      };

      for (auto iterator : patterns)
      {
      if (iterator.first == 0)
      {
      // 这是在处理普通的常规字符
      m_items.push_back(FormateItem::ptr(new StringFormatItem(iterator.second)));
      }
      else if (iterator.second == "d")
      {
      // 这是在处理日志中的日期
      m_items.push_back(FormateItem::ptr(new DateTimeFormatItem(date)));
      }
      else
      {
      // 这是在处理模板字符
      if (auto it = s_format_item.find(iterator.second); it != s_format_item.end())
      {
      // 这说明能够找到对应模板字符的类
      m_items.push_back(it->second(iterator.second));
      }
      else
      {
      // 这表示该字符不是模板字符
      std::cout << "[ERROR] LogFormatter::init() " << "pattern: [" << m_pattern << "] " << "unknown format item: " << iterator.second << std::endl;
      error = true;
      break;
      }
      }
      }

      if(error) {
      m_error = true;
      return;
      }
      }
    • format(LogEvent::ptr event)

      • 作用: 按照 预先解析好的格式项 顺序,将 日志器日志级别日志事件 中的信息逐项写入字符串流,最终拼接成一条 完整的格式化日志字符串 并返回
      1
      2
      3
      4
      5
      6
      7
      std::string LogFormatter::format(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) {
      std::stringstream ss;
      for(auto& i : m_items) {
      i->format(ss, logger, level, event);
      }
      return ss.str();
      }

5️⃣ 日志输出地

  • LogAppender 的作用

    • sylar 框架中,LogAppender 是日志系统中的 核心抽象类。它负责 格式化后的日志 输出到具体的目标位置(如 控制台文件远程服务器 等)。
  • 核心概念

    • 在日志系统中,日志的生成与日志的输出是分离的LogAppender 就是 日志输出端 的抽象类,允许我们将同一条日志灵活地输出到多个位置。
  • 整体架构

    • 概述
      • LogAppender 的目的是将 日志 按照 指定的格式 输出到 我们想要指定的地方
      • LogAppender 一定会拥有成员 LogFormatter,用于解析日志;
      • 同时为了将日志输出到指定地点,我们需要继承 LogAppender,因为我们需要根据想要输出的地点封装不同的类。
    • 不管是 StdCoutLogAppender 还是 FileLogAppender,都只需要覆写 log 函数,将日志输出到不同的地点。就算我们还想将日志输出到不同的地点,我们只需要继承 LogAppender,覆写 log 函数就可以了。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    class LogAppender {
    friend class Logger;
    public:
    typedef std::shared_ptr<LogAppender> ptr;
    typedef Spinlock MutexType;

    /**
    * @brief 析构函数
    */
    virtual ~LogAppender() {}

    /**
    * @brief 写入日志
    * @param[in] logger 日志器
    * @param[in] level 日志级别
    * @param[in] event 日志事件
    */
    virtual void log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) = 0;

    /**
    * @brief 将日志输出目标的配置转成YAML String
    */
    virtual std::string toYamlString() = 0;

    /**
    * @brief 更改日志格式器
    */
    void setFormatter(LogFormatter::ptr val);

    /**
    * @brief 获取日志格式器
    */
    LogFormatter::ptr getFormatter();

    /**
    * @brief 获取日志级别
    */
    LogLevel::Level getLevel() const { return m_level;}

    /**
    * @brief 设置日志级别
    */
    void setLevel(LogLevel::Level val) { m_level = val;}

    virtual bool reopen() { return true;}

    protected:
    /// 日志级别
    LogLevel::Level m_level = LogLevel::DEBUG;
    /// 是否有自己的日志格式器
    bool m_hasFormatter = false;
    /// Mutex
    MutexType m_mutex;
    /// 日志格式器
    LogFormatter::ptr m_formatter;
    };
  • LoggerAppender 种类

    • 控制台(StdCoutLogAppender

      1
      2
      3
      4
      5
      6
      void StdoutLogAppender::log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) {
      if(level >= m_level) {
      MutexType::Lock lock(m_mutex);
      m_formatter->format(std::cout, logger, level, event);
      }
      }
    • 文件(FileAppender

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      void FileLogAppender::log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) {
      if(level >= m_level) {
      uint64_t now = event->getTime();
      if(now >= (m_lastTime + 3)) {
      reopen();
      m_lastTime = now;
      }
      MutexType::Lock lock(m_mutex);

      if(!m_formatter->format(m_filestream, logger, level, event)) {
      std::cout << "error" << std::endl;
      }
      }
      }
    • 日志服务器(LogserverAppender

      • 作用: 把一条达到输出级别的日志先格式化成字符串,再封装成 通知消息,通过 负载均衡logserver 集群里挑一个连接发出去;如果发送失败,就最多重试 3 次。
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        void LogserverAppender::log(Logger::ptr logger, LogLevel::Level level, LogEvent::ptr event) {
        // 日志级别过滤
        if(level >= m_level) {
        // 日志事件格式化成最终字符串
        std::stringstream ss;
        MutexType::Lock lock(m_mutex);
        m_formatter->format(ss, logger, level, event);
        lock.unlock();

        // 把格式化后的日志内容封装成业务消息
        logserver::LogNotify nty;
        nty.set_body(ss.str());
        nty.set_topic(m_topic);
        nty.set_key(m_key);

        // 业务消息包成 Rock 协议通知
        sylar::RockNotify::ptr rock_nty = std::make_shared<sylar::RockNotify>();
        rock_nty->setNotify(100);
        rock_nty->setAsPB(nty);

        // 尝试最多发送 3 次
        for(size_t i = 0; i < 3; ++i) {
        // 懒加载获取日志服务器负载均衡器
        if(!m_lb) {
        auto app = sylar::Application::GetInstance()->getRockSDLoadBalance();
        if(app) {
        m_lb = app->get("logserver", "logserver", true);
        }
        }

        if(!m_lb) {
        continue;
        }

        // 从负载均衡器里选一个可用节点
        auto item = m_lb->get();
        if(!item) {
        continue;
        }

        // 拿到和该节点的 Rock 连接
        auto conn = item->getStreamAs<sylar::RockStream>();
        if(!conn) {
        continue;
        }

        // 通过连接发送消息
        if(conn->sendMessage(rock_nty) <= 0) {
        continue;
        }
        return;
        }

        //SYLAR_LOG_ERROR(g_logger) << "send to logserver fail, " << ss.str();
        }
        }

6️⃣ 日志器

  • Logger 的职责

    • 通过 LogLevel 管理日志的等级控制
      • 每个 Logger 对象都有一个等级(如 DEBUGINFOWARNERRORFATAL),只会记录大于或等于当前等级的日志。
    • 组织多个输出目标( LogAppender
      • Logger 并不直接将日志写入文件或控制台,而是将其分发给多个日志输出器( LogAppender ),这些输出器可以是 文件控制台远程服务器 等。
    • 保证线程安全的日志处理流程
      • 通过互斥锁(如 Spinlock )保证多线程环境下的安全访问,防止日志混乱或丢失。
    • 生成日志元数据
      • 比如记录日志创建时间( m_createTime )、名称( m_name )等,方便后期分析与归档。
  • 整体框架

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    class Logger : public std::enable_shared_from_this<Logger> {
    friend class LoggerManager;
    public:
    typedef std::shared_ptr<Logger> ptr;
    typedef RWSpinlock RWMutexType;

    /**
    * @brief 构造函数
    * @param[in] name 日志器名称
    */
    Logger(const std::string& name = "root");

    /**
    * @brief 写日志
    * @param[in] level 日志级别
    * @param[in] event 日志事件
    */
    void log(LogLevel::Level level, LogEvent::ptr event);

    /**
    * @brief 写debug级别日志
    * @param[in] event 日志事件
    */
    void debug(LogEvent::ptr event);

    /**
    * @brief 写info级别日志
    * @param[in] event 日志事件
    */
    void info(LogEvent::ptr event);

    /**
    * @brief 写warn级别日志
    * @param[in] event 日志事件
    */
    void warn(LogEvent::ptr event);

    /**
    * @brief 写error级别日志
    * @param[in] event 日志事件
    */
    void error(LogEvent::ptr event);

    /**
    * @brief 写fatal级别日志
    * @param[in] event 日志事件
    */
    void fatal(LogEvent::ptr event);

    /**
    * @brief 添加日志目标
    * @param[in] appender 日志目标
    */
    void addAppender(LogAppender::ptr appender);

    /**
    * @brief 删除日志目标
    * @param[in] appender 日志目标
    */
    void delAppender(LogAppender::ptr appender);

    /**
    * @brief 清空日志目标
    */
    void clearAppenders();

    /**
    * @brief 返回日志级别
    */
    LogLevel::Level getLevel() const { return m_level;}

    /**
    * @brief 设置日志级别
    */
    void setLevel(LogLevel::Level val) { m_level = val;}

    /**
    * @brief 返回日志名称
    */
    const std::string& getName() const { return m_name;}

    /**
    * @brief 设置日志格式器
    */
    void setFormatter(LogFormatter::ptr val);

    /**
    * @brief 设置日志格式模板
    */
    void setFormatter(const std::string& val);

    /**
    * @brief 获取日志格式器
    */
    LogFormatter::ptr getFormatter();

    /**
    * @brief 将日志器的配置转成YAML String
    */
    std::string toYamlString();

    bool reopen();
    private:
    /// 日志名称
    std::string m_name;
    /// 日志级别
    LogLevel::Level m_level;
    /// Mutex
    RWMutexType m_mutex;
    /// 日志目标集合
    std::list<LogAppender::ptr> m_appenders;
    /// 日志格式器
    LogFormatter::ptr m_formatter;
    /// 主日志器
    Logger::ptr m_root;
    };
  • 解析

    • 日志器 存放了一组日志输出地 LogAppender,当调用函数 log 记录一个日志的时候,如果该日志的等级大于 日志器 的等级,就会遍历所有的 日志输出地,使用 日志输出地 将其解析并输出到指定的地方。

7️⃣ 日志事件包装器

  • LogEventWrap 的作用

    • 在实现一个高效、灵活的日志系统时,我们往往会设计一个 LogEvent 类来描述一次具体的日志行为,同时用 Logger 类负责将这些事件格式化、输出到终端、文件或远程服务器等目标媒介。
    • 但在实际开发中,仅靠 LogEventLogger 可能并不足以满足需求,特别是在 使用宏定义简化日志语句 的场景下,如何优雅地延迟日志输出、自动触发日志写入成为一个实际问题。
  • 整体框架

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    class LogEventWrap {
    public:

    /**
    * @brief 构造函数
    * @param[in] e 日志事件
    */
    LogEventWrap(LogEvent::ptr e);

    /**
    * @brief 析构函数
    */
    ~LogEventWrap();

    /**
    * @brief 获取日志事件
    */
    LogEvent::ptr getEvent() const { return m_event;}

    /**
    * @brief 获取日志内容流
    */
    std::stringstream& getSS();
    private:
    /**
    * @brief 日志事件
    */
    LogEvent::ptr m_event;
    };
  • 关键函数

    1
    2
    3
    LogEventWrap::~LogEventWrap() {
    m_event->getLogger()->log(m_event->getLevel(), m_event);
    }
    • 解析
      • LogEventWrap 在构造时持有 **日志事件 LogEvent**。
      • 析构时 自动调用 日志事件 对应的日志器的 log() 方法,将当前日志事件正式输出。
      • 这使得 日志语句 可以自然地延迟到作用域结束时统一输出。
  • 关键宏定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    #define SYLAR_LOG_LEVEL(logger, level) \
    if(logger->getLevel() <= level) \
    sylar::LogEventWrap(std::make_shared<sylar::LogEvent>(logger, level, \
    __FILE__, __LINE__, 0, sylar::GetThreadId(),\
    sylar::GetFiberId(), time(0), sylar::Thread::GetName())).getSS()

    /**
    * @brief 使用流式方式将日志级别debug的日志写入到logger
    */
    #define SYLAR_LOG_DEBUG(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::DEBUG)

    /**
    * @brief 使用流式方式将日志级别info的日志写入到logger
    */
    #define SYLAR_LOG_INFO(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::INFO)

    /**
    * @brief 使用流式方式将日志级别warn的日志写入到logger
    */
    #define SYLAR_LOG_WARN(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::WARN)

    /**
    * @brief 使用流式方式将日志级别error的日志写入到logger
    */
    #define SYLAR_LOG_ERROR(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::ERROR)

    /**
    * @brief 使用流式方式将日志级别fatal的日志写入到logger
    */
    #define SYLAR_LOG_FATAL(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::FATAL)

    /**
    * @brief 使用格式化方式将日志级别level的日志写入到logger
    */
    #define SYLAR_LOG_FMT_LEVEL(logger, level, fmt, ...) \
    if(logger->getLevel() <= level) \
    sylar::LogEventWrap(std::make_shared<sylar::LogEvent>(logger, level, \
    __FILE__, __LINE__, 0, sylar::GetThreadId(),\
    sylar::GetFiberId(), time(0), sylar::Thread::GetName())).getEvent()->format(fmt, __VA_ARGS__)

    /**
    * @brief 使用格式化方式将日志级别debug的日志写入到logger
    */
    #define SYLAR_LOG_FMT_DEBUG(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::DEBUG, fmt, __VA_ARGS__)

    /**
    * @brief 使用格式化方式将日志级别info的日志写入到logger
    */
    #define SYLAR_LOG_FMT_INFO(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::INFO, fmt, __VA_ARGS__)

    /**
    * @brief 使用格式化方式将日志级别warn的日志写入到logger
    */
    #define SYLAR_LOG_FMT_WARN(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::WARN, fmt, __VA_ARGS__)

    /**
    * @brief 使用格式化方式将日志级别error的日志写入到logger
    */
    #define SYLAR_LOG_FMT_ERROR(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::ERROR, fmt, __VA_ARGS__)

    /**
    * @brief 使用格式化方式将日志级别fatal的日志写入到logger
    */
    #define SYLAR_LOG_FMT_FATAL(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::FATAL, fmt, __VA_ARGS__)

8️⃣ 日志器管理类

  • LoggerManager 的作用

    • 在构建一个功能完善的日志系统中,我们不仅需要能够生成和输出日志的 Logger 类,还需要一个统一的日志管理中心来组织和协调多个日志器的使用。这就是 LoggerManager 类存在的意义。
  • 为什么需要 LoggerManager ?

    • 如果希望为每个模块配置不同的日志器,以便控制它们的日志级别、输出格式、日志文件路径等。这时,如果我们每次都手动创建和维护多个 Logger 实例,不但容易出错,而且很难集中管理。
  • 整体框架

    loggerManager

  • 重要函数

    • getLogger()

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      Logger::ptr LoggerManager::getLogger(const std::string& name)
      {
      MutexType::Lock lock(m_mutex);

      auto it = m_loggers.find(name);
      if (it != m_loggers.end())
      {
      return it->second;
      }
      Logger::ptr logger(new Logger(name));
      logger->addAppender(LogAppender::ptr(new StdoutLogAppender));
      m_loggers[name] = logger;
      return logger;
      }

      如果在日志器管理类 LoggerManager 中找不到对应名称的日志器 Logger,就自动创建一个对应名称的日志器 Logger,并返回给该日志器。

  • 解析

    • LoggerManager 的构造函数里,默认都有一个日志器 root,该日志器的日志输出地是控制台 StdCoutAppender,并被注册到了 LoggerManager 的成员日志器集合中 m_loggers

    • 主要用途在:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      /**
      * @brief 获得root日志器
      */
      #define SYLAR_LOG_ROOT() sylar::LoggerMgr::GetInstance()->getRoot()

      /**
      * @brief 获得指定名称的日志器
      */
      #define SYLAR_LOG_NAME(name) sylar::LoggerMgr::GetInstance()->getLogger(name)

      /**
      * 单例模式的日志管理器
      */
      using LoggerMgr = sylar::SingleTon<LoggerManager>;
    • 使用单例模式获得 LoggerManager 的实例。

9️⃣ 日志配置

LogAppenderDefine

  • 作用: 描述一个 LogAppender 配置描述结构体
    • 通常用于从 YAML 配置文件 中读取 日志输出器 信息,再根据这些配置去创建真正的 LogAppender

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      struct LogAppenderDefine {
      int type = 0; //1 File, 2 Stdout, 3 Logserver
      LogLevel::Level level = LogLevel::UNKNOW;
      std::string formatter;
      std::string file;

      bool operator==(const LogAppenderDefine& oth) const {
      return type == oth.type
      && level == oth.level
      && formatter == oth.formatter
      && file == oth.file;
      }
      };

LogDefine

  • 作用: 描述一个完整 Logger 的配置项,用于保存某个日志器的名称、日志级别、默认格式器以及它关联的所有输出器配置。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    struct LogDefine {
    std::string name;
    LogLevel::Level level = LogLevel::UNKNOW;
    std::string formatter;
    std::vector<LogAppenderDefine> appenders;

    bool operator==(const LogDefine& oth) const {
    return name == oth.name
    && level == oth.level
    && formatter == oth.formatter
    && appenders == appenders;
    }

    bool operator<(const LogDefine& oth) const {
    return name < oth.name;
    }

    bool isValid() const {
    return !name.empty();
    }
    };

日志配置序列化与反序列化

  • 日志配置反序列化器(LexicalCast<std::string, LogDefine>

    • 作用:
      • 负责将 YAML 形式日志配置 解析为 LogDefine 对象。
      • 它会从配置中提取 logger 的名称、日志级别、格式器以及各个 appender 的配置信息,并组装成程序内部可用的日志配置结构。
      • 本质上,它承担的是 “把配置文件中的文本描述转成内存中的日志配置对象” 的职责,是日志配置加载过程中的入口。
  • 日志配置反序列化器(LexicalCast<LogDefine, std::string>

    • 作用:
      • 将程序内部的 LogDefine 对象重新转换成 YAML 形式的配置内容。
      • logger名称级别格式器 以及所有 appender 配置重新组织成 可输出的配置文本
      • 本质上,它承担的是 “把内存中的日志配置对象转回文本配置” 的职责,便于配置保存、打印、比较或调试。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    template<>
    class LexicalCast<std::string, LogDefine> {
    public:
    LogDefine operator()(const std::string& v) {
    YAML::Node n = YAML::Load(v);
    LogDefine ld;
    if(!n["name"].IsDefined()) {
    std::cout << "log config error: name is null, " << n
    << std::endl;
    throw std::logic_error("log config name is null");
    }
    ld.name = n["name"].as<std::string>();
    ld.level = LogLevel::FromString(n["level"].IsDefined() ? n["level"].as<std::string>() : "");
    if(n["formatter"].IsDefined()) {
    ld.formatter = n["formatter"].as<std::string>();
    }

    if(n["appenders"].IsDefined()) {
    //std::cout << "==" << ld.name << " = " << n["appenders"].size() << std::endl;
    for(size_t x = 0; x < n["appenders"].size(); ++x) {
    auto a = n["appenders"][x];
    if(!a["type"].IsDefined()) {
    std::cout << "log config error: appender type is null, " << a
    << std::endl;
    continue;
    }
    std::string type = a["type"].as<std::string>();
    LogAppenderDefine lad;
    if(type == "FileLogAppender") {
    lad.type = 1;
    if(!a["file"].IsDefined()) {
    std::cout << "log config error: fileappender file is null, " << a
    << std::endl;
    continue;
    }
    lad.file = a["file"].as<std::string>();
    if(a["formatter"].IsDefined()) {
    lad.formatter = a["formatter"].as<std::string>();
    }
    } else if(type == "StdoutLogAppender") {
    lad.type = 2;
    if(a["formatter"].IsDefined()) {
    lad.formatter = a["formatter"].as<std::string>();
    }
    } else if(type == "LogserverAppender") {
    lad.type = 3;
    if(!a["topic"].IsDefined()) {
    std::cout << "log config error: logserverappender topic is null, " << a
    << std::endl;
    continue;
    }
    lad.file = a["topic"].as<std::string>();

    if(a["formatter"].IsDefined()) {
    lad.formatter = a["formatter"].as<std::string>();
    }
    } else {
    std::cout << "log config error: appender type is invalid, " << a
    << std::endl;
    continue;
    }

    ld.appenders.push_back(lad);
    }
    }
    return ld;
    }
    };
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    template<>
    class LexicalCast<LogDefine, std::string> {
    public:
    std::string operator()(const LogDefine& i) {
    YAML::Node n;
    n["name"] = i.name;
    if(i.level != LogLevel::UNKNOW) {
    n["level"] = LogLevel::ToString(i.level);
    }
    if(!i.formatter.empty()) {
    n["formatter"] = i.formatter;
    }

    for(auto& a : i.appenders) {
    YAML::Node na;
    if(a.type == 1) {
    na["type"] = "FileLogAppender";
    na["file"] = a.file;
    } else if(a.type == 2) {
    na["type"] = "StdoutLogAppender";
    } else if(a.type == 3) {
    na["type"] = "LogserverAppender";
    }
    if(a.level != LogLevel::UNKNOW) {
    na["level"] = LogLevel::ToString(a.level);
    }

    if(!a.formatter.empty()) {
    na["formatter"] = a.formatter;
    }

    n["appenders"].push_back(na);
    }
    std::stringstream ss;
    ss << n;
    return ss.str();
    }
    };

全局配置项(g_log_defines

  • 作用:
    • 用于在配置系统中注册一个名为 logs 的配置项,其类型为 std::set<LogDefine>
    • logs 保存的是整个系统所有 logger 的配置集合,相当于日志模块在内存中的总配置入口
    • 后续无论是 配置加载配置比较,还是 热更新监听,都是围绕这个 全局配置变量 展开的。
1
2
sylar::ConfigVar<std::set<LogDefine> >::ptr g_log_defines =
sylar::Config::Lookup("logs", std::set<LogDefine>(), "logs config");

日志配置热更新初始化器(LogIniter

  • 作用:
    • 在构造时为 g_log_defines 注册一个 配置变更监听器,用于感知日志配置的 新增、修改和删除
    • logs 配置发生变化时,它会根 新旧配置差异,动态地创建、更新或移除对应的 Logger
    • 对于变更的 logger,它会重新设置日志级别、格式器,并按配置重新挂载 FileLogAppenderStdoutLogAppenderLogserverAppender
    • 它本质上承担的是 “将配置变化实时同步到日志系统运行状态中” 的职责,是日志热更新机制的核心入口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
struct LogIniter {
LogIniter() {
g_log_defines->addListener([](const std::set<LogDefine>& old_value,
const std::set<LogDefine>& new_value){
SYLAR_LOG_INFO(SYLAR_LOG_ROOT()) << "on_logger_conf_changed";
for(auto& i : new_value) {
auto it = old_value.find(i);
sylar::Logger::ptr logger;
if(it == old_value.end()) {
//新增logger
logger = SYLAR_LOG_NAME(i.name);
} else {
if(!(i == *it)) {
//修改的logger
logger = SYLAR_LOG_NAME(i.name);
} else {
continue;
}
}
logger->setLevel(i.level);
//std::cout << "** " << i.name << " level=" << i.level
//<< " " << logger << std::endl;
if(!i.formatter.empty()) {
logger->setFormatter(i.formatter);
}

logger->clearAppenders();
for(auto& a : i.appenders) {
sylar::LogAppender::ptr ap;
if(a.type == 1) {
ap = std::make_shared<FileLogAppender>(a.file);
} else if(a.type == 2) {
if(!sylar::EnvMgr::GetInstance()->has("d")) {
ap = std::make_shared<StdoutLogAppender>();
} else {
continue;
}
} else if(a.type == 3) {
ap = std::make_shared<LogserverAppender>(a.file);
}
ap->setLevel(a.level);
if(!a.formatter.empty()) {
LogFormatter::ptr fmt = std::make_shared<LogFormatter>(a.formatter);
if(!fmt->isError()) {
ap->setFormatter(fmt);
} else {
std::cout << "log.name=" << i.name << " appender type=" << a.type
<< " formatter=" << a.formatter << " is invalid" << std::endl;
}
}
logger->addAppender(ap);
}
}

for(auto& i : old_value) {
auto it = new_value.find(i);
if(it == new_value.end()) {
//删除logger
auto logger = SYLAR_LOG_NAME(i.name);
logger->setLevel((LogLevel::Level)0);
logger->clearAppenders();
}
}
});
}
};

日志模块整体框架图

参考资料: