在现代软件开发中,日志记录是不可或缺的一部分。它不仅帮助开发者调试程序、追踪问题,还能为系统运行状态提供有价值的参考信息。
计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信
息,帮助快速定位问题并支持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要工具。
日志有现成的解决方案,如:spdlog、glog、Boost.Log、Log4cxx等等。本文将详细介绍如何使用 C++ 实现一个功能强大且灵活的日志系统,包括日志路径配置、日志等级管理、策略模式的运用以及最终的日志输出。
一、需求分析
一个好的日志系统需要满足以下几点:
- 可读性强:日志格式清晰易懂,包含时间、日志等级、进程 ID、文件名和行号等关键信息。
- 支持多种输出方式:例如控制台输出或文件输出。
- 支持动态调整刷新策略:可以根据需求切换不同的日志刷新方式。
- 线程安全:在多线程环境下保证日志输出的一致性。
- 易于扩展:未来可以方便地添加新的日志刷新策略。
基于以上需求,我们将分步实现这个日志系统。
我们想要的日志格式如下:
[可读性很好的时间] [⽇志等级] [进程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
二、日志系统的实现步骤
第零步:构建命名空间
为了避免与其他命名空间的冲突,我们这里将所有的日志实现相关程序放到一个命名空间中:
namespace LogModule {
}
第一步:确认默认路径和日志等级
首先定义日志的默认路径、文件名以及日志等级。
namespace LogModule {
const std::string default_path = "./log"; // 默认日志路径
const std::string default_filename = "log.txt"; // 默认日志文件名
enum class LogLevel {
DEBUG = 1,
INFO = 2,
WARNING = 3,
ERROR = 4,
FATAL = 5
};
}
日志等级枚举 LogLevel
定义了五种日志级别,从低到高依次为 DEBUG
、INFO
、WARNING
、ERROR
和 FATAL
。
第二步:使用策略模式设计日志刷新逻辑
策略模式(Strategy Pattern) 是一种行为型设计模式,它允许定义一系列算法,并将它们封装在独立的类中,使它们可以互换使用。通过这种方式,我们可以轻松地切换不同的日志刷新策略。
为了实现灵活的日志刷新策略,我们采用 策略模式(Strategy Pattern) 。核心思想是将不同的刷新逻辑封装为独立的类,并通过接口统一管理。
1. 策略接口定义
定义抽象基类 LogStrategy
,所有具体的刷新策略都需要继承该接口并实现其方法。
class LogStrategy {
public:
virtual ~LogStrategy() = default; // 虚析构函数
virtual void SyncLog(const std::string &message) = 0; // 纯虚函数,定义日志刷新逻辑
};
- 虚析构函数:确保派生类对象通过基类指针删除时能够正确调用派生类的析构函数。
- 纯虚函数
SyncLog
:定义日志刷新的核心逻辑,具体实现由派生类提供。
具体策略类
- 控制台日志策略
ConsoleLogStrategy
:将日志输出到控制台。 - 文件日志策略
FileLogStrategy
:将日志写入文件。
这两种策略都继承自 LogStrategy
并实现了 SyncLog
方法。
2. 控制台日志策略
控制台日志策略将日志直接输出到显示器上,适合调试场景。
class ConsoleLogStrategy : public LogStrategy {
public:
void SyncLog(const std::string &message) override {
LockGuard lock(_mutex); // 加锁避免多线程竞争
std::cout << message << std::endl;
}
private:
Mutex _mutex; // 线程同步锁
};
3. 文件日志策略
文件日志策略将日志写入指定文件中。
class FileLogStrategy : public LogStrategy {
public:
FileLogStrategy(const std::string &path = default_path, const std::string &filename = default_filename)
: _path(path), _filename(filename) {
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 lock(_mutex); // 加锁避免多线程竞争
std::string fullPath = _path + "/" + _filename;
std::ofstream ostream(fullPath, std::ios::app); // 追加方式写入
if (ostream.is_open()) {
ostream << message << std::endl;
ostream.close();
}
}
private:
std::string _path;
std::string _filename;
Mutex _mutex; // 线程同步锁
};
4、动态切换
在 Logger
类中,我们使用了 std::shared_ptr<LogStrategy>
来管理当前使用的策略。
也就是用父类指针指向不同的子类,实现类似多态的效果,Logger
类默认使用控制台刷新策略,如果后续需要更换,就可以通过调用 EnableConsoleLogStrategy
或 EnableFileLogStrategy
方法,可以动态切换日志刷新方式。
class Logger {
public:
Logger()
{
// 默认使用控制台日志策略
_strategy = std::make_shared<ConsoleLogStrategy>();
}
// 主动设置日志刷新策略
void EnableConsoleLogStrategy()
{
_strategy = std::make_shared<ConsoleLogStrategy>();
}
void EnableFileLogStrategy()
{
_strategy = std::make_shared<FileLogStrategy>();
}
private:
std::shared_ptr<LogStrategy> _strategy;
};
5、策略模式的优势
- 灵活性:可以通过修改
_strategy
指针的指向来切换不同的日志刷新策略。 - 可扩展性:未来可以方便地添加新的刷新策略,只需继承
LogStrategy
并实现SyncLog
方法即可。
第三步:构建日志字符串
日志信息通常由两部分组成:固定的日志头和可变的日志内容。
一条完整的日志信息包含两个部分:
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
- 日志头:
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] -
含有固定的几个信息,需要固定格式的拼接字符串
- 可变的日志内容:
hello world
可变的意思是这部分内容不限数量,如cout << "hello" << a << 3.14 << 0.001
1. 日志头的构造
日志头包含时间、日志等级、进程 ID、文件名和行号等信息。我们将其封装到 LogMessage
类中,这个类是 Logger
类的内部类。
日志头所需字段
需要拼接固定的日志头,需要获取各个不同的字段:
当前时间、日志等级、进程pid、当前程序名、该日志调用的行号
我们将这些字段封装到类 class LogMessage
中
class LogMessage
{
private:
std::string _time; // 时间
LogLevel _level; // 日志等级
int _pid; // 本进程pid
std::string _fileName; // 文件名
int _line; // 行号
std::string _logInfo; // 一行完整的日志信息
};
接下来就讨论各个字段从哪来:
日志头所需字段的获取
这两个字段都可以自己获取,其他的就需要外部传进来
std::string _time; // 时间
int _pid; // 本进程pid
时间 _time
的获取
// 获取当前可读时间
std::string getCurrTime()
{
// 时间格式: 2024-08-04 12:27:03
time_t now = time(nullptr); // 获取当前的时间戳
struct tm curr;
localtime_r(&now, &curr); // 将时间戳转换为本地时间
// 格式化时间
char buffer[100];
snprintf(buffer, sizeof(buffer), "%4d-%02d-%02d %02d:%02d:%02d",
curr.tm_year + 1900,
curr.tm_mon + 1,
curr.tm_mday,
curr.tm_hour,
curr.tm_min,
curr.tm_sec);
return buffer;
}
对于日志等级 level
:因为日志等级有多个,本就是动态变化的,需要用户传入
对于文件名和行号:这两个也都是变化的,必须从外部获取
综上, LogMessage
类的构造函数的雏形如下:
// 构造函数:要明确什么字段需要外部传进来(日志等级(由用户选择的)、文件名(后面会通过预处理符传递)、行号(后面会通过预处理符传递))
LogMessage(LogLevel level, const std::string &fileName, int line)
: _time(getCurrTime()),
_level(level),
_pid(getpid()),
_fileName(fileName),
_line(line)
{}
日志头字符串的拼接
因为一个日志头字符串需要接收许多不同类型的数据,将这些数据转换为字符串后再拼接成一个字符串,但这样有些小麻烦,我们可以使用 stringstream
简单来说, stringstream
可以通过 <<
操作符将各种非字符串类型的数据(如整数、浮点数、布尔值等)转换为字符串形式,并插入到一个内部的字符串缓冲区中。之后,你可以通过 str()
方法获取这个完整的字符串。
// 开始拼接日志字符串: [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16]
std::stringstream ssbuffer;
ssbuffer << "[" << _time << "] "
<< "[" << LogLevel2Str(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _fileName << "] "
<< "[" << _line << "] ";
_logInfo = ssbuffer.str();
你会发现这样还是有点小麻烦,也可以使用 C++20中的
std::format
std::string message = std::format("[{}][{}][{}][{}][{}]", _time, LogLevel2Str(_level), _pid, _fileName, _line);
这里可以具体讲讲 C++17中的
stringstream
和 C++20 中的std::format
各自的使用可以发现,
std::format
好用多了,有种回到了C语言中的格式化打印printf
中了!!不过本文选择使用 C++17中的
stringstream
日志等级转为字符串
因为日志等级转换过来都是整数,而我们日志信息上需要的是字符串形式,所以需要转换一下:
// 日志等级转字符串:因为日志等级本身是一个枚举类型,会被替换成数值,但我们需要将其原本字符串显示出来,因此需要将其转换为字符串
std::string LogLevel2Str(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";
};
return "UNKNOWN";
}
日志头字符串的拼接结果
LogMessage(LogLevel level, const std::string &fileName, int line)
: _time(getCurrTime()),
_level(level),
_pid(getpid()),
_fileName(fileName),
_line(line)
{
std::stringstream ssbuffer;
ssbuffer << "[" << _time << "] "
<< "[" << LogLevel2Str(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _fileName << "] "
<< "[" << _line << "] ";
_logInfo = ssbuffer.str();
}
要想组成一条完整的日志信息,其次是处理日志可变内容信息
2. 可变日志内容的处理
通过重载 <<
操作符,支持任意类型的数据拼接到日志字符串中。
template <typename T>
LogMessage &operator<<(const T &value) {
std::stringstream ssbuffer;
ssbuffer << value;
_logInfo += ssbuffer.str();
return *this;
}
我们调用日志程序:
LOG(level) << "hello" << a << 3.14 << 11;
我们需要一一处理每个 <<
后面的数据,我们需要将这些数据提取并转换为字符串,则需要设计上面这个运算符 <<
重载函数,同时因为这些数据类型不同,因此进一步设计为模板函数。
在该函数内部,通过 stringstream
的字符串流,将所有传入的不同类型数据都统一转换为字符串,并进一步拼接到 日志字符串 _logInfo
中
第四步:日志类的封装
为了简化日志的使用,我们定义了一个 Logger
类,负责管理日志策略和生成日志消息。
class Logger {
public:
Logger()
{
// 默认使用控制台日志策略
_strategy = std::make_shared<ConsoleLogStrategy>();
}
// 主动设置日志刷新策略
void EnableConsoleLogStrategy()
{
_strategy = std::make_shared<ConsoleLogStrategy>();
}
void EnableFileLogStrategy()
{
_strategy = std::make_shared<FileLogStrategy>();
}
private:
std::shared_ptr<LogStrategy> _strategy;
};
第五步:宏定义与圆括号重载
通过宏定义简化日志记录的调用方式。
#define LOG(level) logger(level, __FILE__, __LINE__)
实际使用时:
LOG(LogLevel::DEBUG) << "hello world" << '\n';
这样使用,实际上,前面的这段我们会使用一个宏替换,替换成关于 logger
类的构造代码
#define LOG(level) logger(level, __FILE__, __LINE__)
LOG(LogLevel::DEBUG) 替换成 logger(LogLevel::DEBUG, __FILE__, __LINE__)
这看似替换成了一个 logger
类的带参构造构造出来的一个 临时对象,实则不然,我们并不需要 logger
类对象,没有什么帮助,我们本质上是需要 logger
类的内部类 LogMessage
,只有这个 内部类 LogMessage
才能构建出一条日志信息(简单来说,我们要的是 内部类 LogMessage
,而非 logger
类)
因此,我们需要想办法通过 logger(LogLevel::DEBUG, __FILE__, __LINE__)
这条程序,获取一个内部类 LogMessage
对象(我们才能进一步构建日志信息)
这里我们可以通过圆括号重载的方式解决。
LogMessage operator()(LogLevel type, std::string filename, int line)
{
return LogMessage(type, filename, line);
}
通过圆括号重载函数,创建一个内部类 class LogMessage
的 临时对象,并返回
从这里可以看出,我们这句程序 logger(LogLevel::DEBUG, __FILE__, __LINE__)
看似给 logger
类传递参数,本质上还是给 Logger
类的内部类 class LogMessage
传参,这个 LogMessage(type, filename, line)
,就是 内部类 class LogMessage
的 临时对象了!
因此,
// 原先代码
logger(LogLevel::DEBUG, __FILE__, __LINE__)
// 调用圆括号重载:返回内部类 class LogMessage 的 临时对象
LogMessage << "hello world" << '\n'
前面我们讲解了 LogMessage
类的运算符 <<
重载函数,而此处,该临时对象就根据内部重载的运算符 << ,将后续的所有可变数据转换为字符串拼接到 日志字符串中
问题:为什么这个圆括号运算符重载的返回值没有引用,而是拷贝?
LogMessage operator()(LogLevel type, std::string filename, int line)
{
return LogMessage(type, filename, line);
}
答:
- 函数内部创建的对象是一个临时对象。
- 临时对象的生命周期仅限于当前函数的作用域,因此无法安全地返回对它的引用。
- 如果尝试返回临时对象的引用,会导致悬空引用(dangling reference),这是未定义行为。
__FILE__, __LINE__
的含义
__FILE__
和__LINE__
是 C/C++ 中的预定义宏,用于在编译时自动获取代码的文件路径和行号信息。它们是编译器内置的功能,常用于调试、日志记录和错误追踪。以下是详细解释:
1.
__FILE__
含义:
在代码中使用__FILE__
时,编译器会将其替换为当前代码所在的源文件名称(包含路径,具体格式取决于编译器)。示例:
printf("当前文件:%s\n", __FILE__);
输出可能为:
当前文件:/home/user/project/main.c
2.
__LINE__
含义:
在代码中使用__LINE__
时,编译器会将其替换为当前代码所在的行号(从文件开头计数的整数)。示例:
printf("当前行号:%d\n", __LINE__);
如果这行代码是文件的第 42 行,输出为:
当前行号:42
3. 实际应用场景
场景 1:调试和日志记录
// 本程序名:main.cc // 自定义调试输出宏 #define LOG(message) \ printf("[DEBUG] %s:%d - %s\n", __FILE__, __LINE__, message) int main() { LOG("程序启动"); // 输出: [DEBUG] main.c:6 - 程序启动 return 0; }
输出: [DEBUG] main.cc:7 - 程序启动 // 因为__FILE__检测到本程序是处于文件main.cc, __LINE__检测到调用位置处于第 7 行
4. 注意事项
编译时替换:
__FILE__
和__LINE__
的值在预处理阶段确定,与代码运行时的位置无关。头文件中的行为:
如果在头文件中使用这两个宏,它们会指向头文件自身的路径和行号,而非包含该头文件的源文件。#ifdef DEBUG #define LOG(message) printf("[DEBUG] %s:%d - %s\n", __FILE__, __LINE__, message) #else #define LOG(message) #endif
第六步:打印日志信息
有了前面的刷新策略的设计和日志信息的构建,现在就差临门一脚,刷新打印我们的日志信息:
1、问题:内部类 LogMessage
如何获取外部类 Logger
的成员方法?
在 Logger
类的内部类 LogMessage
中,构建一个对 Logger
类的对象引用
作为一个参数传入内部类 LogMessage
class LogMessage
{
public:
// 构造函数:
LogMessage(..., ..., ..., Logger & logger)
:
//...
_logger(logger)
{
//...
}
private:
//...
Logger & _logger; // 在Logger类中内部类LogMessage中,构建一个对Logger类的对象引用
};
并在这里传入外部类 Logger
LogMessage operator()(LogLevel type, std::string filename, int line)
{
return LogMessage(type, filename, line, *this);
}
2、问题:如何将日志信息刷新?
析构时,将日志字符串刷新到文件中!!
通过前面传入的外部类 Logger
的 *this
,调用外部类的策略类指针 _strategy
,进而调用该类中成员函数 SyncLog
,也就是刷新策略
~LogMessage()
{
// 析构函数: 析构时,将日志字符串刷新到⽂件中
_logger._strategy->SyncLog(_logInfo);
}
问题:为什么将这个日志信息刷新逻辑放到析构函数里?
答:因为我们之前传递的就是临时对象,出了这一行,就会自动析构,也就刚好调用刷新逻辑,一气呵成,刚刚好!!
添加一层保护:有可能上层的策略还没设置,还是空指针的状态就要注意,用一层 if
保护起来
~LogMessage()
{
if(_logger._strategy)
{
// 析构函数: 析构时,将日志字符串刷新到⽂件中
_logger._strategy->SyncLog(_logInfo);
}
}
至此我们就能将日志字符串刷新到指定的位置了:例如将日志刷新到显示器上,这些日子就展示了所有日志信息(时间、pid、程序名、行号….)
第七步:自由更换刷新策略
前面我们写了两种日志刷新策略,默认为控制台刷新,我们可以通过调用 Enable
函数来修改刷新策略
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy()
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy()
#include <iostream>
#include "Log.hpp"
using namespace LogModule;
int main()
{
// 我们只需开启一次转换
ENABLE_FILE_LOG_STRATEGY();
LOG(LogLevel::DEBUG) << "hello world";
LOG(LogLevel::DEBUG) << "hello world";
LOG(LogLevel::DEBUG) << "hello world";
LOG(LogLevel::DEBUG) << "hello world";
LOG(LogLevel::DEBUG) << "hello world";
return 0;
}
代码执行如下:
在指定路径下创建指定目录和指定日志文件,并将日志信息打印到该文件里面
第八步:获取预处理文件
我通过 g++ -E Main.cc -o Main.i
获取预处理阶段的文件 Main.i
可以发现,在这个阶段,__FILE__
和 __LINE__
已经被替换了!
并且,LOG(LogLevel::DEBUG)
已经被替换成 logger(LogLevel::DEBUG, "Main.cc", 8)
第九步:优化建议
我们可以向函数中传入日志等级,让其修改文件路径和文件名,如创建文件 log.txt.debug
、log.txt.fatal
、log.txt.warning
….这样的分类式打印日志信息
三、完整示例代码
以下是完整的代码示例:
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <filesystem> // C++17的新特性
#include <time.h>
#include <sstream>
#include <memory>
#include <unistd.h>
#include "Mutex.hpp"
/*
[可读性很好的时间] [⽇志等级] [进程pid] [打印对应⽇志的⽂件名][⾏号] - 消息内容,⽀持可变参数
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
*/
using namespace MutexModule;
namespace LogModule
{
// 日志路径和文件名
const std::string defalut_path = "./log/";
const std::string defalut_filename = "log.txt";
// 日志等级
enum class LogLevel
{
DEBUG = 1,
INFO = 2,
WARNING = 3,
ERROR = 4,
FATAL = 5
};
// 获取当前可读时间
std::string getCurrTime()
{
// 时间格式: 2024-08-04 12:27:03
time_t now = time(nullptr); // 获取当前的时间戳
struct tm curr;
localtime_r(&now, &curr); // 将时间戳转换为本地时间
// 格式化时间
char buffer[100];
snprintf(buffer, sizeof(buffer), "%4d-%02d-%02d %02d:%02d:%02d",
curr.tm_year + 1900,
curr.tm_mon + 1,
curr.tm_mday,
curr.tm_hour,
curr.tm_min,
curr.tm_sec);
return buffer;
}
// 日志等级转字符串:
std::string LogLevel2Str(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";
};
return "UNKNOWN";
}
// 日志策略:
class LogStrategy
{
public:
virtual ~LogStrategy() = default; // 策略的构造函数
virtual void SyncLog(const std::string &message) = 0; // 不同模式核⼼是刷新⽅式的不同
};
// 控制台⽇志策略,就是⽇志只向显⽰器打印,⽅便我们debug
class ConsoleLogStrategy : public LogStrategy
{
public:
void SyncLog(const std::string &message)
{
// 将给定的字符串往显示器上打印(因为可以存在多线程同时占用显示器,打印该日志,导致数据顺序不一致,因此需要加锁)
LockGuard lock(_mutex);
std::cout << message << std::endl;
}
private:
Mutex _mutex;
};
// ⽂件日志策略,就是日志往文件中打印
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &path = defalut_path, const std::string &filename = defalut_filename)
: _path(path), _filename(filename)
{
// 本类创建的对象,就会自动创建一个目标的日志文件
// 如果该文件存在, 则打开并打印; 若不存在, 则创建并打印
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)
{
LockGuard lock(_mutex);
// 将日志信息写入文件中
std::string fullPath = _path + _filename;
// 流式文件写入(因为多线程同时写会导致数据混乱,因此需要加锁)
std::ofstream ostream(fullPath, std::ios::app); // 追加⽅式
if (!ostream.is_open())
return;
ostream << message << std::endl;
ostream.close();
}
~FileLogStrategy() {}
private:
std::string _path;
std::string _filename;
Mutex _mutex;
};
// 具体的日志类:构建刷新的字符串
class Logger
{
public:
Logger()
{
// 默认使用控制台日志策略
_strategy = std::make_shared<ConsoleLogStrategy>();
}
// 主动设置日志刷新策略
void EnableConsoleLogStrategy()
{
_strategy = std::make_shared<ConsoleLogStrategy>();
}
void EnableFileLogStrategy()
{
_strategy = std::make_shared<FileLogStrategy>();
}
// 日志字符串类: 构建日志字符串
class LogMessage
{
public:
// 构造函数:
LogMessage(LogLevel level, const std::string &fileName, int line, Logger & logger)
: _time(getCurrTime()),
_level(level),
_pid(getpid()),
_fileName(fileName),
_line(line),
_logger(logger)
{
// 开始拼接日志字符串: [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16]
std::stringstream ssbuffer;
ssbuffer << "[" << _time << "] "
<< "[" << LogLevel2Str(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _fileName << "] "
<< "[" << _line << "] - ";
_logInfo = ssbuffer.str();
}
// 重载<<运算符: 实现日志实际内容的拼接
// 实际上日志字符串整体分为两个部分: 日志头和日志内容
// 日志头: [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16]
// 日志内容: hello world
// 因此需要重载<<运算符,将日志内容拼接在日志头后面
template <typename T>
LogMessage &operator<<(const T &value)
{
std::stringstream ssbuffer;
ssbuffer << value;
_logInfo += ssbuffer.str();
return *this;
}
~LogMessage()
{
// 析构函数: 析构时,将日志字符串刷新到⽂件中
_logger._strategy->SyncLog(_logInfo);
}
private:
std::string _time; // 时间
LogLevel _level; // 日志等级
int _pid; // 本进程pid
std::string _fileName; // 文件名
int _line; // 行号
std::string _logInfo; // 一行完整的日志信息
Logger & _logger; // 在Logger类中内部类LogMessage中,构建一个对Logger类的对象引用
};
// 重载():
// 故意拷⻉,形成LogMessage临时对象,后续在被<<时,会被持续引⽤,
// 直到完成输⼊,才会⾃动析构临时LogMessage,⾄此也完成了⽇志的显⽰或者刷新
// 同时,形成的临时对象内包含独⽴⽇志数据
// 未来采⽤宏替换,进⾏⽂件名和代码⾏数的获取
LogMessage operator()(LogLevel type, std::string filename, int line)
{
return LogMessage(type, filename, line, *this);
}
private:
std::shared_ptr<LogStrategy> _strategy; // 日志刷新策略
};
// 全局的日志对象
Logger logger;
// 宏替换: 进⾏⽂件名和代码⾏数的获取
#define LOG(level) logger(level, __FILE__, __LINE__)
// 提供选择使⽤何种⽇志策略的⽅法:这个需要用户自己选择
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.EnableConsoleLogStrategy()
#define ENABLE_FILE_LOG_STRATEGY() logger.EnableFileLogStrategy()
}
四、总结
本文详细介绍了如何使用 C++ 实现一个功能强大的日志系统。通过策略模式的设计,我们实现了灵活的日志刷新逻辑;通过宏定义和模板技术,简化了日志记录的调用方式;同时,整个系统具备良好的可扩展性和线程安全性。