日志系统设计 与 策略模式实现
目录
一、设计模式概述
二、日志系统基础
1、日志系统的重要性
2、日志格式规范
必须包含的字段
可选字段
3、现有日志解决方案
三、策略模式详解
1、策略模式定义
2、在日志系统中的应用
3、预期日志格式
四、日志系统实现解析
1、头文件结构
2、策略模式实现
1. 策略基类
2. 具体策略实现
3、日志等级与时间戳处理
4、核心Logger类实现
5、全局访问与宏定义
6、注意:关于多态(Polymorphism)和智能指针(std::unique_ptr)的使用
1. 这份代码是否安全?
2. 对象截断(Object Slicing)何时发生?
3. 代码示例分析
4. 如何确保多态调用?
(1) 基类必须定义虚函数
(2) 派生类必须覆盖虚函数
(3) 通过基类指针调用
5. 总结
6. 额外建议
7、完整日志系统实现代码
五、线程安全实现
互斥锁封装
六、使用示例与分析
1、基本使用示例
2、输出结果分析
3、设计优势
七、改进与扩展建议
1、性能优化方向
2、功能扩展建议
八、总结
一、设计模式概述
在IT行业快速发展的背景下,软件开发领域涌现出大量开发者,技术水平参差不齐。为了帮助初级开发者更好地理解和实现常见问题的解决方案,资深开发者总结了一系列经典的设计模式。
设计模式是软件设计中常见问题的可重用解决方案,它们不是具体的代码实现,而是解决特定问题的模板或策略。设计模式主要分为三大类:
-  
创建型模式:关注对象的创建机制(如工厂模式、单例模式)
 -  
结构型模式:处理对象间的组合关系(如适配器模式、装饰器模式)
 -  
行为型模式:描述对象间的通信方式(如策略模式、观察者模式)
 
本案例将重点讲解策略模式在日志系统设计中的应用。
二、日志系统基础
1、日志系统的重要性
计算机日志是记录系统和软件运行事件的专用文件,主要用于实时监控运行状态、追踪异常信息,便于快速定位故障并辅助程序员进行调试修复。作为系统维护的核心工具,日志在故障排查和安全管理中发挥着关键作用,其主要作用包括:
-  
运行监控:实时跟踪系统状态
 -  
问题诊断:记录异常信息帮助快速定位问题
 -  
安全审计:追踪安全相关事件
 -  
性能分析:记录操作耗时等性能数据
 
2、日志格式规范
必须包含的字段
-  
时间戳:精确到秒的事件发生时间(推荐格式:YYYY-MM-DD HH:MM:SS)
 -  
日志等级:表示日志严重程度(DEBUG/INFO/WARNING/ERROR/FATAL)
 -  
日志内容:实际记录的信息
 
可选字段
-  
源代码位置:文件名和行号(FILE, LINE)
 -  
进程/线程信息:进程ID、线程ID
 -  
模块/功能标识:区分不同业务模块
 -  
会话/请求ID:追踪特定请求链路
 
3、现有日志解决方案
业界已有多种成熟的日志库:
-  
C++: spdlog, glog, Boost.Log, Log4cxx
 -  
Java: Log4j, Logback, java.util.logging
 -  
Python: logging模块
 
本案例选择自定义实现,主要为了:
-  
第一,深入理解日志系统内部机制
 -  
第二,演示策略模式的具体应用
 -  
第三,根据特定需求定制功能
 
三、策略模式详解
1、策略模式定义
策略模式(Strategy Pattern)是一种行为设计模式,它定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户端。
核心思想:
-  
定义算法家族(抽象策略接口)
 -  
封装每个算法(具体策略实现)
 -  
客户端通过组合方式使用策略(上下文类)
 
2、在日志系统中的应用
在日志系统中,不同的日志输出方式(控制台、文件、网络等)可以视为不同的策略:
// 策略接口
class LogStrategy {
public:virtual ~LogStrategy() = default;virtual void SyncLog(const std::string &message) = 0;
};// 具体策略:控制台输出
class ConsoleLogStrategy : public LogStrategy {// 实现细节...
};// 具体策略:文件输出
class FileLogStrategy : public LogStrategy {// 实现细节...
}; 
3、预期日志格式
为优化日志设计,我们采用了策略模式。关于策略模式的详细介绍,请参考相关代码和博客内容。我们期望的日志格式如下:
[可读性很好的时间] [⽇志等级] [进程pid] [打印对应⽇志的⽂件名][⾏号] - 消息内容,⽀持可变参数
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [17] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [18] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [20] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [21] - hello world
[2024-08-04 12:27:03] [WARNING] [202938] [main.cc] [23] - hello world 
四、日志系统实现解析
1、头文件结构
#ifndef __LOG_HPP__
#define __LOG_HPP__// 系统头文件
#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> // C++17
#include <sstream>
#include <fstream>
#include <memory>
#include <ctime>
#include <unistd.h>// 自定义模块
#include "Mutex.hpp"namespace LogModule {// 实现细节...
}#endif 
2、策略模式实现
1. 策略基类
class LogStrategy {
public:virtual ~LogStrategy() = default;virtual void SyncLog(const std::string &message) = 0;
}; 
这段代码定义了一个名为 LogStrategy 的抽象基类(Abstract Base Class),用于实现策略模式(Strategy Pattern)中的策略接口。
1. 类定义与目的
class LogStrategy {
public:virtual ~LogStrategy() = default;virtual void SyncLog(const std::string &message) = 0;
}; 
-  
作用:定义日志记录的策略接口,允许不同的日志记录方式(如文件日志、控制台日志、网络日志等)通过继承该类实现多态行为。
 -  
设计模式:这是策略模式的典型应用,将算法(这里是日志记录方式)封装为对象,便于动态切换。
 
2. 成员函数解析
(1) 虚析构函数
virtual ~LogStrategy() = default; 
关键点:
-  
声明为
virtual,确保通过基类指针删除派生类对象时,能正确调用派生类的析构函数(避免资源泄漏)。 -  
= default表示使用编译器生成的默认析构函数(简洁且高效)。 
为什么需要:如果基类析构函数非虚,通过基类指针 delete 派生类对象时,只会调用基类的析构函数,导致派生类部分未被正确释放。(重点!!!)
(2) 纯虚函数 SyncLog
virtual void SyncLog(const std::string &message) = 0; 
关键点:
-  
= 0表示这是一个纯虚函数,LogStrategy成为抽象类,不能直接实例化。 -  
派生类必须实现此函数,提供具体的日志记录逻辑。
 
