引言
随着C++20的落地,C++引入了许多新特性和改进,开发高效且功能丰富的轮子应该会更加容易。
日志记录是大多数应用程序不可或缺的部分,笔者很好奇如果用最新的C++技术实现一个精简强大的日志工具是什么体验,今天跟笔者一起展开这次编程冒险旅程吧!
日志工具的设计要点
设计一个有效的日志系统需要考虑多方面的需求和约束。以下是一些核心设计要点:
- 多数据类型支持:支持多种类型数据输出为字符串
- 支持时间戳
- 支持日志文件管理,使得日志文件不会无限增大
- 跨平台,只使用C/C++标准库,确保在win/mac下正确运行。
- 线程安全
接下来,我们一一攻克上述的技术要点。
1. 多数据类型支持
利用C++的模板和运算符重载,我们可以轻松支持多种数据类型的日志记录。核心思想是通过重载流插入运算符<<
来实现。例如:
class Logger {
public:
template<typename T>
Logger& operator<<(const T& value) {
buffer << value;
return *this;
}
private:
std::ostringstream buffer;
};
这段代码允许我们将任何支持<<
运算符的类型直接记录到日志中,包括自定义类型,只要相应的运算符被正确实现。
2. 时间戳支持
时间戳是诊断问题时的关键信息。在新的C++中,我们可以使用<chrono>
库来获取高精度的时间戳。下面是我们的实现:
#include <chrono>
#include <iomanip>
std::string currentTimestamp() {
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
auto tm = *std::localtime(&time_t);
std::ostringstream oss;
oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
return oss.str();
}
但是tm结构体信息没有毫秒信息,无法打印毫秒精度的时间戳,这显然跟我们的定位“强大”的日志库不符,因此需要在put_time 完成后额外单独算一下毫秒:
auto now_ms = std::chrono::time_point_cast<std::chrono::milliseconds>(now).time_since_epoch().count();
// 使用 put_time 输出日期和时间
stream << std::put_time(&now_tm, " %Y-%m-%d %H:%M:%S.");
stream << (now_ms % 1000); // 输出毫秒部分
这样就能看到毫秒部分了:
3. 日志文件管理
自动管理日志文件是一个复杂的需求,涉及到文件大小、数量及其滚动。以下是管理日志滚动的基本思路:
void rotateLogs() {
const std::string oldLog = "log_old.txt";
const std::string newLog = "log.txt";
std::rename(newLog.c_str(), oldLog.c_str());
}
但是仅仅日志重命名是不够的,还需要清理过久的日志文件,否则用户的磁盘就很容易被我们日志占满。要实现自动重命名和删除过期文件,并支持用户指定日志文件的大小和最多文件个数,我们引入了变量max_files和max_file_size:我们最终实现如下:
void writeLog(...) {
// 写日志处(逻辑省略)
if (log_stream.tellp() >= static_cast<std::streamoff>(max_file_size)) {
// 当当前文件达到阈值,则调用rotateLogs创建新文件和移除旧文件
rotateLogs();//滚动现有文件
initializeLogger();//创建新文件
}
}
void rotateLogs() {
if (std::filesystem::exists(log_file_prefix + "." + std::to_string(max_files) + __SIMPLE_LOG_SUFFIX)) {
//删掉最后一个
std::filesystem::remove(log_file_prefix + "." + std::to_string(max_files) + __SIMPLE_LOG_SUFFIX);
}
for (size_t i = max_files; i > 1; --i) {
auto src = log_file_prefix + "." + std::to_string(i - 1) + __SIMPLE_LOG_SUFFIX;
auto dst = log_file_prefix + "." + std::to_string(i) + __SIMPLE_LOG_SUFFIX;
if (std::filesystem::exists(src)) {
std::filesystem::rename(src, dst);
}
}
if (std::filesystem::exists(log_file_prefix + __SIMPLE_LOG_SUFFIX)) {
//把当前文件设置为第一个
std::filesystem::rename(log_file_prefix + __SIMPLE_LOG_SUFFIX, log_file_prefix + ".1" + __SIMPLE_LOG_SUFFIX);
}
}
我们可以用一个流程图来总结文件管理的总体流程:
4. 跨平台支持
这个很好办,我们只要全部使用C++标准库和C标准库的API即可。
例如上一个文件管理这个部分,我们使用的是std::filesystem。
因此不需要展开冗述了。
支持多线程
在多线程环境中,日志记录系统必须是线程安全的。通常可以通过使用互斥锁来实现。
在每条日志开始写入前,在每条日志完成写入后加锁,例如:
#include <mutex>
class Logger {
public:
void log(const std::string& message) {
std::lock_guard<std::mutex> guard(mutex);
// 写入日志到文件或其他媒介
}
private:
std::mutex mutex;
};
这样就能确保了当多个线程尝试写日志时,每次只有一个线程能进行写操作。
实现了线程安全的目标。
多线程踩坑
在测试多线程稳定性的时候,踩了两个坑:
第一个坑:localtime
测试时发现在windows平台正常运行,但是在mac平台偶先崩溃,崩溃信息是无效指令,在c++lib中。经过排查定位发现问题出在localtime这一行代码中:
std::string currentTimestamp() {
// 略...
auto tm = *std::localtime(&time_t);
// 略...
std::localtime是线程不安全的,笔者一开始以为不安全的意思是可能拿到的结果不准,但是在mac下却是偶尔崩溃的表现。因此老老实实地改成了线程安全的版本。更多信息请参考:https://en.cppreference.com/w/c/chrono/localtime
C++26考虑要去除C++的“未定义”行为,终于明白“未定义”有多坑了,std::localtime在多线程环境下是未定义的行为,也就是说在某些平台正常运行,在某些平台崩溃了,甚至跑飞了都有可能。笔者这次就遇到在mac平台下面跑飞了的情况,堆栈上完全看出不出问题出在哪,一堆汇编指令和乱码,让人心乱如麻。
由于MSVC的localtime_s和标准的不兼容,这块要用上条件编译来解决:
std::string currentTimestamp() {
auto now = std::chrono::system_clock::now();
std::time_t now_time = std::chrono::system_clock::to_time_t(now);
std::tm now_tm = {};
#ifdef _WIN32
// 参考:https://en.cppreference.com/w/c/chrono/localtime
if (localtime_s(&now_tm, &now_time) == 0) {
#else
if (localtime_r(&now_time, &now_tm) != null) {
#endif // _WIN32
// 略...
第二个坑:加锁导致线程切换,在移动文件后导致C++运行时崩溃
这个是windows上发现的,会阻塞在fstream的open函数中。触发问题的代码经过精简后如下:
void wirteLog() {
auto fileSize = doWrite();
if (fileSize > maxSize) {
rotateLogs();
}
}
void doWrite() {
std::lock_guard<std::mutex> guard(mutex);
// 略...
}
void rotateLogs() {
std::lock_guard<std::mutex> guard(mutex);
// 略...
}
这里的问题在于多线程场景中,日志写完之后,会释放锁,doWrite和rotateLogs不是一个原子操作。这时如果日志文件达到上限,doWrite完成后释放了锁,其他线程会写入文件流,当其他线程写完后释放锁,再回来rotateLogs时,先执行日志文件流关闭,移动文件后,又创建同名文件,但最终阻塞在创建文件上。如果创建文件的名是新的,就不会阻塞。粗略看了一下应该是vc的runtime的bug,多线程下,不同写入写入同一个fstream,又由另一个线程close&open同名文件就会出现。可能是某个逻辑认为当前其他线程还有任务没执行完,要等待其他线程才能完成创建新的文件。
这段代码在逻辑上也有另一个问题,那就是会触发多次rotateLogs,在大量线程测试中发现,很多线程都会进入if (fileSize > maxSize) 的成功判断里,在拿锁时等待,这样导致后续一大波rotateLogs函数被执行,不符合预期。
因此,这段代码改为将doWrite和rotateLogs合并为一个原子操作,在同一次锁中完成完整操作即可。代码最终改为如下:
void wirteLog() {
std::lock_guard<std::mutex> guard(mutex);// 一次锁定
auto fileSize = doWrite();
if (fileSize > maxSize) {
rotateLogs();
}
}
void doWriteWithOutLock() {
// 略...
}
void rotateLogsWithOutLock() {
// 略...
}
性能优化
在很多场景中,日志的写入频率可能会很高,这时日志记录可能成为一个瓶颈。
为了体现出我们实现的日志工具的“强大”,我们要对其进行一些优化策略:
1. 减少不必要的std::string构造
我们先把currentTimestamp的实现从:
std::string currentTimestamp()
改为:
template<class StreamType>
inline void printCurrentTime(StreamType& stream)
直接把数据写入流中,避免返回额外的string和字符串拼接操作。
2.尽量使用std::string_view
我们在记录日志文件的路径时,避免日志过程,实现了一个函数,只记录文件名,而非完整的文件路径。
但是C++的__FILE__宏拿到的路径是完整路径,因此我们需要对这个路径进行处理。
处理函数从:
std::string extractFilename(const std::string &path) {
// 实现略..
}
改为:
inline std::string_view extractFilename(const std::string_view path) const {
// 实现略..
}
3. 减少LogStream的大小
在我们最终的实现中,LogStream是一个临时对象,在每次打印日志时都会构造:
inline auto log(LogLevel level, const char* file, int line) {
return LogStream<CSimpleLogger>(*this, level, file, line);
}
因此移除了LogStream类中可以精简的数据成员,例如自动锁,减少体积。
最终版本
整合上面所有实现,经过进一步的优化后,最终我们实现的日志工具如下:
#define _CRT_SECURE_NO_WARNINGS 1
#define __STDC_WANT_LIB_EXT1__ 1
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <mutex>
#include <chrono>
#include <ctime>
#include <iomanip>
#include <string_view>
#include <thread>
#include <vector>
#include <filesystem>
#include <assert.h>
#include <time.h>
/*
## 概述
`Logger` 是一个用于记录应用程序运行时信息的日志记录工具类。它提供了多线程安全的日志记录功能,并支持日志文件自动滚动,即当文件达到一定大小时自动创建新文件,并在达到指定文件数量时删除最旧的日志文件。
## 特性
- 多线程安全的日志记录。
- 支持设置单个日志文件的最大大小,默认为10MB。
- 支持设置最大日志文件数量,默认为4个。
- 简单精小,直观简洁,开箱即用,仅仅依赖C++标准库
- 输出格式:[Level] [threadID] [TimeStamp] [file@line] : [message]
- 当日志文件大小超过设定值时,自动创建新日志文件,序号越小日志文件越新。
- 当日志文件数量超过设定值时,删除最旧的日志文件(序号最大的)。
- 可以通过定义 __SIMPLE_LOG_SUFFIX 值修改日志后缀,默认为".log"
## 构造函数说明
Logger(const std::string& filename, size_t maxSize , size_t maxFiles);
- **参数**:
- `filename`: 基础的日志文件名。不需要带扩展名,会自动添加.log扩展名
- `maxSize`: 单个日志文件的最大大小,默认为20MB。
- `maxFiles`: 最大日志文件数量,默认为4。
## 使用说明
- 需要配合SIMPLE_LOG_INFO宏使用
- 建议二次封装SIMPLE_LOG_INFO宏,免去宏的第一个参数。
*/
#ifndef __SIMPLE_LOG_SUFFIX
#define __SIMPLE_LOG_SUFFIX ".log"
#endif // __SIMPLE_LOG_SUFFIX
class CSimpleLogger {
public:
enum class LogLevel { INFO, WARNING, ERROR };
CSimpleLogger(const std::string& filename, size_t maxSize = 20 * 1024 * 1024, size_t maxFiles = 4)
: log_file_prefix(filename), max_file_size(maxSize), max_files(maxFiles) {
initializeLogger_NoLock();
}
~CSimpleLogger() {
if (log_file.is_open()) {
log_file.close();
}
}
class LogStream {
public:
LogStream(CSimpleLogger& logger, LogLevel level, const char* file, int line)
: logger(logger) {
logger.log_mutex.lock();
// 格式:[Level] [threadID] [TimeStamp] [file@line] : [message]
logger.log_file << logger.logLevelToString(level) << std::this_thread::get_id();
logger.printCurrentTime();
logger.log_file << logger.extractFilename(file) << "@" << line << ": ";
}
~LogStream() {
logger.log_file << std::endl; //换行
logger.checkAndRotate_NoLock(); // 检查是否需要滚动日志文件
logger.log_mutex.unlock();
}
template<typename T>
LogStream& operator<<(const T& msg) {
logger.log_file << msg;
return *this;
}
// 禁止拷贝和赋值
LogStream(const LogStream&) = delete;
LogStream& operator=(const LogStream&) = delete;
private:
CSimpleLogger& logger;
};
LogStream log(LogLevel level, const char* file, int line) {
return LogStream(*this, level, file, line);
}
private:
std::ofstream log_file;
std::mutex log_mutex;
std::string log_file_prefix;
size_t max_file_size;
size_t max_files;
void initializeLogger_NoLock() {
try {
if (log_file.is_open()) {
log_file.close();
}
log_file.open(log_file_prefix + __SIMPLE_LOG_SUFFIX, std::ios::app);
for (int i = 0; !log_file.is_open(); ++i) {
// 实在没办法 用temp备用的文件名,但是这个系列文件名不会自动删除
log_file.open(log_file_prefix + "." + std::to_string(i) + ".temp." + __SIMPLE_LOG_SUFFIX, std::ios::app);
}
}
catch (...) {
assert(false);
}
}
void rotateLogs_NoLock() {
try {
printf("rotateLogs\n");
log_file.close();
if (std::filesystem::exists(log_file_prefix + "." + std::to_string(max_files) + __SIMPLE_LOG_SUFFIX)) {
//删掉最后一个
std::filesystem::remove(log_file_prefix + "." + std::to_string(max_files) + __SIMPLE_LOG_SUFFIX);
}
for (size_t i = max_files; i > 1; --i) {
auto src = log_file_prefix + "." + std::to_string(i - 1) + __SIMPLE_LOG_SUFFIX;
auto dst = log_file_prefix + "." + std::to_string(i) + __SIMPLE_LOG_SUFFIX;
if (std::filesystem::exists(src)) {
std::filesystem::rename(src, dst);
}
}
if (std::filesystem::exists(log_file_prefix + __SIMPLE_LOG_SUFFIX)) {
//把当前文件设置为第一个
std::filesystem::rename(log_file_prefix + __SIMPLE_LOG_SUFFIX, log_file_prefix + ".1" + __SIMPLE_LOG_SUFFIX);
}
}
catch (...) {
assert(false);
}
}
void checkAndRotate_NoLock() {
if (log_file.tellp() >= static_cast<std::streamoff>(max_file_size)) {
rotateLogs_NoLock();
initializeLogger_NoLock();
}
}
std::string_view extractFilename(const std::string_view path) const {
#ifdef _WIN32
const char path_separator = '\\';
#else
const char path_separator = '/';
#endif
size_t pos = path.find_last_of(path_separator);
if (pos != std::string_view::npos) {
return path.substr(pos + 1);
}
return path;
}
void printCurrentTime() {
auto now = std::chrono::system_clock::now();
std::time_t now_time = std::chrono::system_clock::to_time_t(now);
std::tm now_tm = {};
if (localtime_r(&now_time, &now_tm) == 0) {
auto now_ms = std::chrono::time_point_cast<std::chrono::milliseconds>(now).time_since_epoch().count();
// 使用 put_time 输出日期和时间
log_file << std::put_time(&now_tm, " %Y-%m-%d %H:%M:%S.");
log_file << (now_ms % 1000) << " \t"; // 输出毫秒部分,并加制表符
} else {
}
}
inline const char* logLevelToString(LogLevel level) const {
switch (level) {
case LogLevel::INFO: return "INFO ";
case LogLevel::WARNING: return "WARN ";
case LogLevel::ERROR: return "ERRO ";
default: return "UNKNOWN ";
}
}
};
// 宏用于自动添加文件名和行号
#define SIMPLE_LOG_INFO(logger) logger.log(CSimpleLogger::LogLevel::INFO, __FILE__, __LINE__)
#define SIMPLE_LOG_WARNING(logger) logger.log(CSimpleLogger::LogLevel::WARNING, __FILE__, __LINE__)
#define SIMPLE_LOG_ERROR(logger) logger.log(CSimpleLogger::LogLevel::ERROR, __FILE__, __LINE__)
void logMessages(CSimpleLogger& logger, int thread_id, int message_count) {
for (int i = 0; i < message_count / 3; ++i) {
SIMPLE_LOG_INFO(logger) << "Thread " << thread_id << " logging message " << i;
SIMPLE_LOG_WARNING(logger) << "Test Warning" << i;
SIMPLE_LOG_ERROR(logger) << "Test Error" << i;
}
}
int main() {
try {
CSimpleLogger logger("my_log");
const int num_threads = 20;
const int messages_per_thread = 1000000;
std::vector<std::thread> threads;
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back(logMessages, std::ref(logger), i, messages_per_thread);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Logging complete. Check log.txt for results." << std::endl;
}
catch (const std::exception& e) {
std::cerr << "Logging failed: " << e.what() << std::endl;
}
return 0;
}
还没完,还要支持fmt
都2024年了,还用老掉牙的流式格式化,肯定称不上一个合格的日志库。
于是,我们在C++20的环境下,增加了fmt模式的格式化支持:
// 宏用于自动添加文件名和行号
#define SIMPLE_LOG_INFO(logger) logger.log(LogLevel::INFO, __FILE__, __LINE__)
#define SIMPLE_LOG_WARNING(logger) logger.log(LogLevel::WARNING, __FILE__, __LINE__)
#define SIMPLE_LOG_ERROR(logger) logger.log(LogLevel::ERROR, __FILE__, __LINE__)
#if (__cplusplus >= 202002L || _MSVC_LANG >= 202002L)
#include <format>
// 使用 std::format 实现格式化日志记录
#define SIMPLE_LOG_INFO_FMT(logger, fmt, ...) SIMPLE_LOG_INFO(logger) << std::format(fmt, __VA_ARGS__)
#define SIMPLE_LOG_WARNING_FMT(logger, fmt, ...) SIMPLE_LOG_WARNING(logger) << std::format(fmt, __VA_ARGS__)
#define SIMPLE_LOG_ERROR_FMT(logger, fmt, ...) SIMPLE_LOG_ERROR(logger) << std::format(fmt, __VA_ARGS__)
#endif
这样,就能用上流行又使用的 format 的功能了。
更进一步:独立线程
日志库的性能非常重要,那么,我们如何: 如何减少日志使用者阻塞的时间?
是否可以将日志写入逻辑放到单独线程中,日志使用者在写入日志时,是需要完成字符串格式化任务,后续的文件写入和文件管理的事宜,都放在独立的线程完成呢?
也就是说,实现一个类似下面架构的日志工具:
重构
要实现上述目标不难,不过我们如何充分利用我们已经实现的代码,在我们已有代码的基础上,添加少量代码就能实现这个功能呢?这是一项有意思的挑战。
为了达到目标,我进行了如下重构:
- CLogStream和LogLevel抽出来,不再作为CSimpleLogger的内嵌类。
- 把所有日志内容格式化的功能全部挪到了CLogStream类中。具体来说,把函数logLevelToString、printCurrentTime、extractFilename三个函数迁移过去。
- 把加锁功能也从CLogStream中移动到Logger中去,具体来说,增加两个函数实现:onStartLogItem和onEndLogItem。这样,Logger就能自己决定如何加锁。
- 经过上述改动后,CSimpleLogger类就只提供文件写入和文件管理功能。职责比较单一。
- 在新的设计中,写入文件在独立的线程,因此不需要加锁。为了复用CSimpleLogger的文件写入和文件管理功能,为CSimpleLogger增加一个目标参数,使其支持是否加锁。
- CLogStream改为模板类,支持指定Logger。采用协约编程方式,要求Logger提供以下接口:
inline Stream log(LogLevel level, const char* file, int line)
void onStartLogItem()
void onEndLogItem()
std::ofstream log_stream;
- 综合5和6,CSimpleLogger的核心代码如下:
template<bool bSupportMulitThread = true>
class CSimpleLogger {
public:
void onStartLogItem() {
if (bSupportMulitThread) {
//通过模板参数决定是否需要上锁。这里可以利用模板特例化实现编译期优化,但会增加代码体积。
//实际上,如果不需要上锁,利用编译期的死代码消除优化可以避免损耗,也是能达到一样的效果。
log_mutex.lock();
}
}
void onEndLogItem() {
if (bSupportMulitThread) {
log_mutex.unlock();
}
}
// 略...
- 新增类CQueuedLogger,用于实现线程队列的日志工具。内部使用内嵌类CProducerConsumer封装生产者-消费者模式。
- 整合代码,总体优化。
支持线程队列的日志工具的完整代码如下:
经过重构后,代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#define __STDC_WANT_LIB_EXT1__ 1
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <mutex>
#include <chrono>
#include <ctime>
#include <iomanip>
#include <string_view>
#include <thread>
#include <vector>
#include <filesystem>
#include <assert.h>
#include <time.h>
/*
## 概述
`Logger` 是一个用于记录应用程序运行时信息的日志记录工具类。它提供了多线程安全的日志记录功能,并支持日志文件自动滚动,即当文件达到一定大小时自动创建新文件,并在达到指定文件数量时删除最旧的日志文件。
## 特性
- 多线程安全的日志记录。
- 支持设置单个日志文件的最大大小,默认为10MB。
- 支持设置最大日志文件数量,默认为4个。
- 简单精小,直观简洁,开箱即用,仅仅依赖C++标准库
- 输出格式:[Level] [threadID] [TimeStamp] [file@line] : [message]
- 当日志文件大小超过设定值时,自动创建新日志文件,序号越小日志文件越新。
- 当日志文件数量超过设定值时,删除最旧的日志文件(序号最大的)。
- 可以通过定义 __SIMPLE_LOG_SUFFIX 值修改日志后缀,默认为".log"
## 构造函数说明
Logger(const std::string& filename, size_t maxSize , size_t maxFiles);
- **参数**:
- `filename`: 基础的日志文件名。不需要带扩展名,会自动添加.log扩展名
- `maxSize`: 单个日志文件的最大大小,默认为20MB。
- `maxFiles`: 最大日志文件数量,默认为4。
## 使用说明
- 需要配合SIMPLE_LOG_INFO宏使用
- 建议二次封装SIMPLE_LOG_INFO宏,免去宏的第一个参数。
*/
#ifndef __SIMPLE_LOG_SUFFIX
#define __SIMPLE_LOG_SUFFIX ".log"
#endif // __SIMPLE_LOG_SUFFIX
enum class LogLevel { INFO, WARNING, ERROR };
template <class CLogger>
class CLogStream {
public:
CLogStream(CLogger& logger, LogLevel level, const char* file, int line)
: logger(logger) {
logger.onStartLogItem();
// 格式:[Level] [threadID] [TimeStamp] [file@line] : [message]
logger.log_stream << logLevelToString(level) << std::this_thread::get_id();
printCurrentTime(logger.log_stream);
logger.log_stream << extractFilename(file) << "@" << line << ": ";
}
~CLogStream() {
logger.log_stream << std::endl; //换行
logger.onEndLogItem();
}
template<typename T>
CLogStream& operator<<(const T& msg) {
logger.log_stream << msg;
return *this;
}
// 禁止拷贝和赋值
CLogStream(const CLogStream&) = delete;
CLogStream& operator=(const CLogStream&) = delete;
private:
inline std::string_view extractFilename(const std::string_view path) const {
// path使用std::string_view而非std::string提高性能
#ifdef _WIN32
const char path_separator = '\\';
#else
const char path_separator = '/';
#endif
size_t pos = path.find_last_of(path_separator);
if (pos != std::string_view::npos) {
return path.substr(pos + 1);
}
return path;
}
template<class StreamType>
inline void printCurrentTime(StreamType& stream) {
auto now = std::chrono::system_clock::now();
std::time_t now_time = std::chrono::system_clock::to_time_t(now);
std::tm now_tm = {};
#ifdef _WIN32
// 参考:https://en.cppreference.com/w/c/chrono/localtime
if (localtime_s(&now_tm, &now_time) == 0) {
#else
if (localtime_r(&now_time, &now_tm) == 0) {
#endif // _WIN32
auto now_ms = std::chrono::time_point_cast<std::chrono::milliseconds>(now).time_since_epoch().count();
// 使用 put_time 输出日期和时间
stream << std::put_time(&now_tm, " %Y-%m-%d %H:%M:%S.");
stream << (now_ms % 1000) << " \t"; // 输出毫秒部分,并加制表符
}
else {
assert(false);
}
}
inline const char* logLevelToString(LogLevel level) const {
switch (level) {
case LogLevel::INFO: return "INFO ";
case LogLevel::WARNING: return "WARN ";
case LogLevel::ERROR: return "ERRO ";
default: return "UNKNOWN ";
}
}
private:
CLogger& logger;
};
template<bool bSupportMulitThread = true>
class CSimpleLogger {
public:
CSimpleLogger(const std::string& filename, size_t maxSize = 20 * 1024 * 1024, size_t maxFiles = 4)
: log_file_prefix(filename), max_file_size(maxSize), max_files(maxFiles) {
initializeLoggerWithOutLock();
}
~CSimpleLogger() {
if (log_stream.is_open()) {
log_stream.close();
}
}
// 下面的public是Log接口,协约式编程 必须实现
inline auto log(LogLevel level, const char* file, int line) {
return CLogStream<CSimpleLogger>(*this, level, file, line);
}
void onStartLogItem() {
//通过模板参数决定是否需要上锁。这里可以利用模板特例化实现编译期优化,但会增加代码体积。
//实际上,如果不需要上锁,利用编译期的死代码消除优化可以避免损耗,也是能达到一样的效果。
if (bSupportMulitThread) {
log_mutex.lock();
}
}
void onEndLogItem() {
checkAndRotateWithOutLock(); // 检查是否需要滚动日志文件
if (bSupportMulitThread) {
log_mutex.unlock();
}
}
std::ofstream log_stream;
private:
std::mutex log_mutex;
std::string log_file_prefix;
size_t max_file_size;
size_t max_files;
void initializeLoggerWithOutLock() {
try
{
if (log_stream.is_open()) {
log_stream.close();
}
log_stream.open(log_file_prefix + __SIMPLE_LOG_SUFFIX, std::ios::app);
for (int i = 0; !log_stream.is_open(); ++i) {
// 实在没办法 用temp备用的文件名,但是这个系列文件名不会自动删除
log_stream.open(log_file_prefix + "." + std::to_string(i) + ".temp." + __SIMPLE_LOG_SUFFIX, std::ios::app);
}
}
catch (...)
{
assert(false);
}
}
void rotateLogsWithOutLock() {
try
{
//printf("rotateLogs\n");
log_stream.close();
if (std::filesystem::exists(log_file_prefix + "." + std::to_string(max_files) + __SIMPLE_LOG_SUFFIX)) {
//删掉最后一个
std::filesystem::remove(log_file_prefix + "." + std::to_string(max_files) + __SIMPLE_LOG_SUFFIX);
}
for (size_t i = max_files; i > 1; --i) {
auto src = log_file_prefix + "." + std::to_string(i - 1) + __SIMPLE_LOG_SUFFIX;
auto dst = log_file_prefix + "." + std::to_string(i) + __SIMPLE_LOG_SUFFIX;
if (std::filesystem::exists(src)) {
std::filesystem::rename(src, dst);
}
}
if (std::filesystem::exists(log_file_prefix + __SIMPLE_LOG_SUFFIX)) {
//把当前文件设置为第一个
std::filesystem::rename(log_file_prefix + __SIMPLE_LOG_SUFFIX, log_file_prefix + ".1" + __SIMPLE_LOG_SUFFIX);
}
}
catch (...)
{
assert(false);
}
}
void checkAndRotateWithOutLock() {
if (log_stream.tellp() >= static_cast<std::streamoff>(max_file_size)) {
rotateLogsWithOutLock();
initializeLoggerWithOutLock();
}
}
};
// 宏用于自动添加文件名和行号
#define SIMPLE_LOG_INFO(logger) logger.log(LogLevel::INFO, __FILE__, __LINE__)
#define SIMPLE_LOG_WARNING(logger) logger.log(LogLevel::WARNING, __FILE__, __LINE__)
#define SIMPLE_LOG_ERROR(logger) logger.log(LogLevel::ERROR, __FILE__, __LINE__)
#if (__cplusplus >= 202002L || _MSVC_LANG >= 202002L)
#include <format>
// 使用 std::format 实现格式化日志记录
#define SIMPLE_LOG_INFO_FMT(logger, fmt, ...) SIMPLE_LOG_INFO(logger) << std::format(fmt, __VA_ARGS__)
#define SIMPLE_LOG_WARNING_FMT(logger, fmt, ...) SIMPLE_LOG_WARNING(logger) << std::format(fmt, __VA_ARGS__)
#define SIMPLE_LOG_ERROR_FMT(logger, fmt, ...) SIMPLE_LOG_ERROR(logger) << std::format(fmt, __VA_ARGS__)
#endif
//
// 线程队列版本的日志器
// 原理:使用生产者消费者模型,写入日志在另一个线程。写入日志的调用可以返回非常快,提高使用者性能
class CQueuedLogger {
private:
template <typename T> class CProducerConsumer {
public:
CProducerConsumer(int max_capacity = 10) : capacity(max_capacity) {};
void produce(T value) {
std::unique_lock<std::mutex> lock(mtx);
while (buffer.size() == capacity) { // 如果缓冲区满了,则等待
cond_var.wait(lock);
}
buffer.push_back(value);
cond_var.notify_one(); // 通知消费者
}
T consume() {
std::unique_lock<std::mutex> lock(mtx);
while (buffer.empty()) { // 如果缓冲区为空,则等待
cond_var.wait(lock);
}
auto value = buffer.front();
buffer.erase(buffer.begin());
cond_var.notify_one(); // 通知生产者
return value;
}
private:
std::vector<T> buffer;
const size_t capacity = 10; // 缓冲区容量
std::mutex mtx;
std::condition_variable cond_var;
};
public:
CQueuedLogger(const std::string& filename, size_t maxSize = 20 * 1024 * 1024, size_t maxFiles = 4) :
m_singleThreadLoger(filename, maxSize, maxFiles),
producerConsumer(500),
write_log_thread([this]() {this->logThreadProc(); }) {
}
~CQueuedLogger() {
need_exit_log_thread = true;
write_log_thread.join();
}
// 下面的public是Log接口,协约式编程 必须实现
inline auto log(LogLevel level, const char* file, int line) {
return CLogStream<CQueuedLogger>(*this, level, file, line);
}
void onStartLogItem() {
log_mutex.lock();
}
void onEndLogItem() {
// 结果在log_stream里面
producerConsumer.produce(log_stream.str());
log_stream.str("");//清空
log_mutex.unlock();
}
std::stringstream log_stream;
private:
void logThreadProc() {
while (need_exit_log_thread == false) {
m_singleThreadLoger.onStartLogItem();
m_singleThreadLoger.log_stream << producerConsumer.consume();
m_singleThreadLoger.onEndLogItem();
}
}
private:
std::mutex log_mutex;
CSimpleLogger<false> m_singleThreadLoger;
CProducerConsumer<std::string> producerConsumer;
std::thread write_log_thread;
bool need_exit_log_thread = false;
};
//
void logMessages(CSimpleLogger<true>& logger, int thread_id, int message_count) {
for (int i = 0; i < message_count / 3; ++i) {
SIMPLE_LOG_INFO(logger) << "Thread " << thread_id << " logging message " << i;
SIMPLE_LOG_WARNING(logger) << "Test Warning" << i;
SIMPLE_LOG_ERROR(logger) << "Test Error" << i;
SIMPLE_LOG_INFO_FMT(logger, "fmt log: {}", i); // c++ 20 支持
}
std::cout << "## logMessages threadid:" << thread_id << " Done!\n";
}
void logMessagesQueued(CQueuedLogger& logger, int thread_id, int message_count) {
for (int i = 0; i < message_count / 3; ++i) {
SIMPLE_LOG_INFO(logger) << "Thread " << thread_id << " logging message " << i;
SIMPLE_LOG_WARNING(logger) << "Test Warning" << i;
SIMPLE_LOG_ERROR(logger) << "Test Error" << i;
SIMPLE_LOG_INFO_FMT(logger, "fmt log: {}", i); // c++ 20 支持
}
std::cout << "@@ logMessagesQueued threadid:" << thread_id << " Done!\n";
}
int main() {
try {
CSimpleLogger logger("my_log");
CQueuedLogger queuedLogger("queued_log");
const int num_threads = 20;
const int messages_per_thread = 10000;
std::vector<std::thread> threads;
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back(logMessages, std::ref(logger), i, messages_per_thread);
}
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back(logMessagesQueued, std::ref(queuedLogger), i, messages_per_thread);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Logging complete. Check log.txt for results." << std::endl;
}
catch (const std::exception& e) {
std::cerr << "Logging failed: " << e.what() << std::endl;
}
return 0;
}
性能对比
我们对比两个Logger的性能,看看对日志使用方是否有性能提升。
第一次跑
把上面的最终代码运行起来后,我们发现,CQueuedLogger在性能上没有明显的优势。从以下测试用例完成速度可见,CQueuedLogger没有明显比CSimpleLogger快。
看一下火焰图,可以发现CQueuedLogger使用者的大部分时间都被锁消耗了:
如果想要进一步增加CQueuedLogger的性能,必须想办法将这个锁的影响降低。
我们可以使用线程本地存储,为每个线程提供一个stream对象,这样就能在日志使用者那里避免锁的使用了。
说干就干。
具体来说,想办法把std::stringstream log_stream;为每个线程提供一个。
首先,我们需要将封装一个函数,代替直接访问log_stream:
auto& stream() {
return this->log_stream;
}
然后,在CQueuedLogger的stream实现中,返回线程本地存储的stream,删除掉成员变量的stream:
auto& stream() {
static thread_local std::stringstream log_stream;
return log_stream;
}
//std::stringstream log_stream; // 成员变量删除
第二次跑
压抑不住激动的心情再次运行,发现性能没有明显变化,查看火焰图,发现这回耗时点从lock变成了put_time:
这说明std::put_time内部用了锁,访问了某个共享变量。之前由于锁在外面,所以主要在外面等待,外面的锁拿掉了,std::put_time内部的锁耗时阻塞就暴露出来了。
这当然不能接受。于是用简单粗暴的方式代替了std::put_time的调用:
// stream << std::put_time(&now_tm, " %Y-%m-%d %H:%M:%S."); //std::put_time内部有锁,避免使用。改为:
stream << " " << (now_tm.tm_year + 1900) << "-" // tm_year 是从 1900 年开始计算的
<< (now_tm.tm_mon + 1) << "-" // tm_mon 是从 0 开始计算的
<< now_tm.tm_mday << " "
<< now_tm.tm_hour << ":"
<< now_tm.tm_min << ":"
<< now_tm.tm_sec << ".";
改完之后,两个Logger的性能都有很好的提升。不过printCurrentTime还是有一定的开销,还是不够理想:
要不把printCurrentTime的功能挪到写入文件的时候再拼接吧。在使用方就先把时间戳记下来就好了。不过这个改动成本比较大,而且破坏了我们的设计原则(LogStream负责日志格式化)。思来想去,还是改成sprintf吧。想要获得更好的性能,C的API确实更简单粗暴。
于是,上面格式化时间的代码改为了:
// stream << std::put_time(&now_tm, " %Y-%m-%d %H:%M:%S."); //std::put_time内部有锁,避免使用
char buffer[32];
int written = std::snprintf(buffer, sizeof(buffer), " %04d-%02d-%02d %02d:%02d:%02d.%03ld\t",
now_tm.tm_year + 1900, now_tm.tm_mon + 1, now_tm.tm_mday,
now_tm.tm_hour, now_tm.tm_min, now_tm.tm_sec, now_ms % 1000);
stream << buffer;
终于,从火焰图可以看出来,logMessagesQueued使用CQueuedLogger占用的CPU周期更短:
不过在Release配置下,CQueuedLogger的优势不如Debug那样夸张。最终运行也是CQueuedLogger先跑完所有测试任务:
再进一步
在实际的生产环境中,日志往往还需要加密和压缩,这时CQueuedLogger的优势就更明显了。加密和压缩逻辑可以全部挪到日志线程中实现,而避免阻塞使用方调用。
加密和封装的设计:
我们需要再抽象一个类,代替std::ofstream和std::stringstream的直接使用,使得在文件写入时可以对文件内容进行加工处理。
借助std::stream的扩展性,我们很容易实现对写入内容的预处理:
constexpr char XOR_ENCRYPTION_KEY = 'K';
class XOREncryptionBuffer : public std::streambuf {
public:
XOREncryptionBuffer(std::streambuf* buf, char key)
: originalBuffer(buf), encryptionKey(key) {}
protected:
virtual int overflow(int ch) override {
if (ch != EOF) {
// Encrypt the character using XOR
char encryptedChar = static_cast<char>(ch) ^ encryptionKey;
if (originalBuffer->sputc(encryptedChar) == EOF) {
return EOF;
}
}
return ch;
}
virtual std::streamsize xsputn(const char* s, std::streamsize count) override {
std::string temp;
temp.reserve(count);
for (std::streamsize i = 0; i < count; ++i) {
// Encrypt each character using XOR
temp.push_back(s[i] ^ encryptionKey);
}
return originalBuffer->sputn(temp.data(), temp.size());
}
private:
std::streambuf* originalBuffer;
char encryptionKey;
};
template <class BaseStream>
class LogOutStream : public BaseStream {
public:
template <typename... Args>
LogOutStream(Args&&... args)
: BaseStream(std::forward<Args>(args)...),
streamBuf(BaseStream::rdbuf(), XOR_ENCRYPTION_KEY) {
BaseStream::rdbuf(&streamBuf);
}
private:
XOREncryptionBuffer streamBuf;
};
打算用LogOutStream代替Loger的实际的流,但是尝试一轮后发现,在最新的 C++ 标准中,std::ofstream 不再直接支持通过 rdbuf() 设置自定义缓冲区。
这与标准库实现的更严格的封装和安全性设计有关。后续的C++ 标准库中的 std::basic_ofstream 类在某些实现中被设计为不允许用户直接替换其内部的缓冲区。
因此,CSimpleLogger不能再直接使用std::ofstream,因为std::ofstream的可扩展性不够好。
最终,我们直接从std::streambuf派生,直接使用std::basic_filebuf写入。
核心代码如下:
class XOREncryptionBuffer : public std::streambuf {
public:
XOREncryptionBuffer(char key)
: encryptionKey(key) {}
protected:
virtual int overflow(int ch) override {
if (ch != EOF) {
// Encrypt the character using XOR
//char encryptedChar = static_cast<char>(ch) ^ encryptionKey;
char encryptedChar = static_cast<char>(ch) ;
if (fileBuffer.sputc(encryptedChar) == EOF) {
return EOF;
}
}
return ch;
}
virtual std::streamsize xsputn(const char* s, std::streamsize count) override {
std::string temp;
temp.reserve(count);
for (std::streamsize i = 0; i < count; ++i) {
// Encrypt each character using XOR
//temp.push_back(s[i] ^ encryptionKey);
temp.push_back(s[i]);
}
return fileBuffer.sputn(temp.data(), temp.size());
}
public:
std::filebuf fileBuffer;
private:
char encryptionKey;
};
class LogOutStream : public std::ostream {
public:
LogOutStream()
: std::ostream(&streamBuf),
streamBuf(__SIMPLE_LOG_ENCRYPTION_XOR_KEY) {
}
// 这些接口我们用到的
bool is_open() {
return streamBuf.fileBuffer.is_open();
}
void close() {
streamBuf.fileBuffer.close();
}
int open(const std::string FileName, int OpenFlag) {
return streamBuf.fileBuffer.open(FileName, OpenFlag) != nullptr;
}
long long tellp() {
return streamBuf.fileBuffer.pubseekoff(0, ios_base::cur, ios_base::out);
}
private:
XOREncryptionBuffer streamBuf;
};
进一步调整和说明
整体再次进行一次优化,包括:
- 去除宏的使用,把更多的灵活性通过参数指定。
- 引入SLOG_USE_WCHAR,支持指定wchar或者char。
- 放入命名空间SLog。
- 写一段使用指引和设计概要放在源文件中方便理解使用。
- 强调CSimpleLogger和CQueuedLogger的性能差异。方便知道开发者恰当选择。
具体来说:
- CSimpleLogger适合于大部分的场景,使用简单可靠,性能高。
- 对于性能特别敏感,日志量特别大,使用者对日志调用的时间要求极高的场景,可以使用CQueuedLogger。
- CQueuedLogger的优点就是选择合适的Queued大小,就可以做到几乎不会阻塞使用者。
- 另外,对于复杂加密和压缩的场景,CQueuedLogger的优势也更好,因为压缩逻辑在独立线程完成,不影响使用者。
- CQueuedLogger的缺点是需要额外消耗一个线程资源。
最后,笔者有个观点,那就是【精简短小的代码就是高可扩展性的设计】。因此,没有过分地将日志格式化和日志加密和压缩进行可扩展的设计,因为笔者相信使用者可以轻松修改源码实现他们需要的“扩展性”。
因此在说明中补充了下述内容:
本日志系统的代码设计力求简洁明了,使得用户能够轻松理解其内部逻辑并按需进行扩展。通过精简核心功能,不仅降低了维护成本,还为用户提供了广泛的自定义空间。使用者可以根据自己的需求轻松修改源码,实现对日志输出格式的定制,例如调整时间戳格式、增加额外的信息字段等。此外,加密算法和压缩算法的实现也可以在此基础上进行扩展,以适应不同的应用场景,如对敏感数据进行加密保护或对大量日志数据进行压缩存储,从而提高存储效率和安全性。
逻辑的简单直接,除了能提供良好的可扩展性,还能提高可靠程度,不再展开讨论了。
言归正传,在笔者的测试用例中,CQueuedLogger的优势并不大,在windows环境中,Release下CQueuedLogger性能更好,Debug下CSimpleLogger性能更好。在macOS环境中,无论是Debug还是Release,CQueuedLogger的性能都不如CSimpleLogger。由此可见:
- 在没有复杂压缩和加密逻辑下,CSimpleLogger的性能已经足够好。
- CQueuedLogger的主要性能瓶颈在于笔者的测试用例线程非常多,请求写日志的线程产生的日志量远远大于写入线程能消耗的日志量,导致大部分使用日志的线程在等待队列空的情况。
虽然笔者的测试用例用了大量线程请求写日志,在实际情况几乎不会出现。但是我们也不免思考,如何在大量线程写日志的情况下,依然保持最佳性能状态。
笔者首先想到的是,如果想要CQueuedLogger进一步提高性能,需要用一个无锁队列来实现。通过无锁队列的加入,CQueuedLogger可以释放出惊人的性能潜力。但是本文写道这里已经过万字,无锁队列也很复杂,笔者计划后面我们再进行专题讨论吧。
最后的性能优化
虽然无锁队列能释放更多潜力,但是我们在现有方案中也有进一步优化的办法。那就是在consume函数中,我们可以一次返回全部的任务,避免日志写入线程频繁加锁。
也就是说,在总体上,将原来每写入一条日志需要加两次锁,改为大部分情况下只需要加一次锁:
从代码变化来看,优化前:
T consume() {
std::unique_lock<std::mutex> lock(mtx);
while (buffer.empty()) { // 如果缓冲区为空,则等待
cond_var.wait(lock);
}
auto value = buffer.front();
buffer.erase(buffer.begin());
cond_var.notify_one(); // 通知生产者
return value;
}
优化后:
void consume(std::vector<T> &out) {
std::unique_lock<std::mutex> lock(mtx);
while (buffer.empty()) { // 如果缓冲区为空,则等待
cond_var.wait(lock);
}
assert(out.empty());
std::swap(out, buffer);//直接把整个buffer都返回
cond_var.notify_one(); // 通知生产者
}
这样可以大大改善使用线程在日志队列满的时候的等待。
优化后,就几乎不需要进入日志队列满的情况等待了:
经过这一次优化后,CQueuedLogger终于遥遥领先于CSimpleLogger。
运行测试结果:
在03:21:25 时间点运行,最后一个logMessagesQueued完成是在03:21:31(耗时6s),最后一个logMessage是在03:21:43(耗时18s)。而且logMessage在6秒后(logMessagesQueued完成后)没人跟他竞争cpu和磁盘IO,所以CQueuedLogger的性能至少是CSimpleLogger的三倍以上。
最后还是多嘴一句,这种开20个线程啥也不干光写日志的情况在实际软件运行中是不会发生的,因此CSimpleLogger的性能已经足够用了。CQueuedLogger需要额外一个线程专门写日志,使用时需要考虑队列消耗的内存和额外增加的线程成本,避免滥用CQueuedLogger。
恭喜你看到这里。经过上面的编码体验,可以看出代码的演化规律。
- 从基础功能开始:首先实现最基本的日志记录功能,确保可以正常记录信息。
- 添加特性:逐步引入更多的特性,如日志级别、时间戳、线程ID等,增强日志系统的实用性。
- 支持多线程:考虑到现代应用程序通常运行在多线程环境中,引入线程安全机制,保证日志记录的一致性和可靠性。
- 提供宏支持:定义宏来简化日志记录的操作,使开发者可以更方便地记录日志信息。
- 格式化支持:在支持 C++20 的环境中,利用
std::format
提供格式化输出功能,使日志信息更加规范和易读。 - 扩展性考虑:代码设计尽可能简洁,便于扩展。用户可以根据需要轻松地添加或修改日志格式、加密算法、压缩算法等功能。
- 性能优化:随着功能的增加,持续优化性能。
- 重新思考新的架构:引入队列式的日志架构,追求更高性能
- 重构代码,实现新的架构:通过更合理的设计,力求代码复用度最高。
- 重构优化:进一步整理代码,统一命名风格,支持宽字符等。
- 文档说明:提供详细的文档说明,帮助开发者快速理解和使用日志系统。
- 测试验证:通过单元测试和集成测试验证日志系统的正确性和稳定性。
通过这种逐步演进的方式,代码逐渐变得更加完善和强大,同时保持了良好的可维护性和可扩展性,好的代码是不断打磨出来的,写代码的乐趣也在其中。
最终代码
最终,我们这一次实现的的高质量高性能的C++日志工具完整代码如下(代码仓库:https://github.com/kevinyangli/slog):
#include <iostream>
#include <fstream>
#include <streambuf>
#include <mutex>
#include <string>
#include <string_view>
#include <chrono>
#include <filesystem>
#include <cassert>
#include <thread>
#include <sstream>
/*
// CSimpleLogger.h
//
// 设计思路:
// 这个简单的日志系统旨在提供一种灵活的方式记录日志,支持多线程环境下的安全日志记录,
// 并可以根据需要选择是否启用基于XOR的简单加密功能。此外,该系统还支持日志文件的自动轮换,当达到指定大小时会创建新的日志文件。
//
// 使用细节:
// 1. 日志级别:定义了三种基本的日志级别(信息、警告、错误),可以通过枚举类 LogLevel 来指定。
// 2. 日志输出流:提供了 CLogStream 类模板,用于构造一个日志消息。用户可以通过流式接口
// 将消息附加到当前的日志条目中。
// 3. 日志类:CSimpleLogger 是一个模板类,允许用户根据需要选择是否启用加密以及是否支持多线程。
// 4. 日志文件管理:支持日志文件的最大大小及最大文件数量配置,当达到配置值时会自动进行日志文件轮换。
// 5. 时间戳与线程ID:每个日志条目都会包含时间戳和记录日志时所在的线程ID。
// 6. 文件路径:会自动提取并记录日志输出位置的文件名及行号。
// 7. 字符类型:通过预处理宏 LOG_USE_WCHAR 可以选择使用宽字符或普通字符。
// 7. 精简短小的代码就是高可扩展性。使用者可以按需修改源码,实现输出日志格式的自定义,加密算法,压缩算法实现加密和压缩等。
//
// 示例用法:
// CSimpleLogger<> logger("example_log", 1024 * 1024 * 20, 5); // 创建一个日志实例,最大20MB,最多保留5个日志文件
// SIMPLE_LOG_INFO(logger) << "This is an info message.";
// SIMPLE_LOG_WARNING(logger) << "This is a warning message.";
// SIMPLE_LOG_ERROR(logger) << "This is an error message.";
//
// 如果编译器支持 C++20:
// SIMPLE_LOG_INFO_FMT(logger, "{} message.", "Info");
// SIMPLE_LOG_WARNING_FMT(logger, "{} message.", "Warning");
// SIMPLE_LOG_ERROR_FMT(logger, "{} message.", "Error");
//
// 注意事项:
// - 日志记录功能默认为线程安全,如果不需要线程安全,可以在创建日志对象时指定。
// - 日志文件的名称和路径应该具有足够的权限允许程序进行读写操作。
// - 日志记录可能会消耗较多的磁盘空间,特别是在高流量的应用场景下。
// - 如果启用了加密功能,请确保有适当的方法解密日志文件以便于后续查看或分析。
// - CSimpleLogger和CQueuedLogger的差异:
// 0. CSimpleLogger会在使用者写入日志时写入文件和加密/压缩;CQueuedLogger则将日志交给专门的日志线程写入文件和加密/压缩。
// 1. CSimpleLogger适合于大部分的场景,使用简单可靠,性能高。
// 2. 对于性能特别敏感,日志量特别大,使用者对日志调用的时间要求极高的场景,可以使用CQueuedLogger。
// 3. CQueuedLogger的优点就是选择合适的Queued大小,就可以做到几乎不会阻塞使用者。
// 4. 另外,对于复杂加密和压缩的场景,CQueuedLogger的优势也更好,因为压缩逻辑在独立线程完成,不影响使用者。
// 5. CQueuedLogger的缺点是需要额外消耗一个线程资源。
//
// - 本日志系统的代码设计力求简洁明了,使得用户能够轻松理解其内部逻辑并按需进行扩展。通过精简核心功能,不仅降低了维护成本,还为用户提供了广泛的自定义空间。
// 使用者可以根据自己的需求轻松修改源码,实现对日志输出格式的定制,例如调整时间戳格式、增加额外的信息字段等。此外,加密算法和压缩算法的实现也可以在此基础上进行扩展,以适应不同的应用场景,如对敏感数据进行加密保护或对大量日志数据进行压缩存储,从而提高存储效率和安全性。
*/
// 通过定义SLOG_CONFIG_USE_WCHAR=1使用wchar_t版本。默认为char。
#ifndef SLOG_CONFIG_USE_WCHAR
#define SLOG_CONFIG_USE_WCHAR 0
#endif
#define SLOG_CONFIG_ENCRYPTION_XOR_KEY 'L' // 加密密钥
namespace SLog {
#if SLOG_CONFIG_USE_WCHAR
#define SLOG_CONFIG_FILE_SUFFIX L".log" // 日志文件后缀
typedef wchar_t Char;
typedef std::char_traits<Char>::int_type CharInt;
typedef std::wstreambuf StringBuf;
typedef std::wfilebuf FileBuffer;
typedef std::wostringstream OutputStream;
typedef std::wostream OutputStreamType;
typedef std::wstring StringType;
typedef std::wstring_view StringView;
typedef std::wstringstream StringStream;
inline StringType numberToString(std::size_t n) {
return std::to_wstring(n);
}
#define SLOG_LITERAL(x) L ## x
#define SLOG_LITERAL1(x) SLOG_LITERAL(x)
#define __SLOG_FILE__ SLOG_LITERAL1(__FILE__)
#else
#define SLOG_CONFIG_FILE_SUFFIX ".log" // 日志文件后缀
typedef char Char;
typedef std::char_traits<Char>::int_type CharInt;
typedef std::streambuf StringBuf;
typedef std::filebuf FileBuffer;
typedef std::ostringstream OutputStream;
typedef std::ostream OutputStreamType;
typedef std::string StringType;
typedef std::string_view StringView;
typedef std::stringstream StringStream;
inline StringType numberToString(std::size_t n) {
return std::to_string(n);
}
#define SLOG_LITERAL(x) x
#define __SLOG_FILE__ __FILE__
#endif
namespace SimpleLogEncryption {
template<bool enableEncryption>
class XOREncryptionBuffer : public StringBuf {
public:
XOREncryptionBuffer(Char key) : encryptionKey(key) {}
protected:
// 日志后处理核心逻辑,如果需要定制化加密逻辑,增加压缩逻辑,修改这overflow和xsputn这两个函数即可。这里的XOR加密逻辑本质上是一个示例。
virtual CharInt overflow(CharInt ch) override {
if (!enableEncryption) {
return fileBuffer.sputc(ch);
}
else {
return fileBuffer.sputc(static_cast<Char>(ch) ^ encryptionKey);
}
return ch;
}
virtual std::streamsize xsputn(const Char* s, std::streamsize count) override {
if (!enableEncryption) {
return fileBuffer.sputn(s, count);
}
else {
std::basic_string<Char> temp;
temp.reserve(count);
for (std::streamsize i = 0; i < count; ++i) {
temp.push_back(s[i] ^ encryptionKey);
}
return fileBuffer.sputn(temp.data(), temp.size());
}
}
public:
FileBuffer fileBuffer;
private:
Char encryptionKey;
};
template<bool enableEncryption>
class LogOutStream : public OutputStreamType {
public:
LogOutStream()
: OutputStreamType(&streamBuf),
streamBuf(SLOG_CONFIG_ENCRYPTION_XOR_KEY) {}
bool is_open() {
return streamBuf.fileBuffer.is_open();
}
void close() {
streamBuf.fileBuffer.close();
}
int open(const StringType& FileName, int OpenFlag) {
return streamBuf.fileBuffer.open(FileName.c_str(), OpenFlag) != nullptr;
}
long long tellp() {
return streamBuf.fileBuffer.pubseekoff(0, std::ios_base::cur, std::ios_base::out);
}
private:
XOREncryptionBuffer<enableEncryption> streamBuf;
};
}
enum class LogLevel { INFO, WARNING, ERROR };
template <class CLogger>
class CLogStream {
public:
CLogStream(CLogger& logger, LogLevel level, const Char* file, int line)
: logger(logger) {
logger.onStartLogItem();
// 日志格式化核心逻辑,如果需要定制化格式化,可以修改这部分代码即可。
logger.stream() << logLevelToString(level) << std::this_thread::get_id();
printCurrentTime(logger.stream());
//logger.stream() << extractFilename(file).data() << SLOG_LITERAL("@") << line << SLOG_LITERAL(": ");
}
~CLogStream() {
logger.stream() << std::endl; // 换行
logger.onEndLogItem();
}
template<typename T>
CLogStream& operator<<(const T& msg) {
logger.stream() << msg;
return *this;
}
CLogStream(const CLogStream&) = delete;
CLogStream& operator=(const CLogStream&) = delete;
private:
inline StringView extractFilename(const StringView& path) const {
#ifdef _WIN32
auto path_separator = SLOG_LITERAL('\\');
#else
auto path_separator = SLOG_LITERAL('/');
#endif
size_t pos = path.find_last_of(path_separator);
if (pos != StringView::npos) {
return path.substr(pos + 1);
}
return std::move(path);
}
template<class StreamType>
inline void printCurrentTime(StreamType& stream) {
auto now = std::chrono::system_clock::now();
std::time_t now_time = std::chrono::system_clock::to_time_t(now);
std::tm now_tm = {};
#ifdef _WIN32
if (localtime_s(&now_tm, &now_time) == 0) {
#else
if (localtime_r(&now_time, &now_tm)) {
#endif // _WIN32
long long now_ms = std::chrono::time_point_cast<std::chrono::milliseconds>(now).time_since_epoch().count();
char buffer[32];
std::snprintf(buffer, sizeof(buffer), " %04d-%02d-%02d %02d:%02d:%02d.%03d\t",
now_tm.tm_year + 1900, now_tm.tm_mon + 1, now_tm.tm_mday,
now_tm.tm_hour, now_tm.tm_min, now_tm.tm_sec, (int)(now_ms % 1000));
stream << buffer;
}
else {
assert(false);
}
}
inline const char* logLevelToString(LogLevel level) const {
switch (level) {
case LogLevel::INFO:
return "INFO ";
case LogLevel::WARNING:
return "WARN ";
case LogLevel::ERROR:
return "ERRO ";
default:
return "UNKNOWN ";
}
}
private:
CLogger& logger;
};
template<bool enableEncryption = false, bool bSupportMulitThread = true>
class CSimpleLogger {
public:
CSimpleLogger(const StringType& filename, size_t maxSize = 20 * 1024 * 1024, size_t maxFiles = 4)
: log_file_prefix(filename),
max_file_size(maxSize),
max_files(maxFiles) {
initializeLoggerWithOutLock();
}
~CSimpleLogger() {
if (log_stream.is_open()) {
log_stream.close();
}
}
auto log(LogLevel level, const Char* file, int line) {
return CLogStream<CSimpleLogger>(*this, level, file, line);
}
void onStartLogItem() {
if (bSupportMulitThread) {
log_mutex.lock();
}
}
void onEndLogItem() {
checkAndRotateWithOutLock();
if (bSupportMulitThread) {
log_mutex.unlock();
}
}
auto& stream() {
return this->log_stream;
}
private:
SimpleLogEncryption::LogOutStream<enableEncryption> log_stream;
private:
std::mutex log_mutex;
StringType log_file_prefix;
size_t max_file_size;
size_t max_files;
void initializeLoggerWithOutLock() {
try {
if (log_stream.is_open()) {
log_stream.close();
}
log_stream.open(log_file_prefix + SLOG_CONFIG_FILE_SUFFIX, std::ios::app);
for (int i = 0; !log_stream.is_open(); ++i) {
log_stream.open(log_file_prefix + SLOG_LITERAL(".") + numberToString(i) + SLOG_LITERAL(".temp") + SLOG_CONFIG_FILE_SUFFIX, std::ios::app);
}
}
catch (...) {
assert(false);
}
}
void rotateLogsWithOutLock() {
try {
log_stream.close();
if (std::filesystem::exists(log_file_prefix + SLOG_LITERAL(".") + numberToString(max_files) + SLOG_CONFIG_FILE_SUFFIX)) {
std::filesystem::remove(log_file_prefix + SLOG_LITERAL(".") + numberToString(max_files) + SLOG_CONFIG_FILE_SUFFIX);
}
for (size_t i = max_files; i > 1; --i) {
auto src = log_file_prefix + SLOG_LITERAL(".") + numberToString(i - 1) + SLOG_CONFIG_FILE_SUFFIX;
auto dst = log_file_prefix + SLOG_LITERAL(".") + numberToString(i) + SLOG_CONFIG_FILE_SUFFIX;
if (std::filesystem::exists(src)) {
std::filesystem::rename(src, dst);
}
}
if (std::filesystem::exists(log_file_prefix + SLOG_CONFIG_FILE_SUFFIX)) {
std::filesystem::rename(log_file_prefix + SLOG_CONFIG_FILE_SUFFIX, log_file_prefix + SLOG_LITERAL(".1") + SLOG_CONFIG_FILE_SUFFIX);
}
}
catch (...) {
assert(false);
}
}
void checkAndRotateWithOutLock() {
if (log_stream.tellp() >= static_cast<std::streamoff>(max_file_size)) {
rotateLogsWithOutLock();
initializeLoggerWithOutLock();
}
}
};
//
// 线程队列版本的日志器
// 原理:使用生产者消费者模型,写入日志在另一个线程。写入日志的调用可以返回非常快,提高使用者性能。通过调节queuedSize可以获得一个内存和性能平衡的最佳参数。
template<bool enableEncryption = false, int queuedSize = 500>
class CQueuedLogger {
private:
template <typename T> class CProducerConsumer {
public:
CProducerConsumer(int max_capacity = 10) : capacity(max_capacity) {};
void produce(T value) {
std::unique_lock<std::mutex> lock(mtx);
while (buffer.size() == capacity) { // 如果缓冲区满了,则等待
cond_var.wait(lock);
}
buffer.push_back(std::move(value));
cond_var.notify_one(); // 通知消费者
}
void consume(std::vector<T> &out) {
std::unique_lock<std::mutex> lock(mtx);
while (buffer.empty()) { // 如果缓冲区为空,则等待
cond_var.wait(lock);
}
assert(out.empty());
std::swap(out, buffer);//直接把整个buffer都返回
cond_var.notify_one(); // 通知生产者
}
private:
std::vector<T> buffer;
const size_t capacity = 10; // 缓冲区容量
std::mutex mtx;
std::condition_variable cond_var;
};
public:
CQueuedLogger(const StringType& filename, size_t maxSize = 20 * 1024 * 1024, size_t maxFiles = 4) :
m_singleThreadLoger(filename, maxSize, maxFiles),
producerConsumer(queuedSize),
write_log_thread([this]() {this->logThreadProc(); }) {
}
~CQueuedLogger() {
need_exit_log_thread = true;
write_log_thread.join();
}
// 下面的public是Log接口,协约式编程 必须实现
inline auto log(LogLevel level, const Char* file, int line) {
return CLogStream<CQueuedLogger>(*this, level, file, line);
}
void onStartLogItem() {
}
void onEndLogItem() {
// 结果在stream里面
producerConsumer.produce(stream().str());
stream().str(StringType());//清空
}
auto& stream() {
static thread_local StringStream log_stream;
return log_stream;
}
private:
void logThreadProc() {
while (need_exit_log_thread == false) {
m_singleThreadLoger.onStartLogItem();
std::vector<StringType> logs;
producerConsumer.consume(logs);
for (auto it = logs.begin(); it != logs.end(); ++it) {
m_singleThreadLoger.stream() << *it;
//m_singleThreadLoger.onEndLogItem(); onEndLogItem应该要在这里调用,但是我们为了追求更高的性能,选择最后调用。
}
m_singleThreadLoger.onEndLogItem();
}
}
private:
CSimpleLogger<enableEncryption, false> m_singleThreadLoger;
CProducerConsumer<StringType> producerConsumer;
std::thread write_log_thread;
bool need_exit_log_thread = false;
};
}
// 宏用于自动添加文件名和行号
#define SIMPLE_LOG_INFO(logger) logger.log(LogLevel::INFO, __SLOG_FILE__, __LINE__)
#define SIMPLE_LOG_WARNING(logger) logger.log(LogLevel::WARNING, __SLOG_FILE__, __LINE__)
#define SIMPLE_LOG_ERROR(logger) logger.log(LogLevel::ERROR, __SLOG_FILE__, __LINE__)
#if (__cplusplus >= 202002L || _MSVC_LANG >= 202002L)
#include <format>
// 使用 std::format 实现格式化日志记录
#define SIMPLE_LOG_INFO_FMT(logger, fmt, ...) SIMPLE_LOG_INFO(logger) << std::format(fmt, __VA_ARGS__)
#define SIMPLE_LOG_WARNING_FMT(logger, fmt, ...) SIMPLE_LOG_WARNING(logger) << std::format(fmt, __VA_ARGS__)
#define SIMPLE_LOG_ERROR_FMT(logger, fmt, ...) SIMPLE_LOG_ERROR(logger) << std::format(fmt, __VA_ARGS__)
#endif
//
using namespace SLog;
void logMessages(CSimpleLogger<false, true>& logger, int thread_id, int message_count) {
for (int i = 0; i < message_count / 3; ++i) {
SIMPLE_LOG_INFO(logger) << SLOG_LITERAL("Thread ") << thread_id << SLOG_LITERAL(" logging message ") << i;
SIMPLE_LOG_WARNING(logger) << SLOG_LITERAL("Test Warning") << i;
SIMPLE_LOG_ERROR(logger) << SLOG_LITERAL("Test Error") << i;
#if (__cplusplus >= 202002L || _MSVC_LANG >= 202002L)
SIMPLE_LOG_INFO_FMT(logger, SLOG_LITERAL("fmt log: {}"), i); // c++ 20 支持
#endif
}
std::cout << std::chrono::system_clock::now() << " ## logMessages threadid:" << thread_id << " Done!\n";
}
void logMessagesQueued(CQueuedLogger<false>& logger, int thread_id, int message_count) {
for (int i = 0; i < message_count / 3; ++i) {
SIMPLE_LOG_INFO(logger) << SLOG_LITERAL("Thread ") << thread_id << SLOG_LITERAL(" logging message ") << i;
SIMPLE_LOG_WARNING(logger) << SLOG_LITERAL("Test Warning") << i;
SIMPLE_LOG_ERROR(logger) << SLOG_LITERAL("Test Error") << i;
#if (__cplusplus >= 202002L || _MSVC_LANG >= 202002L)
SIMPLE_LOG_INFO_FMT(logger, SLOG_LITERAL("fmt log: {}"), i); // c++ 20 支持
#endif
}
std::cout << std::chrono::system_clock::now() << " @@ logMessagesQueued threadid:" << thread_id << " Done!\n";
}
int main() {
try {
CSimpleLogger<false> logger(SLOG_LITERAL("my_log"));
CQueuedLogger<false> queuedLogger(SLOG_LITERAL("queued_log"));
const int num_threads = 20;
const int messages_per_thread = 100000;
std::cout << "Start test at:" << std::chrono::system_clock::now() << "use " << num_threads << " thread ,each write " << messages_per_thread << " logs" << std::endl;
std::vector<std::thread> threads;
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back(logMessages, std::ref(logger), i, messages_per_thread);
threads.emplace_back(logMessagesQueued, std::ref(queuedLogger), i, messages_per_thread);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Logging complete. Check log.txt for results." << std::endl;
}
catch (const std::exception& e) {
std::cerr << "Logging failed: " << e.what() << std::endl;
}
return 0;
}