5.C++日志库的实现-c++ linux编程:从0实现muduo库系列
重点内容
视频讲解:《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 与异步日志系统集成,提高性能
- 设计双缓冲机制,支持高并发场景
- 日志压缩与滚动:
- 支持日志文件自动滚动和压缩
- 实现日志清理策略,避免磁盘占用过多