这个日志库的重构并没有完全参考muduo里的日志库,只是基于muduo里日志库的一些特点实现的一个简易日志库。

该日志库适配muduo的特点

  • 异步模型,避免日志写入阻塞业务线程;
  • 全局单例,整个程序只有一个日志实例,避免多实例文件写入冲突
  • 内部用线程安全的阻塞队列缓存日志,保证多线程生产日志的线程安全

noncopyable.h

1
2
3
4
5
6
7
8
9
10
11
#pragma once

class noncopyable
{
public:
noncopyable(const noncopyable&) = delete;
noncopyable& operator=(const noncopyable&) = delete;
protected:
noncopyable() = default;
~noncopyable() = default;
};

禁止类的拷贝 / 赋值,保证单例唯一性。

AssistFunc.h

1
2
3
4
5
6
7
template<typename T>
std::string to_string_func(T&& arg)
{
std::ostringstream oss;
oss << std::forward<T>(arg);
return oss.str();
}

万能引用+完美转发实现复杂类型的拼接。

LogQueue

封装日志队列,线程安全的阻塞队列

LogQueue.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#pragma once

#include <queue>
#include <mutex>
#include <condition_variable>

class LogQueue
{
public:
LogQueue();

void push(const std::string& msg);
bool pop(std::string& msg);
void shutdown();

private:
std::queue<std::string> queue_;
std::mutex mutex_;
std::condition_variable cond_;
bool is_shutdown_;
};

LogQueue.cc

1
2
3
4
5
6
7
8
#include "LogQueue.h"

#include <string>

LogQueue::LogQueue() : is_shutdown_(false)
{

}

构造函数,将关闭标识符置false。

1
2
3
4
5
6
void LogQueue::push(const std::string& msg)
{
std::lock_guard<std::mutex> lock(mutex_);
queue_.push(msg);
cond_.notify_one();
}

业务线程打日志时,把日志消息推入队列。防止多个业务线程同时 push 导致队列数据错乱,所以要加锁。push 后通过条件变量通知阻塞的消费线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool LogQueue::pop(std::string& msg)
{
std::unique_lock<std::mutex> lock(mutex_);

cond_.wait(lock, [this](){
return !queue_.empty() || is_shutdown_;
});

if(is_shutdown_ && queue_.empty())
{
return false;
}

msg = queue_.front();
queue_.pop();
return true;
}

消费线程(worker_thread_)循环调用 pop 取日志消息,写入文件,队列空时,消费线程挂起(不占用 CPU),直到有新消息或队列关闭,cond_.wait这里用lambda做“条件判断”,避免虚假唤醒,也可用while循环判断。

if(is_shutdown_ && queue_.empty()){ return false; },在关闭前把日志消息全取出。

1
2
3
4
5
6
void LogQueue::shutdown()
{
std::lock_guard<std::mutex> lock(mutex_);
is_shutdown_ = true;
cond_.notify_all();
}

Logger 析构时调用,通知队列 停止工作,唤醒所有阻塞的消费线程(避免线程卡死在 cond_.wait())。

Logger

日志类

Logger.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#pragma once 

#include "noncopyable.h"
#include "LogQueue.h"
#include "AssistFunc.h"

#include <atomic>
#include <thread>
#include <fstream>
#include <vector>
#include <sstream>
#include <cstdlib>

enum class LogLevel
{
INFO,
DEBUG,
WARN,
ERROR,
FATAL
};

定义日志级别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Logger : private noncopyable
{
public:

Logger(const std::string& fileName = "default.log");
~Logger();

static Logger& getInstance();

//打日志
template<typename... Args>
void log(LogLevel level, const std::string& format, Args&&... args);


private:
std::string getCurrentTime();

template<typename... Args>
std::string formatMessage(const std::string& format, Args&&... args);

LogQueue log_queue_;
std::mutex log_file_mutex;
std::ofstream log_file_;
std::thread worker_thread_;
std::atomic<bool> exit_flag_;
};
  • 构造函数提供默认参数,可当默认构造函数,适配单例模板。
  • log配合formatMessage拼接出日志。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
template<typename... Args>
void Logger::log(LogLevel level, const std::string& format, Args&&... args)
{
std::string level_str;
switch(level)
{
case LogLevel::INFO:
level_str = "[INFO] ";
break;
case LogLevel::DEBUG:
level_str = "[DEBUG] ";
break;
case LogLevel::WARN:
level_str = "[WARN] ";
break;
case LogLevel::ERROR:
level_str = "[ERROR] ";
break;
case LogLevel::FATAL:
level_str = "[FATAL] ";
break;
}
log_queue_.push(level_str + formatMessage(format, std::forward<Args>(args)...));

if (level == LogLevel::FATAL)
{
// 1. 强制刷新文件
if (log_file_.is_open())
{
log_file_.flush();
}

// 2. 给日志线程一点时间,把队列里所有日志写完
std::this_thread::sleep_for(std::chrono::milliseconds(20));

// 3. 退出进程
exit(1);
}
}
  • 模板函数的实现最好在头文件中。

  • 用变长参数模板结合完美转发实现。

  • FATAL日志直接退出。

  • DEBUG级别可以设置日志开关,这里没有实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