参数:接收一个 const std::string& 类型的消息,表示待记录的日志内容。
设计意图:强制所有派生策略实现统一的日志接口,但内部行为可以不同(如写入文件、发送到服务器等)。
2. 具体策略实现
控制台日志策略:
class ConsoleLogStrategy : public LogStrategy {
public:ConsoleLogStrategy() = default;void SyncLog(const std::string &message) override {LockGuard lockguard(_mutex); // 线程安全std::cout << message << gsep;}private:Mutex _mutex; // 互斥锁保证线程安全
}; 
这段代码定义了一个具体的日志策略类 ConsoleLogStrategy,继承自 LogStrategy,用于将日志输出到控制台。
1. 类定义与继承关系
class ConsoleLogStrategy : public LogStrategy {
public:ConsoleLogStrategy() = default;void SyncLog(const std::string &message) override;
private:Mutex _mutex;
}; 
-  
继承:通过
public LogStrategy继承基类的纯虚接口,表明这是一个具体的策略实现。 -  
构造函数:
= default表示使用编译器生成的默认构造函数,无需额外初始化逻辑。 -  
线程安全:通过
Mutex _mutex和LockGuard实现多线程环境下的安全输出。 
2. 关键实现:SyncLog 方法
void SyncLog(const std::string &message) override {LockGuard lockguard(_mutex); // 加锁std::cout << message << gsep; // 输出日志
} 
(1) 线程安全设计
-  
Mutex _mutex:成员变量,用于保护共享资源(这里是std::cout)。 -  
LockGuard(我们可以想象是std::lock_guard<Mutex>的别名,但是我这里使用的是自己已经封装好了的Mutex.hpp):-  
在构造时自动加锁(
_mutex.lock())。 -  
在作用域结束时(如函数退出)自动解锁(通过 RAII 机制)。
 -  
避免手动管理锁导致的死锁或忘记解锁问题。
 
 -  
 
(2) 日志输出
-  
std::cout:标准输出流,将日志打印到控制台。 -  
gsep(在代码中已经定义,是全局变量):-  
为分隔符(如
"\r\n"),用于格式化日志。 -  
也可以改为局部常量或通过参数配置,减少全局状态依赖。
 
 -  
 
文件日志策略:
// 文件打印日志的策略 : 子类
const std::string defaultpath = "./log";
const std::string defaultfile = "my.log";class FileLogStrategy : public LogStrategy {
public:// 构造函数FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile): _path(path), _file(file) {// 初始化日志目录if (!std::filesystem::exists(_path)) {try {std::filesystem::create_directories(_path);} catch (const std::filesystem::filesystem_error &e) {std::cerr << e.what() << '\n';}}}// 实现基类的纯虚函数void SyncLog(const std::string &message) override {LockGuard lockguard(_mutex);std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;std::ofstream out(filename, std::ios::app); // 追加模式if (out.is_open()) {out << message << gsep;out.close();}}private:std::string _path;  // 日志文件所在路径std::string _file;  // 日志文件名Mutex _mutex;       // 互斥锁,保证线程安全
}; 
FileLogStrategy 是日志模块中实现文件日志策略的类,继承自 LogStrategy 基类。
成员变量
-  
_path: 存储日志文件的目录路径:默认值为"./log"(由defaultpath常量定义) -  
_file: 存储日志文件名:默认值为"my.log"(由defaultfile常量定义) -  
_mutex: 互斥锁对象:用于保证多线程环境下文件写入的线程安全 
构造函数
FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile): _path(path), _file(file) {// 初始化日志目录if (!std::filesystem::exists(_path)) {try {std::filesystem::create_directories(_path);} catch (const std::filesystem::filesystem_error &e) {std::cerr << e.what() << '\n';}}
} 
参数:
-  
path: 日志目录路径(可选,默认defaultpath) -  
file: 日志文件名(可选,默认defaultfile) 
功能:
-  
初始化成员变量
_path和_file -  
检查日志目录是否存在,如果不存在则尝试创建
 -  
使用
try-catch捕获可能的文件系统异常并输出错误信息 
构造函数这里使用了 C++17 引入的 <filesystem> 库来处理文件系统操作。下面讲解std::filesystem 的相关功能以及这段代码的作用:
1. std::filesystem 简介
    std::filesystem 是 C++17 标准中引入的库,用于处理文件和目录路径、遍历文件系统、查询文件信息等操作。它提供了跨平台的文件系统操作接口,替代了之前的平台相关 API(如 Windows 的 <windows.h> 或 POSIX 的 <dirent.h>)。

2. 代码功能解析
这段代码是一个构造函数,用于初始化一个文件日志策略类 FileLogStrategy,主要功能是确保日志目录存在:
FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile): _path(path), _file(file) {  // 初始化成员变量// 检查目录是否存在if (!std::filesystem::exists(_path)) {  // 如果路径不存在try {// 创建目录(包括所有必要的父目录)std::filesystem::create_directories(_path);} catch (const std::filesystem::filesystem_error &e) {// 捕获并处理文件系统错误std::cerr << e.what() << '\n';}}
} 
3. 关键函数说明
(1) std::filesystem::exists()

-  
作用:检查给定路径是否存在(文件或目录)
 -  
参数:接受一个路径对象(可以是
std::string、std::filesystem::path或字符数组) -  
返回值:
bool,存在返回true,否则false -  
示例:
bool exists = std::filesystem::exists("/path/to/check"); 
(2) std::filesystem::create_directories()

-  
作用:创建目录及其所有不存在的父目录(类似
mkdir -p) -  
参数:路径对象
 -  
异常:如果创建失败会抛出
std::filesystem::filesystem_error -  
与
create_directory()的区别:-  
create_directory()只能创建单级目录,父目录必须已存在 -  
create_directories()可以创建多级目录 
 -  
 -  
示例:
std::filesystem::create_directories("path/to/log/dir"); 
(3) std::filesystem::filesystem_error

-  
作用:文件系统操作失败时抛出的异常类型
 -  
常用方法:
-  
what():返回错误描述字符串 -  
path1():返回第一个相关路径 -  
path2():返回第二个相关路径(如果有) -  
code():返回std::error_code对象 
 -  
 
4. 完整工作流程
-  
构造函数接收日志路径
_path和文件名_file -  
使用
exists()检查路径是否存在 -  
如果不存在,尝试用
create_directories()创建目录 -  
如果创建过程中出错(如权限不足),捕获异常并输出错误信息
 
