57、STL与日志系统:日志级别、日志格式化、日志轮转
日志系统,说白了就是程序的「黑匣子」。
我做了这么多年C++开发,几乎每个线上项目都离不开它。没有日志,出了问题你连从哪查起都不知道。但很多人写日志就是简单printf一下,等到日志文件撑爆磁盘、或者想查某个级别的信息却淹没在大量无用输出里时,才后悔当初没好好设计。
这一章,我们就用STL来打造一个轻量但实用的日志系统。我会把日志级别、格式化输出、文件轮转这几个核心点都讲透。
日志级别:从DEBUG到FATAL
日志级别是日志系统的第一道门槛。我个人习惯至少分四级:DEBUG、INFO、WARN、ERROR。有些场景还会加一个FATAL,表示程序即将崩溃。
为什么要分级?你想想看,开发时你需要看到所有细节,但线上运行时,DEBUG信息会刷爆你的磁盘。所以我们要在运行时能动态调整输出级别。
核心思路:用枚举定义级别,用比较运算符控制输出。
enum class LogLevel : uint8_t {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
FATAL = 4
};
class Logger {
public:
void setLevel(LogLevel level) { minLevel_ = level; }
void log(LogLevel level, const std::string& msg) {
if (level < minLevel_) return; // 级别不够,直接丢弃
// 后续处理...
}
private:
LogLevel minLevel_ = LogLevel::INFO; // 默认只输出INFO及以上
};
嗯,这里要注意:级别比较用小于号,因为数值越小越详细。我曾经见过有人搞反了,结果线上DEBUG日志满天飞,磁盘直接打满。那次教训挺深刻的。
日志格式化:用STL字符串流搞定
日志不能光秃秃地输出一条消息。我们需要时间戳、线程ID、文件名、行号这些信息。用C的sprintf?太容易出错了。我推荐用std::ostringstream,安全又灵活。
#include <sstream>
#include <iomanip>
#include <chrono>
#include <ctime>
std::string formatLog(LogLevel level, const std::string& file,
int line, const std::string& msg) {
std::ostringstream oss;
// 时间戳
auto now = std::chrono::system_clock::now();
auto timeT = std::chrono::system_clock::to_time_t(now);
oss << std::put_time(std::localtime(&timeT), "%Y-%m-%d %H:%M:%S");
// 日志级别
static const char* levelNames[] = {"DEBUG", "INFO", "WARN", "ERROR", "FATAL"};
oss << " [" << levelNames[static_cast<int>(level)] << "]";
// 位置信息
oss << " (" << file << ":" << line << ")";
// 消息正文
oss << " " << msg;
return oss.str();
}
这里有个小技巧:std::put_time配合std::localtime能精确到秒。如果你需要毫秒级精度,可以用std::chrono::duration_cast。我在做金融交易系统时,日志精度要求到毫秒,就是靠这个搞定的。
避坑指南:我曾经在日志格式化里直接传文件名指针,结果文件重命名后指针悬空了。后来我改用std::string_view或者直接传std::string,再也没出过问题。
日志轮转:别让日志撑爆磁盘
日志轮转是生产环境的必备功能。说白了就是:当日志文件太大时,自动切分成新文件,并清理旧的。
我常用的策略有两种:按文件大小轮转,或者按时间轮转。这里我实现一个按大小轮转的版本,用STL的std::filesystem来操作文件。
#include <fstream>
#include <filesystem>
#include <vector>
#include <algorithm>
namespace fs = std::filesystem;
class RotatingFileLogger {
public:
RotatingFileLogger(const std::string& baseName,
size_t maxFileSize = 10 * 1024 * 1024, // 10MB
int maxFiles = 5)
: baseName_(baseName), maxFileSize_(maxFileSize), maxFiles_(maxFiles) {
openCurrentFile();
}
void write(const std::string& formattedLog) {
currentFile_ << formattedLog << "\n";
currentFile_.flush();
if (currentFile_.tellp() > maxFileSize_) {
rotate();
}
}
private:
void openCurrentFile() {
currentFile_.open(baseName_, std::ios::app);
}
void rotate() {
currentFile_.close();
// 删除最旧的文件
fs::path oldestFile = baseName_ + "." + std::to_string(maxFiles_);
if (fs::exists(oldestFile)) {
fs::remove(oldestFile);
}
// 依次重命名:log.txt.4 -> log.txt.5, log.txt.3 -> log.txt.4, ...
for (int i = maxFiles_ - 1; i >= 1; --i) {
fs::path from = baseName_ + "." + std::to_string(i);
fs::path to = baseName_ + "." + std::to_string(i + 1);
if (fs::exists(from)) {
fs::rename(from, to);
}
}
// 当前文件重命名为 .1
fs::rename(baseName_, baseName_ + ".1");
// 重新打开新文件
openCurrentFile();
}
std::string baseName_;
size_t maxFileSize_;
int maxFiles_;
std::ofstream currentFile_;
};
这段代码的核心逻辑是:每次写入后检查文件大小,超了就轮转。轮转时把旧文件依次编号,最老的删掉。这样你永远只保留最近N个日志文件。
注意:多线程环境下,上面的代码不是线程安全的。你需要加锁,或者用原子操作。我一般用std::mutex配合std::lock_guard来保护write和rotate方法。
整合:一个完整的日志系统
把上面三块拼起来,就是一个可用的日志系统了。我还加了一个宏,方便在代码里快速打日志。
#define LOG_DEBUG(msg) logger.log(LogLevel::DEBUG, __FILE__, __LINE__, msg)
#define LOG_INFO(msg) logger.log(LogLevel::INFO, __FILE__, __LINE__, msg)
#define LOG_WARN(msg) logger.log(LogLevel::WARN, __FILE__, __LINE__, msg)
#define LOG_ERROR(msg) logger.log(LogLevel::ERROR, __FILE__, __LINE__, msg)
// 使用示例
LOG_INFO("Server started on port " + std::to_string(port));
LOG_ERROR("Connection failed: " + errMsg);
我个人习惯把logger做成全局单例,这样任何地方都能直接打日志。但要注意,全局对象的初始化顺序是个坑。我一般用std::call_once来延迟初始化,确保安全。
知识体系图
下面这张图展示了本章的核心结构,从日志级别到最终输出,一目了然。
总结
这一章我们做了三件事:用枚举控制日志级别,用ostringstream格式化输出,用filesystem实现文件轮转。这三块组合起来,就是一个能上生产环境的日志系统。
当然,真实项目里还会加更多功能,比如异步写入、网络传输、日志压缩等。但核心骨架就是这些。你掌握了这个,再去看spdlog、log4cpp这些库的源码,会发现它们本质上也是这套思路。
最后提醒一句:日志是给未来的自己看的。写清楚点,别偷懒。你永远不知道明天线上会出什么幺蛾子。
核心要点回顾:
- 日志级别用枚举,运行时动态过滤
- 格式化用
std::ostringstream,安全高效 - 轮转用
std::filesystem,按大小或时间切分 - 多线程环境记得加锁