template<typename... Args>
std::string Logger::formatMessage(const std::string& format, Args&&... args)
{
std::vector<std::string> arg_strings = {to_string_func(std::forward<Args>(args))...};
std::ostringstream oss;
size_t pos = 0;
size_t arg_index = 0;
size_t placeholder = format.find("{}", pos);

while(placeholder != std::string::npos)
{
oss << format.substr(pos, placeholder - pos); // 拼接占位符前的文本
if(arg_index < arg_strings.size())
{
oss << arg_strings[arg_index++];// 替换占位符为参数
}
else
{
oss << "{}";
}
pos = placeholder + 2;
placeholder = format.find("{}", pos);

}

oss << format.substr(pos);
while(arg_index < arg_strings.size())
{
oss << arg_strings[arg_index++];
}

return "[" + getCurrentTime() + "] " + oss.str();
}
  • 核心思路:遍历格式字符串,逐个替换 {} 为对应的参数,处理逻辑:

​ 例:format="a={}, b={}" + 参数 123, "test" → 先拼 "a=" → 替换 {}"123" → 拼 ", b=" → 替换 {}"test"

  • 容错设计

    参数数量 < 占位符数量时,保留 {}(比如 format="a={},b={}" 只传 1 个参数 → "a=123,b={}"),避免程序崩溃;

    参数数量 > 占位符数量时,把多余的参数追加到日志末尾(比如 format="a={}" + 参数 123, "test""a=123test");

1
2
3
4
5
#define LOG_INFO(fmt, ...)  Logger::getInstance().log(LogLevel::INFO, fmt, ##__VA_ARGS__)
#define LOG_DEBUG(fmt, ...) Logger::getInstance().log(LogLevel::DEBUG, fmt, ##__VA_ARGS__)
#define LOG_WARN(fmt, ...) Logger::getInstance().log(LogLevel::WARN, fmt, ##__VA_ARGS__)
#define LOG_ERROR(fmt, ...) Logger::getInstance().log(LogLevel::ERROR, fmt, ##__VA_ARGS__)
#define LOG_FATAL(fmt, ...) Logger::getInstance().log(LogLevel::FATAL, fmt, ##__VA_ARGS__)

把复杂的模板函数调用简化为极简的宏调用,对外暴露的核心易用性接口。

Logger.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Logger::Logger(const std::string& fileName)
: log_file_(fileName, std::ios::out | std::ios::app)
, exit_flag_(false)
{
if(!log_file_.is_open())
{
throw std::runtime_error("Failed to open log file");
}

worker_thread_ = std::thread([this](){
std::string msg;
while(log_queue_.pop(msg))
{
// 加锁保证文件写入和析构的线程安全
std::lock_guard<std::mutex> lock(log_file_mutex);
if (log_file_.is_open())
{
log_file_ << msg << std::endl;
log_file_.flush(); // 强制刷盘,避免日志丢失
}
// 控制台输出
std::cout << msg << std::endl;
}
// 线程退出前最后刷盘
std::lock_guard<std::mutex> lock(log_file_mutex);
if (log_file_.is_open())
{
log_file_.flush();
}
});
}

构造函数实现

  • std::ios::app以追加模式打开日志,防止历史日志被覆盖。
  • 用lambda表达式启动工作线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Logger::~Logger()
{
exit_flag_ = true;
log_queue_.shutdown();

if(worker_thread_.joinable())
{
worker_thread_.join();
}

if(log_file_.is_open())
{
log_file_.close();
}
}

析构函数,非常值得学习的实现。

join() 等待工作线程结束,确保队列中所有日志都被写入文件后,主线程才退出 —— 如果直接析构不 join,工作线程可能被强制终止,导致日志丢失。

1
2
3
4
Logger& Logger::getInstance() {
static Logger instance;
return instance;
}

C++11后线程安全的单例实现。

1
2
3
std::string Logger::getCurrentTime() {
return Timestamp::now().toString();
}

结合时间戳实现当前时间的格式化获取。

测试代码

1
2
3
4
5
6
7
#include "Logger.h"

int main()
{
LOG_INFO("arg1:{}, arg2:{}, arg3:{}", 'A', 123, "lpz");
return 0;
}

Logger1

Logger2

当前日志无法自己设置日志文件名称,但留了设计余地,后续可以加设置名字逻辑。

源码地址

AssistFunc.h:https://gitee.com/lpzdinghai/lpzmuduo/blob/master/AssistFunc.h

LogQueue.h:https://gitee.com/lpzdinghai/lpzmuduo/blob/master/LogQueue.h

LogQueue.cc:https://gitee.com/lpzdinghai/lpzmuduo/blob/master/LogQueue.cc

Logger.h:https://gitee.com/lpzdinghai/lpzmuduo/blob/master/Logger.h

Logger.cc:https://gitee.com/lpzdinghai/lpzmuduo/blob/master/Logger.cc