日志
在之前写代码的过程中,测试代码都是之间像显示器上输出内容;
当多线程像显示器文件输出时,由于没有做任何的防护,就有可能导致多线程输出信息混在一起,不方便观察。
而计算机中的日志记录系统和软件运行中发生事件的文件,作用就是:监控运行状态,记录异常信息,帮助快速定位问题并支持程序员进行问题修复。
是系统维护、故障排查和安全管理的重要工具。
简单来说:日志就像生活中的日记一样,记录程序运行时运行状态、异常信息等等。
对于一个合格的日志,要具有以下指标:
时间戳、日志等级、日志内容
文件名、行号、进程/线程
id
最主要的就是时间戳、日志等级和日志内容。
一般来说,日志等级可以分为:
DEBUG:调试信息
INFO:正常输出
WARNING:告警信息
ERROR:错误信息(能够运行结束)
FATAL:错误(不能运行:打开文件失败等等)
日志有现成的解决方案,spdlog
、glog
、Boost.Log
等等;
这里自定义实现一个日志(采用设计模式 - 策略模式)。
刷新策略
要自定义实现一个日志,这里首先来实现一种刷新策略;
假设现在存在一条日志信息,可以刷新到显示器文件中(显示器策略)、也可以刷新到指定文件中(文件策略)
这里,我们就可以设计一个基类:
logflush
,其中存在一个虚函数flush
;对于一种刷新策略,就要继承基类
logflush
并重写flush
方法实现自己的刷新策略。
cpp
class logflush
{
virtual void flush(std::string massage) = 0;
~logflush() = delete;
};
显示器刷新策略
向显示器文件中刷新,这里直接使用std::cout
即可
注意:日志可以被多线程使用,显示器文件就是临界资源,要对临界区进行加锁 (这里就使用之间封装的Mutex
、lockgroup
)
cpp
// 显示器刷新
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
来创建一个路径。
cpp
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";
最后关闭文件。
为了保证线程安全,要进行加锁
cpp
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
;
要实现日志,首先就要有上述的刷新策略,这里默认使用显示器刷新策略:
cpp
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
这样的形式输出,就需要提供一个方法根据日志等级获取相对对应字符串。
cpp
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、文件名、行号,日志信息;
cpp
class Logmassage
{
public:
private:
std::string _time; // 日志时间
Level _level; // 日志等级
pid_t _pid; // 进程id
std::string _filename; // 文件名
int _line; // 行号
std::string _logmassage; // 完整的日志信息
Log *_log; // log指针,方便刷新日志信息
};
这里像日志等级,文件名、行号等不能自行获取的,就要通过构造函数参数传递进来;
然后根据这些信息,构建出来完整的日志信息。
构建完整的日志信息,可以使用
C语言
中的sprintf/snprintf
来实现;这里使用
C++
中的stringstream
类。
cpp
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; // 进程id
std::string _filename; // 文件名
int _line; // 行号
std::string _logmassage; // 完整的日志信息
Log *_log; // log指针,方便刷新日志信息
};
这里在Logmassage
类中存在Log* _log
的指针,方便进行日志信息刷新
2. 输入日志信息
上述已经完成了整个日志的框架,但是还缺少信息;
这里想要实现的使用日志的方式,就像cout <<
这样使用<<
来输入日志信息,所以就要实现operator<<
方法;
并且,可以连续使用多个<<
,在使用operator
时,要将返回值设置成Logmassage&
。
cpp
template <typename T>
Logmassage &operator<<(const T &data)
{
std::stringstream ss;
ss << data;
_logmassage += ss.str();
return *this;
}
最后,为了方便使用在Loamassage
析构方法中,刷新该日志信息;
cpp
~Logmassage()
{
if (_log->_logflush)
{
_log->_logflush->flush(_logmassage);
}
}
3. 使用日志
到现在,就已经将日志大概实现了出来;
但是,按照现在实现的日志,我们使用起来存在问题:
创建
Logmassage
就需要存在一个已经有的Log
指针,而我们使用日志就要先创建Log
对象。
所以这里就要实现一个仿函数,在调用Log()
时,用来构建Logmassage
对象并返回。
cpp
Logmassage operator()(const std::string &time, Level level, const std::string &filename, int line)
{
return Logmassage(time, level, filename, line, this);
}
但是,就算实现了仿函数,我们要使用该日志时,还是非常麻烦的,需要传递什么时间,日志等级,文件名,行号 ,有没有更加简单粗暴的,就想要只传递时间,后面跟上<< 信息
就可以使用日志的?
cpp
LOG(Level::DEBUG) << "hello log";
当然是可以实现的,时间需要调用GetTime
方法;文件名和行号,我们知道宏__FILE__
和__LINE__
指的就是文件名和行号;
所以,我们就可以实现一个宏,调用是只需传递日志等级,就可以使用日志。
cpp
Log log;
#define LOG(level) Log(GetTime(), level, __FILE__,__LINE__)
并且将log
定义成全局的,在使用时只需使用即可。