这个日志库的重构并没有完全参考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) { if (log_file_.is_open()) { log_file_.flush(); }
std::this_thread::sleep_for(std::chrono::milliseconds(20));
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; }
|


当前日志无法自己设置日志文件名称,但留了设计余地,后续可以加设置名字逻辑。
源码地址
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