0. 引言
使用C++构建一个简单的线程安全的日志头文件(head-only)的loghelper.hpp,适用于简单快速的调试程序,又不想引入比较复杂的日志模块。
1. 完整代码
我们将日志系统封装在一个Logger
类中,采用单例模式确保全局只有一个Logger
实例。该类负责日志级别的管理、日志文件的处理以及日志的输出。
// loghelper.hpp
#ifndef LOGGER_H_
#define LOGGER_H_
#include <cstdint>
#include <sys/stat.h>
#include <cstring>
#include <iostream>
#include <string>
#include <utility>
#include <unistd.h>
#include <memory>
#include <sstream>
#include <cstdio>
#include <cstdlib>
#include <mutex>
// Log levels enumeration
enum class LogLevel : std::int32_t
{
TRACE = 0,
DEBUG = 1,
INFO = 2,
WARN = 3,
ERROR = 4,
CRITICAL = 5,
OFF = 6
};
// RAII class to automatically close file upon destruction
class FileCloser
{
public:
explicit FileCloser(FILE* file) : file_(file) {}
~FileCloser()
{
if (file_ != nullptr)
{
fclose(file_);
}
}
FILE* get() const { return file_; }
private:
FILE* file_;
};
// Logger class implementing the Singleton pattern
class Logger
{
public:
// Retrieves the single instance of Logger
static Logger& Instance()
{
static Logger instance;
return instance;
}
// Initializes the logger with the log file path and log level
void Init(const std::string& logFile, LogLevel level = LogLevel::INFO)
{
std::lock_guard<std::mutex> lock(mutex_);
logFilePath_ = logFile;
logLevel_ = level;
if (access(logFilePath_.c_str(), F_OK) != 0)
{
fprintf(stderr, "Error: Log file does not exist: %s. Creating a new one.\n", logFilePath_.c_str());
FILE* logFilePtr = fopen(logFilePath_.c_str(), "w");
if (logFilePtr != nullptr)
{
fclose(logFilePtr);
fprintf(stdout, "Log file created: %s\n", logFilePath_.c_str());
}
else
{
fprintf(stderr, "Error: Unable to create log file: %s\n", logFilePath_.c_str());
logFilePath_.clear();
}
}
else
{
fprintf(stdout, "Log file exists: %s\n", logFilePath_.c_str());
}
}
// Logs a message with the given level, function name, and line number
template <typename... Args>
void Log(
LogLevel level,
const char* src_func,
int32_t src_line,
const char* fmt_str,
Args&&... fmt_args)
{
if (level < logLevel_ || logLevel_ == LogLevel::OFF)
{
return;
}
char tmp_buf[kLogBufferSize];
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-security"
int32_t sp_res = std::snprintf(tmp_buf, sizeof(tmp_buf), fmt_str, std::forward<Args>(fmt_args)...);
#pragma GCC diagnostic pop
if (sp_res < 0)
{
return;
}
std::stringstream ss;
ss << "[" << LogLevelToString(level) << "] ";
ss << "[" << src_func << ":" << src_line << "] ";
ss << tmp_buf << "\n";
std::string logEntry = ss.str();
std::lock_guard<std::mutex> lock(mutex_);
// Output to terminal
std::cout << logEntry;
// Output to file
if (!logFilePath_.empty())
{
// Check if file size exceeds the maximum limit
uint64_t fileSize = GetFileSize(logFilePath_.c_str());
if (fileSize != static_cast<uint64_t>(-1) && fileSize >= kMaxFileSize)
{
ForceRenameLogFile();
}
// Open the log file in append mode
FILE* logFile = fopen(logFilePath_.c_str(), "a");
FileCloser closer(logFile);
if (logFile != nullptr)
{
fprintf(logFile, "%s", logEntry.c_str());
}
else
{
fprintf(stderr, "Error: Unable to open log file: %s\n", logFilePath_.c_str());
}
}
}
// Disable copy constructor and assignment operator
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
private:
Logger() : logLevel_(LogLevel::INFO), logFilePath_("") {}
// Retrieves the size of the specified file
uint64_t GetFileSize(const char* filePath)
{
struct stat statBuf;
if (stat(filePath, &statBuf) == 0)
{
return statBuf.st_size;
}
return static_cast<uint64_t>(-1); // Use unsigned type -1 to indicate error
}
// Renames the current log file to create a backup
void ForceRenameLogFile()
{
if (logFilePath_.empty())
{
fprintf(stderr, "Error: Log file path is empty.\n");
return;
}
// Construct the backup file path
std::string backupFilePath = logFilePath_ + ".1";
// Construct the mv command
std::string command = "mv -f " + logFilePath_ + " " + backupFilePath;
fprintf(stdout, "Renaming log file: %s -> %s\n", logFilePath_.c_str(), backupFilePath.c_str());
// Execute the command using system()
system(command.c_str());
}
// Converts a LogLevel to its string representation
std::string LogLevelToString(LogLevel level)
{
switch (level)
{
case LogLevel::TRACE: return "TRACE";
case LogLevel::DEBUG: return "DEBUG";
case LogLevel::INFO: return "INFO";
case LogLevel::WARN: return "WARN";
case LogLevel::ERROR: return "ERROR";
case LogLevel::CRITICAL: return "CRITICAL";
case LogLevel::OFF: return "OFF";
default: return "UNKNOWN";
}
}
static constexpr uint32_t kLogBufferSize = 1024;
static constexpr uint64_t kMaxFileSize = 10 * 1024 * 1024; // 10MB
LogLevel logLevel_;
std::string logFilePath_;
std::mutex mutex_;
};
// Macro definitions for simplified logging
#ifndef LOG_MACROS_H
#define LOG_MACROS_H
#define LOG_INFO(...) \
Logger::Instance().Log(LogLevel::INFO, __FUNCTION__, __LINE__, __VA_ARGS__)
#define LOG_DBG(...) \
Logger::Instance().Log(LogLevel::DEBUG, __FUNCTION__, __LINE__, __VA_ARGS__)
#define LOG_ERR(...) \
Logger::Instance().Log(LogLevel::ERROR, __FUNCTION__, __LINE__, __VA_ARGS__)
#define LOG_PANIC(...) \
do { \
Logger::Instance().Log(LogLevel::CRITICAL, __FUNCTION__, __LINE__, __VA_ARGS__); \
exit(1); \
} while(0)
#endif // LOG_MACROS_H
#endif // LOGGER_H_
2. 使用示例
以下是一个示例程序,展示如何使用上述日志系统:
#include "Logger.h"
int main()
{
// Initialize the logger with the log file path and desired log level
Logger::Instance().Init("application.log", LogLevel::DEBUG);
LOG_INFO("Application started with PID: %d", getpid());
LOG_DBG("This is a debug message.");
LOG_ERR("An error occurred: %s", "Sample error");
// Simulate logging to reach the file size limit
// Adjust kMaxFileSize in Logger.h for testing purposes if needed
for(int i = 0; i < 100000; ++i)
{
LOG_INFO("Logging line number: %d", i);
}
LOG_PANIC("Critical failure: %s", "Unable to continue");
return 0;
}
解释:
-
初始化日志系统:
Logger::Instance().Init("application.log", LogLevel::DEBUG);
设置日志文件路径为
application.log
,并将日志级别设置为DEBUG
,这意味着DEBUG
及以上级别的日志将被记录。 -
记录不同级别的日志:
LOG_INFO("Application started with PID: %d", getpid()); LOG_DBG("This is a debug message."); LOG_ERR("An error occurred: %s", "Sample error");
通过宏定义简化日志记录,自动包含函数名和行号信息。
-
模拟大量日志记录:
for(int i = 0; i < 100000; ++i) { LOG_INFO("Logging line number: %d", i); }
这段代码用于模拟日志文件达到大小限制(10MB)的情况,触发日志轮转机制。
-
记录严重错误并终止程序:
LOG_PANIC("Critical failure: %s", "Unable to continue");
记录一个严重错误后,立即终止程序执行。
3. 代码说明
流程图
上述流程图如下:
日志级别定义
通过枚举类型LogLevel
定义不同的日志级别,从TRACE
到CRITICAL
,再到OFF
,可以灵活控制日志的输出细粒度。
// Log levels enumeration
enum class LogLevel : std::int32_t
{
TRACE = 0,
DEBUG = 1,
INFO = 2,
WARN = 3,
ERROR = 4,
CRITICAL = 5,
OFF = 6
};
文件管理与RAII
为了确保日志文件在程序结束时能够正确关闭,我们引入了一个RAII(资源获取即初始化)类FileCloser
。该类在析构时自动关闭文件,避免资源泄漏。
// RAII class to automatically close file upon destruction
class FileCloser
{
public:
explicit FileCloser(FILE* file) : file_(file) {}
~FileCloser()
{
if (file_ != nullptr)
{
fclose(file_);
}
}
FILE* get() const { return file_; }
private:
FILE* file_;
};
线程安全
在多线程环境下,多个线程可能会同时尝试写入日志文件。为了避免数据竞争和日志内容混乱,我们在Logger
类中使用std::mutex
来保护关键区域,确保同一时间只有一个线程可以执行写入操作。
class Logger
{
public:
// ... 其他成员函数 ...
private:
// ... 其他成员变量 ...
std::mutex mutex_;
};
在需要保护的代码段使用std::lock_guard
自动管理锁的获取和释放:
void Init(const std::string& logFile, LogLevel level = LogLevel::INFO)
{
std::lock_guard<std::mutex> lock(mutex_);
// 初始化日志文件路径和日志级别
}
宏定义简化日志调用
为了简化日志的调用,我们使用宏定义来封装Logger
类的日志函数。这样,开发者只需调用LOG_INFO
、LOG_DBG
或LOG_ERR
即可记录日志,而无需关心具体的实现细节。
// Macro definitions for simplified logging
#ifndef LOG_MACROS_H
#define LOG_MACROS_H
#define LOG_INFO(...) \
Logger::Instance().Log(LogLevel::INFO, __FUNCTION__, __LINE__, __VA_ARGS__)
#define LOG_DBG(...) \
Logger::Instance().Log(LogLevel::DEBUG, __FUNCTION__, __LINE__, __VA_ARGS__)
#define LOG_ERR(...) \
Logger::Instance().Log(LogLevel::ERROR, __FUNCTION__, __LINE__, __VA_ARGS__)
#define LOG_PANIC(...) \
do { \
Logger::Instance().Log(LogLevel::CRITICAL, __FUNCTION__, __LINE__, __VA_ARGS__); \
exit(1); \
} while(0)
#endif // LOG_MACROS_H