5. 现代 C++ 改进建议
-  
可以使用
std::filesystem::path类型代替std::string表示路径
 -  
C++17 允许使用结构化绑定处理异常:
catch (const std::filesystem::filesystem_error& e) {auto [ec, msg] = std::make_pair(e.code(), e.what());std::cerr << "Error " << ec << ": " << msg << '\n'; } 
6. 编译注意事项
使用 std::filesystem 需要:
-  
C++17 或更高版本支持(编译选项如
-std=c++17) -  
链接文件系统库(如 g++ 需要
-lstdc++fs,但较新版本已集成) 
这个构造函数展示了文件系统库的典型用法:路径检查 + 目录创建 + 错误处理,是日志系统初始化的常见模式。
SyncLog 方法
void SyncLog(const std::string &message) override {LockGuard lockguard(_mutex);std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;std::ofstream out(filename, std::ios::app); // 追加模式if (out.is_open()) {out << message << gsep;out.close();}
} 
参数:message - 要写入的日志消息
功能:
-  
使用
LockGuard获取互斥锁,保证线程安全 -  
构造完整的日志文件路径:
-  
如果路径已以 '/' 结尾,直接拼接文件名
 -  
否则添加 '/' 后再拼接文件名
 
 -  
 -  
以追加模式 (
std::ios::app) 打开文件流 -  
如果文件成功打开:
-  
写入日志消息和分隔符 (
gsep,定义为"\r\n") -  
关闭文件流
 
 -  
 
        这个函数实现了一个线程安全的日志同步写入功能,使用了 std::ofstream 进行文件操作。下面讲解 std::ofstream 的相关用法以及整个函数的逻辑。
1. std::ofstream 详解
    std::ofstream 是 C++ 标准库 <fstream> 提供的输出文件流类,用于向文件写入数据。它是 std::ostream 的派生类,支持 << 操作符,类似于 std::cout,但写入文件而非控制台。

(1) 构造函数
std::ofstream out(filename, std::ios::app); 
-  
作用:打开文件
filename,准备写入。 -  
参数:
-  
filename:文件路径(std::string或const char*)。 -  
std::ios::app:打开模式,表示追加模式(append),新数据写入文件末尾,不会覆盖原有内容。 
 -  
 -  
其他常见打开模式(可组合使用):

-  
std::ios::out:默认模式,写入文件(覆盖原有内容)。 -  
std::ios::binary:二进制模式。 -  
std::ios::trunc:如果文件存在,清空内容(默认行为,除非指定app或ate)。 -  
std::ios::ate:打开后定位到文件末尾(但写入时仍可移动指针)。 
 -  
 
(2) is_open()

if (out.is_open()) 
-  
作用:检查文件是否成功打开。
 -  
返回值:
bool,成功打开返回true,否则false(如权限不足、路径不存在等)。 
(3) << 操作符
out << message << gsep; 
-  
作用:向文件写入数据,类似于
std::cout。 -  
支持类型:字符串、数字、自定义类型(需重载
<<)。 
(4) close()

out.close(); 
-  
作用:关闭文件流,释放资源。
 -  
注意:析构时会自动调用,但显式关闭是良好实践。
 
2. 代码逻辑解析
void SyncLog(const std::string &message) override {LockGuard lockguard(_mutex);  // 1. 加锁,保证线程安全// 2. 构造完整文件路径(确保路径以 '/' 结尾)std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;// 3. 以追加模式打开文件std::ofstream out(filename, std::ios::app);// 4. 检查文件是否成功打开if (out.is_open()) {out << message << gsep;  // 5. 写入日志消息和分隔符out.close();             // 6. 关闭文件}
} 
(1) 线程安全
LockGuard lockguard(_mutex); 
-  
使用
LockGuard(可能是std::lock_guard<std::mutex>的封装)保护共享资源(日志文件),防止多线程同时写入导致数据混乱。 
(2) 路径拼接
std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file; 
确保路径格式正确:
-  
如果
_path已经以/结尾(如"/var/log/"),直接拼接_file。 -  
否则,补上
/(如_path="logs",_file="app.log"→"logs/app.log")。 
(3) 文件打开模式
std::ofstream out(filename, std::ios::app); 
std::ios::app:
-  
每次写入都在文件末尾追加,不会覆盖原有内容。
 -  
适合日志场景,避免丢失历史记录。
 
(4) 写入日志
out << message << gsep; 
-  
message:要写入的日志内容。 -  
gsep:可能是全局定义的日志分隔符(如"\n"或"\r\n")。 
(5) 关闭文件
out.close(); 
-  
显式关闭文件,确保数据刷新到磁盘(虽然析构时也会自动关闭)。
 
特点分析
-  
线程安全:
-  
使用
Mutex和LockGuard确保多线程环境下文件写入的原子性 -  
每个文件操作都在互斥锁保护下进行
 
 -  
 -  
路径处理:
-  
智能处理路径结尾的斜杠,确保路径拼接正确
 -  
使用 C++17 的
std::filesystem进行目录操作 
 -  
 -  
文件操作:
-  
使用追加模式 (
std::ios::app) 打开文件,确保新日志追加到文件末尾 -  
检查文件是否成功打开后再进行写入
 
 -  
 -  
错误处理:
-  
捕获并处理目录创建可能抛出的异常
 -  
静默处理文件打开失败的情况(仅不写入,不抛出异常)
 
 -  
 
3、日志等级与时间戳处理
// 日志等级枚举
enum class LogLevel {DEBUG,INFO,WARNING,ERROR,FATAL
};// 日志等级转字符串
std::string Level2Str(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";}
}// 获取格式化时间戳
std::string GetTimeStamp() {time_t curr = time(nullptr);struct tm curr_tm;localtime_r(&curr, &curr_tm); // 线程安全的时间转换char timebuffer[128];snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",curr_tm.tm_year+1900,curr_tm.tm_mon+1,curr_tm.tm_mday,curr_tm.tm_hour,curr_tm.tm_min,curr_tm.tm_sec);return timebuffer;
} 
这段代码实现了一个线程安全的日志系统,支持控制台输出和文件写入两种策略,并采用了策略模式设计。下面我将从日志等级、时间戳、策略模式、线程安全、宏定义等方面详细解析。
1. 日志等级 (LogLevel)
enum class LogLevel {DEBUG,INFO,WARNING,ERROR,FATAL
}; 
-  
作用:定义日志的级别,用于区分日志的重要性。
 -  
