文章目录
前言:
一. 日志系统的核心设计理念
1.1 日志的核心组成要素
1.2 日志系统的两大核心阶段
1.3 为什么选择策略模式?
二. 前置基础模块实现
2.1 RAII 风格互斥锁封装(线程安全基石)
2.2 格式化时间戳模块(附对应模块测试)
2.3 类型安全的日志等级模块
三. 基于策略模式的日志刷新核心实现
3.1 抽象策略基类 LogStrategy
3.2 控制台日志策略 ConsoleLogStrategy
3.3 文件日志策略 FileLogStrategy
四. 日志主体类与流式输出设计
4.1 Logger 主类的整体架构
4.2 LogMessage 内部类:RAII 实现日志自动刷新
4.3 完整的 Logger 类实现
五. 日志系统的线程安全与可重入性深度解析
六. 实战:日志系统完整使用示例(附带完整Logger.hpp代码呈现)
6.1 完整Logger.hpp代码
6.2 完整测试代码
6.3 进阶优化方向
结尾:
一. 日志系统的核心设计理念
1.1 日志的核心组成要素
一条合格的工业级日志,必须包含必选字段和可选扩展字段,确保问题可追溯、状态可监控:
必选核心字段:
时间戳:可读性强的年月日时分秒格式,精准定位事件发生时间
日志等级:区分事件严重程度,支持分级过滤与告警
日志内容:用户自定义的业务 / 调试信息
可选扩展字段:
进程 PID / 线程 ID:多进程 / 多线程环境下定位执行流
文件名与行号:精准定位日志打印的代码位置
自定义扩展字段:如模块名、用户 ID 等业务信息
本文实现的日志格式如下,完全兼容主流日志库的规范:
[2026-04-16 21:33:18] [DEBUG] [1030871] [Main.cc] [10] - hello world hello Lotso, 3.14
AI写代码
bash
1
1.2 日志系统的两大核心阶段
日志的生命周期可拆分为两个完全解耦的阶段,这是我们设计的核心依据:
日志形成阶段:将时间戳、等级、文件名、行号、用户内容等信息,拼接成一条完整的格式化字符串,与日志输出目的地无关
日志刷新阶段:将格式化完成的日志字符串,写入到指定目的地(控制台、文件、数据库、网络等),仅关注写入逻辑
两个阶段解耦后,我们可以独立扩展刷新逻辑,而无需修改日志格式化的核心代码,这正是策略模式的最佳应用场景。
可以看看图中的初始化代码框架,我们使用了命名空间,所以后面的代码中可能大家看起来会有缩进啥的,有的我有给带上,有的没有,大家可以最后再看看整体代码,里面包含完整的头文件和其他注意的地方
1.3 为什么选择策略模式?
策略模式是行为型设计模式的一种,核心思想是定义一系列算法(策略),将每个算法封装起来,并让它们可以互相替换。
在日志系统中,不同的日志刷新方式就是不同的策略:控制台输出、文件持久化、网络上报都是独立的刷新算法。使用策略模式带来了这些核心优势:
开闭原则:新增日志刷新目的地(如数据库),只需新增一个策略类,无需修改原有代码
解耦:日志格式化核心逻辑与刷新逻辑完全分离,代码职责单一
动态切换:程序运行时可随时切换日志策略,比如调试阶段用控制台输出,生产环境用文件持久化
二. 前置基础模块实现
日志系统的核心前提是线程安全,同时需要时间戳、日志等级等基础能力支撑,我们先实现这些底层模块。
2.1 RAII 风格互斥锁封装(线程安全基石)
多线程环境下,控制台、日志文件都是临界资源,多个线程同时写入会导致内容交错、乱序,必须通过互斥量保证临界区的原子性。
我们基于 Linux 原生的pthread_mutex封装互斥锁,并通过 RAII 机制管理锁的生命周期,避免手动解锁导致的死锁、内存泄漏问题,这也是 C++11 std::lock_guard的核心实现原理。这个工作我们之前就做过了,直接拿过来就可以了
Mutex.hpp
#ifndef MUTEX_HPP
#define MUTEX_HPP
#include <iostream>
#include <pthread.h>
// 互斥锁封装类:提供加锁/解锁及获取原始锁的接口
class Mutex
{
public:
// 构造函数:初始化互斥锁
Mutex()
{
pthread_mutex_init(&_lock, nullptr);
}
// 析构函数:销毁互斥锁
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
// 加锁操作
void Lock()
{
pthread_mutex_lock(&_lock);
}
// 解锁操作
void UnLock()
{
pthread_mutex_unlock(&_lock);
}
// 获取原始互斥锁指针,用于需要原生 pthread_mutex_t 的接口
pthread_mutex_t* Origin()
{
return &_lock;
}
private:
pthread_mutex_t _lock; // POSIX 互斥锁
};
// RAII 风格的锁守卫类:构造时加锁,析构时解锁,自动管理锁的生命周期
class LockGuard
{
public:
// 构造函数:接收一个 Mutex 指针,并立即加锁
LockGuard(Mutex* lockptr) : _lockptr(lockptr)
{
_lockptr->Lock();
}
// 析构函数:自动解锁
~LockGuard()
{
_lockptr->UnLock();
}
private:
Mutex* _lockptr; // 指向被管理的互斥锁
};
#endif
AI写代码
cpp
运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
核心设计解析:
禁用拷贝:互斥量是系统资源,不允许拷贝和赋值,避免重复释放、死锁等问题(这个里面没有,大家可以自己加上)
RAII 机制:LockGuard在对象构造时加锁,析构时自动解锁,即使代码中途抛出异常,也能保证锁被释放,彻底避免手动解锁的遗漏
接口封装:屏蔽原生pthread库的接口细节,提供更符合 C++ 面向对象的使用方式
2.2 格式化时间戳模块(附对应模块测试)
时间戳是日志的核心字段,我们需要实现秒级、可重入、格式化的时间戳获取功能。
这里重点注意:C 标准库的localtime函数是不可重入的,多线程环境下会出现数据错乱,因此必须使用可重入版本localtime_r,它由调用者提供结构体缓冲区,避免了全局静态变量的竞态问题。
时间戳实现代码(在命名空间里面,我没带上)
// 1. 获取时间
std::string GetTimeStamp()
{
time_t currentTime = time(nullptr); // 默认获取当前时区的时间(获取到的是秒级时间戳)
// 我们希望把这个时间转换成年-月-日 时:分:秒
struct tm dataTime;
// 使用带 _r (Reentrant) 后缀的版本 localtime_r 而不是普通的 localtime。
// 因为普通版本内部使用静态全局变量保存结果,在多线程写日志时极易发生数据覆盖错乱;
// _r 版本要求我们自己传入存放结果的地址(&dataTime),保证了多线程环境下的绝对安全。
localtime_r(¤tTime, &dataTime);
// 准备一个足够大的字符数组作为格式化字符串的缓冲区
char dataTimeStr[128];
// 使用 snprintf 将时间结构体安全地格式化为类似 [2026-04-16 19:21:32] 的排版
// %4d: 4位数字对齐; %02d: 2位数字,不足两位的在高位自动补0(如 4月 显示为 04)
snprintf(dataTimeStr, sizeof(dataTimeStr), "%4d-%02d-%02d %02d:%02d:%02d",
dataTime.tm_year + 1900, // 坑点修复:tm_year 表示的是自 1900 年起经过的年数,必须加上 1900
dataTime.tm_mon + 1, // 坑点修复:tm_mon 范围是 [0, 11](0代表1月),必须加上 1
dataTime.tm_mday, // 日([1, 31])
dataTime.tm_hour, // 时
dataTime.tm_min, // 分
dataTime.tm_sec // 秒
);
// 字符数组会自动隐式转换为 std::string 对象返回
return dataTimeStr;
}
AI写代码
cpp
运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
核心细节解析:
可重入性保障:使用localtime_r替代localtime,确保多线程环境下时间转换不会出现数据竞争
格式化补零:通过%02d确保月、日、时、分、秒始终是两位数字,保证日志格式的一致性
时间偏移修正:tm_year需要 + 1900 得到真实年份,tm_mon需要 + 1 得到真实月份,这是tm结构体的标准规范
测试代码
#include <iostream>
#include <memory>
#include <unistd.h>
#include "Logger.hpp"
using namespace LogModule;
// 测试时间戳模块
void testTime()
{
for(int i = 0; i < 5; i++)
{
std::cout << GetTimeStamp() << std::endl;
sleep(1);
}
}
int main()
{
// 1. 测试时间
testTime();
return 0;
}
AI写代码
cpp
运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2.3 类型安全的日志等级模块
日志等级用于区分事件的严重程度,我们使用 C++11 的enum class实现类型安全的日志等级,避免普通枚举的隐式类型转换问题,同时提供枚举到字符串的转换能力。
日志等级实现代码(在命名空间里面,我没带上)
// 2. 日志等级 -- 枚举类型(整数)转换成字符串类型
// 使用 enum class (强类型枚举) 而不是普通 enum
// 优势:1. 具有独立的作用域,避免命名冲突;2. 不允许隐式类型转换,更加类型安全
enum class LogLevel
{
DEBUG, // 调试信息:用于开发过程中输出详细状态,帮助定位问题。生产环境通常关闭
INFO, // 常规信息:记录程序的关键运行节点,用于了解系统正常运行的状态
WARNING, // 警告信息:出现了预期之外的情况,但系统仍能继续运行,需要引起关注
ERROR, // 错误信息:发生了运行时错误,导致当前操作失败,但主程序依然存活
FATAL // 致命错误:最高严重级别。出现了无法恢复的问题,程序即将崩溃或被迫退出
};
/**
* @brief 将日志等级枚举转换为可读的字符串
* @param level 日志等级枚举值
* @return 对应的字符串表示。由于 cout 不直接支持打印强类型枚举,必须进行此类映射转换
*/
std::string LogLevel2String(LogLevel level)
{
switch(level)
{
case LogLevel::DEBUG:
return "DEBUG";
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
default:
// 防御性编程:处理未知的枚举值,防止程序出现未定义行为
return "UNKNOWN";
}
}
AI写代码
cpp
运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
核心设计解析:
类型安全:enum class不会隐式转换为整型,避免了错误的等级赋值,编译期即可发现类型问题
等级分层:遵循业界通用的 5 级日志规范,覆盖从调试到致命错误的全场景
字符串转换:通过 switch 语句实现枚举到字符串的映射,确保日志中输出可读性强的等级名称,而非整型数字
测试代码
#include <iostream>
#include <memory>
#include <unistd.h>
#include "Logger.hpp"
using namespace LogModule;
// 测试日志类枚举类型转字符类型模块
void testEnum()
{
std::cout << LogLevel2String(LogLevel::DEBUG) << std::endl;
std::cout << LogLevel2String(LogLevel::INFO) << std::endl;
std::cout << LogLevel2String(LogLevel::WARNING) << std::endl;
std::cout << LogLevel2String(LogLevel::ERROR) << std::endl;
std::cout << LogLevel2String(LogLevel::FATAL) << std::endl;
}
int main()
{
// 2. 测试枚举类转字符串类型
testEnum();
return 0;
}
AI写代码
cpp
运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
三. 基于策略模式的日志刷新核心实现
基于策略模式的设计,我们先定义抽象的刷新策略基类,再分别实现控制台和文件两种具体的刷新策略,后续可无限扩展其他策略。
3.1 抽象策略基类 LogStrategy
抽象基类定义了所有刷新策略必须实现的纯虚接口,同时使用虚析构函数确保子类对象能正确析构。
namespace LogModule
{
// 3. 刷新策略
// 基类: 策略模式 (Strategy Pattern)
// 核心思想:将“日志的产生”与“日志的刷新目的地”解耦。
// 通过定义统一的接口,使得程序可以在运行时动态决定将日志输出到控制台、文件、数据库或网络。
class LogStrategy
{
public:
// 虚析构函数:在多态体系中,基类必须拥有虚析构函数。
// 这样当我们通过基类指针删除派生类对象时,才能确保调用到子类的析构函数,防止内存泄漏。
virtual ~LogStrategy() = default; // 不在这里析构
// 核心刷新接口:这是一个纯虚函数。
// 纯虚函数的核心作用是定义一种“契约”,强制派生类(子类)必须实现具体的逻辑。
// 不同的子类可以根据自己的策略(如 ConsoleStrategy 或 FileStrategy)来实现不同的刷新行为。
virtual void SyncLog(const std::string &message) = 0; // 强制子类对其进行重写
};
}
AI写代码
cpp
运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
设计说明:
纯虚函数SyncLog定义了策略的统一接口,入参是格式化完成的日志字符串,子类只需关注具体的写入逻辑
虚析构函数是 C++ 多态的基础规范,避免通过基类指针释放子类对象时出现内存泄漏
3.2 控制台日志策略 ConsoleLogStrategy
控制台策略负责将日志输出到标准错误流(stderr),核心是保证多线程环境下的输出原子性,避免日志交错。用到了我们自己的互斥锁记得包含对应头文件 ,我这里就不写了
namespace LogModule
{
// 策略1: 控制台日志策略
// 子类:继承自策略基类,用于将日志直接刷新到标准输出(显示器),常用于本地开发与调试
class ConsoleLogStrategy: public LogStrategy
{
public:
// 构造函数与析构函数:当前策略不涉及复杂资源申请,故使用默认实现即可
ConsoleLogStrategy(){}
~ConsoleLogStrategy(){}
/**
* @brief 实现具体的日志同步逻辑——刷新到控制台
* @param message 组装好的完整日志字符串
*/
void SyncLog(const std::string &message) override // 检查重写的错误
{
// 【核心原理】显示器(stdout)在多线程环境下属于“临界资源”。
// 如果不加保护,多个线程同时调用 std::cout 会导致各条日志的字符在屏幕上发生“交织”或乱码 。
// 使用自定义的 LockGuard 配合互斥锁,确保这一系列操作的原子性 。
LockGuard logGuard(&_mutex);
std::cout << message << std::endl;
}
private:
// 互斥锁:专门用于保护当前控制台输出的原子性,防止并发打印时消息错乱
Mutex _mutex;
};
}
AI写代码
cpp
运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
核心细节解析:
线程安全保障:控制台是全局临界资源,通过互斥锁保证同一时刻只有一个线程能执行输出操作,彻底避免多线程日志交错
stderr 输出:使用std::cerr而非std::cout,因为 stderr 无缓冲区,日志会实时输出,避免程序崩溃时缓冲区日志丢失
RAII 锁管理:使用LockGuard自动管理锁,无需手动解锁,代码更健壮
3.3 文件日志策略 FileLogStrategy
文件策略负责将日志持久化到磁盘文件,核心功能包括:自动创建日志目录、追加模式写入、线程安全保障,使用 C++17 的filesystem库处理目录和文件操作(记得带上对应头文件)。
#include <fstream>
#include <filesystem>
AI写代码
cpp
运行
1
2
namespace LogModule
{
// 定义全局默认路径与文件名常量
const static std::string gdefaultlogdir = "./log/";
const static std::string gdefaultlogfilename = "log.txt";
// 策略2:文件类日志策略
// 子类:继承自策略基类,实现将日志持久化到磁盘文件的逻辑
class FileLogStrategy: public LogStrategy
{
public:
/**
* @brief 构造函数:初始化日志路径并确保目录环境就绪
* @param logdir 日志存储目录
* @param logfilename 日志文件名称
*/
FileLogStrategy(const std::string &logdir = gdefaultlogdir, const std::string &logfilename = gdefaultlogfilename)
:_logdir(logdir),
_logfilename(logfilename)
{
// 【重点】构造阶段即进行加锁保护。
// 理由:判断目录是否存在并创建目录属于“先检查再执行(Check-Then-Act)”模式,
// 必须保证这一系列操作的原子性,防止多线程同时创建导致竞态冲突 。
LockGuard lockGuard(&_mutex);
// 使用 C++17 的 <filesystem> 库进行跨平台路径检查
if(std::filesystem::exists(_logdir))
{
return;
}
else
{
try
{
// 递归创建目录(类似于 Linux 命令 mkdir -p),如果路径中包含多级不存在的目录会一并创建
std::filesystem::create_directories(_logdir);
}
catch (std::filesystem::filesystem_error &e)
{
// 捕获文件系统异常(如权限不足、磁盘空间不足等)并输出错误信息
std::cerr << e.what() << std::endl;
}
}
}
// 析构函数:由于不涉及手动管理的堆内存或特殊文件句柄(使用局部变量流管理),故使用默认实现
~FileLogStrategy(){}
/**
* @brief 执行具体的日志落盘操作
* @param message 待写入的完整日志字符串
*/
void SyncLog(const std::string &message) override
{
// 加锁保护:防止多线程同时写入同一文件导致内容交织(Interleaving)乱码
LockGuard logGuard(&_mutex);
// 构造完整的目标文件路径
std::string target = _logdir + _logfilename;
// 以追加模式(std::ios::app)打开文件流:
// 核心逻辑:保证每条新日志都写在文件末尾,不会覆盖已有日志内容 。
std::ofstream out(target, std::ios::app); // 追加
if(!out.is_open()) // 打开文件检查
{
return; // 如果因权限或路径问题打开失败,则放弃本次写入,防止程序崩溃
}
// 将消息流式写入文件,并手动添加换行符以符合日志排版规范
out << message << "\n"; // 流式写入
// 文件流离开作用域或显式调用 close 会自动触发刷新并关闭文件
out.close();
}
private:
std::string _logdir; // 存储目录路径
std::string _logfilename; // 存储文件名称
Mutex _mutex; // 用于保障当前策略类实例在多线程环境下的线程安全
};
}
AI写代码
cpp
运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
核心设计解析:
自动目录创建:构造函数中检查日志目录是否存在,不存在则通过create_directories递归创建,避免手动创建目录的繁琐
追加模式写入:使用std::ios::app打开文件,所有日志都会追加到文件末尾,不会覆盖历史日志,符合日志系统的通用规范
异常处理:目录创建时捕获filesystem的异常,避免目录创建失败导致程序崩溃
线程安全:文件写入全程加锁,保证多线程环境下不会出现半行日志、内容交错的问题
测试代码
#include <iostream>
#include <memory>
#include <unistd.h>
#include "Logger.hpp"
using namespace LogModule;
// 测试刷新策略
void testStrategy()
{
std::string message1 = "console: hello Log, hello Lotso!";
std::string message2 = "file: hello Log, hello Lotso!";
std::unique_ptr<LogStrategy> strategy = std::make_unique<ConsoleLogStrategy>(); // 父类指针指向子类对象
strategy->SyncLog(message1);
strategy->SyncLog(message1);
strategy->SyncLog(message1);
strategy->SyncLog(message1);
strategy->SyncLog(message1);
strategy = std::make_unique<FileLogStrategy>(); // 父类指针指向子类对象
strategy->SyncLog(message2);
strategy->SyncLog(message2);
strategy->SyncLog(message2);
strategy->SyncLog(message2);
strategy->SyncLog(message2);
}
int main()
{
// 3. 测试策略
testStrategy();
return 0;
}
AI写代码
cpp
运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
四. 日志主体类与流式输出设计
完成基础模块和策略模式的实现后,我们来实现日志系统的主体类,核心目标是:兼容 glog 的流式调用风格、自动拼接日志元信息、RAII 自动触发日志刷新。
4.1 Logger 主类的整体架构
Logger类是日志系统的对外入口,核心职责包括:
管理当前使用的日志刷新策略,支持动态切换
提供仿函数接口,生成日志消息对象
封装策略切换的便捷接口
4.2 LogMessage 内部类:RAII 实现日志自动刷新
LogMessage是Logger的内部类,是整个日志系统最巧妙的设计:
构造函数中完成日志元信息(时间、等级、PID、文件名、行号)的拼接
重载<<运算符,支持流式拼接任意类型的日志内容
析构函数中自动触发日志刷新,利用临时对象的生命周期实现 “写完即刷新”
4.3 完整的 Logger 类实现
#include <memory>
#include <sstream>
#include <unistd.h>
AI写代码
cpp
运行
1
2
3
namespace LogModule // 定义日志模块命名空间
{
/**
* @brief Logger 类:日志系统的核心统筹管理者
* 负责维护日志刷新策略(显示器/文件)并作为产生日志消息的入口
*/
class Logger
{
public:
// 构造函数:初始化时默认开启控制台刷新策略
Logger()
{
UseConsoleLogStrategy();
}
// 切换策略:动态更换为控制台输出策略
void UseConsoleLogStrategy()
{
_strategy = std::make_unique<ConsoleLogStrategy>();
}
// 切换策略:动态更换为文件输出策略
void UseFileLogStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
~Logger(){};
/**
* @brief LogMessage 内部类:代表单条日志消息的生命周期管理
* 核心设计思想:利用临时对象的生命周期(RAII)实现日志的自动组装与刷新
*/
class LogMessage
{
public:
/**
* @brief 构造函数:构建日志的“左半部分”(前缀信息)
* 包括时间、等级、PID、文件名和行号,并预置到流中
*/
LogMessage(LogLevel level, const std::string &filename, int line, Logger &self)
: _currenttime(GetTimeStamp()) // 获取当前格式化时间
, _loglevel(LogLevel2String(level)) // 等级转字符串
, _pid(getpid()) // 获取进程 PID
, _filename(filename)
, _line(line)
, _logger(self) // 持有外部统帅类的引用,用于后续刷新
{
std::stringstream ss;
// 像拼积木一样组装固定格式的前缀
ss << "[" << _currenttime << "] "
<< "[" << _loglevel << "] "
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _line << "] "
<< "- ";
_loginfo = ss.str(); // 直接拼上去
}
/**
* @brief 析构函数:整个设计的灵魂(RAII 自动刷新)
* 当这一行日志代码执行完毕(临时对象销毁)时,自动触发物理刷新
*/
~LogMessage()
{
if(_logger._strategy)
{
// 走到尽头了,调用刷新策略刷新出来
_logger._strategy->SyncLog(_loginfo);
}
}
/**
* @brief 模板重载 <<:构建日志的“右半部分”(用户自定义内容)
* 利用模板接纳任意类型,并通过 stringstream 实现安全的字符串转换与链式拼接
*/
template <typename T>
LogMessage& operator << (const T& info)
{
std::stringstream ss;
ss << info; // 自动处理 int, double, string 等类型转换
_loginfo += ss.str(); // 将内容追加到完整日志字符串中
return *this; // 返回自身引用,支持像 cout 一样的连续 << 调用
}
private:
std::string _currenttime;
std::string _loglevel;
int _pid;
std::string _filename;
int _line;
std::string _loginfo; // 存储整条待刷新的日志字符串
Logger &_logger; // 外部类引用:让消息知道自己隶属于哪个 Logger
};
/**
* @brief 仿函数重载:作为“桥梁”连接宏调用与内部消息对象
* Logger对象打印日志的时候,故意返回一个LogMessage的临时对象
*/
LogMessage operator() (LogLevel level, const std::string filename, int line)
{
// 创建并返回临时对象,开启后续的流式 << 操作
return LogMessage(level, filename, line, *this);
}
private:
std::unique_ptr<LogStrategy> _strategy; // 多态策略指针:决定日志去向
};
// 定义一个全局模块的Logger对象, 方便后续的使用
Logger logger;
/**
* @brief LOG 宏:对外提供的极简调用接口
* 自动捕获当前代码的 __FILE__ (文件名) 和 __LINE__ (行号)
*/
#define LOG(level) logger(level, __FILE__, __LINE__)
// 还是使用宏: 提供动态调整日志策略的便捷开关
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleLogStrategy()
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileLogStrategy()
}
AI写代码
cpp
运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
核心设计深度解析:
仿函数机制:重载operator()让Logger对象可以像函数一样调用,返回一个LogMessage临时对象,这是实现流式调用的核心
RAII 自动刷新:LogMessage是临时对象,当整条LOG(xxx) << "xxx"语句执行完毕后,临时对象会被析构,析构函数中自动调用策略的刷新接口,无需用户手动触发刷新
模板化流式运算符:通过模板重载<<运算符,支持 int、double、string、char 等任意可流输出的类型,实现和std::cout一致的使用体验
预定义宏封装:
__FILE__:编译期自动替换为当前源文件名
__LINE__:编译期自动替换为当前代码行号
LOG宏将繁琐的参数传递封装为极简的调用方式,完全对齐 glog 的使用风格
全局单例:定义全局的logger对象,整个程序共用一个日志实例,避免重复创建,同时保证策略切换全局生效
测试代码
#include <iostream>
#include <memory>
#include <unistd.h>
#include "Logger.hpp"
using namespace LogModule;
// 总体测试
void test()
{
// 开启控制台策略
ENABLE_CONSOLE_LOG_STRATEGY();
LOG(LogLevel::DEBUG) << "CONSOLE: hello Lotso " << 7.9 << " bd";
LOG(LogLevel::INFO) << "CONSOLE: hello Lotso " << 7.9 << " bd";
LOG(LogLevel::WARNING) << "CONSOLE: hello Lotso " << 7.9 << " bd";
LOG(LogLevel::ERROR) << "CONSOLE: hello Lotso " << 7.9 << " bd";
LOG(LogLevel::FATAL) << "CONSOLE: hello Lotso " << 7.9 << " bd";
// 开启文件策略
ENABLE_FILE_LOG_STRATEGY();
LOG(LogLevel::DEBUG) << "FILE: hello Lotso " << 7.9 << " bd";
LOG(LogLevel::INFO) << "FILE: Lotso " << 7.9 << " bd";
LOG(LogLevel::WARNING) << "FILE: hello Lotso " << 7.9 << " bd";
LOG(LogLevel::ERROR) << "FILE: hello Lotso " << 7.9 << " bd";
LOG(LogLevel::FATAL) << "FILE: hello Lotso " << 7.9 << " bd";
}
int main()
{
// 4. 整体测试
test();
return 0;
}
AI写代码
cpp
运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
五. 日志系统的线程安全与可重入性深度解析
多线程环境下,日志系统的线程安全是重中之重,我们从多个维度做了全面保障:
临界资源的互斥保护:控制台、日志文件都是全局临界资源,所有写入操作都通过互斥锁保证原子性,同一时刻只有一个线程能执行写入操作,彻底避免内容交错、乱序。
可重入函数的使用:时间戳获取使用localtime_r而非不可重入的localtime,避免多线程同时调用时出现时间数据错乱;所有函数均不使用全局静态变量,所有状态都保存在对象内部,保证重入安全。
锁的粒度与 RAII 管理:锁的粒度严格控制在写入操作的最小范围,避免长时间持有锁导致性能下降;同时通过LockGuard的 RAII 机制保证锁一定会被释放,即使写入过程中出现异常,也不会出现死锁。
无锁的日志格式化阶段:日志的格式化(拼接头部、用户内容)是在每个线程的LogMessage临时对象中完成的,每个线程的日志格式化完全独立,没有共享资源竞争,无需加锁,最大化提升了并发性能。
六. 实战:日志系统完整使用示例(附带完整Logger.hpp代码呈现)
我们通过一个完整的示例,展示日志系统的基础使用、策略切换、多线程安全验证。
6.1 完整Logger.hpp代码
#ifndef LOGGER_HPP
#define LOGGER_HPP
#include <fstream>
#include <iostream>
#include <ctime>
#include <cstdio>
#include <memory>
#include <sstream>
#include <string>
#include <filesystem>
#include <unistd.h>
#include "Mutex.hpp"
namespace LogModule
{
// 1. 获取时间
std::string GetTimeStamp()
{
time_t currentTime = time(nullptr); // 默认获取当前时区的时间
// 我们希望把这个时间转换成年-月-日 时:分:秒
struct tm dataTime;
// 使用线程安全的版本 localtime_r,防止在多线程并发获取时间时
// 因为共享静态全局变量而导致的时间数据覆盖错乱。
localtime_r(¤tTime, &dataTime);
char dataTimeStr[128];
// 使用 snprintf 保证缓冲区不溢出,%02d 确保时间位宽不足时自动补0(如09秒)
snprintf(dataTimeStr, sizeof(dataTimeStr), "%4d-%02d-%02d %02d:%02d:%02d",
dataTime.tm_year + 1900, // tm_year 是从1900年开始计算的偏移量
dataTime.tm_mon + 1, // tm_mon 范围是 [0, 11],需加1修正
dataTime.tm_mday,
dataTime.tm_hour,
dataTime.tm_min,
dataTime.tm_sec
);
return dataTimeStr;
}
// 2. 日志等级 -- 枚举类型(整数)转换成字符串类型
// 使用 enum class 强类型枚举,避免命名污染,提高类型检查的严谨性
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
/**
* @brief 辅助函数:将枚举常量映射为可读字符串
* 解决强类型枚举无法直接通过 std::cout 打印的问题
*/
std::string LogLevel2String(LogLevel level)
{
switch(level)
{
case LogLevel::DEBUG:
return "DEBUG";
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
default:
return "UNKNOWN";
}
}
// 3. 刷新策略
// 基类: 策略模式
// 设计意图:将“日志消息的生成”与“日志消息的输出去向”解耦,方便后续扩展网络、数据库等输出端
class LogStrategy
{
public:
// 虚析构函数:确保通过基类指针释放子类对象时,子类的资源(如文件句柄)能被正确释放
virtual ~LogStrategy() = default; // 不在这里析构
// 纯虚函数:定义统一的刷新接口规范
virtual void SyncLog(const std::string &message) = 0; // 强制子类对其进行重写
};
// 策略1: 控制台日志策略
// 子类
class ConsoleLogStrategy: public LogStrategy
{
public:
ConsoleLogStrategy(){}
~ConsoleLogStrategy(){}
void SyncLog(const std::string &message) override // 检查重写的错误
{
// 显示器在多线程下是“临界资源”,加锁防止多线程输出字符交织(Interleaving)
LockGuard logGuard(&_mutex);
std::cout << message << std::endl;
}
private:
Mutex _mutex;
};
const static std::string gdefaultlogdir = "./log/";
const static std::string gdefaultlogfilename = "log.txt";
// 策略2:文件类日志策略
// 子类
class FileLogStrategy: public LogStrategy
{
public:
// 构造函数:初始化路径并利用 C++17 库确保目录环境就绪
FileLogStrategy(const std::string &logdir = gdefaultlogdir, const std::string &logfilename = gdefaultlogfilename)
:_logdir(logdir),
_logfilename(logfilename)
{
// 创建目录前加锁,防止多线程同时执行判断与创建操作引发的竞态条件
LockGuard lockGuard(&_mutex);
if(std::filesystem::exists(_logdir))
{
return;
}
else
{
try
{
// 递归创建目录(mkdir -p),若权限不足或磁盘满会抛出异常
std::filesystem::create_directories(_logdir);
}
catch (std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << std::endl;
}
}
}
~FileLogStrategy(){}
void SyncLog(const std::string &message) override
{
// 文件 I/O 是昂贵的临界资源操作,加锁保证单条日志写入的原子性
LockGuard logGuard(&_mutex);
std::string target = _logdir + _logfilename;
// 使用 std::ios::app (append) 追加模式,保证新旧日志共存而不被覆盖
std::ofstream out(target, std::ios::app); // 追加
if(!out.is_open()) // 打开文件
{
return;
}
out << message << "\n"; // 流式写入并换行
out.close(); // 关闭文件流,触发缓冲区刷新
}
private:
std::string _logdir;
std::string _logfilename;
Mutex _mutex;
};
/**
* @brief Logger 类:日志系统的中央控制器
* 内部嵌套了 LogMessage 类来实现精妙的 RAII 自动刷新机制
*/
class Logger
{
public:
Logger()
{
UseConsoleLogStrategy(); // 默认策略
}
void UseConsoleLogStrategy()
{
_strategy = std::make_unique<ConsoleLogStrategy>();
}
void UseFileLogStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
~Logger(){};
// 内部类:负责单条日志的组装和析构刷新
class LogMessage
{
public:
// 构造函数:预组装日志“前缀”部分
LogMessage(LogLevel level, const std::string &filename, int line, Logger &self)
: _currenttime(GetTimeStamp())
, _loglevel(LogLevel2String(level))
, _pid(getpid())
, _filename(filename)
, _line(line)
, _logger(self) // 保存引用,以便在析构时找到所属的 Logger 进行刷新
{
std::stringstream ss;
ss << "[" << _currenttime << "] "
<< "[" << _loglevel << "] "
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _line << "] "
<< "- ";
_loginfo = ss.str(); // 此时前缀已拼入缓冲区
}
/**
* @brief 核心设计:RAII 机制触发刷新
* 当 LOG(...) << "msg"; 这行语句执行完毕,临时对象生命周期结束,
* 在析构函数中调用策略接口,保证日志在写完即刻、必然被刷出。
*/
~LogMessage()
{
if(_logger._strategy)
{
// 走到尽头了,调用刷新策略刷新出来
_logger._strategy->SyncLog(_loginfo);
}
}
// 用模版重载 << 运算符:接纳各种类型(int, string, double等)
template <typename T>
LogMessage& operator << (const T& info)
{
std::stringstream ss;
ss << info; // 自动完成类型转换
_loginfo += ss.str(); // 追加到内容主体中
return *this; // 返回引用支持链式调用,如 LOG << a << b << c;
}
private:
std::string _currenttime;
std::string _loglevel;
int _pid;
std::string _filename;
int _line;
std::string _loginfo;
Logger &_logger; // 外部类引用:用于访问具体刷新策略
};
/**
* @brief 重载仿函数 operator()
* 这是桥梁:将宏参数传入,并返回一个持有 Logger 权限的临时消息对象
*/
LogMessage operator() (LogLevel level, const std::string filename, int line)
{
return LogMessage(level, filename, line, *this);
}
private:
// 使用 unique_ptr 配合策略基类实现运行时多态
std::unique_ptr<LogStrategy> _strategy; // 策略
};
// 定义一个全局模块的Logger对象, 方便后续的使用
Logger logger;
// 定义宏:捕获编译器内置变量 __FILE__ 和 __LINE__,简化用户调用 API
#define LOG(level) logger(level, __FILE__, __LINE__)
// 便捷切换输出目的地的宏定义
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleLogStrategy()
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileLogStrategy()
}
#endif
AI写代码
cpp
运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
6.2 完整测试代码
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include "Logger.hpp" // 我们实现的日志头文件
using namespace LogModule;
// 多线程测试函数:10个线程同时打印日志
void *thread_log_test(void *arg)
{
char *thread_name = (char *)arg;
for (int i = 0; i < 5; i++)
{
LOG(LogLevel::INFO) << thread_name << " 执行日志打印, 循环次数: " << i;
usleep(1000);
}
return nullptr;
}
int main()
{
// 1. 基础控制台日志输出
std::cout << "===== 控制台日志测试 =====" << std::endl;
ENABLE_CONSOLE_LOG_STRATEGY();
LOG(LogLevel::DEBUG) << "这是DEBUG调试日志, 数值: " << 3.14159;
LOG(LogLevel::INFO) << "这是INFO常规日志, 服务启动成功";
LOG(LogLevel::WARNING) << "这是WARNING警告日志, 配置缺失, 使用默认值";
LOG(LogLevel::ERROR) << "这是ERROR错误日志, 文件读取失败";
LOG(LogLevel::FATAL) << "这是FATAL致命日志, 内存耗尽, 服务退出";
// 2. 切换为文件日志策略
std::cout << "\n===== 文件日志测试 =====" << std::endl;
ENABLE_FILE_LOG_STRATEGY();
LOG(LogLevel::INFO) << "切换为文件日志策略, 日志将持久化到./log/log.txt";
LOG(LogLevel::DEBUG) << "文件日志测试, 支持链式拼接: " << "字符串 " << 1234 << " 浮点数 " << 2.71828;
// 3. 多线程线程安全测试
std::cout << "\n===== 多线程日志测试 =====" << std::endl;
pthread_t t1, t2, t3, t4;
pthread_create(&t1, nullptr, thread_log_test, (void *)"thread-1");
pthread_create(&t2, nullptr, thread_log_test, (void *)"thread-2");
pthread_create(&t3, nullptr, thread_log_test, (void *)"thread-3");
pthread_create(&t4, nullptr, thread_log_test, (void *)"thread-4");
// 等待所有线程执行完毕
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
pthread_join(t4, nullptr);
LOG(LogLevel::INFO) << "多线程日志测试完成, 无乱序、无交错";
return 0;
}
AI写代码
cpp
运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
6.3 进阶优化方向
本篇博客实现的日志系统已满足基础的工业级需求,还可以从以下方向做进阶优化,适配更高性能、更复杂的业务场景:
异步日志机制:当前是同步写入,磁盘 IO 会阻塞业务线程。可实现双缓冲区异步日志,业务线程将日志写入内存缓冲区,后台线程专门负责磁盘写入,彻底消除 IO 阻塞。
日志滚动与分片:支持按文件大小、按天 / 小时切割日志文件,避免单个日志文件过大,同时支持过期日志自动清理。
日志分级过滤:支持设置全局日志等级,比如生产环境关闭 DEBUG 级日志,减少日志量和 IO 开销。
更多策略扩展:实现网络日志策略(上报到日志中心)、数据库日志策略、syslog 系统日志策略等,基于策略模式可无缝扩展。
线程 ID 打印:新增线程 ID 字段,多线程环境下问题定位更精准。
日志格式化优化:支持用户自定义日志格式,适配不同的日志采集规范。
————————————————
结语:从零实现一个线程安全的日志系统,不仅是造轮子的过程,更是对 Linux 多线程编程、设计模式、C++RAII 机制、可重入函数等核心知识点的深度实践。本文基于策略模式实现的日志系统,做到了格式化与刷新逻辑解耦、线程安全、使用便捷、可无限扩展,完全兼容 glog 的流式使用风格,同时通过源码级的拆解,让我们理解了成熟日志库背后的底层实现原理。在实际开发中,我们可以直接使用 spdlog 等成熟的开源库,但只有理解了底层实现,才能在遇到日志乱序、程序崩溃日志丢失、多线程性能瓶颈等问题时,精准定位并解决问题,这也是底层能力的核心价值。
————————————————
版权声明:本文为CSDN博主「草莓熊Lotso」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/2503_91389547/article/details/160240282
————————————————
版权声明:本文为CSDN博主「草莓熊Lotso」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/2503_91389547/article/details/160240282