重点内容
视频讲解:《C++Linux编程进阶:从0实现muduo C++网络框架系列》-第5讲.实现C++日志库
代码改动
lesson5代码
实现:base/LogStream.h/cc
实现:base/Logging.h/cc
examples/test_basic_log.cc
examples/test_logging.cc
特别要注意CMakeLists.txt的 宏定义改动,debug模式时,lesson4声明的DEBUG宏定义和日志库的Debug日志级别符号有冲突,如果不修改会产生意想不到的编译报错。
1. 日志系统整体架构
1.1 日志系统整体架构
LOG_DEBUG << "debug log test";
LOG_INFO << "info log test";
1.2 模块具体作用
1.2.1 日志宏 (LOG_XXX)
比如LOG_DEBUG, LOG_INFO等
提供简洁易用的接口,自动捕获文件名和行号,根据当前日志级别决定是否记录日志,创建临时Logger对象并返回流式接口
使用范例:
LOG_DEBUG << "debug log test";
LOG_INFO << "int: " << 42;
1.2.2 Logger 类
日志系统核心类,管理日志生命周期,提供静态方法控制全局日志行为(级别、输出方式),根据日志级别和上下文创建日志实例,析构时完成日志输出。
LOG_INFO << "info log test"; 实际调用
if (mymuduo::Logger::logLevel() <= mymuduo::Logger::INFO) \
mymuduo::Logger(__FILE__, __LINE__).stream() << "info log test";
1.2.3 Logger::Impl 类
日志格式化分为两大模块:
Logger的内部实现类,封装日志格式化逻辑,管理时间戳和源文件信息,处理日志的具体格式,添加行号、文件名等上下文信息。
比如日志格式:20250324 09:08:15.248185 WARN warn 输出 - test_basic_log.cc:26
20250324 09:08:15.248185 时间戳
WARN 日志级别打印
test_basic_log.cc 文件名
:26 行号
1.2.4 LogStream 类
提供流式接口(<<操作符),管理内部缓冲区,实现各种数据类型的格式化转换,特别优化了数值转换效率,支持链式调用。
包括Logger::impl类里的时间戳 日志级别 文件名 行号的缓存,
这里我们重点讲流式接口 <<操作符,范例如下所示:
mymuduo::LogStream logstream;
logstream << "LogStream 输出" << 78 << "abc";
std::cout << "cout: " << logstream.buffer().toString() << std::endl;
输出:
cout: LogStream 输出78abc
1.2.5 FixedBuffer 类
预分配固定大小的内存缓冲区,避免动态内存分配,提供高效的追加、重置等基本操作,作为LogStream的内部存储。
1.2.6 输出函数 (OutputFunc/FlushFunc)
日志最终写入的目的地,函数指针类型(OutputFunc和FlushFunc),允许自定义日志输出目标和刷新方式,可以是控制台、文件、网络套接字或自定义设备,由OutputFunc和FlushFunc控制,支持灵活配置。
1.2.7 日志级别 (LogLevel)
定义日志的严重程度(TRACE、DEBUG、INFO、WARN、ERROR、FATAL),控制日志过滤,配合宏实现条件记录,支持运行时调整。
1.2.8 Fmt 格式化类
结合C风格格式化字符串的灵活性和C++类型安全性,预格式化数据到内部缓冲区,通过<<操作符集成到LogStream中。
// 测试整数格式化
Fmt intFmt("%d", 42);
std::cout << "整数格式化: " << intFmt.data() << std::endl;
// 测试浮点数格式化
Fmt floatFmt("%.2f", 3.14159);
std::cout << "浮点数格式化: " << floatFmt.data() << std::endl;
打印输出:
整数格式化: 42 浮点数格式化: 3.14
1.2.9 SourceFile 类
高效处理源文件路径,从完整路径中提取文件名,避免运行时重复计算,优化日志性能,存储在Logger::Impl中提供位置信息。
// 测试不同路径形式
std::cout << "原始的文件名获取:" << __FILE__ << std::endl;
const char* paths[] = {
"/home/user/project/file.cpp",
"src/file.cpp",
"file.cpp"
};
for (const char* path : paths)
{
Logger::SourceFile sf(path);
std::cout << "原始路径: " << path << "\n";
std::cout << "提取文件名: " << sf.data_ << "\n";
std::cout << "文件名长度: " << sf.size_ << "\n\n";
}
打印输出
原始的文件名获取:/home/lqf/long/spark_muduo/lesson5/examples/test_basic_log.cc 原始路径: /home/user/project/file.cpp 提取文件名: file.cpp 文件名长度: 8 原始路径: src/file.cpp 提取文件名: file.cpp 文件名长度: 8 原始路径: file.cpp 提取文件名: file.cpp 文件名长度: 8
2 格式化日志输出LogStream类
2.1 核心设计理念
LogStream 的核心设计理念是提供一个高效、类型安全、易用的日志流式接口,主要体现在:
1.流式语法设计:
通过重载 << 操作符,实现类似 std::cout 的链式调用语法
并重载不同的数据类型
self& operator<<(short);
self& operator<<(unsigned short);
self& operator<<(const char* str)
self& operator<<(const std::string& v)
每个 << 操作符返回自身引用(self&),支持链式表达式,比如LOG_INFO << "int: " << 42;
2.高效内存管理(设计FixedBuffer类):
使用预分配的固定大小缓冲区而非动态内存分配
避免了频繁的内存分配/释放操作,减少内存碎片
2.2 设计框架图
2.3 核心实现分析
2.3.1 内存管理策略
LogStream 通过 FixedBuffer 模板类管理内存,采用两种预定义大小:
// 在detail命名空间中定义两种常用缓冲区大小
const int kSmallBuffer = 4000; // 4KB,用于一般日志消息
const int kLargeBuffer = 4000*1000; // 4MB,用于特别长的日志消息
// 定义LogStream使用的缓冲区类型
typedef detail::FixedBuffer<detail::kSmallBuffer> Buffer;
FixedBuffer 的核心在于预分配内存并使用指针跟踪当前位置:
template<int SIZE>
class FixedBuffer : noncopyable {
private:
char data_[SIZE]; // 固定大小的字符数组
char* cur_; // 当前写入位置
public:
FixedBuffer() : cur_(data_) {}
void append(const char* buf, size_t len) {
if (avail() > static_cast<int>(len)) {
memcpy(cur_, buf, len);
cur_ += len;
}
}
// 其他辅助方法...
};
这种设计避免了动态内存分配,特别适合短生命周期、高频调用的日志场景。
2.3.2 优化的数值转换算法
LogStream 对数值转换做了专门优化,使用自定义的转换算法替代标准库函数:
// Wilson的高效整数转字符串算法
template<typename T>
size_t convert(char buf[], T value) {
T i = value;
char* p = buf;
do {
int lsd = static_cast<int>(i % 10);
i /= 10;
*p++ = zero[lsd]; // 使用预定义的字符表 查表获取数字
} while (i != 0);
if (value < 0) {
*p++ = '-';
}
*p = '\0';
std::reverse(buf, p); // 反转得到正确顺序
return p - buf;
}
// 十六进制转换算法,用于指针
size_t convertHex(char buf[], uintptr_t value) {
// 类似实现...
}
这些算法直接操作字符数组,避免了格式化函数的开销和临时对象创建。
2.3.3 流式接口实现
LogStream 通过大量的操作符重载实现流式接口:
// 将不同类型的数据格式化写入缓冲区
LogStream& LogStream::operator<<(int v) {
formatInteger(v);
return *this; // 返回自身引用,支持链式调用
}
LogStream& LogStream::operator<<(const char* str) {
if (str) {
buffer_.append(str, strlen(str));
}
else {
buffer_.append("(null)", 6);
}
return *this;
}
// 其他类型的重载...
关键是每个操作符都返回自身引用(*this),使得多个 << 操作可以连续调用。
2.4 类型安全与格式化
LogStream 通过模板和静态断言提供类型安全保证:
// 将所有整数类型统一处理
template<typename T>
void LogStream::formatInteger(T v) {
// 实现整数格式化...
}
// Fmt类使用静态断言确保类型安全
template<typename T>
Fmt::Fmt(const char* fmt, T val) {
// 编译期类型检查
static_assert(std::is_arithmetic<T>::value == true,
"Must be arithmetic type");
// 格式化...
}
3 章节总结
1.重点内容:
理解如何使用日志级别控制日志是否记录
理解LogStream如何使用输出操作符 以C++ cout方式格式化日志。
2.扩展:
异步日志支持:
将 LogStream 与异步日志系统集成,提高性能
设计双缓冲机制,支持高并发场景
日志压缩与滚动:
支持日志文件自动滚动和压缩
实现日志清理策略,避免磁盘占用过多