在Linux环境下开发应用程序时,日志系统就像程序的黑匣子,记录着运行时每一个关键动作。我曾在凌晨三点被紧急呼叫排查线上问题,正是完善的日志记录让我在10分钟内定位到内存泄漏的根源。一个设计良好的日志类不仅能帮助开发者快速诊断问题,还能为系统运维提供重要依据。
Linux系统本身提供了syslog这样的标准日志服务,但应用程序通常需要更灵活的自定义日志方案。我们需要实现的日志类应当具备以下核心能力:多级别日志输出(DEBUG/INFO/WARNING/ERROR)、日志格式化、输出目标控制(文件/控制台)、线程安全以及日志轮转管理。这些特性构成了一个生产级日志系统的基础骨架。
日志类的核心接口应该简洁明了,我推荐采用以下设计模式:
cpp复制class Logger {
public:
enum Level {
DEBUG = 0,
INFO,
WARNING,
ERROR
};
static Logger& getInstance();
void setLogLevel(Level level);
void setOutput(const std::string& filename);
void debug(const std::string& message);
void info(const std::string& message);
void warning(const std::string& message);
void error(const std::string& message);
private:
Logger(); // 单例模式
void log(Level level, const std::string& message);
};
这种设计采用单例模式确保全局唯一性,通过枚举定义日志级别,并提供不同级别的快捷方法。内部实现中所有日志调用最终都会路由到私有的log()方法统一处理。
在多线程环境下,日志系统必须保证线程安全。我推荐使用mutex锁配合RAII模式:
cpp复制#include <mutex>
class Logger {
// ...
private:
std::mutex log_mutex_;
};
void Logger::log(Level level, const std::string& message) {
std::lock_guard<std::mutex> lock(log_mutex_);
// 实际的日志记录操作
}
提示:避免在锁内进行耗时操作(如网络I/O),这会导致线程阻塞。应该只锁定必要的资源访问部分。
良好的日志格式应当包含以下要素:
实现示例:
cpp复制#include <chrono>
#include <iomanip>
#include <sstream>
std::string Logger::formatMessage(Level level, const std::string& message) {
auto now = std::chrono::system_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()) % 1000;
auto timer = std::chrono::system_clock::to_time_t(now);
std::tm bt = *std::localtime(&timer);
std::ostringstream oss;
oss << std::put_time(&bt, "%Y-%m-%d %H:%M:%S")
<< '.' << std::setfill('0') << std::setw(3) << ms.count()
<< " [" << levelToString(level) << "]"
<< " [thread:" << std::this_thread::get_id() << "] "
<< message;
return oss.str();
}
频繁的字符串操作可能成为性能瓶颈,我总结了几个优化点:
文件输出是日志系统最常用的目标,需要注意几个关键点:
cpp复制#include <fstream>
class Logger {
// ...
private:
std::ofstream log_file_;
};
void Logger::setOutput(const std::string& filename) {
std::lock_guard<std::mutex> lock(log_mutex_);
if (log_file_.is_open()) {
log_file_.close();
}
log_file_.open(filename, std::ios::app);
if (!log_file_) {
throw std::runtime_error("无法打开日志文件: " + filename);
}
}
注意:文件打开模式使用std::ios::app以保证追加写入,避免覆盖已有日志。
当日志文件过大时需要轮转,常见策略有:
实现示例:
cpp复制void Logger::checkRotate() {
if (!log_file_.is_open()) return;
const uintmax_t MAX_SIZE = 10 * 1024 * 1024; // 10MB
auto pos = log_file_.tellp();
if (pos >= MAX_SIZE) {
log_file_.close();
std::string old_name = filename_ + ".1";
std::rename(filename_.c_str(), old_name.c_str());
log_file_.open(filename_, std::ios::app);
}
}
对于高性能场景,同步日志可能成为瓶颈。异步日志通过将日志写入队列,由后台线程处理:
cpp复制#include <queue>
#include <condition_variable>
class AsyncLogger : public Logger {
public:
AsyncLogger();
~AsyncLogger();
void log(Level level, const std::string& message) override;
private:
void workerThread();
std::queue<std::string> log_queue_;
std::thread worker_;
std::condition_variable cv_;
bool shutdown_ = false;
};
AsyncLogger::AsyncLogger() {
worker_ = std::thread(&AsyncLogger::workerThread, this);
}
void AsyncLogger::log(Level level, const std::string& message) {
std::string formatted = formatMessage(level, message);
{
std::lock_guard<std::mutex> lock(log_mutex_);
log_queue_.push(formatted);
}
cv_.notify_one();
}
void AsyncLogger::workerThread() {
while (true) {
std::unique_lock<std::mutex> lock(log_mutex_);
cv_.wait(lock, [this]{
return !log_queue_.empty() || shutdown_;
});
if (shutdown_ && log_queue_.empty()) break;
while (!log_queue_.empty()) {
std::string msg = log_queue_.front();
log_queue_.pop();
lock.unlock();
// 实际写入操作
if (log_file_.is_open()) {
log_file_ << msg << std::endl;
}
lock.lock();
}
}
}
生产环境中经常需要动态调整日志级别:
cpp复制#include <atomic>
class Logger {
// ...
private:
std::atomic<Level> current_level_{INFO};
};
void Logger::setLogLevel(Level level) {
current_level_.store(level, std::memory_order_relaxed);
}
void Logger::log(Level level, const std::string& message) {
if (level < current_level_.load(std::memory_order_relaxed)) {
return;
}
// ... 后续处理
}
在实现日志系统时,我踩过几个典型的坑:
同步vs异步:异步日志提高了性能,但在程序崩溃时可能丢失最后几条日志。关键业务场景可以考虑混合模式——ERROR级别日志同步写入,其他级别异步处理。
缓冲区设置:文件输出时适当设置缓冲区大小(通常8KB-64KB为宜),太大可能丢失重要日志,太小则影响性能。
异常处理:日志系统自身不能抛出异常,否则可能掩盖真正的程序错误。所有I/O操作都应该捕获异常并静默处理。
经过多年实践,我总结了这些日志编写原则:
ERROR级别:仅用于真正的错误情况(如外部服务调用失败、资源获取失败等),附带足够上下文信息(错误码、相关参数等)。
WARNING级别:用于异常但程序能继续运行的情况(如降级处理、重试操作等)。
INFO级别:记录关键业务流程节点(如"用户登录成功"、"订单创建完成")。
DEBUG级别:用于开发调试的详细信息(如函数参数、中间计算结果)。
一个反面案例:
cpp复制logger.debug("Processing data..."); // 无用的日志
改进后的版本:
cpp复制logger.debug(fmt::format("Processing user {} request, params: {}", user_id, params));
完善的日志系统需要验证以下方面:
测试示例(使用Catch2框架):
cpp复制TEST_CASE("Logger level filtering") {
Logger& logger = Logger::getInstance();
logger.setLogLevel(Logger::WARNING);
std::stringstream ss;
logger.setOutput(ss);
logger.debug("Debug message");
logger.info("Info message");
logger.warning("Warning message");
logger.error("Error message");
std::string output = ss.str();
REQUIRE(output.find("Debug") == std::string::npos);
REQUIRE(output.find("Info") == std::string::npos);
REQUIRE(output.find("Warning") != std::string::npos);
REQUIRE(output.find("Error") != std::string::npos);
}
在实际项目中,还需要验证:
在现代微服务架构中,可以考虑将日志发送到集中式系统:
传统文本日志不利于自动化分析,可以考虑JSON格式:
json复制{
"timestamp": "2023-07-20T14:32:45.123Z",
"level": "ERROR",
"thread": 1234,
"message": "Database connection failed",
"context": {
"host": "db01.prod",
"port": 5432,
"error": "Connection timeout"
}
}
实现时可以使用现成的JSON库(如nlohmann/json),或者直接输出格式化字符串。
对于极致性能要求的场景:
这些优化虽然提高了性能,但也增加了实现复杂度,应该根据实际需求权衡。