优点:
-  
enum class避免命名冲突(强类型枚举)。 -  
扩展性强,可轻松添加新级别。
 
 -  
 
| 枚举值 | 日志等级 | 典型用途 | 
|---|---|---|
DEBUG | 调试级 | 用于开发调试的详细信息,通常在生产环境中关闭。 | 
INFO | 信息级 | 记录程序正常运行时的关键信息(如服务启动、配置加载等)。 | 
WARNING | 警告级 | 表示潜在问题,程序仍能运行但可能需要关注(如磁盘空间不足)。 | 
ERROR | 错误级 | 记录影响部分功能的错误,但程序仍可继续运行(如请求参数无效)。 | 
FATAL | 致命级 | 导致程序无法继续运行的严重错误(如数据库连接失败),通常触发进程终止。 | 
补充说明:
-  
严重程度递增:
DEBUG→INFO→WARNING→ERROR→FATAL(从低到高,通常高严重等级会触发通知机制) -  
生产环境建议:
-  
开发环境:可开启
DEBUG -  
生产环境:通常只记录
INFO及以上等级(WARNING/ERROR/FATAL) 
 -  
 -  
常见应用场景:
-  
FATAL错误可能伴随日志文件输出和系统告警。 -  
DEBUG日志可能包含敏感信息,需谨慎使用。 
 -  
 
        在C++中,enum class(也称为强类型枚举或作用域枚举)是C++11引入的一种改进的枚举类型定义方式。相比于传统的C风格枚举(enum),enum class具有以下优势和特性:
1. 作用域隔离(Scoped Enumeration)
-  
传统
enum的问题:枚举值会暴露在外部作用域中,可能导致命名冲突。enum LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL }; enum Color { RED, GREEN, BLUE }; // 错误:DEBUG与RED冲突(如果值相同) -  
enum class的解决方案:枚举值的作用域限定在枚举类内部,必须通过枚举类型名访问。enum class LogLevel { DEBUG, INFO, WARNING, ERROR, FATAL }; enum class Color { RED, GREEN, BLUE }; LogLevel level = LogLevel::DEBUG; // 正确 Color c = Color::RED; // 正确 
2. 强类型(Strong Typing)
-  
传统
enum的问题:枚举值可以隐式转换为整数(如int),可能导致误用。enum LogLevel { DEBUG, INFO }; int x = DEBUG; // 隐式转换为int,可能引发逻辑错误 -  
enum class的解决方案:枚举值不会隐式转换为整数,必须显式转换。enum class LogLevel { DEBUG, INFO }; // int x = LogLevel::DEBUG; // 错误:不能隐式转换 int x = static_cast<int>(LogLevel::DEBUG); // 必须显式转换 
3. 避免命名污染
-  
传统枚举的枚举值(如
DEBUG)可能与其他全局标识符冲突,而enum class的枚举值必须通过EnumType::Value访问,避免了污染外部命名空间。 
4. 可以指定底层类型
-  
enum class允许显式指定枚举的底层整数类型(默认为int),节省内存或匹配接口要求。enum class LogLevel : uint8_t { DEBUG, INFO, WARNING, ERROR, FATAL }; // 每个枚举值仅占用1字节 
代码示例解析
enum class LogLevel {DEBUG,INFO,WARNING,ERROR,FATAL
}; 
-  
含义:定义了一个强类型枚举
LogLevel,包含5个枚举值,分别表示日志级别。 -  
用途:
-  
在日志系统中限制日志级别的取值范围。
 -  
避免直接使用整数或字符串表示级别,提高代码可读性和类型安全性。
 -  
