目录
我们今天学习日志与策略模式。
什么是日志
计算机中的⽇志是记录系统和软件运⾏中发⽣事件的⽂件,主要作⽤是监控运⾏状态、记录异常信 息,帮助快速定位问题并⽀持程序员进⾏问题修复。它是系统维护、故障排查和安全管理的重要⼯ 具。

我们本身Linux系统内部本身就是有日志的,ll /var/log查看系统内部的日志。

但是这个格式我们特别不喜欢呀!我们今天要基于策略模式实现一个自己的日志。

线程的id就没有必要打印了,反正也是一堆地址,我们的内容就分为了前半部分系统基于我们传入的参数联合系统自己提供的内容+我们自己要打印的信息。
基于策略模式实现日志
准备工作
我们需要一个log.hpp完成我们的日志代码,log就是日志的意思,然后一个main.cc打印日志信息进行测试,makefile调用的就是之前的。

日志信息分为系统部分+自己要打印的内容,系统部分用于识别日志基本信息和哪个进程调用的等等,所以前面这个部分是基本固定的无非就是我们传的参数不同,后面由于我们要打印的内容种类很多,所以需要支持可变参数,所以估计<<需要自己重载一下,前面这个我们想要依靠比如一个函数或者一个宏函数进行调用直接打印。
cpp
int main()
{
//LOG(DEBUG) << "hello world" << 3.14 << a << b;
return 0;
}
就像上面这种形式,直传一个标志日志错误信息类型的参数给LOG函数就可以打印多个信息。
makefile编写
cpp
BIN=log
CC=g++
SRC = $(wildcard *.cc)
OBJ = $(SRC:.cc=.o)
$(BIN):$(OBJ)
$(CC) -o $@ $^ -std=c++17 -lpthread
%.o:%.cc
$(CC) -c $<
.PTHONY:clean
clean:
rm -f $(BIN) $(OBJ)
log.hpp编写
准备工作
我们这里的策略模式就是将日志打印信息打印在屏幕或者文件或者更多地方,所以如果要打印在文件里面就需要文件名和所在的路径,我们可以在logmodule命名空间里面先初始化默认的路径+文件名防止没有进行传递,日志等级也是需要写的,我们使用枚举类型进行枚举。一般有5个等级如下:
cpp
namespace logmodule
{
const string defaultlogpath = "./lg/";
const string defaultlogname = "log.txt";
//日志等级
enum class loglevel
{
DEBUG = 1, //存储更详细的软件或系统的元数据
INFO, //存储软件或系统的元数据
WARING, //告警信息
ERROR, //错误信息
FATAL //严重错误信息
};
日志需要实时显示时间,我们在全局域构建一个实时显示时间的函数方便之后调用,然后这里使用localtime_r函数进行得到存储时间的结构体 struct tm cur,为什么选择带_r的呢,因为相对比没有带_r的本体,这种带_r的函数支持多线程重入,可以让将来多个线程调用日志的时候直接调用就行不分先后顺序。localtime_r第一个参数需要一个时间戳,使用time函数返回一个时间戳,参数为nullptr,localtime_r的返回值可以不用管了。
最后将时间以统一的格式写到字符串中,年是从1900开始计算的记得加上1900,月记得加1,月的范围是[0, 11]
cpp
//获取当前系统时间
string currentime()
{
char buffer[1024];
struct tm cur;
time_t tim = time(nullptr);
localtime_r(&tim, &cur);
snprintf(buffer, sizeof(buffer), "%4d-%02d-%02d %02d:%02d:%02d",
cur.tm_year+1900,
cur.tm_mon + 1,
cur.tm_mday,
cur.tm_hour,
cur.tm_min,
cur.tm_sec);
return buffer;
}
一个日志的构成需要有1。日志字符串,2。需要有刷新落盘,就是显示位置,往哪里写入,2的默认位置和1的日志等级已经解决了。所以这也暗示了日志的打印那么一大串都是字符串。3。刷新的策略,刷新在文件里面还是显示器呀。我们接着来完成3
刷新策略编写
我们这次选择在文件中和在显示器中进行落盘,打印函数统一为synclog,将选择在显示器和文件分别进行类的封装,在各自的类中实现这个打印函数,这边是两个类都在各自的类中实现这个函数,将来打印当前方式有很多难道都在自己的类中独立实现吗,所以我们选择构建刷新策略父类,将其弄成抽象类,将本次的刷新方法构建成一个纯虚函数,然后让我们的刷新到不同地方的类继承这个刷新策略的类,然后重写纯虚函数进而实现数据的刷新,策略模式通过接口或抽象类定义算法族,降低了类之间的直接依赖。按照上面的策略就需要先构建抽象类---刷新策略父类
刷新方法类
cpp
//3.刷新策略
class logstrategy
{
public:
virtual ~logstrategy() = default;
//刷新函数
virtual void synclog(const string& message) = 0;
};
析构函数使用默认的default,唯一的纯虚函数就是synclog。
刷新在显示器的类
cpp
//3.1重写纯虚函数向控制台刷新(写入)
class console_logstrategy : public logstrategy
{
public:
console_logstrategy()
{}
void synclog(const string& message)
{
//多线程访问打印要加锁的
lockguard lockguard(mu);
cout << message << endl;
}
~console_logstrategy()
{}
private:
mutex mu;
};
因为显示器也是临界资源,将来多个线程同时向显示器打印所以为了不让打印混乱,所以需要加锁的,锁使用的就是之前写的自己封装的类,这里不说了,自己拷贝过来用就可以了。向显示器打印直接cout是不是就可以了呀。
向文件中打印的类
向文件中打印就需要知道目录+文件名,这样才可以知道具体要向哪个路径打印,所以就需要类中传入构造路径的目录+文件名,然后这个类的私有成员就是目录,和文件名,打印自然也是需要加锁的,所以私有成员也需要锁。
在构造函数中无论是使用默认值函数外部提供的,都需要判断这个目录在不在,我们可以使用系统提供的mkdir函数,或者stat,我们这次使用C++17新的文件处理库,filesystem库,这个里面就可以判断一个文件目录在不在和创建一个文件目录。exits的返回值是bool,如果目录在就返回了,接着如果不存在就需要创建一个目录,使用create_directories可以创建一个自己传入的指定目录,用try包裹起来,catch捕获错误信息,filesystem_error会存储所有调用fileststem库函数中的错误,e.what()打印错误,接着重写synclog方法,有路径了,写入文件的方式有很多,但是我们这里直接使用文件输出流ofstream
,ofstream
是 C++ 标准库中的一个类,用于将数据写入文件(文件输出流,Output File Stream )。它属于 <fstream>
头文件,是 std::ostream
的子类,提供了文件操作的功能。关键在于 "输入(Input)" 和 "输出(Output)" 是相对于程序而言的 ,而不是相对于用户或文件。ofstream
(Output File Stream) :程序 输出 数据到文件(程序 → 文件)。
接着构造这个文件,让这个输出流同步到这个文件路径,相当于写入到这个文件,然后选择追加形式写入,所以是ios::app,写入之前要使用is_open判断是不是打开的,接着写入流的方式也很多,可以使用write系统调用,我们使用最简单的形式,这个是流对吧,直接可以使用<<,这个流放左边<<,直接可以写入这个流。写完了记得调用close关闭。
cpp
//3.2重写纯虚函数向文件中刷新(写入)
class filelogstrategy : public logstrategy
{
public:
filelogstrategy(const string& logpath = defaultlogpath, const string& logname = defaultlogname)
:_logpath(logpath)
,_logname(logname)
{
//先检查一下这个路径存不存在,可以使用mkdir,stat
//在 C++ 中处理文件系统操作,可以使用 <filesystem> 库
if (filesystem::exists(_logpath))
{
//如果存在就返回
return;
}
try
{
//没有就创建一个当前目录
filesystem::create_directories(_logpath);
}
catch(const filesystem::filesystem_error e)
{
std::cerr << e.what() << '\n';
}
}
void synclog(const string& message)
{
lockguard lockguard(mu);
//写入文件我们使用文件流写入,写入文件流
string log = _logpath + _logname; //当前路径=文件目录+文件名
//std::ofstream 是 C++ 标准库中用于文件输出的类(定义在 <fstream> 头文件中)。
//
ofstream out(log, ios::app); //日志写入一定是追加的
//下面这个返回值:bool 类型
if (!out.is_open())
{
return;
}
out << message << "\n";
out.close();
}
~filelogstrategy()
{}
private:
string _logpath;
string _logname;
mutex mu;
};
锁会调用自己的析构函数,其他的不需要析构,所以相当于不需要写析构函数。
日志类---定义刷新方式
终于要写日志信息了吗,我们要让别人可以选择这个刷新的方式,也是要让这个刷新方式类可以切换呀,刷新方式类如果毫无关系怎么切?所以上面这个策略模式设计的优点的体现了,我们不同的刷新方式类是抽象类的子类吧,子类可以构造父类吧,这个抽象类(刷新策略类)可以使用不同的实现方法子类进行构造,根据多态原理,对象始终是子类类型,只是能通过父类接口访问,也就是说日志类中私有成员是父类的话,构造函数用不同的子类构造就是得到不同的子类,不就实现了不同的刷新方式了吗?所以之前为什么通过抽象类的方式重写刷新函数,就是为了指定刷新方式方便!!!
cpp
class logger
{
public:
logger()
{
_strategy = make_shared<console_logstrategy>();
}
void Enableconsolelog()
{
_strategy = make_shared<console_logstrategy>();
}
void Eablefilelog()
{
//构造智能指针指向的对象可以直接使用缺省值
_strategy = make_shared<filelogstrategy>();
}
~logger()
{}
private:
shared_ptr<logstrategy> _strategy;
};
抽象类虽然不能定义对象但是还是可以定义一个智能指针指向它的。构造默认使用显示器打印,调用不同的函数进行指向不同的刷新方法类,可以使用缺省值直接不用传参构造。
假设选择了不同的刷新方式,那接着要进行日志信息的书写了,我们将日志信息打印成一个字符串然后输出的,使用logmessage类进行日志信息的拼接。由于信息的书写和指定哪个刷新方式的类是强相关的,所以将logmessage类内嵌在logger里面,作为内部类,拼接完直接选择输出方式比较方便。
logmessage构造日志内容

构造日志信息
可以看到上面的打印一条信息全是括号包裹的部分是限制死的,所以直接初始化时就可以打印了,后面这个这些是不是通过自己<<输入的,限制死的部分传入相关的参数就可以进行变动了,传入的参数就是日志等级,调用日志文件名和行号呀。一个日志的私有成员就是用括号框起来的这些,再加上保存一条完整的信息的字符串。打印入字符串先使用字符串流,打印入内存内部的字符串,然后再使用str提取这个字符串直接赋值给_loginfo。
cpp
private:
string _currtime; //当前日志时间
loglevel _level; //日志等级
pid_t _pid; //当前进程pid
string _filename; //源文件名称
int _line; //日志所在的行号
string _loginfo; //一条完整的信息
};
可以注意到由于枚举类型原本看上去是字符串,但是打印处理的是底层的整形数字,所以还需要函数强转一下。如下函数提取成字符串,也可以使用unordered_map。
cpp
//转枚举类型为字符型,也可以使用unordered_map
//在 C++ 中,枚举类型(enum)默认情况下打印会隐式转换为整型
string leveltostring(loglevel level)
{
switch(level)
{
case loglevel::DEBUG:
return "DEBUG";
case loglevel::INFO:
return "INFO";
case loglevel::WARING:
return "WARING";
case loglevel::ERROR:
return "ERROR";
case loglevel::FATAL:
return "FATAL";
default:
return "NONE";
}
}
cpp
class logmessage
{
public:
logmessage(loglevel level, const string& filename, int line)
:_currtime(currentime())
,_level(level)
,_pid(::getpid())
,_filename(filename)
,_line(line)
{
stringstream ssbuffer;
ssbuffer << "[" << _currtime << "]"
<< "[" << leveltostring(_level) << "]"
<< "[" << _pid << "]"
<< "[" << _filename << "]"
<< "[" << _line << "]";
_loginfo = ssbuffer.str();
}
自己灵活输入打印的内容是输入流和前面的限制死的是不是要同时打印给一个字符串_loginfo保存起来。所以就需要再logmessage内部重写<<方法。
重载operator<<
cpp
template<class T>
logmessage& operator<< (const T& info)
{
stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
使用追加写入,返回日志信息是因为可能会紧跟着很多个<<,防止找不到打印对象。
重载operator()
打印信息的方式是系统死板的部分+自己打印的信息,外面调用的方式LOG(level)+<<若干个自己打印的信息,但是LOG(level)要打印系统死板内容就需要调用logmessage的构造函数没错吧,很显然logmessage的构造函数有3个呀,要传入3个呀,说明LOG(level)是宏函数呀,然后怎么调到logmessage的构造函数呢,就需要宏函数的实际调用目标是一个对象(那3个参数),也就意味着外面又需要重载operator(),让这个operator()里面返回一个调用构造函数的对象,这里一定得是拷贝返回不能是引用。
构造函数的3个参数里面的调用行号可以使用宏__LINE__,调用文件可以使用宏__FILE__。
__LINE__
是 C/C++ 中的一个 预定义宏 (Predefined Macro),它会在编译时被替换为当前代码所在的 行号(整数类型)。它常用于调试、日志记录或错误报告中,帮助开发者快速定位代码位置。
__FILE__
是 C/C++ 中的一个 预定义宏 (Predefined Macro),它会在编译时被替换为当前源文件的 文件名(字符串) 。通常与 __LINE__
结合使用,用于调试、日志记录或错误追踪,帮助开发者快速定位代码位置。
行号的类型默认是int,调用文件名的类型默认是string。用这两个宏是不是就知道是在哪行调用的LOG(level),进而知道了是在哪行调用的构造函数,进而知道了是哪行什么文件调用的生成日志信息了呀。
cpp
//同样是构造LOG,所以需要传递那三个参数
//log(...) 是对象调用,只会查找 logger 类的成员
logmessage operator() (loglevel level, const string& filename, int line)
{
return logmessage(level, filename, line);
}
logger log;
//一个对象不能被构造两次,没有带类名肯定先匹配重载函数
#define LOG(level) log(level, __FILE__, __LINE__)
调用刷新策略
怎么调用刷新策略呀,调用完LOG(level)之后是不是相当于调用了logmessage的构造函数,然后log这里再调用了operator(),返回了一个临时的logmessage然后在这里面完成了日志信息的全部构造组装,组装好的一条日志信息就在_loginfo,之后由于外面之前operator里面创建的是临时对象,相当于_loginfo完整生成完后就析构了,这时知道为什么operator()不能返回引用了吧,就是要让其调用析构函数,这时在logmessage的析构函数里面进行调用刷新策略,刷新策略在logger里面呀,所以logmessage类需要添加一个logger对象,然后operator()构造时希望直接传入*this指代logger比较省事,所以这个operator()得在logger域里面,再说了我们不是拿着log对象去调用的operator(),虽然是内部类operator()写在logmessage还是会找不到的,因为C++检查类方法只会在自己的类域里面找,内部类不属于自己的类域是看不见的。
如果logger指定了刷新方法就直接调用了,记得传入_loginfo。
cpp
//析构函数里面刷新,相当于写入完了再刷新
~logmessage()
{
if (_logger._strategy)
{
_logger._strategy->synclog(_loginfo);
}
}
cpp
class logmessage
{
public:
logmessage(loglevel level, const string& filename, int line, logger& logger)
:_currtime(currentime())
,_level(level)
,_pid(::getpid())
,_filename(filename)
,_line(line)
,_logger(logger)
{
stringstream ssbuffer;
ssbuffer << "[" << _currtime << "]"
<< "[" << leveltostring(_level) << "]"
<< "[" << _pid << "]"
<< "[" << _filename << "]"
<< "[" << _line << "]";
_loginfo = ssbuffer.str();
}
template<class T>
logmessage& operator<< (const T& info)
{
stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
//析构函数里面刷新,相当于写入完了再刷新
~logmessage()
{
if (_logger._strategy)
{
_logger._strategy->synclog(_loginfo);
}
}
private:
string _currtime; //当前日志时间
loglevel _level; //日志等级
pid_t _pid; //当前进程pid
string _filename; //源文件名称
int _line; //日志所在的行号
string _loginfo; //一条完整的信息
logger& _logger; //负责不同的策略刷新
};
//同样是构造LOG,所以需要传递那三个参数
//log(...) 是对象调用,只会查找 logger 类的成员
logmessage operator() (loglevel level, const string& filename, int line)
{
return logmessage(level, filename, line, *this);
}
private:
shared_ptr<logstrategy> _strategy;
};
logger log;
//一个对象不能被构造两次,没有带类名肯定先匹配重载函数
#define LOG(level) log(level, __FILE__, __LINE__)
};
有人就又有疑问了,你怎么指定log(...)一定调用的就是operator有可能直接匹配log的构造函数呀,这个不可能的,因为上面的logger log已经构造一遍了,不能构造两次,其次C++规定对象()就是默认调用operator(),只有类 对象()才是调用的构造函数。类内部不可能调用重载方法。
整体代码展示
cpp
#pragma once
#include<iostream>
#include<string>
#include<unistd.h>
#include<filesystem> //
#include<fstream> //
#include<time.h> //
#include<sstream> //
#include<memory> //
#include"mutex.hpp"
using namespace std;
using namespace lockmodule;
namespace logmodule
{
const string defaultlogpath = "./log/";
const string defaultlogname = "log.txt";
//日志等级
enum class loglevel
{
DEBUG = 1, //存储更详细的软件或系统的元数据
INFO, //存储软件或系统的元数据
WARING, //告警信息
ERROR, //错误信息
FATAL //严重错误信息
};
//获取当前系统时间
string currentime()
{
char buffer[1024];
struct tm cur;
time_t tim = time(nullptr);
localtime_r(&tim, &cur);
snprintf(buffer, sizeof(buffer), "%4d-%02d-%02d %02d:%02d:%02d",
cur.tm_year+1900,
cur.tm_mon + 1,
cur.tm_mday,
cur.tm_hour,
cur.tm_min,
cur.tm_sec);
return buffer;
}
//转枚举类型为字符型,也可以使用unordered_map
//在 C++ 中,枚举类型(enum)默认情况下打印会隐式转换为整型
string leveltostring(loglevel level)
{
switch(level)
{
case loglevel::DEBUG:
return "DEBUG";
case loglevel::INFO:
return "INFO";
case loglevel::WARING:
return "WARING";
case loglevel::ERROR:
return "ERROR";
case loglevel::FATAL:
return "FATAL";
default:
return "NONE";
}
}
//3.刷新策略
class logstrategy
{
public:
virtual ~logstrategy() = default;
//刷新函数
virtual void synclog(const string& message) = 0;
};
//3.1重写纯虚函数向控制台刷新(写入)
class console_logstrategy : public logstrategy
{
public:
console_logstrategy()
{}
void synclog(const string& message)
{
//多线程访问打印要加锁的
lockguard lockguard(mu);
cout << message << endl;
}
~console_logstrategy()
{}
private:
mutex mu;
};
//3.2重写纯虚函数向文件中刷新(写入)
class filelogstrategy : public logstrategy
{
public:
filelogstrategy(const string& logpath = defaultlogpath, const string& logname = defaultlogname)
:_logpath(logpath)
,_logname(logname)
{
//先检查一下这个路径存不存在,可以使用mkdir,stat
//在 C++ 中处理文件系统操作,可以使用 <filesystem> 库
if (filesystem::exists(_logpath))
{
//如果存在就返回
return;
}
try
{
//没有就创建一个当前目录
filesystem::create_directories(_logpath);
}
catch(const filesystem::filesystem_error e)
{
std::cerr << e.what() << '\n';
}
}
void synclog(const string& message)
{
lockguard lockguard(mu);
//写入文件我们使用文件流写入,写入文件流
string log = _logpath + _logname; //当前路径=文件目录+文件名
//std::ofstream 是 C++ 标准库中用于文件输出的类(定义在 <fstream> 头文件中)。
//
ofstream out(log, ios::app); //日志写入一定是追加的
//下面这个返回值:bool 类型
if (!out.is_open())
{
return;
}
out << message << "\n";
out.close();
}
~filelogstrategy()
{}
private:
string _logpath;
string _logname;
mutex mu;
};
//4。日志类---构造日志内容并且指定刷新策略
//log类里面主要是指定刷新策略的
class logger
{
public:
logger()
{
_strategy = make_shared<console_logstrategy>();
}
void Enableconsolelog()
{
_strategy = make_shared<console_logstrategy>();
}
void Eablefilelog()
{
//构造智能指针指向的对象可以直接使用缺省值
_strategy = make_shared<filelogstrategy>();
}
~logger()
{}
//logmessage内部类构造日志内容
class logmessage
{
public:
logmessage(loglevel level, const string& filename, int line, logger& logger)
:_currtime(currentime())
,_level(level)
,_pid(::getpid())
,_filename(filename)
,_line(line)
,_logger(logger)
{
stringstream ssbuffer;
ssbuffer << "[" << _currtime << "]"
<< "[" << leveltostring(_level) << "]"
<< "[" << _pid << "]"
<< "[" << _filename << "]"
<< "[" << _line << "]";
_loginfo = ssbuffer.str();
}
template<class T>
logmessage& operator<< (const T& info)
{
stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
//析构函数里面刷新,相当于写入完了再刷新
~logmessage()
{
if (_logger._strategy)
{
_logger._strategy->synclog(_loginfo);
}
}
private:
string _currtime; //当前日志时间
loglevel _level; //日志等级
pid_t _pid; //当前进程pid
string _filename; //源文件名称
int _line; //日志所在的行号
string _loginfo; //一条完整的信息
logger& _logger; //负责不同的策略刷新
};
//同样是构造LOG,所以需要传递那三个参数
//log(...) 是对象调用,只会查找 logger 类的成员
logmessage operator() (loglevel level, const string& filename, int line)
{
return logmessage(level, filename, line, *this);
}
private:
shared_ptr<logstrategy> _strategy;
};
logger log;
//一个对象不能被构造两次,没有带类名肯定先匹配重载函数
#define LOG(level) log(level, __FILE__, __LINE__)
};
main.cc编写
cpp
#include"log.hpp"
using namespace logmodule;
int main()
{
//LOG(DEBUG) << "hello world" << 3.14 << a << b;
LOG(loglevel::DEBUG) << "hello word";
LOG(loglevel::DEBUG) << "hello word";
LOG(loglevel::DEBUG) << "hello word";
LOG(loglevel::DEBUG) << "hello word";
LOG(loglevel::DEBUG) << "hello word";
return 0;
}
调用形式如我们所愿
测试运行结果

由于logger里面默认的往显示器刷新了,所以5条信息都是整成刷新的。
如果要向不同的文件中打印就再指定不同的宏函数帮我们完成。
cpp
//一个对象不能被构造两次,没有带类名肯定先匹配重载函数
#define LOG(level) log(level, __FILE__, __LINE__)
#define ENABLE_FILE() log.Eablefilelog()
#define ENBALE_CONSOLE() log.Enableconsolelog()
};

目录不要跟可执行文件重名了,不然创建不出来的!!!
什么是策略模式
上面演示完了什么是策略模式了,策略模式就是一个基类提供统一的方法,这个策略是某种策略的实现,根据不同的策略调用这个方法完成具体实现,完成基类和方法类进行绑定!!!