好的,各位观众老爷们,大家好!今天咱们来聊聊一个让程序员们又爱又恨的话题——日志!
都说程序猿三大定律:Debug、Bug Fix、日志!日志这玩意儿,平时你可能觉得它可有可无,但真到了线上出问题的时候,它就是救命稻草!但是,默认的日志框架,要么性能不行,要么定制性差,搞得我们抓耳挠腮。
所以,今天我就来教大家如何打造一个高性能、低延迟、高度定制的C++日志系统,让你在关键时刻不再掉链子!
第一章:磨刀不误砍柴工:需求分析与设计
咱们不能一上来就撸代码,得先想清楚我们要干啥。一个优秀的日志系统,起码要满足以下几点:
- 高性能: 不能因为写日志拖慢程序的运行速度,尤其是在高并发场景下。
- 低延迟: 日志要及时记录,不能等到问题都发生了才姗姗来迟。
- 可配置性: 能灵活配置日志级别、输出格式、输出目标等等。
- 易用性: 用起来要简单方便,不能增加程序员的学习成本。
- 扩展性: 方便后续添加新的功能,比如支持更多的输出目标,或者更高级的过滤规则。
- 线程安全: 必须保证在多线程环境下正常工作,避免数据竞争和死锁。
针对这些需求,咱们可以设计一个模块化的架构:
- 日志器 (Logger): 负责接收日志消息,并将其传递给后续模块。
- 格式化器 (Formatter): 负责将日志消息格式化成字符串。
- 输出器 (Appender): 负责将格式化后的日志字符串输出到不同的目标,比如控制台、文件、网络等等。
- 过滤器 (Filter): 负责根据一定的规则过滤日志消息,只输出符合条件的日志。
可以用一个表格来总结一下:
模块 | 职责 |
---|---|
Logger | 接收日志消息,并传递给后续模块。 |
Formatter | 将日志消息格式化成字符串。 |
Appender | 将格式化后的日志字符串输出到不同的目标(控制台、文件、网络等)。 |
Filter | 根据规则过滤日志消息,只输出符合条件的日志。 |
第二章:工欲善其事:基础类与工具类
在开始编写核心模块之前,我们需要一些基础类和工具类来打好地基。
- 日志级别枚举:
enum class LogLevel {
DEBUG,
INFO,
WARN,
ERROR,
FATAL
};
std::ostream& operator<<(std::ostream& os, LogLevel level) {
switch (level) {
case LogLevel::DEBUG: os << "DEBUG"; break;
case LogLevel::INFO: os << "INFO"; break;
case LogLevel::WARN: os << "WARN"; break;
case LogLevel::ERROR: os << "ERROR"; break;
case LogLevel::FATAL: os << "FATAL"; break;
default: os << "UNKNOWN"; break;
}
return os;
}
- 日志事件类:
#include <chrono>
#include <string>
#include <thread>
#include <sstream>
class LogEvent {
public:
LogEvent(LogLevel level, const std::string& filename, int line, const std::string& message)
: level_(level), filename_(filename), line_(line), message_(message), timestamp_(std::chrono::system_clock::now()) {}
LogLevel getLevel() const { return level_; }
const std::string& getFilename() const { return filename_; }
int getLine() const { return line_; }
const std::string& getMessage() const { return message_; }
std::chrono::system_clock::time_point getTimestamp() const { return timestamp_; }
// 获取当前线程ID的字符串表示
std::string getThreadId() const {
std::stringstream ss;
ss << std::this_thread::get_id();
return ss.str();
}
private:
LogLevel level_;
std::string filename_;
int line_;
std::string message_;
std::chrono::system_clock::time_point timestamp_;
};
- 线程安全的单例模式: (避免多个Logger实例造成的混乱)
#include <mutex>
template <typename T>
class Singleton {
public:
static T& getInstance() {
std::call_once(onceFlag, []() { instance.reset(new T()); });
return *instance;
}
private:
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::unique_ptr<T> instance;
static std::once_flag onceFlag;
};
template <typename T>
std::unique_ptr<T> Singleton<T>::instance = nullptr;
template <typename T>
std::once_flag Singleton<T>::onceFlag;
第三章:核心模块:Logger、Formatter、Appender
- Logger 类:
#include <vector>
class Logger {
public:
using AppenderPtr = std::shared_ptr<class Appender>; // Forward declaration
using FormatterPtr = std::shared_ptr<class Formatter>; // Forward declaration
Logger() : level_(LogLevel::INFO) {}
void log(LogLevel level, const std::string& filename, int line, const std::string& message) {
if (level >= level_) {
LogEvent event(level, filename, line, message);
for (auto& appender : appenders_) {
appender->log(event);
}
}
}
void addAppender(AppenderPtr appender) {
appenders_.push_back(appender);
}
void setLevel(LogLevel level) {
level_ = level;
}
LogLevel getLevel() const {
return level_;
}
void setFormatter(FormatterPtr formatter) {
formatter_ = formatter;
for (auto& appender : appenders_) {
appender->setFormatter(formatter_);
}
}
private:
LogLevel level_;
std::vector<AppenderPtr> appenders_;
FormatterPtr formatter_;
};
// Helper macros
#define LOG_DEBUG(logger, msg) logger.log(LogLevel::DEBUG, __FILE__, __LINE__, msg)
#define LOG_INFO(logger, msg) logger.log(LogLevel::INFO, __FILE__, __LINE__, msg)
#define LOG_WARN(logger, msg) logger.log(LogLevel::WARN, __FILE__, __LINE__, msg)
#define LOG_ERROR(logger, msg) logger.log(LogLevel::ERROR, __FILE__, __LINE__, msg)
#define LOG_FATAL(logger, msg) logger.log(LogLevel::FATAL, __FILE__, __LINE__, msg)
- Formatter 类:
#include <sstream>
#include <iomanip>
class Formatter {
public:
virtual std::string format(const LogEvent& event) {
std::stringstream ss;
auto timestamp = event.getTimestamp();
auto time_t_timestamp = std::chrono::system_clock::to_time_t(timestamp);
std::tm tm_timestamp;
#ifdef _MSC_VER // For Visual Studio
localtime_s(&tm_timestamp, &time_t_timestamp);
#else // For other compilers (e.g., GCC, Clang)
localtime_r(&time_t_timestamp, &tm_timestamp);
#endif
ss << std::put_time(&tm_timestamp, "%Y-%m-%d %H:%M:%S"); // Format timestamp
ss << " [" << event.getLevel() << "] "; // Log level
ss << "[" << event.getThreadId() << "] "; // Thread ID
ss << event.getFilename() << ":" << event.getLine() << " - "; // File and line
ss << event.getMessage(); // Message
return ss.str();
}
virtual ~Formatter() = default;
};
- Appender 类:
#include <iostream>
#include <fstream>
#include <mutex>
class Appender {
public:
using FormatterPtr = std::shared_ptr<Formatter>;
virtual void log(const LogEvent& event) {
std::lock_guard<std::mutex> lock(mutex_); // 线程安全
std::string formattedMessage = formatter_->format(event);
append(formattedMessage);
}
void setFormatter(FormatterPtr formatter) {
std::lock_guard<std::mutex> lock(mutex_);
formatter_ = formatter;
}
virtual ~Appender() = default;
protected:
virtual void append(const std::string& formattedMessage) = 0;
FormatterPtr formatter_;
std::mutex mutex_; // 用于线程安全的互斥锁
};
// Console Appender
class ConsoleAppender : public Appender {
protected:
void append(const std::string& formattedMessage) override {
std::cout << formattedMessage << std::endl;
}
};
// File Appender
class FileAppender : public Appender {
public:
FileAppender(const std::string& filename) : filename_(filename) {
file_.open(filename_, std::ios::app); // Open file in append mode
if (!file_.is_open()) {
std::cerr << "Error opening file: " << filename_ << std::endl;
}
}
~FileAppender() override {
if (file_.is_open()) {
file_.close();
}
}
protected:
void append(const std::string& formattedMessage) override {
if (file_.is_open()) {
file_ << formattedMessage << std::endl;
file_.flush(); // Flush buffer to ensure immediate write
}
}
private:
std::string filename_;
std::ofstream file_;
};
第四章:锦上添花:Filter(可选)
如果你需要更精细的日志控制,可以添加Filter模块。
#include <regex>
class Filter {
public:
virtual bool filter(const LogEvent& event) {
return true; // 默认不进行过滤
}
virtual ~Filter() = default;
};
// 基于正则表达式的过滤器
class RegexFilter : public Filter {
public:
RegexFilter(const std::string& pattern, bool matchMessage = true) : pattern_(pattern), matchMessage_(matchMessage) {}
bool filter(const LogEvent& event) override {
if (matchMessage_) {
std::regex re(pattern_);
return std::regex_search(event.getMessage(), re);
} else {
// 可以根据文件名、日志级别等进行过滤
return true;
}
}
private:
std::string pattern_;
bool matchMessage_;
};
要使用Filter,需要在Logger中添加Filter列表,并在log
方法中进行过滤。
// Logger class (updated)
class Logger {
public:
using AppenderPtr = std::shared_ptr<class Appender>;
using FormatterPtr = std::shared_ptr<class Formatter>;
using FilterPtr = std::shared_ptr<class Filter>;
Logger() : level_(LogLevel::INFO) {}
void log(LogLevel level, const std::string& filename, int line, const std::string& message) {
if (level >= level_) {
LogEvent event(level, filename, line, message);
bool shouldLog = true;
for (auto& filter : filters_) {
if (!filter->filter(event)) {
shouldLog = false;
break;
}
}
if (shouldLog) {
for (auto& appender : appenders_) {
appender->log(event);
}
}
}
}
void addAppender(AppenderPtr appender) {
appenders_.push_back(appender);
}
void addFilter(FilterPtr filter) {
filters_.push_back(filter);
}
// ... (其他方法)
private:
LogLevel level_;
std::vector<AppenderPtr> appenders_;
std::vector<FilterPtr> filters_;
FormatterPtr formatter_;
};
第五章:性能优化:异步日志与缓冲
在高并发场景下,同步写日志可能会成为性能瓶颈。为了解决这个问题,我们可以采用异步日志的方式,将日志消息先放入一个缓冲区,然后由一个单独的线程负责将缓冲区中的日志写入到目标。
#include <queue>
#include <condition_variable>
class AsyncAppender : public Appender {
public:
AsyncAppender(std::unique_ptr<Appender> appender) : appender_(std::move(appender)), running_(true) {
worker_thread_ = std::thread([this]() {
while (running_) {
std::unique_lock<std::mutex> lock(mutex_);
condition_.wait(lock, [this]() { return !queue_.empty() || !running_; });
if (!queue_.empty()) {
std::string message = queue_.front();
queue_.pop();
lock.unlock(); // Unlock before writing to appender
appender_->append(message);
}
}
});
}
~AsyncAppender() override {
{
std::lock_guard<std::mutex> lock(mutex_);
running_ = false;
}
condition_.notify_one();
worker_thread_.join();
}
protected:
void append(const std::string& formattedMessage) override {
{
std::lock_guard<std::mutex> lock(mutex_);
queue_.push(formattedMessage);
}
condition_.notify_one();
}
private:
std::unique_ptr<Appender> appender_;
std::queue<std::string> queue_;
std::mutex mutex_;
std::condition_variable condition_;
std::thread worker_thread_;
bool running_;
};
第六章:使用示例:让日志飞起来
int main() {
// 获取 Logger 实例
Logger& logger = Singleton<Logger>::getInstance();
// 创建 Formatter
auto formatter = std::make_shared<Formatter>();
// 创建 Appender (Console)
auto consoleAppender = std::make_shared<ConsoleAppender>();
consoleAppender->setFormatter(formatter);
// 创建 Appender (File)
auto fileAppender = std::make_shared<FileAppender>("my_log.txt");
fileAppender->setFormatter(formatter);
// 将 Appender 添加到 Logger
logger.addAppender(consoleAppender);
logger.addAppender(fileAppender);
// 创建异步Appender,将FileAppender放到异步Appender中
auto asyncFileAppender = std::make_unique<AsyncAppender>(std::make_unique<FileAppender>("my_async_log.txt"));
asyncFileAppender->setFormatter(formatter);
logger.addAppender(std::shared_ptr<AsyncAppender>(asyncFileAppender.release()));
// 设置日志级别
logger.setLevel(LogLevel::DEBUG);
// 记录日志
LOG_DEBUG(logger, "This is a debug message.");
LOG_INFO(logger, "This is an info message.");
LOG_WARN(logger, "This is a warning message.");
LOG_ERROR(logger, "This is an error message.");
LOG_FATAL(logger, "This is a fatal message.");
// 使用循环模拟高并发
for (int i = 0; i < 1000; ++i) {
LOG_INFO(logger, "Loop iteration: " + std::to_string(i));
}
return 0;
}
第七章:总结与展望
今天我们一起打造了一个高性能、低延迟、高度定制的C++日志系统。 当然,这只是一个基础框架,你还可以根据自己的需求进行扩展,比如:
- 支持更多的输出目标,比如数据库、网络等等。
- 支持更高级的过滤规则,比如基于时间、线程等等。
- 支持自定义的日志格式,比如JSON、XML等等。
- 集成到现有的项目中,并进行性能测试。
- 使用第三方库,如spdlog, glog等。
记住,日志系统不是一蹴而就的,需要不断地迭代和优化。希望今天的分享能对你有所帮助,让你在日志的世界里自由翱翔!
感谢各位观众老爷的观看,咱们下期再见!