1. 项目概述
日志系统是每个程序员都绕不开的基础设施。我在开发分布式系统时,经常遇到日志混乱、难以排查的问题。市面上的日志库虽然功能强大,但要么太重,要么不够灵活。于是决定从零开始,用C++实现一个轻量级、可扩展的日志系统。
这个日志系统的核心特点是:
- 采用策略模式实现灵活的日志输出方式(控制台/文件)
- 线程安全,支持多线程并发写入
- 符合标准日志格式,包含时间戳、日志级别、进程ID等关键信息
- 支持自动创建日志目录和文件
2. 核心设计思路
2.1 策略模式的应用
策略模式的核心思想是将算法(在这里是日志输出方式)封装成独立的类,使它们可以相互替换。这种设计有三大优势:
- 解耦:日志内容生成与输出方式分离
- 扩展性:新增输出方式只需添加新策略类
- 运行时切换:可以根据需要动态改变输出目标
在我们的实现中,LogStrategy是抽象基类,ConsoleLogStrategy和FileLogStrategy是具体策略实现。
2.2 线程安全设计
日志系统必须考虑多线程场景。我们采用两种同步机制:
- 互斥锁:使用pthread_mutex_t封装了简单的互斥锁
- RAII守卫:通过
LockGuard类实现自动加锁/解锁
特别要注意文件写入时的线程安全问题。即使使用标准库的ofstream,在多线程环境下直接写入同一个文件也会导致内容混乱。
3. 关键实现细节
3.1 日志格式定义
标准日志格式包含以下字段:
code复制[时间戳] [日志级别] [进程ID] [文件名][行号] - 消息内容
示例:
code复制[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
时间戳采用人类可读格式,便于直接查看。日志级别支持DEBUG、INFO、WARNING等常见等级。
3.2 文件输出策略实现
文件输出策略的核心难点在于:
- 路径处理:需要检查并自动创建不存在的目录
- 文件打开方式:使用追加模式(ios::app)避免覆盖旧日志
- 线程安全:每次写入都需要加锁
关键代码片段:
cpp复制void SyncLog(const string& message) override {
string fullPath = _path + (_path.back() == '/' ? "" : "/") + _file;
ofstream out(fullPath, ios::app);
if(out.is_open()) {
LockGuard lockguard(_mutex);
out << message << "\r\n";
}
}
3.3 控制台输出策略
控制台输出虽然简单,但也要注意:
- 标准输出也是共享资源,需要加锁
- 考虑输出格式化和颜色显示(可扩展)
- 在Windows和Linux下的换行符差异
实现示例:
cpp复制void SyncLog(const string& message) override {
LockGuard lockguard(_mutex);
cout << message << "\r\n";
}
4. 完整实现解析
4.1 互斥锁封装
我们首先封装了基础的互斥锁功能:
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;
};
4.2 策略模式实现
抽象基类定义接口:
cpp复制class LogStrategy {
public:
virtual void SyncLog(const string& message) = 0;
virtual ~LogStrategy() = default;
};
具体策略实现:
cpp复制// 控制台输出策略
class ConsoleLogStrategy : public LogStrategy {
// 实现略...
};
// 文件输出策略
class FileLogStrategy : public LogStrategy {
public:
FileLogStrategy(const string& path = "./log",
const string& file = "my.log")
: _path(path), _file(file)
{
LockGuard lockguard(_mutex);
if (!filesystem::exists(_path))
filesystem::create_directories(_path);
}
// 其他实现略...
};
4.3 日志器主体实现
Logger类负责整合所有功能:
cpp复制class Logger {
public:
Logger() {
// 默认使用控制台输出
Strategy = make_unique<ConsoleLogStrategy>();
}
void EnableConsole() {
Strategy = make_unique<ConsoleLogStrategy>();
}
void EnableFile(const string& path = "./log",
const string& file = "my.log") {
Strategy = make_unique<FileLogStrategy>(path, file);
}
void Log(const string& message) {
Strategy->SyncLog(message);
}
private:
unique_ptr<LogStrategy> Strategy;
};
5. 使用示例与最佳实践
5.1 基本使用方法
cpp复制Logger logger;
// 默认输出到控制台
logger.Log("This goes to console");
// 切换到文件输出
logger.EnableFile("/var/log", "app.log");
logger.Log("This goes to file");
// 切换回控制台
logger.EnableConsole();
logger.Log("Back to console");
5.2 性能优化建议
- 批量写入:积累多条日志后一次性写入,减少锁竞争
- 异步日志:使用生产者-消费者模型,将日志写入移到后台线程
- 日志分级:在生产环境中关闭DEBUG级别日志
5.3 扩展可能性
- 网络日志:实现UDP/TCP日志策略,将日志发送到远程服务器
- 日志轮转:按大小或时间自动分割日志文件
- 彩色输出:在控制台实现不同日志级别的颜色区分
6. 常见问题与解决方案
6.1 权限问题
问题:在Linux系统下,程序可能没有权限在/var/log等目录创建文件。
解决方案:
- 提前创建目录并设置适当权限
- 使用用户主目录下的日志目录
- 捕获异常并提供友好错误提示
6.2 性能瓶颈
问题:频繁的小日志写入会导致性能下降。
解决方案:
- 实现日志缓冲,定期批量写入
- 使用更高效的文件IO方式,如内存映射文件
- 考虑使用无锁队列实现异步日志
6.3 日志丢失
问题:程序崩溃时,缓冲区中的日志可能丢失。
解决方案:
- 实现同步写入模式(牺牲性能换取可靠性)
- 定期调用flush强制写入磁盘
- 使用write()系统调用代替流式IO
7. 实际应用中的经验分享
在线上环境中使用这个日志系统时,我总结了以下几点经验:
- 日志分级很重要:合理使用DEBUG/INFO/WARNING等级别,避免生产环境日志爆炸
- 文件命名要有规律:建议包含日期和服务名称,如"app-20240804.log"
- 监控日志增长:实现日志轮转或清理机制,防止磁盘被占满
- 敏感信息过滤:不要在日志中记录密码等敏感信息
一个实用的技巧是,在服务启动时输出所有关键配置参数,这样在排查问题时可以快速确认运行环境。
对于长期运行的服务,建议实现日志文件按日期或大小自动分割。可以扩展FileLogStrategy,在每次写入前检查文件大小,超过阈值就创建新文件。
