【C++大型项目之高性能服务器框架 (一) 】一切物语的开始:日志系统&配置系统篇

⭐️在这个怀疑的年代,我们依然需要信仰。

个人主页 :YYYing.

⭐️C++大型项目系列专栏:C++大型项目之高性能服务器框架

系列下期内容:暂无


目录

前言:

一、日志系统介绍

二、整体架构图

三、宏定义详解(log.h)

[3.1 SYLAR_LOG_LEVEL ------ 流式日志核心宏](#3.1 SYLAR_LOG_LEVEL —— 流式日志核心宏)

[3.2 各级别流式日志宏](#3.2 各级别流式日志宏)

[3.3 SYLAR_LOG_FMT_LEVEL ------ 格式化日志核心宏](#3.3 SYLAR_LOG_FMT_LEVEL —— 格式化日志核心宏)

[3.4 各级别格式化日志宏](#3.4 各级别格式化日志宏)

[3.5 日志器获取宏](#3.5 日志器获取宏)

[四、LogLevel 类详解](#四、LogLevel 类详解)

[4.1 定义(log.h)](#4.1 定义(log.h))

[4.2 ToString() (log.cc)](#4.2 ToString() (log.cc))

[4.3 FromString() 实现(log.cc)](#4.3 FromString() 实现(log.cc))

[五、LogEvent 类详解](#五、LogEvent 类详解)

[5.1 定义(log.h)](#5.1 定义(log.h))

[5.2 构造函数实现(log.cc)](#5.2 构造函数实现(log.cc))

[5.3 format() 实现(log.cc)](#5.3 format() 实现(log.cc))

[六、LogEventWrap 类详解](#六、LogEventWrap 类详解)

[6.1 定义(log.h)](#6.1 定义(log.h))

[6.2 构造函数与析构函数(log.cc)](#6.2 构造函数与析构函数(log.cc))

[6.3 getSS() 实现(log.cc)](#6.3 getSS() 实现(log.cc))

[七、Logger 类详解](#七、Logger 类详解)

[7.1 定义(log.h)](#7.1 定义(log.h))

[7.2 构造函数(log.cc)](#7.2 构造函数(log.cc))

[7.3 log() ------ 核心分发函数(log.cc)](#7.3 log() —— 核心分发函数(log.cc))

[7.4 便捷方法(log.cc)](#7.4 便捷方法(log.cc))

[7.5 addAppender()(log.cc)](#7.5 addAppender()(log.cc))

[7.6 setFormatter()(log.cc)](#7.6 setFormatter()(log.cc))

[7.7 delAppender() / clearAppenders()(log.cc)](#7.7 delAppender() / clearAppenders()(log.cc))

[八、LogAppender 类详解](#八、LogAppender 类详解)

[8.1 定义(log.h)](#8.1 定义(log.h))

[8.2 setFormatter() / getFormatter()(log.cc)](#8.2 setFormatter() / getFormatter()(log.cc))

[九、LogFormatter 类详解](#九、LogFormatter 类详解)

[9.1 定义(log.h)](#9.1 定义(log.h))

[9.2 构造函数(log.cc)](#9.2 构造函数(log.cc))

[9.3 format() 实现(log.cc)](#9.3 format() 实现(log.cc))

[9.4 init() ------ 模板解析核心(log.cc)](#9.4 init() —— 模板解析核心(log.cc))

[9.4.1 解析状态机](#9.4.1 解析状态机)

[9.4.2 主循环(逐字符扫描)](#9.4.2 主循环(逐字符扫描))

[9.4.3 转义 %%](#9.4.3 转义 %%)

[9.4.4 解析 %X 或 %X{fmt}](#9.4.4 解析 %X 或 %X{fmt})

[9.4.5 将解析结果压入 vec](#9.4.5 将解析结果压入 vec)

[9.4.6 格式项工厂映射表](#9.4.6 格式项工厂映射表)

[9.4.7 生成 m_items](#9.4.7 生成 m_items)

[9.5 各 FormatItem 实现(log.cc)](#9.5 各 FormatItem 实现(log.cc))

[十、StdoutLogAppender 详解](#十、StdoutLogAppender 详解)

[10.1 定义(log.h)](#10.1 定义(log.h))

[10.2 log() 实现(log.cc)](#10.2 log() 实现(log.cc))

[十一、FileLogAppender 详解](#十一、FileLogAppender 详解)

[11.1 定义(log.h)](#11.1 定义(log.h))

[11.2 构造函数(log.cc)](#11.2 构造函数(log.cc))

[11.3 log() 实现(log.cc)](#11.3 log() 实现(log.cc))

[11.4 reopen() 实现(log.cc)](#11.4 reopen() 实现(log.cc))

[十二、LoggerManager 详解](#十二、LoggerManager 详解)

[12.1 定义(log.h)](#12.1 定义(log.h))

[12.2 构造函数(log.cc)](#12.2 构造函数(log.cc))

[12.3 getLogger()(log.cc)](#12.3 getLogger()(log.cc))

十三、配置系统介绍

[13.1 配置结构体](#13.1 配置结构体)

[13.2 YAML 配置 ↔ 结构体转换](#13.2 YAML 配置 ↔ 结构体转换)

[13.3 配置监听器与热更新](#13.3 配置监听器与热更新)

[13.4 ConfigVarBase() 实现(config.h)](#13.4 ConfigVarBase() 实现(config.h))

[13.5 ConfigVar() 实现(config.h)](#13.5 ConfigVar() 实现(config.h))

[13.6 Config() 实现(config.h)](#13.6 Config() 实现(config.h))

[13.7 ListAllMember() 实现(confirf.cc)](#13.7 ListAllMember() 实现(confirf.cc))

[13.8 基本类型集合处理与转换](#13.8 基本类型集合处理与转换)

[13.9 配置变更事件](#13.9 配置变更事件)

[13.10 配置系统与日志系统的联动](#13.10 配置系统与日志系统的联动)

十四、日志系统完整调用链

十五、线程安全总结

十六、学习验证清单

结语

---⭐️封面自取⭐️---



前言:

本项目是基于小电视里sylar大佬的项目来做的一个项目总结,其多为一些项目思考与笔记,可能还会有一些图解之类的讲解,但光看本专栏学习此项目肯定是不足的,多去跟着视频敲敲代码或者自己下去实现实现各个模块。由于小生经验不足,这个系列专栏制作周期可能会稍微有点长,甚至有可能会出现断更的情况,但我尽量往完写,往各位大佬多多包涵,话不多说,我们开启我们项目的第一个内容:日志系统

一、日志系统介绍

日志系统是我们项目首要的重中之重,任何一个系统第一步肯定需要把日志系统设计好,当做服务器框架时,日志是很重要的东西,服务器出问题或者需要做统计还有分析等情况的时候,都需要根据我们日志系统来看,一个好的日志系统可以让我们的开发、后勤事半功倍。

我们日志系统的总体框架是先做一个仿Log4J的日志,总共由Logger,Formatter,Appender组成,Logger是用来定义日志类别的,Formatter是用来定日志格式的,而Appender决定了日志的输出地方,之所以这么区分开,不过也就是因为混在一起很混乱罢了,很显而易见的原因。

这次的内容我们将不会采用像之前内存池那样一步一步去讲,对于内存池的区区2000行代码还好,这里这么做也太地狱了,这里我们只说重点或难理解的代码段与思想,想跟着写代码的过程就去看视频,再看来此专栏。

我们先来看看此日志管理器的整体架构设计:


二、整体架构图

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                         用户代码层                               │
│  SYLAR_LOG_INFO(logger) << "msg";                               │
│  SYLAR_LOG_FMT_INFO(logger, "name=%s", name);                   │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                       LogEventWrap (RAII)                       │
│  构造时创建 LogEvent,析构时自动调用 logger->log()                │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                        LogEvent (日志事件)                       │
│  封装一次日志记录所需的全部信息:文件、行号、线程ID、协程ID、时间等   │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                        Logger (日志器)                          │
│  管理日志级别(m_level) + Appender列表(m_appenders)               │
│  调用 log(level, event) → 遍历所有 Appender                      │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                     LogAppender (输出地)                         │
│  ├─ StdoutLogAppender:输出到 std::cout                          │
│  └─ FileLogAppender:输出到文件,支持自动 reopen                  │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                     LogFormatter (格式化器)                     │
│  解析模板字符串 → 生成 FormatItem 列表 → format() 输出            │
└─────────────────────────────────────────────────────────────────┘

三、宏定义详解(log.h)

那么简单看完我们整体的架构,我们现在就来说说宏定义中的难点:

3.1 SYLAR_LOG_LEVEL ------ 流式日志核心宏

cpp 复制代码
#define SYLAR_LOG_LEVEL(logger, level) \
    if(logger->getLevel() <= level) \
        sylar::LogEventWrap(sylar::LogEvent::ptr(new sylar::LogEvent(logger, level, \
                        __FILE__, __LINE__, 0, sylar::GetThreadId(),\
                sylar::GetFiberId(), time(0), sylar::Thread::GetName()))).getSS()

逐参数拆解:

参数 类型 说明
logger Logger::ptr 目标日志器
level LogLevel::Level 日志级别

宏展开后等价代码:

cpp 复制代码
if (logger->getLevel() <= sylar::LogLevel::INFO)
    sylar::LogEventWrap(
        sylar::LogEvent::ptr(
            new sylar::LogEvent(
                logger,                           // logger: 日志器
                sylar::LogLevel::INFO,            // level: 日志级别
                __FILE__,                         // file: 当前文件名(编译器宏)
                __LINE__,                         // line: 当前行号(编译器宏)
                0,                                // elapse: 程序启动耗时(毫秒),此处固定传0
                sylar::GetThreadId(),             // thread_id: 当前线程ID
                sylar::GetFiberId(),              // fiber_id: 当前协程ID
                time(0),                          // time: 当前时间戳(秒)
                sylar::Thread::GetName()          // thread_name: 当前线程名称
            )
        )
    ).getSS()

关键设计:

  • if(logger->getLevel() <= level)性能优化核心。如果日志器级别高于当前级别(比如 logger 是 WARN,当前是 INFO),则后面的对象构造、字符串流操作全部不会执行。

  • LogEventWrap(...).getSS():创建一个临时对象 ,返回其内部的 stringstream,用户在后面通过 << 向其中写入内容。

  • RAII 自动输出 :语句结束时临时对象销毁,调用 ~LogEventWrap(),内部自动执行 logger->log(level, event)

可能此处会对第三点有点懵,没关系我们后面讲到这个日志事件包装器的时候会解释。


3.2 各级别流式日志宏

cpp 复制代码
#define SYLAR_LOG_DEBUG(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::DEBUG)
#define SYLAR_LOG_INFO(logger)  SYLAR_LOG_LEVEL(logger, sylar::LogLevel::INFO)
#define SYLAR_LOG_WARN(logger)  SYLAR_LOG_LEVEL(logger, sylar::LogLevel::WARN)
#define SYLAR_LOG_ERROR(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::ERROR)
#define SYLAR_LOG_FATAL(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::FATAL)

只是对上面那个宏 SYLAR_LOG_LEVEL 的包装,这次固定了第二个参数 level


3.3 SYLAR_LOG_FMT_LEVEL ------ 格式化日志核心宏

cpp 复制代码
#define SYLAR_LOG_FMT_LEVEL(logger, level, fmt, ...) \
    if(logger->getLevel() <= level) \
        sylar::LogEventWrap(sylar::LogEvent::ptr(new sylar::LogEvent(logger, level, \
                        __FILE__, __LINE__, 0, sylar::GetThreadId(),\
                sylar::GetFiberId(), time(0), sylar::Thread::GetName()))).getEvent()->format(fmt, __VA_ARGS__)

与流式宏的区别:

  • 流式宏最后调用 .getSS(),返回 stringstream&,供用户用 << 追加内容。

  • 格式化宏最后调用 .getEvent()->format(fmt, __VA_ARGS__),直接调用 LogEvent::format() 将格式化字符串写入 m_ss,同样在 LogEventWrap 析构时输出。

不过此处的fomat也有点说法在的,我们接着往下走。


3.4 各级别格式化日志宏

和上面流式一样,不做过多解释。

cpp 复制代码
#define SYLAR_LOG_FMT_DEBUG(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::DEBUG, fmt, __VA_ARGS__)
#define SYLAR_LOG_FMT_INFO(logger, fmt, ...)  SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::INFO, fmt, __VA_ARGS__)
#define SYLAR_LOG_FMT_WARN(logger, fmt, ...)  SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::WARN, fmt, __VA_ARGS__)
#define SYLAR_LOG_FMT_ERROR(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::ERROR, fmt, __VA_ARGS__)
#define SYLAR_LOG_FMT_FATAL(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::FATAL, fmt, __VA_ARGS__)

3.5 日志器获取宏

cpp 复制代码
#define SYLAR_LOG_ROOT() sylar::LoggerMgr::GetInstance()->getRoot()
  • 返回主日志器 (root logger),通过单例 LoggerMgr 获取。
cpp 复制代码
#define SYLAR_LOG_NAME(name) sylar::LoggerMgr::GetInstance()->getLogger(name)
  • 按名称获取日志器。如果不存在,会自动创建 一个新的 Logger,并将其 m_root 指向 root logger。

四、LogLevel 类详解

这个是日志级别类

4.1 定义(log.h)

cpp 复制代码
class LogLevel {
public:
    enum Level {
        UNKNOW = 0,   // 未知级别
        DEBUG = 1,    // 调试
        INFO = 2,     // 信息
        WARN = 3,     // 警告
        ERROR = 4,    // 错误
        FATAL = 5     // 致命
    };
​
    static const char* ToString(LogLevel::Level level);
    static LogLevel::Level FromString(const std::string& str);
};

设计要点:

  • 使用 enum 而非 enum class,因为需要与整数比较(level >= m_level)。

  • 级别数值越大,级别越高。DEBUG < INFO < WARN < ERROR < FATAL


4.2 ToString()log.cc

cpp 复制代码
const char* LogLevel::ToString(LogLevel::Level level) {
    switch(level) {
#define XX(name) \
    case LogLevel::name: \
        return #name; \
        break;
    XX(DEBUG);
    XX(INFO);
    XX(WARN);
    XX(ERROR);
    XX(FATAL);
#undef XX
    default:
        return "UNKNOW";
    }
    return "UNKNOW";
}

此处就很牛福了,写到这其实我相信大部分人的直觉都是,去拿这个level然后就直接对应着switch - case,然后去return,虽然没有任何问题,但在C++中这样写"不优雅"。那么sylar的常用手法就是上面这种写法,看着非常舒适,且耦合性降低了。但有些人没接触过这类写法,我们来看看这块的技巧:

宏技巧:

  • #name 是 C++ 的字符串化操作符,将宏参数转为字符串字面量。

  • XX(DEBUG) 展开为 case LogLevel::DEBUG: return "DEBUG"; break;

  • 优点:避免手写 case 和字符串,减少出错。


4.3 FromString() 实现(log.cc

cpp 复制代码
LogLevel::Level LogLevel::FromString(const std::string& str) {
#define XX(level, v) \
    if(str == #v) { \
        return LogLevel::level; \
    }
    XX(DEBUG, debug);
    XX(INFO, info);
    XX(WARN, warn);
    XX(ERROR, error);
    XX(FATAL, fatal);
    XX(DEBUG, DEBUG);  // 同时支持小写和大写
    XX(INFO, INFO);
    XX(WARN, WARN);
    XX(ERROR, ERROR);
    XX(FATAL, FATAL);
    return LogLevel::UNKNOW;
#undef XX
}

功能: 将字符串 "debug""DEBUG""info" 等转换为对应的枚举值。大小写都支持。如果无法匹配,返回 UNKNOW


五、LogEvent 类详解

5.1 定义(log.h)

LogEvent 封装了一次日志记录所需的全部上下文信息

cpp 复制代码
class LogEvent {
public:
    typedef std::shared_ptr<LogEvent> ptr;
​
    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);
​
    const char* getFile() const { return m_file;}
    int32_t getLine() const { return m_line;}
    uint32_t getElapse() const { return m_elapse;}
    uint32_t getThreadId() const { return m_threadId;}
    uint32_t getFiberId() const { return m_fiberId;}
    uint64_t getTime() const { return m_time;}
    const std::string& getThreadName() const { return m_threadName;}
    std::string getContent() const { return m_ss.str();}
    std::shared_ptr<Logger> getLogger() const { return m_logger;}
    LogLevel::Level getLevel() const { return m_level;}
    std::stringstream& getSS() { return m_ss;}
​
    void format(const char* fmt, ...);
    void format(const char* fmt, va_list al);
​
private:
    const char* m_file = nullptr;       // 文件名(__FILE__)
    int32_t m_line = 0;                 // 行号(__LINE__)
    uint32_t m_elapse = 0;              // 程序启动到现在的毫秒数
    uint32_t m_threadId = 0;            // 线程ID
    uint32_t m_fiberId = 0;             // 协程ID
    uint64_t m_time = 0;                // 时间戳(秒)
    std::string m_threadName;           // 线程名称
    std::stringstream m_ss;             // 日志内容字符串流
    std::shared_ptr<Logger> m_logger;   // 所属日志器
    LogLevel::Level m_level;            // 日志级别
};

成员变量表:

变量名 类型 默认值 含义
m_file const char* nullptr 源码文件名
m_line int32_t 0 源码行号
m_elapse uint32_t 0 程序启动后经过的毫秒数
m_threadId uint32_t 0 线程ID
m_fiberId uint32_t 0 协程ID
m_time uint64_t 0 Unix 时间戳(秒)
m_threadName std::string "" 线程名称
m_ss std::stringstream 默认构造 存储日志内容的流
m_logger Logger::ptr nullptr 该事件所属的 Logger
m_level LogLevel::Level 未初始化 该事件的日志级别

5.2 构造函数实现(log.cc

cpp 复制代码
LogEvent::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)
    :m_file(file)
    ,m_line(line)
    ,m_elapse(elapse)
    ,m_threadId(thread_id)
    ,m_fiberId(fiber_id)
    ,m_time(time)
    ,m_threadName(thread_name)
    ,m_logger(logger)
    ,m_level(level) {
}

纯初始化列表,无函数体。注意 m_ss 没有出现在初始化列表中,使用默认构造。


5.3 format() 实现(log.cc

cpp 复制代码
void LogEvent::format(const char* fmt, ...) {
    va_list al;           // 声明一个 va_list 变量,用于存储可变参数
    va_start(al, fmt);    // 初始化:从 fmt 之后开始提取参数
    format(fmt, al);      // 转发给第二个重载函数处理
    va_end(al);           // 清理 va_list(必须配对使用)
}
​
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);
    }
}
  • vasprintf:GNU 扩展函数,功能类似 vsprintf,但自动 malloc 内存

  • 如果格式化成功(len != -1),将内容写入 m_ss,然后 free(buf)

  • 这是 C 风格格式化(printf)的支持入口。

那么为什么要这么写呢?

这种"双函数"模式是 C++ 日志系统的经典设计

cpp 复制代码
用户调用                    内部转发
┌────────────────────┐     ┌────────────────────┐
│ format("x=%d", 42) │  →  │ format(fmt, al)    │
│     (va_start)     │     │  (vasprintf)       │
└────────────────────┘     └────────────────────┘

好处

  1. 解耦 :外部接口只负责解析 ...,内部只处理 va_list

  2. 可复用 :如果其他地方已经有 va_list,可以直接调用第二个函数

  3. 类型安全边界:把 C 风格的可变参数封装在内部

但仍有一部分潜在问题,不过此处暂时不讨论,感兴趣的可以先问问ai。


六、LogEventWrap 类详解

这个类相当于是在进行对我们一次日志事件的RAII封装

6.1 定义(log.h)

cpp 复制代码
class LogEventWrap {
public:
    LogEventWrap(LogEvent::ptr e);
    ~LogEventWrap();
    LogEvent::ptr getEvent() const { return m_event;}
    std::stringstream& getSS();
private:
    LogEvent::ptr m_event;
};

设计目的:RAII 封装

流式日志的使用方式是:

cpp 复制代码
SYLAR_LOG_INFO(logger) << "hello" << 123;

展开后等价于:

cpp 复制代码
if (logger->getLevel() <= INFO)
    LogEventWrap(event).getSS() << "hello" << 123;

那么展开来讲也就是这里 LogEventWrap(event) 创建了一个临时对象.getSS() 返回了 stringstream&,用户通过 << 向流中写数据。整条语句结束时,临时对象销毁,析构函数自动被调用


6.2 构造函数与析构函数(log.cc

cpp 复制代码
LogEventWrap::LogEventWrap(LogEvent::ptr e)
    :m_event(e) {
}
​
LogEventWrap::~LogEventWrap() {
    m_event->getLogger()->log(m_event->getLevel(), m_event);
}

析构时的行为:

  1. 通过 m_event->getLogger() 获取该事件绑定的 Logger。

  2. 调用 logger->log(m_event->getLevel(), m_event),将日志事件正式交给 Logger 处理。

这也就是流式日志自动输出的秘密 :不需要用户手动调用 log(),靠 C++ 临时对象的析构时机自动触发。


6.3 getSS() 实现(log.cc

cpp 复制代码
std::stringstream& LogEventWrap::getSS() {
    return m_event->getSS();
}

直接透传 LogEvent 内部的 m_ss


七、Logger 类详解

这个类的作用主要是对日志进行操作,如直接写日志到appender,设置formmat等。

7.1 定义(log.h)

cpp 复制代码
class Logger : public std::enable_shared_from_this<Logger> {
friend class LoggerManager;
public:
    typedef std::shared_ptr<Logger> ptr;
    typedef Spinlock MutexType;
​    
    // 构造函数,参数:日志器名称
    Logger(const std::string& name = "root");
​
    // 写日志,参数:级别与等级
    void log(LogLevel::Level level, LogEvent::ptr event);
    // 以下都是写各级别日志
    void debug(LogEvent::ptr event);
    void info(LogEvent::ptr event);
    void warn(LogEvent::ptr event);
    void error(LogEvent::ptr event);
    void fatal(LogEvent::ptr event);
​
    // 此处分别为添加日志目标,删除日志目标,清空日志目标
    void addAppender(LogAppender::ptr appender);
    void delAppender(LogAppender::ptr appender);
    void clearAppenders();
​
    // 返回日志级别,设置日志级别,返回日志名称
    LogLevel::Level getLevel() const { return m_level;}
    void setLevel(LogLevel::Level val) { m_level = val;}
    const std::string& getName() const { return m_name;}
​
    // 设置日志格式器,设置日志格式模板,获取日志格式器,将日志器的配置转成YAML String
    void setFormatter(LogFormatter::ptr val);
    void setFormatter(const std::string& val);
    LogFormatter::ptr getFormatter();
    std::string toYamlString();
​
private:
    std::string m_name;                    // 日志器名称
    LogLevel::Level m_level;               // 日志级别
    MutexType m_mutex;
    std::list<LogAppender::ptr> m_appenders;  // Appender 列表
    LogFormatter::ptr m_formatter;         // 日志格式器
    Logger::ptr m_root;                    // 指向 root logger(备用)
};

继承 std::enable_shared_from_this<Logger> 的原因:

log() 中需要获取自身的 shared_ptr

cpp 复制代码
auto self = shared_from_this();

然后传给 Appender 的 log(self, level, event)


7.2 构造函数(log.cc

cpp 复制代码
Logger::Logger(const std::string& name)
    :m_name(name)
    ,m_level(LogLevel::DEBUG) {
    m_formatter.reset(new LogFormatter("%d{%Y-%m-%d %H:%M:%S}%T%t%T%N%T%F%T[%p]%T[%c]%T%f:%l%T%m%n"));
}

默认格式模板拆解:

bash 复制代码
%d{%Y-%m-%d %H:%M:%S}   → 2023-01-01 12:00:00
%T                      → \t
%t                      → 线程ID
%T                      → \t
%N                      → 线程名称
%T                      → \t
%F                      → 协程ID
%T                      → \t
[%p]                    → [INFO]
%T                      → \t
[%c]                    → [root]
%T                      → \t
%f:%l                   → main.cc:42
%T                      → \t
%m                      → 日志消息
%n                      → 换行

7.3 log() ------ 核心分发函数(log.cc

cpp 复制代码
void Logger::log(LogLevel::Level level, LogEvent::ptr event) {
    if(level >= m_level) {   // 级别过滤
        auto self = shared_from_this();
        MutexType::Lock lock(m_mutex);
        if(!m_appenders.empty()) {
            for(auto& i : m_appenders) {
                i->log(self, level, event);   // 遍历所有 Appender 输出
            }
        } else if(m_root) {
            m_root->log(level, event);        // 没有 Appender,委托给 root
        }
    }
}

逻辑分支:

  1. if(level >= m_level):先进行 Logger 级别的过滤。

  2. 如果有 Appender,逐个调用 appender->log(self, level, event)

  3. 如果没有 Appender 但有 m_root,将日志委托给 root logger(这是一种降级策略)。


7.4 便捷方法(log.cc

cpp 复制代码
void Logger::debug(LogEvent::ptr event) { log(LogLevel::DEBUG, event); }
void Logger::info(LogEvent::ptr event)  { log(LogLevel::INFO, event); }
void Logger::warn(LogEvent::ptr event)  { log(LogLevel::WARN, event); }
void Logger::error(LogEvent::ptr event) { log(LogLevel::ERROR, event); }
void Logger::fatal(LogEvent::ptr event) { log(LogLevel::FATAL, event); }

此处相当于是日志系统提供了两套使用方式:

第一种就是流式日志(最常用,通过宏)

cpp 复制代码
 SYLAR_LOG_INFO(logger) << "Server started on port " << 8080;
  - 优点是写法自然,像 cout 一样。
  - 底层靠 LogEventWrap 的 RAII 自动触发。

第二种就是手动构造 LogEvent + 便捷方法:

cpp 复制代码
// 手动创建一个日志事件
  sylar::LogEvent::ptr event(new sylar::LogEvent(
      logger,
      sylar::LogLevel::INFO,
      __FILE__, __LINE__, 0,
      sylar::GetThreadId(),
      sylar::GetFiberId(),
      time(0),
      sylar::Thread::GetName()
  ));

  // 手动写入内容
  event->getSS() << "Server started on port " << 8080;

  // 直接调用便捷方法输出
  logger->info(event);

或者更简洁地:

cpp 复制代码
  auto event = std::make_shared<LogEvent>(...);
  event->format("Server started on port %d", 8080);
  logger->info(event);

7.5 addAppender()log.cc

cpp 复制代码
void Logger::addAppender(LogAppender::ptr appender) {
    MutexType::Lock lock(m_mutex);
    if(!appender->getFormatter()) {   // 如果 Appender 没有自己的 formatter
        MutexType::Lock ll(appender->m_mutex);
        appender->m_formatter = m_formatter;   // 继承 Logger 的 formatter
    }
    m_appenders.push_back(appender);
}

注意:appender->m_mutex 加锁时,使用的是 appender 自己的锁,不是 Logger 的锁。这是细粒度锁设计。


7.6 setFormatter()log.cc

cpp 复制代码
void Logger::setFormatter(LogFormatter::ptr val) {
    MutexType::Lock lock(m_mutex);
    m_formatter = val;
​
    for(auto& i : m_appenders) {
        MutexType::Lock ll(i->m_mutex);
        if(!i->m_hasFormatter) {   // 只更新那些没有独立 formatter 的 Appender
            i->m_formatter = m_formatter;
        }
    }
}
​
void Logger::setFormatter(const std::string& val) {
    std::cout << "---" << val << std::endl;
    sylar::LogFormatter::ptr new_val(new sylar::LogFormatter(val));
    if(new_val->isError()) {
        std::cout << "Logger setFormatter name=" << m_name
                  << " value=" << val << " invalid formatter"
                  << std::endl;
        return;
    }
    setFormatter(new_val);   // 委托给 ptr 版本
}
  • setFormatter(string) 会先构造 LogFormatter,委托到上面的ptr版本,如果解析出错(isError()),则不设置。

  • setFormatter(ptr) 会将新的 formatter 赋给 Logger,并同步更新所有未设置独立 formatter 的 Appender。


7.7 delAppender() / clearAppenders()log.cc

这块没什么好讲的了。

cpp 复制代码
void Logger::delAppender(LogAppender::ptr appender) {
    MutexType::Lock lock(m_mutex);
    for(auto it = m_appenders.begin(); it != m_appenders.end(); ++it) {
        if(*it == appender) {
            m_appenders.erase(it);
            break;
        }
    }
}
​
void Logger::clearAppenders() {
    MutexType::Lock lock(m_mutex);
    m_appenders.clear();
}

八、LogAppender 类详解

这个类的作用是规定我们日志的输出目标

8.1 定义(log.h)

cpp 复制代码
class LogAppender {
friend class Logger;   // Logger 可以直接访问 Appender 的私有/保护成员
public:
    typedef std::shared_ptr<LogAppender> ptr;
    typedef Spinlock MutexType;
​
    virtual ~LogAppender() {}
    // 写入日志,日志器,日志级别,日志事件
    virtual void log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) = 0;
    // 将日志输出目标的配置转成YAML String
    virtual std::string toYamlString() = 0;
​    
    // 更改日志格式器
    void setFormatter(LogFormatter::ptr val);
    // 获取日志格式器
    LogFormatter::ptr getFormatter();
    // 获取日志级别
    LogLevel::Level getLevel() const { return m_level; }
    // 设置日志级别
    void setLevel(LogLevel::Level val) { m_level = val; }
​
protected:
    LogLevel::Level m_level = LogLevel::DEBUG;   // Appender 自身的级别过滤
    bool m_hasFormatter = false;                  // 是否有独立的 formatter
    MutexType m_mutex;
    LogFormatter::ptr m_formatter;
};

关键设计:

  • friend class Logger:Logger 在 addAppender()setFormatter() 中需要直接访问 m_mutexm_hasFormatter

  • m_hasFormatter:标记该 Appender 是否有独立 的 formatter。如果没有(false),则使用 Logger 的 formatter。

  • m_level:Appender 级别的二次过滤。即使 Logger 允许输出,Appender 也可以拒绝。


8.2 setFormatter() / getFormatter()log.cc

cpp 复制代码
void LogAppender::setFormatter(LogFormatter::ptr val) {
    MutexType::Lock lock(m_mutex);
    m_formatter = val;
    if(m_formatter) {
        m_hasFormatter = true;
    } else {
        m_hasFormatter = false;
    }
}
​
LogFormatter::ptr LogAppender::getFormatter() {
    MutexType::Lock lock(m_mutex);
    return m_formatter;
}

使用 Spinlock 保护 m_formatter,保证线程安全。


九、LogFormatter 类详解

这个类的作用主要是,一个字符一个字符地扫描模板字符串,把普通文本和 %X/%X{fmt} 占位符切分成一个个 FormatItem 对象,存到 m_items 数组里。

它做的不是"输出日志",而是编译格式模板------把声明式的字符串变成命令式的对象序列。后续日志输出时,只需按顺序执行这些命令即可。

9.1 定义(log.h)

LogFormatter 负责解析格式模板字符串 ,并将其拆分为一系列 FormatItem,最终按顺序输出。

cpp 复制代码
class LogFormatter {
public:
    typedef std::shared_ptr<LogFormatter> ptr;

     /**
     * 构造函数
     *  格式模板
     *  %m 消息
     *  %p 日志级别
     *  %r 累计毫秒数
     *  %c 日志名称
     *  %t 线程id
     *  %n 换行
     *  %d 时间
     *  %f 文件名
     *  %l 行号
     *  %T 制表符
     *  %F 协程id
     *  %N 线程名称
     *
     *  默认格式 "%d{%Y-%m-%d %H:%M:%S}%T%t%T%N%T%F%T[%p]%T[%c]%T%f:%l%T%m%n"
     */​    

    LogFormatter(const std::string& pattern);
    // 返回格式化日志文本, 日志器,日志级别,日志事件
    std::string format(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event);
    std::ostream& format(std::ostream& ofs, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event);
​    
    // 日志内容项格式化
    class FormatItem {
    public:
        typedef std::shared_ptr<FormatItem> ptr;
        virtual ~FormatItem() {}
        // 格式化日志到流,os 日志输出流,日志器,日志等级,日志事件
        virtual void format(std::ostream& os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) = 0;
    };
​    // 初始化,解析日志模板
    void init();
    // 是否有错误
    bool isError() const { return m_error;}
    // 返回日志模版
    const std::string getPattern() const { return m_pattern;}
​
private:
    std::string m_pattern;                       // 原始模板字符串
    std::vector<FormatItem::ptr> m_items;        // 解析后的格式项列表
    bool m_error = false;                        // 解析是否有错误
};

9.2 构造函数(log.cc

cpp 复制代码
LogFormatter::LogFormatter(const std::string& pattern)
    :m_pattern(pattern) {
    init();
}

在构造时立即调用 init() 解析模板字符串。


9.3 format() 实现(log.cc

cpp 复制代码
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();
}
​
std::ostream& LogFormatter::format(std::ostream& ofs, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) {
    for(auto& i : m_items) {
        i->format(ofs, logger, level, event);
    }
    return ofs;
}
  • 重载 1:返回 std::string,用于需要字符串的场景。

  • 重载 2:直接写入 std::ostream,避免中间的 stringstream 拷贝,性能更好

  • FileLogAppenderStdoutLogAppender 都使用重载 2。


9.4 init() ------ 模板解析核心(log.cc

这是日志系统中最复杂的函数之一,负责解析形如 "%d{%Y-%m-%d %H:%M:%S}%T%t%T[%p]%T%m%n" 的模板。

9.4.1 解析状态机

cpp 复制代码
void LogFormatter::init() {
    // str, format, type
    std::vector<std::tuple<std::string, std::string, int> > vec;
    std::string nstr;
  • vec:三元组列表 (str, fmt, type)

    • type = 0:普通字符串

    • type = 1:需要解析的格式项(以 % 开头)

  • nstr:累积的普通字符串缓冲区。


9.4.2 主循环(逐字符扫描)

cpp 复制代码
for(size_t i = 0; i < m_pattern.size(); ++i) {
    if(m_pattern[i] != '%') {
        nstr.append(1, m_pattern[i]);   // 普通字符,累积到 nstr
        continue;
    }
}

如果当前字符不是 %,直接追加到 nstr


9.4.3 转义 %%

cpp 复制代码
if((i + 1) < m_pattern.size()) {
    if(m_pattern[i + 1] == '%') {
        nstr.append(1, '%');   // %% 表示字面量 %
        continue;
    }
}

%% 是转义,输出一个普通的 % 字符。


9.4.4 解析 %X%X{fmt}

cpp 复制代码
size_t n = i + 1;
int fmt_status = 0;       // 0=解析key, 1=解析{...}中的fmt
size_t fmt_begin = 0;

std::string str;          // key(如 'd', 't')
std::string fmt;          // { } 中的格式字符串
​
while(n < m_pattern.size()) {
    // 状态0:解析key,遇到非字母且非{和非}的字符,说明key结束
    if(!fmt_status && (!isalpha(m_pattern[n]) && m_pattern[n] != '{' && m_pattern[n] != '}')) {
        str = m_pattern.substr(i + 1, n - i - 1);
        break;
    }
​
    if(fmt_status == 0) {
        if(m_pattern[n] == '{') {
            str = m_pattern.substr(i + 1, n - i - 1);  // key 结束
            fmt_status = 1;
            fmt_begin = n;
            ++n;
            continue;
        }
    } else if(fmt_status == 1) {
        if(m_pattern[n] == '}') {
            fmt = m_pattern.substr(fmt_begin + 1, n - fmt_begin - 1);
            fmt_status = 0;
            ++n;
            break;
        }
    }
    ++n;
    if(n == m_pattern.size()) {
        if(str.empty()) {
            str = m_pattern.substr(i + 1);
        }
    }
}

状态转换:

  • fmt_status = 0:正在读取 key(如 dtm

  • 如果遇到 {,切换到 fmt_status = 1,读取 {} 中的子格式(如 %d{%Y-%m-%d} 中的日期格式)

  • 如果遇到非字母且不是 {} 的字符,key 结束。


9.4.5 将解析结果压入 vec

cpp 复制代码
if(fmt_status == 0) {
    if(!nstr.empty()) {
        vec.push_back(std::make_tuple(nstr, std::string(), 0));
        nstr.clear();
    }
    vec.push_back(std::make_tuple(str, fmt, 1));
    i = n - 1;
} else if(fmt_status == 1) {
        // 解析错误:{ 没有匹配的 }
        std::cout << "pattern parse error: " << m_pattern << " - " << m_pattern.substr(i) << std::endl;
        m_error = true;
        vec.push_back(std::make_tuple("<<pattern_error>>", fmt, 0));
    }
}
​
if(!nstr.empty()) {
    vec.push_back(std::make_tuple(nstr, "", 0));
}
  • 普通字符串(type=0)直接放入 vec

  • 格式项(type=1)放入 vec,后续需要查找对应的 FormatItem 工厂。

  • 如果 fmt_status == 1 结束循环(即遇到未闭合的 {),标记 m_error = true


9.4.6 格式项工厂映射表

cpp 复制代码
static std::map<std::string, std::function<FormatItem::ptr(const std::string& str)> > s_format_items = {
#define XX(str, C) \
    {#str, [](const std::string& fmt) { return FormatItem::ptr(new C(fmt));}}
​
    XX(m, MessageFormatItem),           // %m: 消息内容
    XX(p, LevelFormatItem),             // %p: 日志级别
    XX(r, ElapseFormatItem),            // %r: 累计毫秒数
    XX(c, NameFormatItem),              // %c: 日志器名称
    XX(t, ThreadIdFormatItem),          // %t: 线程id
    XX(n, NewLineFormatItem),           // %n: 换行
    XX(d, DateTimeFormatItem),          // %d: 时间
    XX(f, FilenameFormatItem),          // %f: 文件名
    XX(l, LineFormatItem),              // %l: 行号
    XX(T, TabFormatItem),               // %T: Tab
    XX(F, FiberIdFormatItem),           // %F: 协程id
    XX(N, ThreadNameFormatItem),        // %N: 线程名称
#undef XX
};

这是一个 static 局部变量,只会初始化一次。每个 lambda 是一个工厂函数,接收 fmt 字符串(即 {} 中的内容),返回对应的 FormatItem 对象。


9.4.7 生成 m_items

cpp 复制代码
for(auto& i : vec) {
    if(std::get<2>(i) == 0) {
        // 普通字符串 → StringFormatItem
        m_items.push_back(FormatItem::ptr(new StringFormatItem(std::get<0>(i))));
    } else {
        auto it = s_format_items.find(std::get<0>(i));
        if(it == s_format_items.end()) {
            // 未识别的格式符
            m_items.push_back(FormatItem::ptr(new StringFormatItem("<<error_format %" + std::get<0>(i) + ">>")));
            m_error = true;
        } else {
            // 调用工厂函数
            m_items.push_back(it->second(std::get<1>(i)));
        }
    }
}

9.5 各 FormatItem 实现(log.cc

类名 对应占位符 输出内容
MessageFormatItem %m event->getContent()
LevelFormatItem %p LogLevel::ToString(level)
ElapseFormatItem %r event->getElapse()
NameFormatItem %c event->getLogger()->getName()
ThreadIdFormatItem %t event->getThreadId()
FiberIdFormatItem %F event->getFiberId()
ThreadNameFormatItem %N event->getThreadName()
DateTimeFormatItem %d{...} 按指定格式输出时间
FilenameFormatItem %f event->getFile()
LineFormatItem %l event->getLine()
NewLineFormatItem %n std::endl
TabFormatItem %T "\t"
StringFormatItem 普通字符串 原样输出

DateTimeFormatItem 特殊处理:

cpp 复制代码
class DateTimeFormatItem : public LogFormatter::FormatItem {
public:
    DateTimeFormatItem(const std::string& format = "%Y-%m-%d %H:%M:%S")
        :m_format(format) {
        if(m_format.empty()) {
            m_format = "%Y-%m-%d %H:%M:%S";
        }
    }
​
    void format(std::ostream& os, Logger::ptr logger, LogLevel::Level level, LogEvent::ptr event) override {
        struct tm tm;
        time_t time = event->getTime();
        localtime_r(&time, &tm);   // 线程安全的 localtime
        char buf[64];
        strftime(buf, sizeof(buf), m_format.c_str(), &tm);
        os << buf;
    }
private:
    std::string m_format;
};
  • localtime_r 是 POSIX 线程安全版本(_r 表示 reentrant)。

  • strftimem_format 格式化时间到 buf


十、StdoutLogAppender 详解

这个是输出到控制台的Appender

10.1 定义(log.h)

cpp 复制代码
class StdoutLogAppender : public LogAppender {
public:
    typedef std::shared_ptr<StdoutLogAppender> ptr;
    void log(Logger::ptr logger, LogLevel::Level level, LogEvent::ptr event) override;
    std::string toYamlString() override;
};

10.2 log() 实现(log.cc

cpp 复制代码
void StdoutLogAppender::log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) {
    if(level >= m_level) {   // Appender 级别过滤
        MutexType::Lock lock(m_mutex);
        m_formatter->format(std::cout, logger, level, event);
    }
}
  • 先判断 level >= m_level,Appender 可以独立设置级别,实现更细粒度的过滤。

  • 加锁后调用 m_formatter->format(std::cout, ...) 直接输出到控制台。


十一、FileLogAppender 详解

这个是输出到文件中的Appender

11.1 定义(log.h)

cpp 复制代码
class FileLogAppender : public LogAppender {
public:
    typedef std::shared_ptr<FileLogAppender> ptr;
    FileLogAppender(const std::string& filename);
    void log(Logger::ptr logger, LogLevel::Level level, LogEvent::ptr event) override;
    std::string toYamlString() override;

    // 重新打开日志文件
    bool reopen();
private:
    std::string m_filename;       // 文件路径
    std::ofstream m_filestream;   // 文件流
    uint64_t m_lastTime = 0;      // 上次 reopen 的时间戳
};

11.2 构造函数(log.cc

cpp 复制代码
FileLogAppender::FileLogAppender(const std::string& filename)
    :m_filename(filename) {
    reopen();
}

构造时立即尝试打开文件。


11.3 log() 实现(log.cc

cpp 复制代码
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)) {   // 每隔至少3秒尝试 reopen
            reopen();
            m_lastTime = now;
        }
        MutexType::Lock lock(m_mutex);
        if(!m_formatter->format(m_filestream, logger, level, event)) {
            std::cout << "error" << std::endl;
        }
    }
}

reopen() 策略:

  • m_lastTime 记录上次 reopen 的时间戳。

  • if(now >= m_lastTime + 3):每 3 秒 最多 reopen 一次。

  • 为什么需要 reopen? 因为日志文件可能被日志切割工具(如 logrotate)移动或删除,reopen 可以保证文件句柄有效。


11.4 reopen() 实现(log.cc

cpp 复制代码
bool FileLogAppender::reopen() {
    MutexType::Lock lock(m_mutex);
    if(m_filestream) {
        m_filestream.close();
    }
    return FSUtil::OpenForWrite(m_filestream, m_filename, std::ios::app);
}
  • 先关闭旧流,再以 std::ios::app(追加模式)重新打开。

  • FSUtil::OpenForWrite 是 sylar 的工具函数,负责创建目录、打开文件等。


十二、LoggerManager 详解

这个类是 sylar 日志系统的总调度中心。有了它,所有日志器统一注册、统一查找、统一配置。

12.1 定义(log.h)

cpp 复制代码
class LoggerManager {
public:
    typedef Spinlock MutexType;
​
    LoggerManager();
    // 获取日志器
    Logger::ptr getLogger(const std::string& name);
    void init();
    // 返回主日志器
    Logger::ptr getRoot() const { return m_root;}
    // 将所有的日志器配置转成YAML String
    std::string toYamlString();
​
private:
    MutexType m_mutex;                            // 保护m_logger映射表
    std::map<std::string, Logger::ptr> m_loggers;   // 日志器映射表
    Logger::ptr m_root;                              // 主日志器
};
​
typedef sylar::Singleton<LoggerManager> LoggerMgr;
  • LoggerMgrLoggerManager 的单例包装,通过 LoggerMgr::GetInstance() 获取唯一实例。

12.2 构造函数(log.cc

cpp 复制代码
LoggerManager::LoggerManager() {
    m_root.reset(new Logger);   // 创建 root logger,名称默认 "root"
    // root 默认输出到控制台
    m_root->addAppender(LogAppender::ptr(new StdoutLogAppender));
    // 根日志器自己注册到自己名下
    m_loggers[m_root->m_name] = m_root;
    init();
}

默认行为:

  • 创建名称为 "root" 的主日志器。

  • 给 root 添加一个 StdoutLogAppender(输出到控制台)。

  • 将 root 放入 m_loggers 映射表。


12.3 getLogger()log.cc

cpp 复制代码
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->m_root = m_root;   // 新 logger 的 m_root 指向 root
    m_loggers[name] = logger;
    return logger;
}

关键: 如果 name 不存在,会自动创建 新的 Logger,并将其 m_root 指向 root。这意味着新 logger 如果没有配置 Appender,日志会委托给 root 输出(见 Logger::log() 中的 else if(m_root) 分支)。


十三、配置系统介绍

sylar 的配置系统不是简单的"读配置文件",而是一个运行时配置中心,设计目标包括:

目标 说明
类型安全 配置项有明确的 C++ 类型(int、string、vector、自定义结构体),不是裸字符串
集中管理 所有配置项注册到一个全局表中,支持按名称查找
热更新 配置文件变更后,自动通知关心该配置的模块,无需重启进程
类型转换 自动将 YAML 字符串转为 C++ 对象,支持复杂嵌套结构
回调通知 配置变更时触发回调函数,让模块有机会做清理和重建

sylar 的日志系统与配置系统(config.h)深度集成,支持热更新

13.1 配置结构体

cpp 复制代码
struct LogAppenderDefine {
    int type = 0;                 // 1=File, 2=Stdout
    LogLevel::Level level = LogLevel::UNKNOW;
    std::string formatter;
    std::string file;
};
​
struct LogDefine {
    std::string name;
    LogLevel::Level level = LogLevel::UNKNOW;
    std::string formatter;
    std::vector<LogAppenderDefine> appenders;
};

13.2 YAML 配置 ↔ 结构体转换

通过模板特化 LexicalCast<std::string, LogDefine>LexicalCast<LogDefine, std::string>,实现 YAML 字符串和 C++ 结构体的双向转换。

(此处是需要下载boost库与yaml-cpp的,关于此处我不做赘述,如果不知道的话先学一下)

配置示例bin/conf/log.yml):

cpp 复制代码
logs:
  - name: root
    level: info
    formatter: "%d{%Y-%m-%d %H:%M:%S}%T%t%T%N%T%F%T[%p]%T[%c]%T%f:%l%T%m%n"
    appenders:
      - type: FileLogAppender
        file: ./logs/root.log
      - type: StdoutLogAppender

13.3 配置监听器与热更新

cpp 复制代码
sylar::ConfigVar<std::set<LogDefine> >::ptr g_log_defines =
    sylar::Config::Lookup("logs", std::set<LogDefine>(), "logs config");
​
struct LogIniter {
    LogIniter() {
        g_log_defines->addListener([](const std::set<LogDefine>& old_value,
                    const std::set<LogDefine>& new_value){
            // 遍历 new_value:
            //   - 如果是新增 logger,创建并配置
            //   - 如果是修改 logger,更新级别、formatter、appenders
            // 遍历 old_value:
            //   - 如果某个 logger 在 new_value 中不存在,清空其 appenders(相当于删除)
        });
    }
};
​
static LogIniter __log_init;
  • __log_init 是全局静态变量,在程序启动时构造,注册监听器。

  • 当配置文件修改并重新加载时,g_log_defines 的值变化,回调被触发,日志配置无需重启程序即可生效


13.4 ConfigVarBase() 实现(config.h)

首先我们知道我们现在要用外部配置覆盖硬编码的默认值,所以我们需要考虑类型的转换,其中scalar是基本类型,sequence,mapping是复合类型,最终都将要拆分成scalar来处理。所以我们需要先处理基本类型的转换。

所以由配置文件到我们的配置类,需要有 fromString的方法来处理,又因为我们有时候需要输出配置信息来查看,所以也要有toString的方法来直观的查看配置。

cpp 复制代码
// ========== 基类:所有配置项的抽象接口 ==========
class ConfigVarBase {
public:
    typedef std::shared_ptr<ConfigVarBase> ptr;

    ConfigVarBase(const std::string& name, const std::string& description)
        : m_name(name), m_description(description) {}

    virtual ~ConfigVarBase() {}
    
    // 返回配置参数名称
    const std::string& getName() const { return m_name; }
    // 返回配置参数的描述
    const std::string& getDescription() const { return m_description; }

    // 纯虚接口:将配置转为 YAML 字符串
    virtual std::string toString() = 0;
    // 纯虚接口:从 YAML 字符串加载
    virtual bool fromString(const std::string& val) = 0;
    // 纯虚接口:获取配置的类型名
    virtual std::string getTypeName() const = 0;

protected:
    std::string m_name;        // 配置项名称,如 "logs"、"server.port"
    std::string m_description; // 配置项描述
};

13.5 ConfigVar() 实现(config.h)

使用模板类来实现不同值类型的子类

cpp 复制代码
template<class T, class FromStr = LexicalCast<std::string, T>,
                      class ToStr = LexicalCast<T, std::string>>
class ConfigVar : public ConfigVarBase {
public:
    typedef RWMutex RWMutexType;
    typedef std::shared_ptr<ConfigVar> ptr;
    typedef std::function<void(const T& old_value, const T& new_value)> on_change_cb;

    ConfigVar(const std::string& name, const T& default_value,
              const std::string& description)
        : ConfigVarBase(name, description), m_val(default_value) {}

    // 获取当前参函数值
    const T getValue() {
        RWMutexType::ReadLock lock(m_mutex);
        return m_val;
    }

    // 如果参数的值有发生变化,则通知对应的注册回调函数
    void setValue(const T& v) {
        {
            RWMutexType::ReadLock lock(m_mutex);
            if(v == m_val) {
                return;
            }
            for(auto& i : m_cbs) {
                i.second(m_val, v);
            }
        }
        RWMutexType::WriteLock lock(m_mutex);
        m_val = v;
    }

    // 注册变更回调
    void addCallback(on_change_cb cb);
    // 等一系列回调函数,实现这里不提了

    // 实现基类的序列化/反序列化
    std::string toString() override {
        try {
            //return boost::lexical_cast<std::string>(m_val);
            RWMutexType::ReadLock lock(m_mutex);
            return ToStr()(m_val);
        } catch (std::exception& e) {
            SYLAR_LOG_ERROR(SYLAR_LOG_ROOT()) << "ConfigVar::toString exception "
                << e.what() << " convert: " << TypeToName<T>() << " to string"
                << " name=" << m_name;
        }
        return "";
    }
    bool fromString(const std::string& val) override {
        try {
            setValue(FromStr()(val));
        } catch (std::exception& e) {
            SYLAR_LOG_ERROR(SYLAR_LOG_ROOT()) << "ConfigVar::fromString exception "
                << e.what() << " convert: string to " << TypeToName<T>()
                << " name=" << m_name
                << " - " << val;
        }
        return false;
    }

private:
    T m_val;                                    // 配置项的实际值
    std::vector<on_change_cb> m_cbs;           // 变更回调列表
    RWMutexType m_mutex;                  // 保护 m_val 和 m_cbs
};

13.6 Config() 实现(config.h)

我们还需要有个类来处理yaml配置文件的解析,所以设计一个 Config 类

cpp 复制代码
class Config {
public:
    typedef std::map<std::string, ConfigVarBase::ptr> ConfigVarMap;

    // 查找或创建配置项
    template<class T>
    static typename ConfigVar<T>::ptr Lookup(
        const std::string& name,
        const T& default_value,
        const std::string& description = ""
    );

    // 按名称查找已存在的配置项
    template<class T>
    static typename ConfigVar<T>::ptr Lookup(const std::string& name);

    // 从 YAML 文件加载所有配置
    static void LoadFromYaml(const std::string& file);

    // 访问全局配置表
    static ConfigVarMap& GetDatas();

private:
    static ConfigVarMap s_datas;  // 全局配置表
    static MutexType s_mutex;     // 保护 s_datas
};

13.7 ListAllMember() 实现(confirf.cc

此处就相当于是一个yaml解析器,此函数可以将结构化有层级的yaml内容,拍平成 list,便于查询,我们就是在LoadFomYaml函数中调用此接口去接受外置配置。

cpp 复制代码
static void ListAllMember(const std::string& prefix,
                          const YAML::Node& node,
                          std::list<std::pair<std::string, const YAML::Node> >& output) {
    if(prefix.find_first_not_of("abcdefghikjlmnopqrstuvwxyz._012345678")
            != std::string::npos) {
        SYLAR_LOG_ERROR(g_logger) << "Config invalid name: " << prefix << " : " << node;
        return;
    }
    output.push_back(std::make_pair(prefix, node));
    if(node.IsMap()) {
        for(auto it = node.begin();
                it != node.end(); ++it) {
            ListAllMember(prefix.empty() ? it->first.Scalar()
                    : prefix + "." + it->first.Scalar(), it->second, output);
        }
    }
}

13.8 基本类型集合处理与转换

此处把 boost::lexical_cast 包进 operator(),让它从函数模板 变成可存储、可传递的函数对象类型,方便在泛型代码中作为策略使用。

cpp 复制代码
//F from_type, T to_type
template<class F, class T>
class LexicalCast {
public:
    T operator()(const F& v) {
        return boost::lexical_cast<T>(v);
    }
};

再往下就是一系列基本类型的转化,此处我就不说了,挺多的,去源码那看就行了,不过需要提一嘴的是,此处的lexical_cast要与最上面那个yaml配置区分开,那块是业务的日志模块的扩展,也就是直接对yaml和所需结构体特化,而此处只是一个通用的框架。

他们的协作关系应该是这样的:

如果合在一起:

// 假设 config.h 直接包含 LogDefine 的特化

// 那么 config.h 必须 #include "log.h"

// 任何想使用配置系统的模块都会被强制引入日志模块的依赖

// 框架不再纯净,编译耦合度爆炸


13.9 配置变更事件

支持给配置项注册配置变更通知(也就是配置发生变化了,让程序知道配置发生变化了,然后去做某些操作)。比如对于网络服务器而言,如果服务器端口配置变化了,那程序应该重新起监听端口。

这个功能通过注册回调函数来实现的,配置使用方预先给配置项注册一个配置变更回调函数,配置项发生变化时,触发对应的回调函数以通知调用方。由于一项配置可能在多个地方引用,所以配置变更回调函数应该是一个数组的形式(存为map形式,挨个监听函数通知,类似于观察者模式)。

注意!!!这里使用map存储是因为functional没有比较函数,用map会有唯一的key用于删除,用其他的容器不太容易删除或调用指定的方法

cpp 复制代码
// 在类ConfigVar中添加map:<key,回调函数> uint64_t key,要求唯一,一般可以用hash值
typedef std::function<void(const T &old_value, const T &new_value)> on_change_cb; 
std::map<uint64_t, on_change_cb> m_cbs;

// 添加相应的函数
// 增加监听
void addListener(uint64_t key, on_change_cb cb){
    m_cbs[key] = cb;
}

// 删除监听
void delListener(uint64_t key){
    m_cbs.erase(key);
}

// 获得监听器
on_change_cb getListener(uint64_t key){
    auto it = m_cbs.find(key);
    return it == m_cbs.end() ? nullptr : it->second;
}

// 清空监听器
void clearListener(){
    m_cbs.clear();
}

13.10 配置系统与日志系统的联动

我们将日志系统与配置系统绑定一下:

cpp 复制代码
// ========== 核心:LogIniter 绑定配置与日志系统 ==========
struct LogIniter {
    LogIniter() {
        // 1. 注册配置项:名为 "logs",默认空数组,描述为 "logs config"
        Config::Lookup<std::vector<LogConfig>>(
            "logs", std::vector<LogConfig>(), "logs config"
        );

        // 2. 添加变更回调
        Config::Lookup<std::vector<LogConfig>>("logs")->addCallback(
            [](const std::vector<LogConfig>& old_val,
               const std::vector<LogConfig>& new_val) {
                SYLAR_LOG_INFO(SYLAR_LOG_ROOT()) << "on_logger_conf_changed";

                // 遍历新配置,更新每个 logger
                for (auto& cfg : new_val) {
                    auto logger = LoggerMgr::GetInstance()->getLogger(cfg.name);

                    // 更新级别
                    logger->setLevel(cfg.level);

                    // 清空旧 appender,重建
                    logger->clearAppenders();
                    for (auto& app_cfg : cfg.appenders) {
                        LogAppender::ptr appender;
                        if (app_cfg.type == "FileLogAppender") {
                            appender = std::make_shared<FileLogAppender>(
                                app_cfg.file
                            );
                        } else if (app_cfg.type == "StdoutLogAppender") {
                            appender = std::make_shared<StdoutLogAppender>();
                        }
                        appender->setLevel(app_cfg.level);
                        logger->addAppender(appender);
                    }

                    // 更新 formatter
                    if (!cfg.formatter.empty()) {
                        logger->setFormatter(
                            std::make_shared<LogFormatter>(cfg.formatter)
                        );
                    }
                }
            }
        );
    }
};

// 3. 全局静态对象:main() 之前自动构造,完成绑定
static LogIniter g_log_initer;

然后就可以看看我们的热更新流程了:


十四、日志系统完整调用链

以这行代码为例:

cpp 复制代码
SYLAR_LOG_INFO(logger) << "Server started on port " << 8080;

步骤 1:宏展开

cpp 复制代码
if (logger->getLevel() <= sylar::LogLevel::INFO)
    sylar::LogEventWrap(
        sylar::LogEvent::ptr(
            new sylar::LogEvent(logger, INFO, __FILE__, __LINE__, 0,
                sylar::GetThreadId(), sylar::GetFiberId(), time(0),
                sylar::Thread::GetName())
        )
    ).getSS() << "Server started on port " << 8080;

步骤 2:getSS() 返回 stringstream&

内容被写入 LogEvent::m_ss

步骤 3:语句结束,临时对象销毁

LogEventWrap::~LogEventWrap() 被调用:

cpp 复制代码
m_event->getLogger()->log(m_event->getLevel(), m_event);
// 即:logger->log(INFO, event);

步骤 4:Logger::log()

cpp 复制代码
void Logger::log(LogLevel::Level level, LogEvent::ptr event) {
    if(level >= m_level) {            // 级别过滤
        auto self = shared_from_this();
        MutexType::Lock lock(m_mutex);
        for(auto& i : m_appenders) {
            i->log(self, level, event);   // 分发给每个 Appender
        }
    }
}

步骤 5:StdoutLogAppender::log()(假设是控制台输出)

cpp 复制代码
if(level >= m_level) {   // Appender 级别过滤
    MutexType::Lock lock(m_mutex);
    m_formatter->format(std::cout, logger, level, event);
}

步骤 6:LogFormatter::format()

遍历 m_items,依次调用每个 FormatItem::format(),将结果写入 std::cout


十五、线程安全总结

保护对象 锁类型
Logger m_appenders, m_formatter SpinlockMutexType
LogAppender m_formatter SpinlockMutexType
LoggerManager m_loggers SpinlockMutexType

关键设计:

  • LogEvent 不加锁:每个日志事件是独立的临时对象,不需要共享。

  • LogEventWrap 不加锁:同理,临时对象仅存在于单条语句中。

  • std::stringstream 不是线程安全的,但由于每个 LogEvent 有自己的 m_ss,不涉及多线程竞争。

当然像配置系统中也有很多线程安全设计,例如:

cpp 复制代码
template<class T>
class ConfigVar : public ConfigVarBase {
private:
    mutable MutexType m_mutex;
    T m_val;
    std::vector<on_change_cb> m_cbs;
};
设计点 目的
mutable 修饰 m_mutex getValue()const 方法,但内部需要加锁,mutable 允许在 const 方法中修改锁状态
回调时释放锁 setValue 中复制回调列表后 unlock(),避免回调中反向访问配置系统导致死锁
保护 m_valm_cbs 读配置和注册回调都是线程安全的

十六、学习验证清单

学完后,你应该能:

  • 手写 SYLAR_LOG_INFO 宏的展开形式,并解释 if(logger->getLevel() <= level) 的作用。
  • 解释 LogEventWrap 的 RAII 技巧,说明为什么流式日志不需要手动调用 log()
  • 说出 LogFormatter::init() 中的状态机如何解析 %d{%Y-%m-%d %H:%M:%S}
  • 解释 Logger::addAppender()m_hasFormatter 的作用。
  • 说明 FileLogAppender 为什么需要每隔 3 秒 reopen()
  • 说明配置系统热更新的实现原理(LogIniter + addListener)。

建议自己实现一个最小日志系统,包含:

  1. enum class LogLevel { DEBUG, INFO, WARN, ERROR, FATAL }

  2. struct LogEvent { string msg; LogLevel level; const char* file; int line; }

  3. class Logger { vector<shared_ptr<Appender>> appenders; void log(LogEvent); }

  4. class ConsoleAppender : public Appender

  5. class FileAppender : public Appender

  6. 一个简化版的 LOG_INFO(logger) << "msg"


结语

至此,我们的日志系统与配置系统就完结了,这一块的内容可谓是相当重要,是我们高性能服务器的根基,我们下一篇博客就要开对应的线程与协程模块了,这块也是我们底层性能的基石,希望这个系列能帮到你。

我是YYYing,后面还有更精彩的内容,希望各位能多多关注支持一下主包。

无限进步,我们下次再见!


---⭐️ 封面自取 ⭐️---

相关推荐
加油码1 小时前
Linux 信号详解:从 Ctrl+C 到进程异常退出,真正理解信号机制
linux·服务器·c++
Shadow(⊙o⊙)1 小时前
QT常用控件3.0,font字体设置,toolTip提示,focusPolicy焦点定位原则,中型控件StyleSheet样式表。
服务器·开发语言·前端·c++·qt
勇宝趣学前端1 小时前
RustDesk 私有远程控制服务器部署
运维·服务器
Jtti1 小时前
怎么判断攻击者主要在打高防服务器哪个端口或协议
运维·服务器·网络
中讯慧通1 小时前
微型无人机通信模块:低空链路核心,保障飞行与传输全程稳定
服务器·人工智能·机器人·无人机
神仙别闹2 小时前
基于C语言处理机调度算法的实现
服务器·c语言·算法
草莓熊Lotso2 小时前
【Linux网络】深入理解 TCP 协议(二):序号机制、流量控制与连接管理
linux·运维·服务器·网络·c++·tcp/ip
云飞云共享云桌面3 小时前
SolidWorks服务器+云飞云共享云桌面 = 10人共享方案
linux·运维·服务器·网络·制造
HackTwoHub11 小时前
最新Nessus2026.6.8版本主机漏洞扫描/探测工具Windows/Linux
linux·运维·服务器·安全·web安全·网络安全·安全架构