深入了解linux系统—— 日志
日志
在之前写代码的过程中,测试代码都是之间像显示器上输出内容;
当多线程像显示器文件输出时,由于没有做任何的防护,就有可能导致多线程输出信息混在一起,不方便观察。
而计算机中的日志记录系统和软件运行中发生事件的文件,作用就是:监控运行状态,记录异常信息,帮助快速定位问题并支持程序员进行问题修复。
是系统维护、故障排查和安全管理的重要工具。
简单来说:日志就像生活中的日记一样,记录程序运行时运行状态、异常信息等等。
对于一个合格的日志,要具有以下指标:
时间戳、日志等级、日志内容
文件名、行号、进程/线程
id
最主要的就是时间戳、日志等级和日志内容。
一般来说,日志等级可以分为:
DEBUG:调试信息
INFO:正常输出
WARNING:告警信息
ERROR:错误信息(能够运行结束)
FATAL:错误(不能运行:打开文件失败等等)
日志有现成的解决方案,spdlog
、glog
、Boost.Log
等等;
这里自定义实现一个日志(采用设计模式 - 策略模式)。
刷新策略
要自定义实现一个日志,这里首先来实现一种刷新策略;
假设现在存在一条日志信息,可以刷新到显示器文件中(显示器策略)、也可以刷新到指定文件中(文件策略)
这里,我们就可以设计一个基类:
logflush
,其中存在一个虚函数flush
;对于一种刷新策略,就要继承基类
logflush
并重写flush
方法实现自己的刷新策略。
class logflush{virtual void flush(std::string massage) = 0;~logflush() = delete;};
显示器刷新策略
向显示器文件中刷新,这里直接使用std::cout
即可
注意:日志可以被多线程使用,显示器文件就是临界资源,要对临界区进行加锁(这里就使用之间封装的Mutex
、lockgroup
)
// 显示器刷新class displayflush : public logflush{void flush(std::string massage) override{lockgroup(_mutex);std::cout << massage << std::endl;}private:Mutex _mutex;};
文件刷新策略
向文件中刷新,首先要先打开这个文件,我们就要知道该文件的路径、文件名。
要打开一个文件、如果该文件不存在,调用open
时可以新建;但是,如果路径不存在,我们这里调用就会出错。
所以,我们首先要做的是:判断文件路径是否存在,如果该路径不存在,就要新建。
这里可以使用
std::filesystem:exists
来判断一个路径是否存在(路径存在返回true
,不存在返回false
)使用
std::filesystem::create_directories
来创建一个路径。
static std::string default_path = "./log";static std::string default_name = "log.log";const std::string gsep = "\r\n";class fileflush : public logflush{public:fileflush(const std::string &path = default_path, const std::string &name = default_name): _path(path), _name(name){if (std::filesystem::exists(path)){return;}// 路径不存在,创建try{std::filesystem::create_directories(_path);}catch (const std::filesystem::filesystem_error &e){std::cerr << e.what() << gsep;}}private:std::string _path;std::string _name;std::string _pathname;Mutex _mutex;};
然后就是重写flush
方法:
在打开目标文件之前,要对文件路径和名称进行合并,生成文件绝对路径。
要打开目标文件(以追加方式打开,文件不存在就创建)(这里打开文件可以使用:std::ofstream out(_pathname, std::ios::app);
C++17支持,std::ios::app
表示以追加方式打开文件)
然后就是将日志信息输出到目标文件中,为了方便输出,这里定义一个结尾符:const std::string gsep = "\r\n";
最后关闭文件。
为了保证线程安全,要进行加锁
void flush(std::string massage) override{lockgroup lgp(_mutex);_pathname = _path + (_path.back() == '/' ? ' ' : '/') + _name;std::ofstream out(_pathname, std::ios::app);if (!out.is_open()){// 打开文件失败return;}out << massage << gsep;}
日志信息
1. 构建日志
有了日志刷新策略,现在来实现日志log
;
要实现日志,首先就要有上述的刷新策略,这里默认使用显示器刷新策略:
class Log{public:Log(){_log = std::make_unique<displayflush>();}void EnableDisplayFlush(){_log = std::make_unique<displayflush>();}void EnableFileFlush(){_log = std::make_unique<fileflush>();}private:std::unique_ptr<logflush> _logflush;}
有了上述刷新策略,现在来看日志信息:
[2025-8-24 22:42:35] [DEBUG] [641189] [test.cc] [12] log.txt 2015-8-24
[2025-8-24 22:42:35] [DEBUG] [641189] [test.cc] [13] hello
这里预期的日志信息如上,在一条日志中有存在时间,日志等级,进程pid
,文件名,行号,信息等。
所以,我们就要实现获取时间的接口函数GetTime
,以及日志等级enum class Level
这里,
Level
枚举类型默认输出是整型,我们想要以DEBUG
、INFO
这样的形式输出,就需要提供一个方法根据日志等级获取相对对应字符串。
std::string GetTime(){time_t tm = time(nullptr);struct tm curr;localtime_r(&tm, &curr);std::stringstream ss;ss << curr.tm_year + 1900 << "-"<< curr.tm_mon + 1 << "-"<< curr.tm_mday << " "<< curr.tm_hour << ":"<< curr.tm_min << ":"<< curr.tm_sec;return ss.str();}enum class Level{DEBUG,INFO,WARNING,ERROR,FATAL};std::string GetLevel(Level level){switch (level){case Level::DEBUG:return "DEBUG";case Level::INFO:return "INFO";case Level::WARNING:return "WARNING";case Level::ERROR:return "ERROR";case Level::FATAL:return "FATAL";default:return "UNKONW";}}
有了上述这些内容,现在来实现一条日志信息logmassage
(将其设计成Log
内部类)
一条日志,要具有 时间、日志等级、进程id、文件名、行号,日志信息;
class Logmassage{public:private:std::string _time; // 日志时间Level _level; // 日志等级pid_t _pid; // 进程idstd::string _filename; // 文件名int _line; // 行号std::string _logmassage; // 完整的日志信息Log *_log; // log指针,方便刷新日志信息};
这里像日志等级,文件名、行号等不能自行获取的,就要通过构造函数参数传递进来;
然后根据这些信息,构建出来完整的日志信息。
构建完整的日志信息,可以使用
C语言
中的sprintf/snprintf
来实现;这里使用
C++
中的stringstream
类。
class Logmassage{public:Logmassage(const std::string &time, Level level, const std::string &filename, int line, Log *plog): _time(time), _level(level), _pid(getpid()), _filename(filename), _line(line), _log(log){std::stringstream ss;ss << '[' << _time << ']'<< '[' << GetLevel(_level) << ']' /*枚举类型,默认是整型*/<< '[' << _pid << ']'<< '[' << _filename << ']'<< '[' << _line << "] : ";_logmassage = ss.str();}private:std::string _time; // 日志时间Level _level; // 日志等级pid_t _pid; // 进程idstd::string _filename; // 文件名int _line; // 行号std::string _logmassage; // 完整的日志信息Log *_log; // log指针,方便刷新日志信息};
这里在Logmassage
类中存在Log* _log
的指针,方便进行日志信息刷新
2. 输入日志信息
上述已经完成了整个日志的框架,但是还缺少信息;
这里想要实现的使用日志的方式,就像cout <<
这样使用<<
来输入日志信息,所以就要实现operator<<
方法;
并且,可以连续使用多个<<
,在使用operator
时,要将返回值设置成Logmassage&
。
template <typename T>Logmassage &operator<<(const T &data){std::stringstream ss;ss << data;_logmassage += ss.str();return *this;}
最后,为了方便使用在Loamassage
析构方法中,刷新该日志信息;
~Logmassage(){if (_log->_logflush){_log->_logflush->flush(_logmassage);}}
3. 使用日志
到现在,就已经将日志大概实现了出来;
但是,按照现在实现的日志,我们使用起来存在问题:
创建
Logmassage
就需要存在一个已经有的Log
指针,而我们使用日志就要先创建Log
对象。
所以这里就要实现一个仿函数,在调用Log()
时,用来构建Logmassage
对象并返回。
Logmassage operator()(const std::string &time, Level level, const std::string &filename, int line){return Logmassage(time, level, filename, line, this);}
但是,就算实现了仿函数,我们要使用该日志时,还是非常麻烦的,需要传递什么时间,日志等级,文件名,行号,有没有更加简单粗暴的,就想要只传递时间,后面跟上<< 信息
就可以使用日志的?
LOG(Level::DEBUG) << "hello log";
当然是可以实现的,时间需要调用GetTime
方法;文件名和行号,我们知道宏__FILE__
和__LINE__
指的就是文件名和行号;
所以,我们就可以实现一个宏,调用是只需传递日志等级,就可以使用日志。
Log log;
#define LOG(level) Log(GetTime(), level, __FILE__,__LINE__)
并且将log
定义成全局的,在使用时只需使用即可。