1. 项目背景与核心价值
在Linux系统开发中,日志功能就像空气一样无处不在却又容易被忽视。每个项目都在重复实现相似的日志功能——打开文件、写入格式化的字符串、处理多线程安全、考虑性能开销......这种重复劳动不仅浪费时间,更糟糕的是,每个开发者都可能踩进同样的坑里。
我最近重构一个遗留系统时,发现代码库里有7种不同的日志实现方式。有的用fprintf直接写文件,有的用iostream包装,还有的混用了syslog。这种混乱导致日志格式不统一、性能差异大、关键信息丢失等问题。于是决定用C++策略模式打造一个通用日志框架,让团队从此告别重复造轮子。
这个框架的核心设计理念是:日志记录逻辑与日志输出方式解耦。就像打印机(输出设备)与文档编辑软件(内容生成)的关系——无论你用什么软件编辑文档,都可以选择用激光打印机、喷墨打印机或者PDF虚拟打印机输出。
2. 策略模式在日志系统中的妙用
2.1 策略模式本质解析
策略模式的精髓在于定义算法族,分别封装起来,让它们可以互相替换。放在日志系统这个具体场景中:
- 算法族:各种日志输出方式(文件、控制台、syslog、网络等)
- 互相替换:运行时动态切换输出目标,不影响业务代码
用UML类图表示(伪代码):
cpp复制class ILogStrategy {
public:
virtual ~ILogStrategy() = default;
virtual void write(const LogEntry&) = 0;
};
class FileStrategy : public ILogStrategy { /* 实现文件写入 */ };
class ConsoleStrategy : public ILogStrategy { /* 实现控制台输出 */ };
class SyslogStrategy : public ILogStrategy { /* 实现syslog对接 */ };
class Logger {
std::unique_ptr<ILogStrategy> strategy_;
public:
void setStrategy(std::unique_ptr<ILogStrategy>&& s) {
strategy_ = std::move(s);
}
void log(const std::string& msg) {
auto entry = format(msg); // 格式化日志
strategy_->write(entry); // 策略委托
}
};
2.2 为什么策略模式特别适合日志系统
- 输出方式多变需求:开发阶段用控制台输出方便调试,生产环境可能需要文件或syslog
- 性能考量:文件写入可能需要缓冲策略,网络日志可能需要异步队列
- 扩展性:未来新增日志输出方式(如Kafka)不影响现有代码
实测案例:我们的物联网网关项目需要同时将日志写入本地文件(供运维查看)和MQTT主题(供云端分析)。通过策略模式组合,轻松实现了双路日志输出:
cpp复制auto logger = std::make_unique<Logger>();
logger->setStrategy(
std::make_unique<CompositeStrategy>(
std::make_unique<FileStrategy>("/var/log/gateway.log"),
std::make_unique<MqttStrategy>(mqtt_client)
)
);
3. 框架核心实现详解
3.1 日志条目设计
一个完整的日志条目需要包含哪些信息?经过多个项目验证,这些字段必不可少:
cpp复制struct LogEntry {
std::chrono::system_clock::time_point timestamp;
LogLevel level; // DEBUG/INFO/WARNING/ERROR
std::string thread_id;
std::string source_file;
int source_line;
std::string message;
std::string tag; // 业务标签,如"auth", "db"等
// 格式化输出
std::string format(const std::string& pattern) const;
};
关键细节:timestamp使用system_clock而非steady_clock,因为需要人类可读的日期时间
3.2 线程安全实现方案
多线程日志最常见的坑就是输出内容错乱。我们采用双缓冲技术平衡性能与安全:
- 每个线程持有thread_local的缓冲区
- 写入时先填充本地缓冲区
- 当缓冲区满或超时(如1秒)时,交换到全局队列
- 专用后台线程处理全局队列写入
cpp复制class ThreadSafeBuffer {
std::mutex mtx_;
std::vector<LogEntry> front_buffer_;
std::vector<LogEntry> back_buffer_;
public:
void add(LogEntry&& entry) {
std::lock_guard lock(mtx_);
front_buffer_.push_back(std::move(entry));
}
void swapBuffers() {
std::lock_guard lock(mtx_);
front_buffer_.swap(back_buffer_);
}
void processBackBuffer(ILogStrategy& strategy) {
for (auto& entry : back_buffer_) {
strategy.write(entry);
}
back_buffer_.clear();
}
};
3.3 性能优化技巧
- 避免内存分配:预分配缓冲区,使用内存池管理LogEntry对象
- 时间格式化优化:将
%Y-%m-%d %H:%M:%S格式缓存,每秒只更新一次 - 条件日志:对于DEBUG级别日志,先检查级别再构造消息
cpp复制#define LOG_DEBUG(logger, msg) \
if (logger.level() <= LogLevel::DEBUG) \
logger.log(LogLevel::DEBUG, msg, __FILE__, __LINE__)
4. 实战:扩展自定义策略
假设我们需要添加一个将日志写入Redis的策略:
cpp复制class RedisStrategy : public ILogStrategy {
redisContext* conn_;
std::string list_key_;
public:
explicit RedisStrategy(const std::string& key)
: list_key_(key) {
conn_ = redisConnect("127.0.0.1", 6379);
if (conn_->err) {
throw std::runtime_error("Redis连接失败");
}
}
~RedisStrategy() override {
redisFree(conn_);
}
void write(const LogEntry& entry) override {
auto formatted = entry.format("$time [$level] $msg");
redisCommand(conn_, "RPUSH %s %b",
list_key_.c_str(),
formatted.data(),
formatted.size());
}
};
使用示例:
cpp复制logger.setStrategy(std::make_unique<RedisStrategy>("app:logs"));
logger.info("系统启动完成"); // 日志将进入Redis列表
5. 关键问题排查指南
5.1 日志丢失问题
现象:程序崩溃后最后几条日志没写入文件
原因:缓冲区未刷新
解决方案:
- 注册信号处理函数,在SIGTERM/SIGINT时主动刷新
- 设置缓冲区大小上限(如1MB)
- 实现定时刷新线程(如每秒一次)
cpp复制void setupSignalHandler(Logger& logger) {
std::signal(SIGTERM, [](int) {
Logger::instance().flush();
std::exit(0);
});
// 同理处理SIGINT等
}
5.2 性能瓶颈分析
现象:日志拖慢主业务50%以上性能
优化步骤:
- 用perf工具分析热点
- 常见瓶颈点:
- 时间格式化(占40%+)
- 锁竞争(特别是直接使用fprintf等非线程安全函数)
- 频繁内存分配
- 优化后对比:
| 优化前 | 优化后 | 提升 |
|---|---|---|
| 12,000条/秒 | 85,000条/秒 | 7.1倍 |
6. 高级功能扩展思路
6.1 日志轮转策略
结合logrotate需求,实现按大小/时间自动分割文件:
cpp复制class RotatingFileStrategy : public ILogStrategy {
std::string base_path_;
size_t max_size_;
std::ofstream current_file_;
size_t current_size_ = 0;
void rotate() {
current_file_.close();
std::string new_path = base_path_ + "." + getCurrentTimeStamp();
std::rename(base_path_.c_str(), new_path.c_str());
current_file_.open(base_path_);
current_size_ = 0;
}
public:
void write(const LogEntry& entry) override {
auto msg = entry.format();
if (current_size_ + msg.size() > max_size_) {
rotate();
}
current_file_ << msg;
current_size_ += msg.size();
}
};
6.2 结构化日志支持
为适应ELK等日志分析系统,支持JSON格式输出:
cpp复制struct JsonLogEntry : public LogEntry {
std::map<std::string, std::string> extra_fields;
std::string format() const override {
nlohmann::json j;
j["time"] = formatTime(timestamp);
j["level"] = toString(level);
j["message"] = message;
for (const auto& [k, v] : extra_fields) {
j[k] = v;
}
return j.dump();
}
};
7. 框架完整使用示例
一个完整的初始化配置示例:
cpp复制auto logger = std::make_shared<Logger>();
// 生产环境配置:文件日志+按100MB轮转
if (isProduction()) {
auto strategy = std::make_unique<RotatingFileStrategy>(
"/var/log/myapp.log",
100 * 1024 * 1024
);
strategy->setLevelFilter(LogLevel::INFO); // 生产环境只记录INFO以上
logger->setStrategy(std::move(strategy));
}
// 开发环境配置:彩色控制台输出
else {
logger->setStrategy(std::make_unique<ConsoleColorStrategy>());
}
// 全局访问点
Logger::setInstance(logger);
// 使用示例
LOG_INFO("系统初始化完成");
LOG_DEBUG("连接池大小: %d", pool.size());
这个框架在我们团队落地后,日志相关代码量减少70%,性能问题下降90%,最重要的是——再也没有人抱怨"日志又出问题了"。策略模式的灵活性让我们可以轻松应对各种新的日志需求,真正实现了"一次编写,到处使用"的理想状态。
