一、简介
在软件开发过程中,开发人员经常需要在程序不同阶段或者不同模块中输出一些调试信息,以方便查找问题以及调试程序。而程序的打印日志是一种非常便捷、常用的方法。
C++是一门强大的编程语言,拥有良好的性能和灵活的特性,在日志记录功能方面可以借助多种不同的库实现,而利用C++11标准库提供的特性,开发C++日志记录库是一种简单、轻量、灵活的方式。
二、如何实现日志记录
可以采用printf()函数输出日志信息,但这种方法有以下缺点:
- 无法动态控制日志级别
- 无法更改日志格式
- 无法在不同平台上实现
因此我们需要使用C++日志库来完成这些功能,例如log4cpp、log4cplus、glog等。但这些库存在以下缺点:
- 依赖性强,容易出现编译链接错误
- 使用方法复杂,需要较长的学习周期
- 库文件较大,占用磁盘空间
因此,我们可以自己开发一个轻量、简单的C++日志记录库。
三、实现
下面是利用C++11标准库实现的一个简单的日志记录库:
#include <iostream> #include <fstream> #include <chrono> using namespace std; //日志类型 enum log_type { DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3, }; //获取日志级别字符串 string get_log_level(log_type level) { static const string levels[] = {"DEBUG", "INFO", "WARN", "ERROR"}; return levels[level]; } //获取当前时间并返回字符串格式 string get_time() { time_t now_time = chrono::system_clock::to_time_t(chrono::system_clock::now()); string time_str = ctime(&now_time); //去掉末尾的\n time_str.pop_back(); return time_str; } //日志记录函数 void log(log_type level, const string& message) { static const string file_name = "log.txt"; //打开文件 ofstream ofs(file_name, ios_base::app); if (!ofs) { cerr << "Failed to open file: " << file_name << endl; return; } //获取时间和日志级别 string time_str = get_time(); string level_str = get_log_level(level); //写入日志 ofs << "[" << time_str << "][" << level_str << "] " << message << endl; }
以上的代码中,我们定义了日志类型log_type和相应的字符串表示。然后定义了一个函数get_time(),用于获取当前时间并返回字符串格式的时间。
接下来是最重要的函数log(log_type level, const string& message),该函数用于记录日志信息。在其中,我们首先指定了日志存储的文件名log.txt,并在函数的开头打开文件并检查是否打开成功。如果出错,函数将不执行任何操作。然后,在函数的主体中,我们获取当前时间和日志级别,并将这些信息连同日志信息一起写入到文件中。
最后是一个简单的示例,该示例记录了三个不同级别的日志:
int main() { log(DEBUG, "This is a debug message."); log(INFO, "This is an info message."); log(WARN, "This is a warning message."); log(ERROR, "This is an error message."); return 0; }
该示例记录了四条日志,每条日志都包括了日志级别、时间戳和日志信息,将会被写入log.txt中:
[Sat Nov 21 14:10:57 2020][DEBUG] This is a debug message. [Sat Nov 21 14:10:57 2020][INFO] This is an info message. [Sat Nov 21 14:10:57 2020][WARN] This is a warning message. [Sat Nov 21 14:10:57 2020][ERROR] This is an error message.
四、扩展
上面的C++日志库只具有基本的功能,如果想要更好地适应不同需求,我们可能需要对其进行一些扩展。
以下是对C++日志库扩展的一些常见需求:
1. 动态设置日志级别
在实际开发中,我们可能希望可以在运行时动态更改日志级别,例如实现一个命令行参数--log-level,使得程序可以接受不同的日志级别。
class Logger { public: static Logger& instance() { static Logger logger; return logger; } void set_level(log_type level) { level_ = level; } void log(log_type level, const string& message) { if (level >= level_) { //do log } } private: log_type level_ = INFO; };
以上代码中,我们实现了一个Logger类,并将其设计为单例模式,使用instance()方法获取唯一实例。然后,我们添加了一个set_level()方法,用于动态设置日志级别,并在log()方法中增加了一个级别判断。在代码中,我们定义了一个全局日志级别变量level_,其默认值为INFO。
2. 日志异步写入
如果我们想要在日志记录和程序运行之间取得更好的平衡,我们可能会选择让程序异步写入日志文件。这样可以提高程序的响应性并提高程序的整体效率。
class AsyncLogger { public: AsyncLogger() { //start logger thread } void log(log_type level, const string& message) { lock_guardlock(mutex_); queue_.emplace(level, message); } ~AsyncLogger() { //stop logger thread } private: void logger_thread() { while (true) { unique_lock lock(mutex_); condition_.wait(lock, [this]() { return !queue_.empty() || stop_; }); if (stop_ && queue_.empty()) { break; } auto log = queue_.front(); queue_.pop(); lock.unlock(); //write log to file } } mutex mutex_; condition_variable condition_; queue<pair<log_type, string>> queue_; bool stop_ = false; thread logger_thread_; };
以上代码中,我们实现了一个AsyncLogger类,该类具有一个内部队列和一个工作线程。用户通过调用log()方法将日志消息写入队列,而工作线程负责将这些消息异步写入到日志文件中。在类的析构函数中,停止工作线程。具有停止标志stop_,当设置为True时,工作线程会退出。
3. 预处理的日志消息
预处理的日志消息允许我们将日志消息转换为其它字符序列或格式。这样,我们就可以更灵活地控制日志消息的格式和记录方式。
#define LOG(level, message) \ do {\ stringstream ss;\ ss << message;\ log(level, ss.str()); \ } while(0)
以上代码中,我们使用了一个C++的预处理器宏LOG(),该宏接受两个参数:一个日志级别和一个待记录的信息。当调用该宏时,宏展开为一个logg()函数调用,该函数将日志级别和记录的信息进行了连接。
五、总结
在这篇文章中,我们介绍了如何使用C++11标准库实现一个简单的日志记录库,该库可以直接使用,也可以基于需求进行扩展。实现一个简单的日志库并不难,我们只需要一些C++语言基础和文件流操作。但一旦你要进行扩展,例如添加异步写入、日志轮替等功能,那么你就需要更深入地了解C++11标准库的使用以及多线程编程,这是日志实现中难点之一。