1. 项目概述
在Linux系统开发中,日志系统是每个开发者都绕不开的基础设施。作为一个在Linux环境下摸爬滚打多年的老手,我深知一个设计良好的日志系统对项目维护有多重要。今天就来分享如何从零开始实现一个轻量级但功能完备的日志系统。
这个日志系统将具备以下核心特性:
- 支持多级日志(DEBUG/INFO/WARNING等)
- 线程安全的日志输出
- 灵活的日志输出策略(控制台/文件)
- 完整的日志格式(时间戳、文件行号、进程ID等)
- 基于C++17标准实现
2. 核心设计思路
2.1 为什么选择策略模式
策略模式(Strategy Pattern)在这里特别适用,因为它允许我们在运行时动态改变日志的输出方式。想象一下,在开发阶段你可能希望日志输出到控制台方便调试,而在生产环境则需要写入文件。策略模式让我们可以灵活切换这两种方式,而不需要修改核心日志逻辑。
2.2 日志系统架构设计
整个系统分为三个核心组件:
- 日志格式化器:负责将日志信息格式化为标准字符串
- 日志策略:定义日志输出的抽象接口
- 日志核心:协调前两者,提供用户接口
这种分层设计遵循了单一职责原则,每个组件只做一件事,但要做好。
3. 基础组件实现
3.1 线程安全保证
在多线程环境下,日志系统必须是线程安全的。我们使用pthread_mutex_t封装一个简单的互斥锁:
cpp复制class Mutex {
public:
Mutex() { pthread_mutex_init(&mutex, nullptr); }
~Mutex() { pthread_mutex_destroy(&mutex); }
void Lock() { pthread_mutex_lock(&mutex); }
void Unlock() { pthread_mutex_unlock(&mutex); }
private:
pthread_mutex_t mutex;
};
为了简化锁的使用,再实现一个RAII风格的锁守卫:
cpp复制class LockGuard {
public:
LockGuard(Mutex& mutex) : _mutex(mutex) { _mutex.Lock(); }
~LockGuard() { _mutex.Unlock(); }
private:
Mutex& _mutex;
};
注意:锁对象不可拷贝,这里使用引用确保线程安全
3.2 日志策略接口设计
定义抽象的日志策略接口:
cpp复制class LogStrategy {
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string& message) = 0;
};
4. 具体策略实现
4.1 控制台输出策略
cpp复制class ConsoleLogStrategy : public LogStrategy {
public:
void SyncLog(const std::string& message) override {
LockGuard lock(_mutex);
std::cout << message << "\r\n";
std::cout.flush(); // 确保及时输出
}
private:
Mutex _mutex;
};
4.2 文件输出策略
文件输出策略需要考虑更多细节:
cpp复制class FileLogStrategy : public LogStrategy {
public:
FileLogStrategy(const std::string& path = "./log",
const std::string& file = "app.log")
: _path(path), _file(file) {
// 确保目录存在
if (!std::filesystem::exists(_path)) {
std::filesystem::create_directories(_path);
}
}
void SyncLog(const std::string& message) override {
LockGuard lock(_mutex);
std::string fullPath = _path + "/" + _file;
std::ofstream out(fullPath, std::ios::app);
if (!out.is_open()) {
throw std::runtime_error("Failed to open log file");
}
out << message << "\r\n";
}
private:
std::string _path;
std::string _file;
Mutex _mutex;
};
提示:文件路径处理时要注意跨平台兼容性,Linux使用正斜杠(/)
5. 日志核心实现
5.1 日志级别定义
cpp复制enum class LogLevel {
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
const char* LevelToString(LogLevel level) {
static const char* levels[] = {
"DEBUG", "INFO", "WARNING", "ERROR", "FATAL"
};
return levels[static_cast<int>(level)];
}
5.2 日志记录器核心
cpp复制class Logger {
public:
Logger() : _strategy(std::make_unique<ConsoleLogStrategy>()) {}
void SetStrategy(std::unique_ptr<LogStrategy> strategy) {
LockGuard lock(_mutex);
_strategy = std::move(strategy);
}
void Log(LogLevel level, const std::string& file, int line, const std::string& message) {
std::ostringstream oss;
oss << "[" << GetCurrentTime() << "] "
<< "[" << LevelToString(level) << "] "
<< "[" << getpid() << "] "
<< "[" << file << "] "
<< "[" << line << "] - "
<< message;
LockGuard lock(_mutex);
_strategy->SyncLog(oss.str());
}
private:
std::string GetCurrentTime() {
auto now = std::chrono::system_clock::now();
auto in_time_t = std::chrono::system_clock::to_time_t(now);
std::tm tm;
localtime_r(&in_time_t, &tm);
char buffer[80];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tm);
return buffer;
}
std::unique_ptr<LogStrategy> _strategy;
Mutex _mutex;
};
6. 使用示例与高级功能
6.1 基本使用
cpp复制Logger logger;
// 输出到控制台
logger.Log(LogLevel::DEBUG, __FILE__, __LINE__, "This is a debug message");
// 切换到文件输出
logger.SetStrategy(std::make_unique<FileLogStrategy>("/var/log", "myapp.log"));
logger.Log(LogLevel::INFO, __FILE__, __LINE__, "Switched to file logging");
6.2 宏定义简化调用
为了更方便使用,可以定义一些宏:
cpp复制#define LOG_DEBUG(message) logger.Log(LogLevel::DEBUG, __FILE__, __LINE__, message)
#define LOG_INFO(message) logger.Log(LogLevel::INFO, __FILE__, __LINE__, message)
#define LOG_WARNING(message) logger.Log(LogLevel::WARNING, __FILE__, __LINE__, message)
#define LOG_ERROR(message) logger.Log(LogLevel::ERROR, __FILE__, __LINE__, message)
这样使用时只需:
cpp复制LOG_DEBUG("User logged in");
LOG_ERROR("Failed to connect to database");
7. 性能优化与注意事项
7.1 日志性能瓶颈
日志系统常见的性能问题主要来自:
- 锁竞争
- 频繁的I/O操作
- 字符串处理开销
优化建议:
- 使用双缓冲技术减少锁竞争
- 批量写入减少I/O次数
- 预分配字符串缓冲区
7.2 线程安全陷阱
即使使用了锁,也要注意:
- 避免在日志回调中再次记录日志(死锁风险)
- 确保所有策略实现都是线程安全的
- 考虑使用无锁队列实现异步日志
7.3 日志轮转策略
生产环境还需要考虑日志轮转:
- 按大小轮转(如单个文件超过100MB)
- 按时间轮转(如每天一个新文件)
- 保留历史日志数量限制
可以通过继承FileLogStrategy实现这些功能。
8. 扩展与进阶
8.1 网络日志输出
可以扩展实现网络日志策略:
cpp复制class NetworkLogStrategy : public LogStrategy {
public:
NetworkLogStrategy(const std::string& host, int port)
: _host(host), _port(port) {
// 初始化网络连接
}
void SyncLog(const std::string& message) override {
// 发送日志到网络
}
private:
std::string _host;
int _port;
Mutex _mutex;
};
8.2 日志过滤功能
添加日志级别过滤:
cpp复制void Logger::SetMinLevel(LogLevel level) {
_minLevel = level;
}
bool Logger::ShouldLog(LogLevel level) const {
return static_cast<int>(level) >= static_cast<int>(_minLevel);
}
8.3 异步日志实现
使用生产者-消费者模型实现异步日志:
cpp复制class AsyncLogStrategy : public LogStrategy {
public:
AsyncLogStrategy(std::unique_ptr<LogStrategy> realStrategy)
: _realStrategy(std::move(realStrategy)), _running(true) {
_worker = std::thread(&AsyncLogStrategy::ProcessQueue, this);
}
~AsyncLogStrategy() {
_running = false;
_cond.notify_all();
if (_worker.joinable()) _worker.join();
}
void SyncLog(const std::string& message) override {
{
LockGuard lock(_mutex);
_queue.push(message);
}
_cond.notify_one();
}
private:
void ProcessQueue() {
while (_running) {
std::unique_lock<Mutex> lock(_mutex);
_cond.wait(lock, [this] { return !_queue.empty() || !_running; });
while (!_queue.empty()) {
auto msg = _queue.front();
_queue.pop();
lock.unlock();
_realStrategy->SyncLog(msg);
lock.lock();
}
}
}
std::queue<std::string> _queue;
std::unique_ptr<LogStrategy> _realStrategy;
std::thread _worker;
std::atomic<bool> _running;
Mutex _mutex;
std::condition_variable_any _cond;
};
9. 测试与验证
9.1 单元测试要点
需要重点测试:
- 多线程下的日志顺序
- 文件策略的路径处理
- 日志级别的正确过滤
- 异常情况下的行为(如磁盘满)
9.2 性能测试
使用以下方法评估性能:
cpp复制auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000; ++i) {
LOG_DEBUG("Test message " + std::to_string(i));
}
auto end = std::chrono::high_resolution_clock::now();
记录不同策略下的耗时,评估是否满足项目需求。
10. 部署建议
在生产环境部署时建议:
- 使用异步文件日志策略
- 设置合理的日志级别(通常INFO或WARNING)
- 配置日志轮转策略
- 监控日志文件大小和磁盘空间
- 考虑使用syslog集成
对于高并发场景,可以考虑将日志发送到专门的日志收集系统如ELK栈。
