1:项目结果
做一个最小可用但符合工业级标准的日志库。不需要像 spdlog 那样极致性能,但必须具备生产环境需要的核心特性:同步 / 异步双模式、线程安全、跨平台、无第三方依赖、类型安全接口,写完能直接用到自己的 C++ 项目里。
2:整体框架设计
采用了经典的分层架构,每个模块只负责单一职责,模块间低耦合高内聚,方便后续扩展和调试:
┌─────────────────────────────────────────────────┐ │ 接口层 (log.hpp) │ │ 对外提供最简洁的调用接口,完全隐藏内部实现细节 │ ├─────────────────────────────────────────────────┤ │ 管理层 (logger.hpp) │ │ 日志器建造者、全局单例管理器,统一管理所有日志器│ ├─────────────────────────────────────────────────┤ │ 核心层 (logger.hpp/looper.hpp) │ │ 同步/异步日志器核心逻辑,负责日志的调度与输出 │ ├─────────────────────────────────────────────────┤ │ 基础层 (level.hpp/message.hpp/format.hpp/sink.hpp) │ │ 日志等级定义、消息封装、格式化、落地器基础模块 │ ├─────────────────────────────────────────────────┤ │ 工具层 (util.hpp/buffer.hpp) │ │ 通用工具函数、高性能缓冲区,为所有上层模块服务 │ └─────────────────────────────────────────────────┘3:环境准备
编译器VS2022以上(MSVC v143)+GCC 11.3(Ubuntu 22.04)
构建工具Cmake3.20(跨平台构建),也可以直接使用makefile(不过Windows不支持)
语言标准最高C++11,只用C++11及其以下的标准(保证最大兼容)
4:项目初步工具函数(util.hpp)
工具模块是整个日志库的基石,所有上层模块都会直接或间接调用它。我把所有跨平台的通用函数、基础工具都放在这里,严格禁止其他模块出现任何平台相关代码,这样以后加新平台的时候只需要改这一个文件。(因为最高C++11,并没有使用C++17的文件操控函数,所以很多是通过系统调用实现的)
1:整体思路设计
我没有把所有工具函数都做成全局函数,而是按功能分成了两个静态类:
Date类:负责所有时间相关的工具函数File类:负责所有文件 / 目录相关的工具函数
这样设计的好处是职责更清晰,调用的时候也更直观,比如Date::GetTime()、File::Exists(),一看就知道是做什么的。而且静态类不需要实例化,直接通过类名调用,和全局函数一样方便,不会污染全局命名空间。
2:头文件基础结构+跨平台宏定义
首先是头文件的基础结构和跨平台宏定义,这是整个工具模块的基础:
#pragma once // 头文件保护,防止重复包含 // 标准库头文件 #include <iostream> #include <ctime> #include <string> /* ==================== 跨平台兼容宏定义 ==================== */ #ifdef _WIN32 // Windows平台特有头文件 #include <direct.h> #include <windows.h> // Windows下的stat和mkdir函数名和Linux不同,用宏统一 #define stat _stat // Windows的stat函数叫_stat #define mkdir _mkdir // Windows的mkdir函数叫_mkdir #else // Linux/macOS平台特有头文件 #include <sys/stat.h> #include <sys/types.h> #endif namespace my_log { namespace util { // 时间工具类 class Date { public: // 获取当前系统时间戳(秒级) static time_t GetTime(); }; // 文件/目录工具类 class File { public: // 判断文件/目录是否存在 static bool Exists(const std::string &pathname); // 获取文件所在的目录路径 static std::string Path(const std::string &pathname); // 递归创建多级目录 static void createdirectory(const std::string &pathname); }; } // namespace util } // namespace my_log设计思考:
- 用
#pragma once而不是传统的#ifndef/#define/#endif,更简洁,而且所有现代编译器都支持 - 跨平台宏定义是核心:Windows 和 Linux 的很多系统 API 函数名不同,比如
stat在 Windows 下叫_stat,mkdir在 Windows 下叫_mkdir。我用宏定义把它们统一成了 Linux 的名字,这样后面的代码就不需要再写平台判断了 - 所有函数都声明为
static,不需要创建类的实例,直接通过类名调用 - 用嵌套命名空间
my_log::util,避免和其他库的命名冲突
3:时间工具类Data
需求分析
日志最基础的元信息就是时间,目前只需要秒级时间戳就够了,后续格式化模块会把它转换成 "2024-05-25 12:34:56" 这样的可读格式。如果以后需要更高精度,可以再扩展成毫秒级。
/** * @brief 获取当前系统时间戳(秒级) * @return time_t 自1970-01-01 00:00:00 UTC以来的秒数 */ time_t Date::GetTime() { // time(nullptr)返回当前系统的秒级时间戳 // 这个函数是C标准库函数,所有平台都支持,完全跨平台 return time(nullptr); }设计思考
- 这里用了最基础的 C 标准库函数
time(),而不是 C++11 的std::chrono,原因很简单:足够用,而且最简单 time(nullptr)是完全跨平台的,所有编译器和操作系统都支持,没有任何兼容性问题- 秒级精度对于大多数日志场景来说已经足够了,如果以后需要毫秒级,可以很容易地改成
std::chrono的实现
4:文件工具类File
这是工具模块的核心,包含三个最常用的文件操作函数:判断文件是否存在、获取文件所在目录、递归创建多级目录。
1:判断文件是否存在
需求分析
在创建文件或目录之前,必须先判断它是否已经存在。如果已经存在,就不需要再创建了,避免不必要的错误。
/** * @brief 判断文件或目录是否存在 * @param pathname 文件或目录的路径 * @return bool 存在返回true,不存在返回false */ bool File::Exists(const std::string &pathname) { // 定义stat结构体,用于存储文件/目录的状态信息 struct stat st; // stat函数用于获取文件/目录的状态信息 // 成功返回0,失败返回-1 // 这里的stat已经通过宏定义统一了Windows和Linux的函数名 return stat(pathname.c_str(), &st) == 0; }设计思考
- 用标准库的
stat函数来判断文件是否存在,这是跨平台判断文件存在的标准方法 stat函数不仅能判断文件存在,还能判断是文件还是目录、获取文件大小、修改时间等信息,非常强大- 这里的
stat已经通过前面的宏定义统一了 Windows 和 Linux 的函数名,所以代码里不需要再写平台判断
踩过的坑
一开始我在 Windows 下直接用了stat函数,结果编译报错。问了AI才知道,Windows 的 C 标准库把stat函数重命名成了_stat,而且还有宽字符版本_wstat。所以必须用宏定义把stat统一成_stat,否则 Windows 下编译不通过。
2:获取文件所在目录
需求分析
当我们要创建一个文件时,比如./logs/2024/05/25/app.log,我们需要先获取它所在的目录./logs/2024/05/25,然后创建这个目录,才能打开文件。
这个函数的作用就是:输入一个文件的完整路径,返回它所在的目录路径。
/** * @brief 获取文件所在的目录路径 * @param pathname 文件的完整路径 * @return std::string 文件所在的目录路径 * @example 输入 "./logs/2024/05/25/app.log",返回 "./logs/2024/05/25" * @example 输入 "app.log",返回 "."(当前目录) */ std::string File::Path(const std::string &pathname) { // 查找最后一个路径分隔符的位置 // 同时查找 '/' 和 '\',兼容Windows和Linux的路径格式 size_t pos = pathname.find_last_of("/\\"); // 边界情况处理:如果没有找到任何分隔符,说明文件在当前目录 if (pos == std::string::npos) return "."; // 返回当前目录 // 从开头截取到最后一个分隔符的位置,就是文件所在的目录 return pathname.substr(0, pos); }设计思考
- 边界情况处理:如果输入的是纯文件名(没有任何分隔符),说明文件在当前目录,返回 "."
- 同样用
find_last_of("/\\")同时处理 Windows 和 Linux 的路径分隔符
3:递归创建多级目录(难点函数)
需求分析
这是文件工具类中最复杂的一个函数。当我们要创建./logs/2024/05/25/app.log时,如果./logs/2024/05/25这个目录不存在,文件打开就会失败。所以我们需要一个函数,能够递归创建多级目录,并且跨平台兼容。
/** * @brief 递归创建多级目录 * @param pathname 要创建的目录路径 * @note 如果目录已经存在,直接返回,不做任何操作 */ void File::createdirectory(const std::string &pathname) { // 提前判断:如果路径为空,或者目录已经存在,直接返回 if (pathname.empty() || Exists(pathname)) return; std::string current_path; // 保存当前正在创建的子目录路径 size_t idx = 0; // 当前查找的起始位置 size_t len = pathname.size(); // 路径总长度 // 循环遍历路径,逐级创建目录 while (idx < len) { // 从idx位置开始,查找下一个路径分隔符 size_t pos = pathname.find_first_of("/\\", idx); // 如果没有找到更多分隔符,说明到了最后一级目录 if (pos == std::string::npos) { pos = len; } // 截取从开头到当前分隔符的子路径 current_path = pathname.substr(0, pos); // 如果子路径不为空,并且不存在,就创建它 if (!current_path.empty() && !Exists(current_path)) { #ifdef _WIN32 // Windows下的_mkdir函数只需要一个参数,不需要权限 // 用(void)n; 显式忽略返回值,避免编译器警告 int n = mkdir(current_path.c_str()); (void)n; #else // Linux下的mkdir函数需要第二个参数,指定目录权限 // 0777表示所有用户都有读写执行权限,会被系统的umask修改 mkdir(current_path.c_str(), 0777); #endif } // 移动到下一个分隔符的下一个位置,继续查找 idx = pos + 1; } }设计思考
- 提前判断优化:函数开头先判断路径是否为空,或者目录已经存在,如果是就直接返回,避免不必要的操作
- 逐级创建逻辑:把完整路径按分隔符拆分成多级子目录,从第一级开始,逐个创建
- 跨平台处理:Windows 的
_mkdir函数只需要一个参数,不需要权限;Linux 的mkdir函数需要第二个参数指定目录权限。这里用平台判断分别处理 - 显式忽略返回值:Windows 下的
mkdir函数有返回值,但我们已经提前判断了目录不存在,所以肯定会创建成功。用(void)n;显式忽略返回值,避免编译器产生 "未使用变量" 的警告
踩过的坑
- 只创建最后一级目录:一开始我图省事,直接调用
mkdir创建完整路径,结果发现系统 API 只能创建单级目录。比如要创建./logs/2024/05/25,如果./logs不存在,直接创建就会失败。后来改成逐级创建才解决问题 - Windows 权限问题:Windows 的
_mkdir函数不需要权限参数,而 Linux 的mkdir必须指定权限。一开始我忘了加平台判断,导致 Windows 下编译错误 - 空路径处理:如果输入的路径为空,直接返回,避免后续逻辑出错
5:完整代码
#pragma once #include <iostream> #include <ctime> #include <string> /* ==================== 跨平台兼容宏定义 ==================== */ #ifdef _WIN32 // Windows平台特有头文件 #include <direct.h> #include <windows.h> // Windows下的stat和mkdir函数名和Linux不同,用宏统一 #define stat _stat // Windows的stat函数叫_stat #define mkdir _mkdir // Windows的mkdir函数叫_mkdir #else // Linux/macOS平台特有头文件 #include <sys/stat.h> #include <sys/types.h> #endif namespace my_log { namespace util { /** * @brief 时间工具类 * 提供所有时间相关的工具函数 */ class Date { public: /** * @brief 获取当前系统时间戳(秒级) * @return time_t 自1970-01-01 00:00:00 UTC以来的秒数 */ static time_t GetTime() { return time(nullptr); } }; /** * @brief 文件/目录工具类 * 提供所有文件和目录相关的工具函数 */ class File { public: /** * @brief 判断文件或目录是否存在 * @param pathname 文件或目录的路径 * @return bool 存在返回true,不存在返回false */ static bool Exists(const std::string &pathname) { struct stat st; return stat(pathname.c_str(), &st) == 0; } /** * @brief 获取文件所在的目录路径 * @param pathname 文件的完整路径 * @return std::string 文件所在的目录路径 * @example 输入 "./logs/2024/05/25/app.log",返回 "./logs/2024/05/25" * @example 输入 "app.log",返回 "."(当前目录) */ static std::string Path(const std::string &pathname) { size_t pos = pathname.find_last_of("/\\"); if (pos == std::string::npos) return "."; return pathname.substr(0, pos); } /** * @brief 递归创建多级目录 * @param pathname 要创建的目录路径 * @note 如果目录已经存在,直接返回,不做任何操作 */ static void createdirectory(const std::string &pathname) { if (pathname.empty() || Exists(pathname)) return; std::string current_path; size_t idx = 0; size_t len = pathname.size(); while (idx < len) { size_t pos = pathname.find_first_of("/\\", idx); if (pos == std::string::npos) { pos = len; } current_path = pathname.substr(0, pos); if (!current_path.empty() && !Exists(current_path)) { #ifdef _WIN32 int n = mkdir(current_path.c_str()); (void)n; #else mkdir(current_path.c_str(), 0777); #endif } idx = pos + 1; } } }; } // namespace util } // namespace my_log5:测试代码(AI生成)
#include "util.hpp" #include <iostream> int main() { std::cout << "==================== 工具模块全面测试 ====================\n" << std::endl; // 1. 测试时间戳获取 std::cout << "1. 时间戳获取测试:" << std::endl; time_t now = my_log::util::Date::GetTime(); std::cout << " 当前秒级时间戳:" << now << std::endl; std::cout << " ✅ 时间戳获取正常\n" << std::endl; // 2. 测试文件存在判断 std::cout << "2. 文件存在判断测试:" << std::endl; std::cout << " 当前文件存在:" << (my_log::util::File::Exists("test_util.cpp") ? "✅ 是" : "❌ 否") << std::endl; std::cout << " 不存在的文件:" << (my_log::util::File::Exists("not_exist.txt") ? "❌ 是" : "✅ 否") << std::endl; std::cout << " ✅ 文件存在判断正常\n" << std::endl; // 3. 测试文件目录获取 std::cout << "3. 文件目录获取测试:" << std::endl; std::string test_cases[] = { "./logs/2024/05/25/app.log", "C:\\Users\\user\\project\\app.log", "app.log", "../src/app.log", "/home/user/app.log" }; for (const auto& path : test_cases) { std::string dir = my_log::util::File::Path(path); std::cout << " 输入:\"" << path << "\"" << std::endl; std::cout << " 输出:\"" << dir << "\"" << std::endl; } std::cout << " ✅ 文件目录获取正常\n" << std::endl; // 4. 测试目录创建 std::cout << "4. 目录创建测试:" << std::endl; std::string test_dir = "./test_logs/2024/05/25"; std::cout << " 创建目录:" << test_dir << std::endl; my_log::util::File::createdirectory(test_dir); std::cout << " 目录是否存在:" << (my_log::util::File::Exists(test_dir) ? "✅ 是" : "❌ 否") << std::endl; // 测试重复创建已存在的目录 std::cout << " 重复创建已存在目录..." << std::endl; my_log::util::File::createdirectory(test_dir); std::cout << " ✅ 重复创建无错误\n" << std::endl; std::cout << "==================== 所有测试通过 ====================" << std::endl; return 0; }6:总结
完成的工作
- 完成了项目整体分层架构设计,明确了每个模块的职责
- 实现了工具模块
util.hpp,包含两个静态类:Date类:提供秒级时间戳获取功能File类:提供文件存在判断、文件目录获取、递归创建多级目录功能
- 解决了所有跨平台兼容性问题,一套代码在 Windows 和 Linux 下都能正常编译运行
- 编写了全面的测试用例,覆盖所有正常情况和边界情况
- 在 Windows 和 Linux 两个平台完成了跨平台测试