
⭐️在这个怀疑的年代,我们依然需要信仰。
个人主页 :YYYing.
⭐️C++大型项目系列专栏:C++大型项目之高性能服务器框架
系列下期内容:暂无
目录
[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) │
└────────────────────┘ └────────────────────┘
好处:
-
解耦 :外部接口只负责解析
...,内部只处理va_list -
可复用 :如果其他地方已经有
va_list,可以直接调用第二个函数 -
类型安全边界:把 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);
}
析构时的行为:
-
通过
m_event->getLogger()获取该事件绑定的 Logger。 -
调用
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
}
}
}
逻辑分支:
-
if(level >= m_level):先进行 Logger 级别的过滤。 -
如果有 Appender,逐个调用
appender->log(self, level, event)。 -
如果没有 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_mutex和m_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拷贝,性能更好。 -
FileLogAppender和StdoutLogAppender都使用重载 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(如d、t、m) -
如果遇到
{,切换到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)。 -
strftime按m_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;
LoggerMgr是LoggerManager的单例包装,通过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 |
Spinlock(MutexType) |
LogAppender |
m_formatter |
Spinlock(MutexType) |
LoggerManager |
m_loggers |
Spinlock(MutexType) |
关键设计:
-
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_val 和 m_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)。
建议自己实现一个最小日志系统,包含:
-
enum class LogLevel { DEBUG, INFO, WARN, ERROR, FATAL } -
struct LogEvent { string msg; LogLevel level; const char* file; int line; } -
class Logger { vector<shared_ptr<Appender>> appenders; void log(LogEvent); } -
class ConsoleAppender : public Appender -
class FileAppender : public Appender -
一个简化版的
LOG_INFO(logger) << "msg"宏
结语
至此,我们的日志系统与配置系统就完结了,这一块的内容可谓是相当重要,是我们高性能服务器的根基,我们下一篇博客就要开对应的线程与协程模块了,这块也是我们底层性能的基石,希望这个系列能帮到你。
我是YYYing,后面还有更精彩的内容,希望各位能多多关注支持一下主包。
无限进步,我们下次再见!
---⭐️ 封面自取 ⭐️---
