33、C++项目实战:日志系统设计
日志系统,说白了就是给程序装一个「黑匣子」。
我刚开始做C++项目时,总觉得日志可有可无。直到有一次线上服务半夜挂了,我连程序死在哪一步都不知道……嗯,从那以后,我写的每个项目第一件事就是搭日志系统。
今天我们就手撸一个轻量级、可扩展的C++日志库。你想想看,以后排查问题直接翻日志文件,比打断点调试爽多了。
日志系统到底要解决什么问题?
一个合格的日志系统,至少得搞定这几件事:
- 分级输出:DEBUG、INFO、WARN、ERROR,不同级别能分开看
- 线程安全:多线程写日志不能乱掉
- 文件滚动:日志不能无限增长,按大小或时间切分
- 性能开销低:写日志不能拖慢主业务
我个人习惯把日志系统设计成三层架构,这样后期改起来不费劲。下面这张图能帮你快速理解整体结构:
第一步:定义日志级别和基础结构
先定个枚举,把级别排好。我个人习惯把级别值设成整数,方便比较过滤。
// log_level.h
#pragma once
#include <string>
enum class LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
FATAL = 4
};
inline std::string levelToString(LogLevel level) {
switch (level) {
case LogLevel::DEBUG: return "DEBUG";
case LogLevel::INFO: return "INFO";
case LogLevel::WARN: return "WARN";
case LogLevel::ERROR: return "ERROR";
case LogLevel::FATAL: return "FATAL";
default: return "UNKNOWN";
}
}
💡 我的小习惯:FATAL级别我一般会直接触发abort(),因为走到这步程序已经没法继续跑了。线上环境FATAL日志一定要有告警通知。
第二步:实现线程安全的日志缓冲区
多线程写日志,最怕的就是数据竞争。我早期吃过这个亏——两个线程同时写一行日志,结果拼成了乱码。
解决办法其实很简单:用互斥锁保护写操作,或者用无锁队列。这里我选互斥锁,因为实现简单,对大多数项目够用了。
// log_buffer.h
#pragma once
#include <string>
#include <mutex>
#include <vector>
class LogBuffer {
public:
void append(const std::string& msg) {
std::lock_guard<std::mutex> lock(mutex_);
buffer_.push_back(msg);
}
std::vector<std::string> flush() {
std::lock_guard<std::mutex> lock(mutex_);
std::vector<std::string> ret;
ret.swap(buffer_);
return ret;
}
private:
std::mutex mutex_;
std::vector<std::string> buffer_;
};
⚠️ 注意:千万别在锁里面做文件IO!我曾经见过有人把写文件和加锁放一起,结果高并发下性能直接崩了。正确的做法是:先攒一批日志到内存缓冲区,后台线程定时刷盘。
第三步:文件滚动策略
日志文件不能无限大,否则打开都费劲。我一般用两种滚动策略:
| 策略 | 触发条件 | 适用场景 |
|---|---|---|
| 按大小滚动 | 单个文件超过阈值(如10MB) | 日志量稳定的服务 |
| 按时间滚动 | 每天/每小时新建一个文件 | 需要按时间归档的场景 |
我个人更推荐「按大小+按时间」混合策略。比如每天一个目录,每个文件最大10MB,超过就新建。这样既好找又好管。
// file_roller.h
#pragma once
#include <string>
#include <fstream>
class FileRoller {
public:
FileRoller(const std::string& baseName, size_t maxSize = 10 * 1024 * 1024)
: baseName_(baseName), maxSize_(maxSize), index_(0) {
openNewFile();
}
void write(const std::string& msg) {
if (file_.tellp() > maxSize_) {
file_.close();
openNewFile();
}
file_ << msg << std::endl;
}
private:
void openNewFile() {
std::string fileName = baseName_ + "_" + std::to_string(index_++) + ".log";
file_.open(fileName, std::ios::app);
}
std::ofstream file_;
std::string baseName_;
size_t maxSize_;
int index_;
};
第四步:组装成完整的日志器
把上面这些零件拼起来,就是一个能用的日志系统了。这里我用了单例模式,因为整个程序只需要一个日志实例。
// logger.h
#pragma once
#include "log_level.h"
#include "log_buffer.h"
#include "file_roller.h"
#include <thread>
#include <chrono>
class Logger {
public:
static Logger& instance() {
static Logger inst;
return inst;
}
void setLevel(LogLevel level) { minLevel_ = level; }
void log(LogLevel level, const std::string& msg) {
if (level < minLevel_) return;
std::string formatted = "[" + levelToString(level) + "] " + msg;
buffer_.append(formatted);
}
void startFlushThread() {
flushThread_ = std::thread([this]() {
while (running_) {
std::this_thread::sleep_for(std::chrono::seconds(2));
auto logs = buffer_.flush();
for (auto& log : logs) {
roller_.write(log);
std::cout << log << std::endl; // 同时输出到控制台
}
}
});
flushThread_.detach();
}
void stop() { running_ = false; }
private:
Logger() : minLevel_(LogLevel::DEBUG), running_(true) {
startFlushThread();
}
~Logger() { stop(); }
LogLevel minLevel_;
LogBuffer buffer_;
FileRoller roller_{"app"};
std::thread flushThread_;
std::atomic<bool> running_;
};
// 便捷宏
#define LOG_DEBUG(msg) Logger::instance().log(LogLevel::DEBUG, msg)
#define LOG_INFO(msg) Logger::instance().log(LogLevel::INFO, msg)
#define LOG_WARN(msg) Logger::instance().log(LogLevel::WARN, msg)
#define LOG_ERROR(msg) Logger::instance().log(LogLevel::ERROR, msg)
🔑 关键设计点:
- 后台线程每2秒刷一次盘,避免频繁IO
- 日志级别过滤在写入缓冲区之前,减少无用操作
- 宏定义让调用更简洁,同时保留__FILE__和__LINE__的扩展空间
使用示例
#include "logger.h"
int main() {
Logger::instance().setLevel(LogLevel::INFO);
LOG_DEBUG("这条不会输出,因为级别是INFO"); // 被过滤
LOG_INFO("服务启动成功");
LOG_WARN("内存使用率超过80%");
// 模拟业务逻辑
for (int i = 0; i < 5; ++i) {
LOG_INFO("处理第" + std::to_string(i) + "个请求");
}
LOG_ERROR("数据库连接超时");
std::this_thread::sleep_for(std::chrono::seconds(3));
return 0;
}
避坑指南
做日志系统这些年,我踩过不少坑。挑几个典型的说说:
- 日志打太多拖慢程序——我曾经在热循环里打DEBUG日志,结果性能直接腰斩。解决办法:热路径上只打ERROR级别,或者用条件宏控制。
- 日志文件把磁盘撑爆——有一次线上服务跑了半年,日志文件占了200GB。后来我加了最大文件数和自动清理策略,保留最近7天的日志就够了。
- 多线程日志乱序——如果多个线程同时写,日志时间戳可能错乱。我后来在每条日志里加了线程ID,排查时能按线程过滤。
📌 进阶建议:如果你的项目对性能要求极高,可以考虑用「双缓冲区」技术——一个缓冲区写,一个缓冲区刷盘,交替使用。这样几乎零锁竞争。我曾在QPS 10万+的网关服务上用过,效果很好。
好了,一个完整的日志系统就这样搭起来了。你想想看,以后出问题了直接翻日志,是不是比瞎猜强多了?
实际项目中,你还可以加上异步日志、日志压缩、远程日志收集等功能。但核心思想不变——分层设计、线程安全、异步刷盘。