例如:
void log(LogLevel level, const std::string& message) {if (level == LogLevel::FATAL) {// 处理严重错误} } 
 -  
 
对比总结
| 特性 | 传统enum | enum class | 
|---|---|---|
| 作用域 | 全局 | 枚举类内部 | 
| 类型安全 | 弱(可隐式转换) | 强(需显式转换) | 
| 命名冲突风险 | 高 | 低 | 
| 底层类型指定 | 不支持 | 支持(如: uint8_t) | 
何时使用?
-  
优先使用
enum class:需要类型安全、避免命名冲突或明确底层类型时。 -  
传统
enum:需要与C代码交互或兼容旧代码时(但现代C++应尽量避免)。 
通过enum class,C++提供了更安全、更清晰的枚举类型,是推荐的做法。
日志等级转字符串 (Level2Str)
std::string Level2Str(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";}
} 
-  
作用:将枚举值转换为可读的字符串,便于日志输出。
 -  
示例:
LogLevel level = LogLevel::ERROR; std::cout << Level2Str(level); // 输出 "ERROR" 
2. 时间戳 (GetTimeStamp)
std::string GetTimeStamp() {time_t curr = time(nullptr);struct tm curr_tm;localtime_r(&curr, &curr_tm); // 线程安全的时间转换char timebuffer[128];snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",curr_tm.tm_year+1900,curr_tm.tm_mon+1,curr_tm.tm_mday,curr_tm.tm_hour,curr_tm.tm_min,curr_tm.tm_sec);return timebuffer;
} 
-  
作用:获取当前时间,格式化为
YYYY-MM-DD HH:MM:SS。 -  
关键点:
-  
localtime_r是线程安全的(localtime是非线程安全的)。 -  
tm_year是从 1900 开始的年数,需+1900。 -  
tm_mon是 0~11,需+1。 
 -  
 -  
输出示例:2023-10-05 14:30:45
 
        这段代码定义了一个函数 GetTimeStamp(),用于获取当前系统时间并格式化为易读的字符串(如 "2023-10-05 14:30:45")。以下是逐行解析,重点讲解涉及时间的函数和用法:
1. 获取当前时间戳

time_t curr = time(nullptr); 
time() 函数:
-  
作用:获取当前系统的日历时间(Calendar Time),即从 1970年1月1日00:00:00 UTC(Unix 纪元)开始的秒数。
 -  
参数:
nullptr表示不存储结果,直接返回时间值。 -  
返回值:
time_t类型(通常是整数或浮点数,表示秒数)。 -  
示例:如果当前时间是 1970年1月1日00:00:01 UTC,
time()返回1。 
2. 转换为本地时间结构体
struct tm curr_tm;
localtime_r(&curr, &curr_tm); // 线程安全的时间转换 
struct tm:
-  
是一个结构体,用于存储分解时间(Broken-down Time),包含年、月、日、时、分、秒等字段:
struct tm {int tm_sec; // 秒 [0, 60](含闰秒)int tm_min; // 分 [0, 59]int tm_hour; // 时 [0, 23]int tm_mday; // 日 [1, 31]int tm_mon; // 月 [0, 11](0表示1月!)int tm_year; // 年(实际年份 - 1900)// 其他字段(如星期、夏令时标志等) }; -  
注意:
tm_year是从 1900 年开始的偏移量,tm_mon是 0-based(0=1月)。 
localtime_r() 函数:

-  
作用:将
time_t转换为本地时间的struct tm,线程安全(_r表示 reentrant,可重入)。 -  
对比非线程安全版本:
struct tm* localtime(const time_t* timer); // 返回静态内存,多线程不安全! -  
参数:
-  
&curr:输入的time_t时间戳。 -  
&curr_tm:输出的struct tm结构体。 
 -  
 
下翻手册,我们可以看到struct tm 结构体的定义:

3. 格式化时间字符串
char timebuffer[128];
snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",curr_tm.tm_year + 1900, // 年份需要 +1900curr_tm.tm_mon + 1,     // 月份需要 +1(0=1月)curr_tm.tm_mday,curr_tm.tm_hour,curr_tm.tm_min,curr_tm.tm_sec); 
snprintf() 函数:
-  
作用:将格式化数据写入字符缓冲区,避免缓冲区溢出(比
sprintf()更安全)。 -  
格式化字符串:
-  
%4d:年份(4位宽度,如2023)。 -  
%02d:月份/日期/时间(2位宽度,不足补零,如05)。 
 -  
 -  
字段调整:
-  
tm_year + 1900(因为tm_year是从 1900 年开始的偏移量):将tm_year(如123)转换为实际年份(如2023)。 -  
tm_mon + 1(tm_mon是 0-based(0=1月)):将tm_mon(0-based)转换为自然月(1-12)。 
 -  
 
4. 返回字符串
return timebuffer; 
-  
将格式化后的字符串(如
"2023-10-05 14:30:45")返回。 
完整代码逻辑总结
-  
获取时间戳:
time(nullptr)返回当前时间的time_t值。 -  
转换为本地时间:
localtime_r()将time_t分解为年、月、日等字段。 -  
格式化字符串:将
struct tm的字段拼接为YYYY-MM-DD HH:MM:SS格式。 -  
返回结果:返回格式化后的字符串。
 
4、核心Logger类实现
class Logger {
public:Logger() {EnableConsoleLogStrategy(); // 默认使用控制台策略}// 策略切换方法void EnableFileLogStrategy() {_fflush_strategy = std::make_unique<FileLogStrategy>();}void EnableConsoleLogStrategy() {_fflush_strategy = std::make_unique<ConsoleLogStrategy>();}// 日志消息类(内部类)class LogMessage {public:LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger): _curr_time(GetTimeStamp()),_level(level),_pid(getpid()),_src_name(src_name),_line_number(line_number),_logger(logger) {// 构建日志前缀std::stringstream ss;ss << "[" << _curr_time << "] "<< "[" << Level2Str(_level) << "] "<< "[" << _pid << "] "<< "[" << _src_name << "] "<< "[" << _line_number << "] "<< "- ";_loginfo = ss.str();}// 流式输出支持template <typename T>LogMessage &operator<<(const T &info) {std::stringstream ss;ss << info;_loginfo += ss.str();return *this;}// 析构时自动输出日志~LogMessage() {if (_logger._fflush_strategy) {_logger._fflush_strategy->SyncLog(_loginfo);}}private:std::string _curr_time;LogLevel _level;pid_t _pid;std::string _src_name;int _line_number;std::string _loginfo;Logger &_logger;};// 日志入口操作符LogMessage operator()(LogLevel level, std::string name, int line) {return LogMessage(level, name, line, *this);}private:std::unique_ptr<LogStrategy> _fflush_strategy; // 当前日志策略
}; 
Logger 类是日志系统的核心类,负责协调日志的生成和输出策略。下面从多个方面详细解析这个类的设计:
1. 类结构概述
Logger 类采用了策略模式,将日志输出的具体实现与日志生成逻辑分离。主要包含:
-  
策略管理(切换控制台/文件输出)
 -  
日志消息生成(内部类
LogMessage) -  
流式输出支持(通过
operator<<) 
2. 构造函数与默认行为
Logger() {EnableConsoleLogStrategy(); // 默认使用控制台策略
} 
-  
构造函数中默认启用控制台日志策略
 -  
这种设计确保即使不显式配置,日志系统也能正常工作
 
3. 策略管理方法
void EnableFileLogStrategy() {_fflush_strategy = std::make_unique<FileLogStrategy>();
}void EnableConsoleLogStrategy() {_fflush_strategy = std::make_unique<ConsoleLogStrategy>();
} 
-  
使用
std::unique_ptr管理策略对象,确保自动内存管理 -  
通过这两个方法可以动态切换日志输出目标
 -  
体现了策略模式的核心思想:运行时切换算法
 
4. 内部类 LogMessage
这是日志系统的关键部分,负责:
-  
构建完整的日志消息
 -  
支持流式输出
 -  
在析构时自动输出日志
 
4.1 构造函数
LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger): _curr_time(GetTimeStamp()),_level(level),_pid(getpid()),_src_name(src_name),_line_number(line_number),_logger(logger) {// 构建日志前缀std::stringstream ss;ss << "[" << _curr_time << "] "<< "[" << Level2Str(_level) << "] "<< "[" << _pid << "] "<< "[" << _src_name << "] "<< "[" << _line_number << "] "<< "- ";_loginfo = ss.str();
} 
-  
收集日志所需的所有元信息:时间戳、日志级别、进程ID、源文件名、行号
 -  
构建格式化的日志前缀,例如:
[2023-05-20 14:30:45] [INFO] [1234] [main.cpp] [42] - 
        这段代码定义了一个构造函数,用于初始化日志消息对象(可能是 LogMessage 类)。它使用了 std::stringstream 来构建日志前缀,并通过 Logger &logger 参数关联日志记录器。
1. 构造函数参数分析
LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger): _curr_time(GetTimeStamp()),_level(level),_pid(getpid()),_src_name(src_name),_line_number(line_number),_logger(logger) { ... } 
(1) 参数说明
|   参数  |   类型  |   作用  | 
|---|---|---|
|   
  |   
  |   日志级别(如 DEBUG、INFO、ERROR),用于分类日志重要性。  | 
|   
  |   
  |   源文件名或模块名,标识日志来源。  | 
|   
  |   
  |   代码行号,用于精确定位日志位置。  | 
|   
  |   
  |   日志记录器对象的引用,用于后续实际输出日志。  | 
(2) 成员变量初始化
-  
_curr_time(GetTimeStamp()):调用GetTimeStamp()函数获取当前时间戳(可能是字符串或时间对象),记录日志时间。 -  
_level(level):保存日志级别,后续可能用于过滤或格式化。 -  
_pid(getpid()):调用系统函数getpid()获取进程 ID,用于区分多进程日志。 -  
_src_name(src_name):保存源文件名或模块名。 -  
_line_number(line_number):保存代码行号。 -  
_logger(logger):保存日志记录器的引用,后续通过_logger输出日志。 
2. std::stringstream 的作用

std::stringstream ss;
ss << "[" << _curr_time << "] "<< "[" << Level2Str(_level) << "] "<< "[" << _pid << "] "<< "[" << _src_name << "] "<< "[" << _line_number << "] "<< "- ";
_loginfo = ss.str(); 
(1) 功能
-  
构建日志前缀:将日志的元信息(时间、级别、进程 ID、源文件、行号)格式化为字符串,存储到成员变量
_loginfo中。 -  
示例输出:
[2023-01-01 12:00:00] [INFO] [1234] [main.cpp] [42] - This is a log message. 
(2) 关键操作
-  
std::stringstream:C++ 标准库中的字符串流,支持流式操作(<<),用于高效拼接字符串。 -  
Level2Str(_level):假设是将LogLevel枚举转换为字符串(如DEBUG→"DEBUG")。 -  
ss.str():将流内容转换为std::string,赋值给_loginfo。 
(3) 优势
-  
可读性:结构化前缀使日志易于阅读和分析。
 -  
灵活性:可轻松调整格式(如添加/删除字段)。
 -  
性能:
stringstream比多次字符串拼接更高效。 
3. Logger &logger 的作用和意义
(1) 为什么传递 Logger 引用?
-  
解耦:
LogMessage只负责构建日志内容,不关心具体输出方式(文件、控制台、网络等)。实际输出由Logger实现,遵循单一职责原则。 -  
灵活性:可通过不同
Logger实现切换日志行为(如FileLogger、ConsoleLogger)。 -  
避免拷贝:传递引用避免
Logger对象的拷贝开销。 
(2) 典型用法
后续可能通过 _logger 输出完整日志:
void LogMessage::Output(const std::string &message) {_logger.Log(_loginfo + message); // 组合前缀和实际消息
} 
(3) 设计意义
-  
策略模式:
Logger是日志输出策略,LogMessage是日志内容构建器,二者分离。 -  
可扩展性:新增日志输出方式只需实现新的
Logger子类,无需修改LogMessage。 
4.2 流式输出支持
template <typename T>
LogMessage &operator<<(const T &info) {std::stringstream ss;ss << info;_loginfo += ss.str();return *this;
} 
-  
支持链式调用,如
LOG(INFO) << "Message" << 123 << 3.14 -  
使用模板支持任意类型的输出
 -  
将输入内容转换为字符串(ss.str())并追加到日志消息中
 
4.3 自动输出机制
~LogMessage() {if (_logger._fflush_strategy) {_logger._fflush_strategy->SyncLog(_loginfo);}
} 
-  
利用RAII机制,在对象析构时自动输出日志
 -  
确保即使发生异常也能记录日志
 -  
检查策略对象(可以通过Logger构造选择哪个策略)是否存在,避免空指针解引用

 
5. 日志入口操作符
LogMessage operator()(LogLevel level, std::string name, int line) {return LogMessage(level, name, line, *this);
} 
        这段代码定义了一个函数调用运算符重载(operator()),用于快速创建 LogMessage 对象。它通常出现在 Logger 类中,目的是提供一种简洁的语法来生成日志消息,重载函数调用操作符,提供简洁的API。
作用
-  
这是一个运算符重载,允许
Logger对象像函数一样被调用(即Logger()语法)。 -  
它构造并返回一个
LogMessage对象,将当前Logger对象(*this)传递给LogMessage的构造函数。 -  
返回
LogMessage临时对象,支持流式输出。 
参数:
-  
level: 日志级别 -  
name: 源文件名(通常通过宏传递__FILE__) -  
line: 行号(通常通过宏传递__LINE__) 
典型使用场景
假设在 Logger 类中定义了此运算符,用户可以这样使用:
Logger logger; // 假设已初始化
logger(LogLevel::INFO, "main.cpp", 42) << "This is a log message."; 
这行代码会:
-  
调用
Logger::operator(),生成一个临时的LogMessage对象。 -  
通过
LogMessage的流式操作(如operator<<)追加日志内容。 -  
在
LogMessage析构时(如语句结束时),自动将日志输出到logger。 
临时对象的析构时机:
-  
临时对象会在这个完整表达式结束时被析构。
 -  
在
logger(...) << "message"中:-  
logger(...)返回LogMessage临时对象。 -  
<< "message"调用LogMessage::operator<<,返回LogMessage&(通常是*this)。 -  
完整表达式是
logger(...) << "message",因此临时LogMessage会在整个语句结束时(即;处)被析构。 
 -  
 
参数与返回值
(1) 参数
|   参数  |   类型  |   作用  | 
|---|---|---|
|   
  |   
  |   日志级别(如 INFO、ERROR)。  | 
|   
  |   
  |   源文件名或模块名。  | 
|   
  |   
  |   代码行号。  | 
(2) 返回值
-  
返回一个
LogMessage对象(通常是临时对象,支持链式调用)。 -  
LogMessage的构造函数接受*this(当前Logger对象的引用),用于后续日志输出。 
5、全局访问与宏定义
// 全局日志对象
Logger logger;// 简化使用的宏定义
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy() 
这段代码定义了一个全局日志对象 logger,并通过宏简化日志调用和日志策略配置。以下是详细解析:
1. 全局日志对象
Logger logger; 
-  
作用:声明一个全局的
Logger实例,供整个程序使用。 -  
特点:
-  
全局可见,任何地方都可以直接使用
logger记录日志。 -  
通常在程序启动时初始化(如
main()开头),并在程序结束时自动析构。 
 -  
 -  
潜在问题:
-  
全局变量可能导致依赖耦合,不利于单元测试。
 -  
如果程序是多线程的,需要确保
Logger的线程安全(如加锁或使用线程安全的数据结构)。 
 -  
 
2. 宏定义解析
(1) LOG(level) 宏
#define LOG(level) logger(level, __FILE__, __LINE__) 
-  
作用:简化日志调用,自动填充文件名和行号。
 -  
展开后:
LOG(LogLevel::INFO) << "This is a log message."; // 等价于: logger(LogLevel::INFO, __FILE__, __LINE__) << "This is a log message."; -  
关键点:(详细回顾C语言的章节)
-  
__FILE__是预定义宏,表示当前源文件名(如"main.cpp")。 -  
__LINE__是预定义宏,表示当前行号(如42)。 -  
这样日志会自动记录代码位置,便于调试。
 
 -  
 
使用示例
LOG(LogLevel::DEBUG) << "x = " << x;
LOG(LogLevel::ERROR) << "File not found: " << filename; 
(2) 日志策略配置宏
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy() 
-  
作用:简化日志输出策略的配置。
 -  
展开后:
Enable_Console_Log_Strategy(); // 等价于 logger.EnableConsoleLogStrategy(); Enable_File_Log_Strategy(); // 等价于 logger.EnableFileLogStrategy(); -  
典型用途:
-  
EnableConsoleLogStrategy():启用控制台输出(如std::cout)。 -  
EnableFileLogStrategy():启用文件输出(如写入log.txt)。 
 -  
 -  
设计意图:
-  
通过宏隐藏具体实现,使代码更简洁。
 -  
如果未来
Logger的策略配置方法改名,只需修改宏定义,无需改动所有调用处。 
 -  
 
使用示例
int main() {Enable_Console_Log_Strategy(); // 启用控制台日志Enable_File_Log_Strategy();    // 启用文件日志LOG(LogLevel::INFO) << "Application started.";return 0;
} 
6、注意:关于多态(Polymorphism)和智能指针(std::unique_ptr)的使用
 
        具体来说, _fflush_strategy 是 LogStrategy* 类型,但被赋值为 FileLogStrategy 或 ConsoleLogStrategy 的实例时,是否能正确访问派生类的成员,而不会发生对象截断(Object Slicing)。
1. 这份代码是否安全?
是的,代码是安全的,不会发生对象截断,原因如下:
std::unique_ptr<LogStrategy> 存储的是派生类对象
-  
std::make_unique<FileLogStrategy>()会在堆上创建一个FileLogStrategy对象,并返回std::unique_ptr<FileLogStrategy>,但可以隐式转换为std::unique_ptr<LogStrategy>(因为FileLogStrategy继承自LogStrategy)。 -  
此时
_fflush_strategy仍然指向完整的FileLogStrategy对象,不会发生截断。 
多态调用(Virtual Functions)
-  
只要
LogStrategy定义了虚函数(如void Log(const std::string& message)),并且FileLogStrategy和ConsoleLogStrategy覆盖了这些虚函数,那么通过_fflush_strategy->Log(...)调用时,会正确执行派生类的实现。 -  
如果没有虚函数,则会发生静态绑定(早期绑定),可能调用到基类的实现(但不会截断对象)。
 
2. 对象截断(Object Slicing)何时发生?
对象截断通常发生在以下情况:
LogStrategy strategy = FileLogStrategy(); // ❌ 错误!派生类部分被截断 
-  
这里
FileLogStrategy对象被拷贝构造到LogStrategy对象,但LogStrategy无法存储派生类的额外数据,导致派生类部分被丢弃。 -  
std::unique_ptr方式不会发生这种情况,因为它是通过指针管理完整对象。 
3. 代码示例分析
class Logger {
public:Logger() {EnableConsoleLogStrategy(); // 默认使用控制台策略}void EnableFileLogStrategy() {_fflush_strategy = std::make_unique<FileLogStrategy>(); // 安全,存储完整 FileLogStrategy 对象}void EnableConsoleLogStrategy() {_fflush_strategy = std::make_unique<ConsoleLogStrategy>(); // 安全,存储完整 ConsoleLogStrategy 对象}private:std::unique_ptr<LogStrategy> _fflush_strategy; // 基类指针,指向派生类对象
}; 
_fflush_strategy 可以安全地指向 FileLogStrategy 或 ConsoleLogStrategy,因为:
-  
它们都是
LogStrategy的派生类。 -  
std::unique_ptr会正确管理对象的生命周期(自动释放内存)。 -  
如果
LogStrategy有虚函数,并且派生类正确覆盖它们,那么调用时会执行正确的派生类逻辑。 
4. 如何确保多态调用?
(1) 基类必须定义虚函数
class LogStrategy {
public:virtual ~LogStrategy() = default; // 虚析构函数,确保派生类对象能正确释放virtual void Log(const std::string& message) = 0; // 纯虚函数,强制派生类实现
}; 
(2) 派生类必须覆盖虚函数
class FileLogStrategy : public LogStrategy {
public:void Log(const std::string& message) override {std::cout << "FileLog: " << message << std::endl;}
};class ConsoleLogStrategy : public LogStrategy {
public:void Log(const std::string& message) override {std::cout << "ConsoleLog: " << message << std::endl;}
}; 
(3) 通过基类指针调用
Logger logger;
logger._fflush_strategy->Log("Hello, world!"); // 根据当前策略调用 FileLogStrategy 或 ConsoleLogStrategy 的实现 
5. 总结
-  
代码是安全的,
std::unique_ptr<LogStrategy>可以正确管理FileLogStrategy或ConsoleLogStrategy对象,不会发生截断。 -  
必须使用虚函数,否则
_fflush_strategy->Log(...)会调用LogStrategy的默认实现(如果存在),而不是派生类的实现。 -  
对象截断只发生在值拷贝(非指针/引用)的情况下,而代码使用的是智能指针,因此不会发生。
 
6. 额外建议
-  
如果
LogStrategy是一个接口类(只有纯虚函数),可以将其析构函数设为= default并标记为override:virtual ~LogStrategy() = default; -  
考虑使用
std::move优化策略切换(避免不必要的内存分配):void EnableFileLogStrategy() {_fflush_strategy = std::make_unique<FileLogStrategy>(); // 已有优化 } 
7、完整日志系统实现代码
#ifndef __LOG_HPP__
#define __LOG_HPP__#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> //C++17
#include <sstream>
#include <fstream>
#include <memory>
#include <ctime>
#include <unistd.h>
#include "Mutex.hpp"namespace LogModule
{using namespace MutexModule;const std::string gsep = "\r\n";// 策略模式,C++多态特性// 2. 刷新策略 a: 显示器打印 b:向指定的文件写入//  刷新策略基类class LogStrategy{public:~LogStrategy() = default;virtual void SyncLog(const std::string &message) = 0;};// 显示器打印日志的策略 : 子类class ConsoleLogStrategy : public LogStrategy{public:ConsoleLogStrategy(){}void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex);std::cout << message << gsep;}~ConsoleLogStrategy(){}private:Mutex _mutex;};// 文件打印日志的策略 : 子类const std::string defaultpath = "./log";const std::string defaultfile = "my.log";class FileLogStrategy : public LogStrategy{public:FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile): _path(path),_file(file){LockGuard lockguard(_mutex);if (std::filesystem::exists(_path)){return;}try{std::filesystem::create_directories(_path);}catch (const std::filesystem::filesystem_error &e){std::cerr << e.what() << '\n';}}void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex);std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file; // "./log/" + "my.log"std::ofstream out(filename, std::ios::app);                              // 追加写入的 方式打开if (!out.is_open()){return;}out << message << gsep;out.close();}~FileLogStrategy(){}private:std::string _path; // 日志文件所在路径std::string _file; // 日志文件本身Mutex _mutex;};// 形成一条完整的日志&&根据上面的策略,选择不同的刷新方式// 1. 形成日志等级enum class LogLevel{DEBUG,INFO,WARNING,ERROR,FATAL};std::string Level2Str(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";}}std::string GetTimeStamp(){time_t curr = time(nullptr);struct tm curr_tm;localtime_r(&curr, &curr_tm);char timebuffer[128];snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",curr_tm.tm_year+1900,curr_tm.tm_mon+1,curr_tm.tm_mday,curr_tm.tm_hour,curr_tm.tm_min,curr_tm.tm_sec);return timebuffer;}// 1. 形成日志 && 2. 根据不同的策略,完成刷新class Logger{public:Logger(){EnableConsoleLogStrategy();}void EnableFileLogStrategy(){_fflush_strategy = std::make_unique<FileLogStrategy>();}void EnableConsoleLogStrategy(){_fflush_strategy = std::make_unique<ConsoleLogStrategy>();}// 表示的是未来的一条日志class LogMessage{public:LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger): _curr_time(GetTimeStamp()),_level(level),_pid(getpid()),_src_name(src_name),_line_number(line_number),_logger(logger){// 日志的左边部分,合并起来std::stringstream ss;ss << "[" << _curr_time << "] "<< "[" << Level2Str(_level) << "] "<< "[" << _pid << "] "<< "[" << _src_name << "] "<< "[" << _line_number << "] "<< "- ";_loginfo = ss.str();}// LogMessage() << "hell world" << "XXXX" << 3.14 << 1234template <typename T>LogMessage &operator<<(const T &info){// a = b = c =d;// 日志的右半部分,可变的std::stringstream ss;ss << info;_loginfo += ss.str();return *this;}~LogMessage(){if (_logger._fflush_strategy){_logger._fflush_strategy->SyncLog(_loginfo);}}private:std::string _curr_time;LogLevel _level;pid_t _pid;std::string _src_name;int _line_number;std::string _loginfo; // 合并之后,一条完整的信息Logger &_logger;};// 这里故意写成返回临时对象LogMessage operator()(LogLevel level, std::string name, int line){return LogMessage(level, name, line, *this);}~Logger(){}private:std::unique_ptr<LogStrategy> _fflush_strategy;};// 全局日志对象Logger logger;// 使用宏,简化用户操作,获取文件名和行号#define LOG(level) logger(level, __FILE__, __LINE__)#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}#endif 
五、线程安全实现
互斥锁封装
#pragma once
#include <iostream>
#include <pthread.h>namespace MutexModule {class Mutex {public:Mutex() {pthread_mutex_init(&_mutex, nullptr);}void Lock() {pthread_mutex_lock(&_mutex);}void Unlock() {pthread_mutex_unlock(&_mutex);}~Mutex() {pthread_mutex_destroy(&_mutex);}pthread_mutex_t *Get() {return &_mutex;}private:pthread_mutex_t _mutex;};// RAII锁守卫class LockGuard {public:LockGuard(Mutex &mutex) : _mutex(mutex) {_mutex.Lock();}~LockGuard() {_mutex.Unlock();}private:Mutex &_mutex;};
} 
六、使用示例与分析
1、基本使用示例
#include "Log.hpp"
using namespace LogModule;void fun() {int a = 10;LOG(LogLevel::FATAL) << "hello world" << 1234 << ", 3.14" << 'c' << a;
}int main() {// 使用控制台日志LOG(LogLevel::DEBUG) << "hello world";// 切换到文件日志Enable_File_Log_Strategy();LOG(LogLevel::WARNING) << "hello world";fun();return 0;
} 
2、输出结果分析
控制台输出示例 和 文件日志内容(my.log)如下:

3、设计优势
-  
策略模式灵活性:运行时切换日志输出方式、易于扩展新的输出策略(如网络日志、数据库日志)
 -  
线程安全性:每个策略实现内部使用互斥锁、RAII模式的LockGuard确保异常安全
 -  
易用性:宏定义简化调用、流式接口支持多种数据类型
 -  
性能考虑:日志格式化与输出分离、析构时自动触发输出,确保异常情况也能记录
 
七、改进与扩展建议
1、性能优化方向
-  
异步日志:引入生产者-消费者模型、使用队列缓冲日志消息
 -  
日志级别过滤:在Logger类中添加级别过滤、避免不必要日志的格式化开销
 -  
批量写入:文件日志策略积累一定量后批量写入、减少IO操作次数
 
2、功能扩展建议
-  
日志滚动策略:按文件大小或时间分割日志、自动清理旧日志文件
 -  
多输出目标:支持同时输出到控制台和文件、不同级别输出到不同目标
 -  
格式化定制:支持自定义日志格式模板、添加颜色输出(控制台)
 -  
上下文信息:支持添加请求ID等跨调用链信息、线程本地存储优化上下文传递
 
八、总结
本日志系统实现展示了策略模式在软件开发中的典型应用:
-  
分离变与不变:不变:日志记录的核心流程;变:不同的输出方式(策略)
 -  
开闭原则:对扩展开放(新增策略)、对修改关闭(无需改动Logger类)
 -  
组合优于继承:通过组合方式使用策略、避免继承带来的强耦合
 
这种设计使得系统既保持了核心功能的稳定性,又具备了足够的灵活性来应对变化的需求。在实际项目中,可以根据具体需求在此基础上进行扩展和优化。
