app开发 上传wordpressseo站外推广

news/2025/10/8 19:25:20/文章来源:
app开发 上传wordpress,seo站外推广,网站模板 数据库,wordpress运行流程日志系统的功能也就是将一条消息格式化后写入到指定位置#xff0c;这个指定位置一般是文件#xff0c;显示器#xff0c;支持拓展到数据库和服务器#xff0c;后面我们就知道如何实现拓展的了#xff0c;支持不同的写入方式(同步异步)#xff0c;同步:业务线程自己写到文…        日志系统的功能也就是将一条消息格式化后写入到指定位置这个指定位置一般是文件显示器支持拓展到数据库和服务器后面我们就知道如何实现拓展的了支持不同的写入方式(同步异步)同步:业务线程自己写到文件中也就是open一个文件如何调用write。异步:业务线程不负责打开文件和调用write写数据而是用一个vector容器保存数据业务线程往里面写然后让另一个线程去写。 日志输出我们用日志器对象来完成听不懂没关系后面我们就知道项目有哪些模块了。 一 常用小功能模块 后面项目实现有些小功能时常用到我们先实现了方便后面使用。 模块实现 获取日期静态接口是为了免去实例化对象。 namespace logs {namespace util{// 1 获取系统时间class Date{public:static time_t now(){return (time_t)time(nullptr);}};}; }; 具体实现 namespace logs {namespace util{// 2 判断文件是否存在// 3 获取文件所在路径// 4 创建目录class file{public:static bool exits(const std::string filename){return access(filename.c_str(), F_OK) 0; 返回0表示找到了}static std::string pathname(const std::string path){int pos path.find_last_of(/\\,-1);std::string ret path.substr(0, pos);return ret;}static bool CreateDreactor(const std::string path){int pos 0;int front 0;while(pos ! -1){pos path.find_first_of(/\\,pos);// ./test/test.cstd::string pathname path.substr(0,pos);if(pos -1)//假如path test.c{mkdir(pathname.c_str(),0777);}elsepos pos 1; 继续往后找目录分隔符if(exits(pathname)) 如果目录已经存在就不创建了continue;mkdir(pathname.c_str(),0777);} return true;}};}; }; 这个access函数可以根据选项判断文件是否存在是否可以被读被写。不过这个是linux下的系统调用windows无法识别我们可以用一个通用接口来代替。 使用如下。不用考虑struct stat是是什么我们看返回值判断文件是否存在。 比较复杂的就是创建目录函数第一次截取./abc创建第二次截取./abc/bcd再创建这没问题吗?没有mkdir就是一层一层创建的。所以我们要用mkdir创建./abc/bcd/test.c要先mkdir  ./abc然后mkdir  ./abc/bcd最后mkdrir ./abc/bcd/test.c。 由此得substr的第二个参数这里必须pos,不能是pos1,当我们要创建 ./test/test2的时候可是最后一次find返回-1此时如果substr(0,pos1)就会什么都截取不了。 功能测试 测试结果: 二 日志等级类模块 模块实现 1 定义各个日志等级的宏 namespace logs {class loglevel{public:enum class level{UNKNOW 0,//未知等级错误DEBUG,//进行debug调试的日志信息INFO,//用户提示信息WARN,//警告信息ERROR,//错误信息FATAL,//致命错误信息OFF,//关闭日志};};}; enum class value?不是直接enum value吗实际上enum class value是c11新推出的它的区别在于内部定义的宏是有作用域的要使用必须level::DEBUG。 也就是说像enum Date内部的成员是在类似全局域任何地方都可以直接使用所以像上图那样枚举成员重复就会报错但是用enum class就没事因为此时成员都被划分在各自的类域内了。 2 提供一个接口将宏转为字符串为什么不一开始定义成字符串呢?不行一开始必须是整型因为我们将来需要等级之间用来比较如果是字符串不好比较为什么要比较呢因为我们需要设置一个功能那就是项目日志门槛功能我们可以设置项目运行时日志只有高于某个等级才可以输出就可以过滤很多不必要的消息。 namespace logs {class loglevel{public:static std::string to_String(const level lv)必须是const的不然外部无法传递,如果不是引用传递则无所谓{switch(lv){case level::DEBUG:{ return DEBUG;break;}case level::INFO:{return INFO;break;}case level::WARN:{return WARN;break;}case level::ERROR:{return ERROR;break;}case level::FATAL:{return FATAL;break;}case level::OFF:{return FATAL;break;}default:{return UNKONW;break;}}};};}; 功能测试 void test2()//测试levels.hpp内的小组件 {std::coutlogs::loglevel::to_String(logs::loglevel::level::DEBUG)std::endl;std::coutlogs::loglevel::to_String(logs::loglevel::level::ERROR)std::endl;std::coutlogs::loglevel::to_String(logs::loglevel::level::FATAL)std::endl;std::coutlogs::loglevel::to_String(logs::loglevel::level::INFO)std::endl;std::coutlogs::loglevel::to_String(logs::loglevel::level::UNKNOW)std::endl;std::coutlogs::loglevel::to_String(logs::loglevel::level::WARN)std::endl;std::coutlogs::loglevel::to_String(logs::loglevel::level::OFF)std::endl; }三 日志消息类设计 时间等级和主体消息都是日志信息的重要部分时间和等级还可以用来过滤尽可能减少不必要信息的干扰。 下面这个类就是日志消息类 namespace logs {struct LogMsg{LogMsg(loglevel::level level,const std::string file,const std::string logger,const std::string payload,size_t line): _time(util::Date::now())//复用util.hpp中的now函数实现,_level(level),_file(file),_line(line),_tid(std::this_thread::get_id()),_payload(payload),_logger(logger){;}public:time_t _time;//日志时间loglevel::level _level;//日志等级,要指定类域std::string _file;//文件名size_t _line;//行号std::string _logger;//日志器名称std::string _payload;//日志消息主体std::thread::id _tid;//线程id}; }; 可是光有日志消息不行啊我们还得将上述元素排列格式化好也就是格式化上述准备的元素接下来就来实现一个格式化类。 四 格式化模块 模块实现 从这里开始就有点不好理解了实现这个模块只要大致了解提供的接口实现完后再来理解就清晰多了。 我们要对一条消息进行格式化也就是对一条消息的各个部分进行格式化所以我们把对一整个日志消息类的格式化变成对各个部分格式化这部分的实现我称为格式化子项类的实现。即便下面各个类的函数成员是开放的但是每个类之间的函数还是处于不同的类域就不会出现命名冲突的问题所以每当我们想写个函数就顺便写个类封装起来。 成员如下一个成员一个格式化子项类 // time_t _time;//日志时间 // loglevel::level _level;//日志等级,要指定类域 // std::string _file;//文件名 // size_t _line;//行号 // std::string _logger;//日志器名称 // std::string _payload;//日志消息主体 // std::thread _tid;//线程id格式化子项类 namespace logs {class Format{public:using ptr std::shared_ptrFormat; c11支持的取别名virtual void format(std::ostream out,const logs::LogMsg lsg) 0;};等级处理函数class LevelFormat:public Format{public:void format(std::ostream out,const logs::LogMsg lsg)override{outlogs::loglevel::to_String(lsg._level);}这里就复用了先前实现的将等级常量转字符串的方法。};时间处理函数class TimeFormat:public Format{public:TimeFormat(const std::string pattern):_pattern(pattern){;}void format(std::ostream out,const logs::LogMsg lsg)override{struct tm st;localtime_r(lsg._time, st); 将时间戳转为tm结构体char arr[32] {0};strftime(arr, sizeof(arr), _pattern.c_str(),st);将tm结构体内的时间按指定格式转到arr数组中,显然这个格式我们可以指定outarr;}private:std::string _pattern;};class LinelFormat:public Format{public:void format(std::ostream out,const logs::LogMsg lsg)override{out(lsg._line);}}; class LoggerFormat:public Format{public:void format(std::ostream out,const logs::LogMsg lsg)override{outlsg._logger;}};消息主体处理类class PayloadFormat:public Format{public:void format(std::ostream out,const logs::LogMsg lsg)override{outlsg._payload;}};文件名处理类class FileFormat:public Format{public:void format(std::ostream out,const logs::LogMsg lsg)override{outlsg._file;}};线程id处理类class TidFormat:public Format{public:void format(std::ostream out,const logs::LogMsg lsg)override{outlsg._tid;}};换行处理类class NlineFormat:public Format{public:void format(std::ostream out,const logs::LogMsg lsg)override{out\n;}};TAB缩进处理类class TableFormat:public Format{public:void format(std::ostream out,const logs::LogMsg lsg)override{out\t;}};其余字符处理类class OtherFormat:public Format{public:OtherFormat(const std::string val):_val(val){;}std::string _val;void format(std::ostream out,const logs::LogMsg lsg)override{out_val;}}; };当然看完上面代码可能还有不少问题1 out是什么?好像每个format函数都把消息类的成员输出到out中这个out可以认为是一个缓冲区。当我们按顺序调用不同类内的format函数就会在缓冲区内形成一个日志消息。 例如 下面这个就是缓冲区内的日志消息就是我们先调用TimeFormat类内的函数输出时间后输出名称最后输出主体消息才有的这个格式化好的消息那我们怎么知道先调用哪个类内的format函数显然这里需要一个格式后面提。 而格式化子项类则负责将消息类中的成员取出加以处理后放到缓冲区。显然有个调用者提供了缓冲区然后再按顺序调用格式化子项类中的函数这样一条日志消息就做好了这个我们后面再细说。 2 为什么要抽象出一个Format父类这个也得后提如果还有其它问题慢慢来解决完这几个就能对这部分的实现有一个比较清晰的认识应该也能自主解决了。  我们得再提及一个类格式化类前面的那个叫格式化子项类诶咋还有个类来看看它提供了什么功能。 Formater类成员1保存了一个日志格式这个格式是我们传入的。 %t%d的含义如下 parttern函数负责解析这个格式解析完后就调用CreateFormat创建格式化子项对象保存到数组中也就是说上面的格式就转成对应格式化子项对象在数组中的排序当我们遍历这个数组去调用format函数时就是在按我们给的格式顺序去构建日志消息而由于每个格式化子项都是不同的类型可是vector只能存一个类型所以才要抽象出一个格式化子项父类然后所有格式化子项类都继承这个父类这样vector才能接受所有格式化子项对象。 //格式化类class Formater{public:Formater(const std::string format [%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n)void format(std::ostream out,const LogMsg msg);std::string format(const LogMsg msg);//返回日志消息private:bool parttern();Format::ptr CreateFormat(const std::string key,const std::string val);private:std::string _format;//日志格式std::vectorFormat::ptr _vf;//保存格式处理函数,后续按顺序调用}; }; 也就是说我们首先接收格式然后解析格式当我们能够把下面的格式字符串拆成一个个%d,%t...我们就可以直接判断应该调用哪个格式化子项函数了。 值得注意的是由于我们经常使用printf潜意识告诉我们%d表示输出整型但那个是printf内部的规定现在这里是我们自己实现的类我们想%d对应什么就对应什么。 Formater(const std::string format [%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n):_format(format){assert(parttern()); 开始解析格式字符串,必须成功,失败也就没必要继续下去了。} parttern函数实现。 private:bool parttern(){std::vectorstd::pairstd::string,std::string vss;std::string key,val;//这两个变量不能放在for内部key记录的格式字符,例如%d中的d%p中的pval记录的主要是普通字符然后vss容器用来保存这两个变量的值for(int i 0; i _format.size();){if(_format[i] ! %)成立表示是普通字符 例如abc%d,前面的abc都会被val保存起来,直接输出,普通字符就是不需要做格式化处理的字符{val.push_back(_format[i]);continue; 跳过下面,去判断下一个字符是不是%}遇到%,如果是%%此时我们认为是将%%看成是一个%的普通字符就像两个\\一样if(_format[i] % _format[i1] %)//是普通字符{val.push_back(_format[i]);i2;//跳过%continue;}else 解析%d{if(val.size())// 先push先前的普通字符到vss数组中vss.push_back(std::make_pair(key,val));val ; 然后清空val 例子:ab%dif(i1 _format.size())//例如%{std::cout%之后没有格式化字符std::endl;return false;}key _format[i];i;//例如:%d我要跳过%d中的d后面不然会被当成普通字符添加到日志消息中%d{%H},判断后面是否有格式子串,也有可能到结尾,我们认为{}是前面一个格式字符%的子串 因为%d是表示时间,但是日期也可以有格式,{}内部保存的就是对时间的格式化。if(i _format.size() _format[i] {){int pos _format.find_first_of(},i);if(pos -1){std::cout无匹配的},输入错误std::endl;return false;}i i;val _format.substr(i,pos-i);此时val就截取了时间格式,后续传给格式化子项函数i pos1;//跳过%H:%S:%S这个格式子串} vss.push_back(std::make_pair(key,val));val ;key ;}}vss.push_back(std::make_pair(key,val));//例如%dabc,处理末尾的原始字符看完上面代码我们可以知道格式字符串中的%d, 都按顺序保存到了vss容器中我们在这里统一调用CreateFormat()函数创建格式化子项对象保存到_vf中。for(auto e : vss){_vf.push_back(CreateFormat(e.first,e.second));}return true;} 我们在解析字符串的时候说过key是保存%d中的d这样在下面这个函数内就可以做判断如果不把下面这个判断封装成函数那在parttern函数中每遇到个%难道都做一次下面这个判断吗? 我们还在parttern函数中将key和val统一保存到vector而不是遇到个%就判断是否调用CreateFormat()函数不然的话类内成员函数间的耦合度很高如果觉得不高那就把CreateFormat()参数改一改看看是你的代码修改简便还是我上面的代码修改起来简便。下面这个函数只创建两种格式化子项时传了val要好好体会val是什么。 Format::ptr CreateFormat(const std::string key,const std::string val){if(key d) return std::make_sharedTimeFormat(val);if(key t) return std::make_sharedTidFormat();if(key c) return std::make_sharedLoggerFormat();if(key f) return std::make_sharedFileFormat();if(key l) return std::make_sharedLinelFormat();if(key p) return std::make_sharedLevelFormat();if(key m) return std::make_sharedPayloadFormat();if(key n) return std::make_sharedNlineFormat();if(key T) return std::make_sharedTableFormat();if(key ) return std::make_sharedOtherFormat(val);//例如遇到%gstd::cout%之后的格式字符输入错误:% keystd::endl;abort();return Format::ptr();//返回什么都行都不会被调用,因为程序都终止了} 可是把格式化子项对象保存到数组有什么用呢? 我们还实现了两个format函数这两个就是做最后的消息组装我们来看看具体实现。 这两个函数怎么好像在调用format?调用自己?当然不是 _vf数组中的成员是Format类型的不是Formatter类型的调用的肯定是类内成员函数啦所以Format类内函数的out是什么呢?就是我们在这里定义的std::stringstream ss或者是外部定义的stringstream变量也就是说最后日志消息就被格式化到这个类型的变量里了。 namespace logs {class Formater{public:using ptr std::shared_ptrFormater;void format(std::ostream out , const LogMsg msg){for(auto e : _vf){e-format(out,msg);}}std::string format(const LogMsg msg) 返回日志消息{std::stringstream ss;format(ss,msg);return ss.str();} private:std::string _format;//日志格式std::vectorFormat::ptr _vf;//保存格式处理函数,后续按顺序调用}; }; 功能测试 void test3()//测试日志消息类和格式化类 {定义了一条日志logs::LogMsg msg(logs::loglevel::level::DEBUG,__FILE__,日志器1,测试日志,__LINE__);给日志定格式 logs::Formater ft;std::coutft.format(msg); Formater类内的format函数会执行格式化子项函数并且将结果返回,我们直接打印看一看 } 没有给Formater ft传入格式时结果如下用了缺省格式。 如果传了结果如下此时我们可以发现只有我们输入了对应的格式字符日志消息才会输出对应的部分具体原因大家可以结合先前代码理解。 经过前面几步一条日志已经被制作出来了难道日志做出来就是直接cout吗?如何控制输出方向呢?就由接下来的落地类来实现了 五 日志落地类 模块实现 将日志消息输出到指定位置支持拓展日志消息落地到不同位置如何支持拓展的呢?后面我们就知道了。下面这几个落地位置是我们自己实现的。 1 标准输出 2 指定文件 3 滚动文件(也就是按大小和时间切换新文件保存)  如果我们是只实现一个类内函数的话那显然在后续增加落地方向的时候就要修改类内函数这显然是不符合开闭原则的而下面这种抽象出基类在派生实现落地逻辑的设计后续增加落地方向可以不修改原来代码而是直接添加一个新的子类符合开闭原则。 namespace logs {class sink{public:智能指针类型取别名,因为后续要使用智能指针来管理sink的子类对象using ptr std::shared_ptrsink;virtual void log(const char* dst,size_t size) 0;};//标准输入输出class Stdoutsink:public sink{public:void log(const char* dst,size_t size)override{std::cout.write(dst,size);}不能直接cout第一次用cout内部的函数,因为直接cout是无法指定大小的}; }; 下面这个就是文件落地类实现。 ​class Filesink:public sink{public:Filesink(const std::string pathname) 既然是输出到文件显然要别人先传个文件名啦:_pathname(pathname){util::file::CreateDreactor(util::file::pathname(_pathname));创建文件还要创建对应的目录,就像./test/test.log要把test目录页创建出来,可以复用一中实现的功能 }void log(const char* dst,size_t size)override{_ofs.write(dst,size); 直接写入assert(_ofs.good());}std::string _pathname;std::ofstream _ofs;};我一开始没有将_ofs设为类内成员而是在log内定义一个局部变量后面发现这样每次都要open所以就定义成成员一直存在了滚动文件落地类实现 可是要切换多个文件首先就要有多个文件名吧接下来看看多个文件名如何构建就是用基础文件名拓展文件名拓展文件名一般是时间用年月日时分秒结尾会不会一秒内生成两个文件呢?实际上不太可能但我们的代码比较简单很容易出现一直打开同一个文件写入的情况后面提。 这个滚动文件是根据文件大小来滚动的也就是当文件超出一定大小后就要切换新文件了这个文件容量是我们规定的可是如何知道文件已经使用的大小呢?显然也要有个成员记录写入的字节数获取文件属性也可以只是效率有点低。 class Rollsink:public sink{public:Rollsink(const std::string basename,int max_size):_basename(basename),_filename(basename),_max_size(max_size) 这个是文件最大容量当写入字节数超过这个容量就要切换文件了{CreateNewfile(); 构建文件名util::file::CreateDreactor(util::file::pathname(_filename));//创建文件_ofs.open(_filename,std::ofstream::binary | std::ofstream::app);assert(_ofs.is_open());}有了地址和长度就可以获取数据并写入了void log(const char* dst,size_t size)override{if(_cur_size _max_size) 判断是否要切换文件{_ofs.close(); 一定要关闭不然就出现资源泄露了CreateNewfile();_cur_size 0; 一定要清零,不然会反复进入if语句就会打开同一个文件_ofs.open(_filename,std::ofstream::binary | std::ofstream::app); assert(_ofs.is_open());}//打开并写入_ofs.write(dst,size);_cur_size size;assert(_ofs.good());}void CreateNewfile(){std::stringstream sst;sst _basename;struct tm st;const time_t t time(nullptr); localtime_r(t,st); 这个函数可以将时间戳转为struct tm类型的结构体sst st.tm_year1900; sst st.tm_mon1;sst st.tm_mday;如果写入操作时间很短我们就再加个count做后缀。sst -;_filename sst.str(); 用类内成员记录新文件名}int _count 0;std::ofstream _ofs; std::string _filename;std::string _basename; 文件基础名 int _max_size; 文件容量int _cur_size 0; 当前文件使用字节数}; 这个是tm类内的成员有年月日时分秒。 下面这个Factorysink封装了上述落地类的创建方式免得一个类的构造函数发生更改然后要所有代码都改动所以就封装了一个静态接口来创建可是上面几个落地类的构造函数的参数是不同的这个时候我们就想起了用可变参数模板函数静态可变参数的模板我也是第一次用。 //创建上面的日志落地类class Factorysink{public:templatetypename T,typename ...Argsstatic std::shared_ptrT Create(Args ...args) {return std::make_sharedT(std::forwardArgs(args)...);}}; 接收参数用了万能引用右值引用是:int args虽然都有但是万能引用的类型是不确定的而右值引用的类型确定的传参的时候还用了完美转发保持参数在传递时特性不改变也就是让右值不会变成左值。这个实际上是个简单工厂模式所有类的对象都在一个类内创建显然由于模板的存在后续有新的落地类这个工厂类都不会改动。 模块测试 void test4()//测试日志消息落地类 {logs::LogMsg msg(logs::loglevel::level::DEBUG,__FILE__,日志器1,测试日志,__LINE__);logs::Formater ft;std::string ret ft.format(msg);//此时是把消息准备好了下面是创建消息的落地类以及调用log函数开始落地写入。auto sptr logs::Factorysink::Createlogs::Stdoutsink(); sptr-log( ret.c_str(),ret.size());auto sptr2 logs::Factorysink::Createlogs::Filesink(./logs/test.log);sptr2-log( ret.c_str(),ret.size());auto sptr3 logs::Factorysink::Createlogs::Rollsink(./test/test.log,1024);int i 0;while(i 5*1024) 写够5*1024字节显然应该创建5个文件{i ret.size();sptr3-log(ret.c_str(),ret.size());} } 滚动文件bug并没有出现切换第一次发现因为我每次写入日志没有对_cur_size做还有就是要清零不然会每次写入都if成立去打开文件而且由于写入过快然后就会进入同一个文件。 不过为什么清零了后还是只有一两个文件呢因为执行得太快了一两秒内可能写完了然后每次Createfile就会打开同一个文件所以我们用时间还是不太好切换最好加个计数器。 结果如下。 六 落地模块拓展举例 用户编写的落地方式如何被使用? 例如用户想来个按时间切换的滚动文件落地类当前时间为一百秒十秒切换一个文件所以在到110秒的时候要切换文件了。 拓展实现 我们又定义了一个枚举类型当传不同的常量表示切换时间间隔分别是秒分时天。 enum class TimeGap {GAP_SECOND,GAP_MINUTE,GAP_HOUR,GAP_DAY, }; class RollBytimesink : public logs::sink { public:RollBytimesink(const std::string basename,TimeGap gap_size):_basename(basename),_filename(basename){switch (gap_size) 确认时间间隔大小{case TimeGap::GAP_SECOND: _gap_size 1;break;case TimeGap::GAP_MINUTE: _gap_size 60;break;case TimeGap::GAP_HOUR: _gap_size 3600;break;case TimeGap::GAP_DAY: _gap_size 3600*24;break;}_old_time logs::util::Date::now();CreateNewfile();//构建文件名logs::util::file::CreateDreactor(logs::util::file::pathname(_filename));_ofs.open(_filename , std::ofstream::binary | std::ofstream::app);assert(_ofs.is_open());}当当前时间大于_old_time_gap_size时表明此时已经过了一个时间间隔是时候创建新文件了void log(const char* dst,size_t size)override{if(logs::util::Date::now() _old_time _gap_size){_old_time _gap_size;_ofs.close();CreateNewfile();_ofs.open(_filename,std::ofstream::binary | std::ofstream::app); assert(_ofs.is_open());}//打开并写入_ofs.write(dst,size);assert(_ofs.good());}void CreateNewfile(){std::stringstream sst;sst _basename;struct tm st;const time_t t time(nullptr);localtime_r(t,st);sst st.tm_year1900;sst st.tm_mon1;sst st.tm_mday;sstst.tm_hour;sstst.tm_min;sstst.tm_sec;_filename sst.str();}std::ofstream _ofs;std::string _filename;std::string _basename;int _old_time;int _gap_size;//切换时间间隔 };拓展测试 写入五秒此时应该会有五个文件 void test5() {logs::LogMsg msg(logs::loglevel::level::DEBUG,__FILE__,日志器1,测试日志,__LINE__);logs::Formater ft;std::string ret ft.format(msg);//此时是把消息准备好了//下面是解决消息的输出方向auto sptr3 logs::Factorysink::CreateRollBytimesink(./test/test.log, TimeGap::GAP_SECOND);auto i logs::util::Date::now();while(i 5 logs::util::Date::now())//往文件输出日志五秒,应该创建五个文件{sptr3-log(ret.c_str(),ret.size());usleep(10000);} } 当然打印的日志时间是固定的因为那是制作日志消息时的时间还有就是注意文件名要更换不然每次都会打开同一个文件。当我们终于可以输出日志消息到任意地方了此时就有新问题难道每次我都要创建一个LogMsg对象然后自己创建格式化器去格式化日志消息对象返回一个string保存的格式化好的日志消息再创建落地类实现日志落地吗? 对于使用者来说有点麻烦。所以需要一个新模块对先前功能做整合。 七 日志器模块 我们希望使用者只需要创建logger对象调用这个对象的debug,info,warn,error,fatal等函数就可以打印出对应等级的日志支持解析可变参数列表和日志消息主体的格式。还支持同步和异步日志器。 同步日志器:直接对日志消息进行输出。 异步日志器:将日志消息放到缓冲区由异步工作器线程进行输出。 好像支持很多功能啊没事我们先实现最简单的同步日志器。 日志器模块成员介绍 这个模块成员是什么呢先来个日志器名称然后是格式化Formater类的对象显然这个成员需要一个日志格式还需要一个vector对象这个对象内部放着多个日志落地对象因为一条日志可能既需要打印到屏幕上还要输出到文件中。 namespace logs {class Logger{protected:Logger(std::string logger,std::vectorsink::ptr sinks , loglevel::level lev,Formater::ptr formater) 这个是外部传入的格式化器:_logger(logger),_limit_level(lev),_sinks(sinks),_formater(formater)从日志消息类成员我们知道如果要构建一条日志消息外部需要传入文件名,行号(这两个绝对不能在函数中通过宏获取)还有消息主体和格式,让我们内部构建对应级别的日志消息但是消息主体也不需要用户自己输入可以将参数和格式传入我们内部自己组建消息主体void Debug(const std::string file,size_t line,const char* format,...);void Info(const std::string file,size_t line,const char* format,...);void War(const std::string file,size_t line,const char* format,...);void Fatal(const std::string file,size_t line,const char* format,...);void Error(const std::string file,size_t line,const char* format,...);void OFF(const std::string file,size_t line,const char* format,...);virtual void log(const char* dst,size_t size) 0;std::mutex mutex; 锁的作用后提Formater::ptr _formater;std::string _logger; 日志器名称std::atomicloglevel::level _limit_level; std::vectorsink::ptr _sinks;//保存了该日志器的落地方向}; };日志器的名称是唯一标识的作用后提后面我们是可以将日志器统一管理的而日志器名就是日志器对象的唯一标识。这个限制等级是经常要被访问的在目前的接口来看几乎不对其进行修改应该不用担心线程安全的问题如果后面担心后面会修改就设为原子性懒得加锁了因为如果一个线程执行要申请很多锁就比较容易冲突执行比较慢。 所以日志器内部的Debug函数负责构建Debug日志Info函数构建一条info级别的日志然后子类实现log来决定这条日志的去向。为啥不直接在父类内实现呢? 为什么要子类来实现呢? 因为我们要实现同步日志器和异步日志器这两个日志器的区别在于落地方式的不同注意不是落地方向同步日志器是直接输出而异步日志器是交给缓冲区由其它线程去输出但是日志的构建大家都是一样的所以抽象出基类共同使用。抽象基类还有个好处是?可以用基类指针对子类日志器管理和操作这也得后面提了。 抽象类内部实现 看着多但只要了解了一个Debug函数其它几个都是几乎一模一样的。 namespace logs {class Logger{protected:Logger(std::string logger,std::vectorsink::ptr sinks,std::string format):_logger(logger),_sinks(sinks),_formater(new Formater(format)){;}void Debug(const std::string file,size_t line,const char* format,...){if(_limit_level loglevel::level::DEBUG)return;//制作消息va_list vp; 获取可变参数的开头va_start(vp,format);char* ret;vasprintf(ret,format,vp); 这个函数可以将可变参数按照format格式转到ret指向的空间中serialize(loglevel::level::DEBUG,file,ret,line);free(ret);释放资源va_end(vp);}void Info(const std::string file,size_t line,const char* format,...){if(_limit_level loglevel::level::INFO)return;//制作消息va_list vp;va_start(vp,format);char* ret;vasprintf(ret,format,vp);serialize(loglevel::level::INFO,file,ret,line);free(ret);va_end(vp);}void War(const std::string file,size_t line,const char* format,...){if(_limit_level loglevel::level::WARN)return;//制作消息va_list vp;va_start(vp,format);char* ret;vasprintf(ret,format,vp);serialize(loglevel::level::WARN,file,ret,line);free(ret);va_end(vp);}void Fatal(const std::string file,size_t line,const char* format,...){if(_limit_level loglevel::level::FATAL)return;//制作消息va_list vp;va_start(vp,format);char* ret;vasprintf(ret,format,vp);serialize(loglevel::level::FATAL,file,ret,line);free(ret);va_end(vp);}void Error(const std::string file,size_t line,const char* format,...){if(_limit_level loglevel::level::ERROR)return;//制作消息va_list vp;va_start(vp,format);char* ret;vasprintf(ret,format,vp);serialize(loglevel::level::ERROR,file,ret,line);free(ret);va_end(vp);}void OFF(const std::string file,size_t line,const char* format,...){if(_limit_level loglevel::level::OFF)return;//制作消息va_list vp;va_start(vp,format);char* ret;vasprintf(ret,format,vp);serialize(loglevel::level::OFF,file,ret,line);free(ret);va_end(vp);}这个函数是前面几个成员函数的公共部分因为它们都要构建出一条日志消息void serialize(loglevel::level level,const std::string file,char* cmsg,size_t line){LogMsg msg(level,file,_logger,cmsg,line);开始格式化std::string retstring _formater-format(msg);//进行落地log(retstring.c_str(),retstring.size());调用的是下面的log函数}virtual void log(const char* dst,size_t size) 0;std::mutex mutex;Formater::ptr _formater;std::string _logger;//日志器名称std::atomicloglevel::level _limit_level;std::vectorsink::ptr _sinks;//保存了该日志器的落地方向}; }; 子类同步日志器实现 各个sink直接调用log函数。 class sync_Logger:public Logger{public:sync_Logger(std::string logger,std::vectorsink::ptr sinks,loglevel::level lev,Formater::ptr formater):Logger(logger,sinks,lev,formater) 这是在调用基类的构造函数{;}void log(const char* dst,size_t size){std::unique_lockstd::mutex lock(_mutex);if(_sinks.size() 0)return;for(auto e : _sinks) 如果是多线程同时调用这个log函数在写入时要加锁保护虽然现在是单线程提前保护一下呗。{e-log(dst,size);}}}; 同步日志器模块测试 封装后的使用: 封装前的使用 使用起来代码量差不多啊为什么还要有个日志器封装呢?其实从上面可以看出使用者减少了接收日志消息的操作也不用自己调用落地模块的log函数去落地了。 但是使用还不够简便下面我们再做一些优化简化使用难度。 八 日志器建造者模块 从下面的测试图中我们知道我们使用日志器要先创建很多对象这些对象都是给日志器传的参数然后我们才能创建一个日志器有点麻烦我们能不能封装一个类传参给这个类这个类去帮我们创建日志器成员然后构建日志器并返回这就要用到建造者模式了。 首先抽象一个建造者类然后派生出子类建造者类。为什么这里要分出父类子类呢? 直接实现一个类不就好了吗?这个问题和后面的全局日志器实现有关因为创建局部和全局的日志器有个处理不一样但是零部件的构建是一样的同理父类实现一份两个子类共享多香。 //日志器建造者类class loggerbuild{public:loggerbuild():_limit_level(loglevel::level::DEBUG),_type(LoggerType::sync_Logger){;}void buildlevel(loglevel::level lev){_limit_level lev;}void buildlogname(const std::string name){_logger name;}void buildformater(const std::string parttern){_formater std::make_sharedFormater(parttern);} void buildloggertype(LoggerType type){_type type;}templatetypename T,typename ...Argsvoid buildsink(Args ...args){_sinks.push_back(std::make_sharedT(std::forwardArgs(args)...));}virtual Logger::ptr build() 0;std::mutex _mutex;LoggerType _type;Formater::ptr _formater;std::string _logger;//日志器名称std::atomicloglevel::level _limit_level;std::vectorsink::ptr _sinks;//保存了该日志器的落地方向}; 根据类型返回同步和异步日志器这些都是局部的日志器都是只能定义的地方使用其它作用域要使用得通过传参等方式比较麻烦全局的则是任意地方都可以获取日志器对象来输出日志。 enum class LoggerType{Asynclooper,sync_Logger}; 根据枚举类型返回不同的日志器对象class Localloggerbuild:public loggerbuild{Logger::ptr build()override{if(_type LoggerType::Asynclooper) 异步,这个return可以在实现完异步日志器再补全return std::make_sharedAsync_Logger(_logger,_sinks,_limit_level,_formater);return std::make_sharedsync_Logger(_logger,_sinks,_limit_level,_formater);}}; 模块测试 九 异步日志器设计思想 我们前面说过异步日志器是把数据给一个缓冲区然后由异步工作器线程去处理缓冲区中的数据显然这里有两个线程一个是业务线程将数据写给缓冲区一个是工作线程处理日志如果他们用的是一个缓冲区那就要用我先前博客提到的环形队列和消息队列的两种生产消费者模型本文用的是双缓冲区实现的生产消费者模型这三种实现并无优劣之分。 不过如果是双缓冲区的话工作线程处理完任务缓冲区内的数据如何拿到任务写入缓冲区的数据呢拷贝?不我们是用swap函数这个和拷贝有点区别。所以如果工作线程处理完自己缓冲区的数据了外部缓冲区是空的就不能交换就得等待这个等待的实现就是用信号量。业务线程也不能一直往任务写入缓冲区写如果满了得等工作线程这个等待也是用信号量。       这里面还要实现一个缓冲区模块还要一个日志处理模块内部包含一个线程对日志消息做落地操作最后这两个模块都服务于异步日志器模块有点不好理解我打算先看看异步日志器需要什么? 我们从同步日志器的实现来分析同步日志器是直接在自己的log函数内部调用sink类的log函数往文件或者屏幕输出了但如果是异步日志器应该输出到缓冲区可是如何找到缓冲区呢? 显然此时需要一个缓冲区对象做异步日志器的成员当然传参也不是不可以但是什么都要外部创建对使用者来说是比较麻烦的。 再来想想那缓冲区内部放什么呢是直接放一条格式化好的字符串而不是放一个logmsg对象。如果是放logmsg对象工作线程处理函数获取数据的时候要多拷贝一次缓冲区内的数据下来做格式化而不是直接获取处理。可是这个缓冲区要提供什么功能我们还有点模糊不急慢慢来这已经是最后一个关卡了过了我们就可以将各个模块串联起来我写博客提到的很多细节也是我实现完才想起来的我是没办法在实现前就想得很清楚。 如果日志器内部就用一个char*指向一个区域然后直接把数据丢到这里面之后难道在日志器里面再定义一个线程然后让这个线程去读取char*指向的空间显然这里一个类负责的功能太过复杂不符合高内聚低耦合既负责log输出到缓冲区还负责创建线程处理缓冲区数据我们可以先实现一个Asynclooper(日志工作器)类这个类对象成员有两个缓冲区会创立线程会去读缓冲区数据进行落地操作也会提供接口给外部对缓冲区进行写入其实也就是给异步日志器类增加一个成员负责落地操作。 那缓冲区是直接弄一个char*还是再封装一下呢? 如果不封装那Asynclooper就要负责缓冲区的扩容读写位置的维护以免被覆盖算了缓冲区也封装成类吧这样以后别的类还能复用对应的接口接下来就看看缓冲区的设计如下。 十 缓冲区实现 思想介绍 在单缓冲区中读位置和写位置是不同的我们需要通过对应的位置关系判断缓冲区是空还是满。 实现 类设计内部用vector保存数据而且要有两个下标分别控制读写位置。 class buffer{public://往缓冲区push数据void push(const char* dst,size_t len)返回可读缓冲区的长度size_t ReadAbleSize()size_t WriteAbleSize()返回可读位置的起始地址const char* begin()bool empty()移动读写位置不能让外部直接访问成员void movewriter(size_t len)void moveReader(size_t len)重置归零,交换了缓冲区后日志器写入的缓冲区就变空了需要重新设置读写下标void reset()void swap(logs::buffer buf) 交换缓冲区private:void EnSureEnoughsize(size_t len)//保证扩容后可以容纳len个字符std::vectorchar _buffer;int _writer_index 0;int _reader_index 0; }; 具体实现 namespace logs {#define DEFAULT_BUFFER_SIZE (1*1024)//缓冲区大小默认为1kb这两个宏的意义在EnSureEnoughsize() 扩容函数中可以体现#define THRESHOLD_BUFFER_SIZE (4*1024) //阈值默认为4kb#define INCREASE_BUFFER_SIZE (1*1024) //超过阈值后每次增长1kbclass buffer{public:buffer():_buffer((DEFAULT_BUFFER_SIZE)){;}//往缓冲区push数据void push(const char* dst,size_t len){EnSureEnoughsize(len);std::copy(dst,dstlen,_buffer[_writer_index]);movewriter(len);}size_t ReadAbleSize(){ return _writer_index - _reader_index; }size_t WriteAbleSize(){ return _buffer.size() - _writer_index; }//返回可读位置的起始地址const char* begin(){return _buffer[_reader_index];}bool empty(){return _reader_index _writer_index;}void movewriter(size_t len){_writer_index len;}void moveReader(size_t len){_reader_index len;}void reset(){_reader_index 0;_writer_index 0;}void swap(logs::buffer buf){std::swap(_buffer,buf._buffer); 这个swap只是交换了vector内部的指针不会去拷贝指向空间内的数据。std::swap(_writer_index,buf._writer_index);std::swap(_reader_index,buf._reader_index);}private: 不对外提供的设为私有void EnSureEnoughsize(size_t len){if(_buffer.size() - _writer_index len) 可写入无需扩容return;int newsize 0;while(_buffer.size() - _writer_index len){if(_buffer.size() THRESHOLD_BUFFER_SIZE)//缓冲区大小小于阈值,size增长为翻倍,直到足够写入newsize _buffer.size()*2;elsenewsize _buffer.size() INCREASE_BUFFER_SIZE;大小大于阈值,size增长为线性增长_buffer.resize(newsize); //扩容 } }std::vectorchar _buffer;int _writer_index 0;int _reader_index 0; }; }; 扩容情况主要用于测试测试在大量写入时的效率如何实际运行的时候的资源是有限的不会运行我们无限扩容。缓冲区只负责写数据和扩容是否要限制大小由上层决定给上层足够的选择空间。空间不需要释放频繁申请释放浪费时间到日志器实现我们就知道缓冲区交换的作用。 测试缓冲区模块 void test8()//测试缓冲区模块 {logs::buffer buf;std::ifstream ifs;ifs.open(./logs/test.log,std::ifstream::binary); 打开文件ifs.seekg(0,std::ifstream::end);移动到文件末尾size_t size ifs.tellg(); 返回当前文件到文件起始位置的差,这就是文件的内容大小了ifs.seekg(0,std::ifstream::beg);再移回开头std::string rec; 提前开辟好大小rec.resize(size);ifs.read(rec[0],size); 将文件数据读取到rec字符串中buf.push(rec.c_str(),rec.size()); 写入缓冲区buf.movewriter(rec.size()); 更新下标std::ofstream ofs(./logs/test2.log,std::ofstream::binary);从缓冲区读数据到新文件int readsize buf.ReadAbleSize();for(int i 0; i size;i){ofs.write(buf.begin(),1);buf.moveReader(1);} } 最后我们在外部用命令比较两文件是否一致一致说明缓冲区没问题如何判断两个文件一不一样如下。 十一 工作器实现 工作器设计 我们前面说了实现缓冲区是给工作器内部提供缓冲区对象让工作器可以创建线程对缓冲区进行读写。所以管理成员如下。    1 双缓冲区对象。虽然工作器是对消费缓冲区的数据做处理例如进行输出落地但具体操作我们是不好直接写死的所以我们让外部来指定所以就有了个回调函数。那为什么是双缓冲区呢?因为异步日志器是把数据给任务缓冲区工作器要将任务缓冲区和日志处理缓冲区进行交换然后处理总不能将任务缓冲区对象变成异步日志器的成员那工作器怎么获取呢?我们看完工作器的实现就知道不把两个缓冲区都放在工作器内不好管理。 条件变量和锁的用处我们在实现中理解。 实现 namespace logs {enum class safe{ASYNC_SAFE,ASYNC_UNSAFE,};class Asynclooper{public:using Functor std::functionvoid(buffer );Asynclooper(Functor factor, loopersafe safe loopersafe::ASYNC_SAFE):_callback(factor),_stop(false),_safe(safe),_thread(std::thread(Asynclooper::threadRun,this)){;}成员初始化,创建一个线程,第一个参数传的是执行函数,普通函数直接传函数名就可以了传成员函数格式如上参数是this指针。~Asynclooper(){stop();}这个push接口是给外部的日志器使用的void push(const char *dst, size_t len){{std::unique_lockstd::mutex(_mutex); // 加锁访问缓冲区if(_safe safe::ASYNC_SAFE)要用lambda表达式或者某个可调用的函数 _pro_condition.wait(_mutex,[](){return _pro_buffer.WriteAbleSize() len;});若len太大,则会一直在阻塞_pro_buffer.push(dst, len);_pro_buffer.movewriter(len);}_con_condition.notify_all();}可能工作线程在交换缓冲区时陷入了休眠当我们添加了数据时就可以唤醒工作线程void stop(){_stop true;_con_condition.notify_all();_thread.join();}private:void threadRun() 当工作器定义好后就会一直在这里访问两个缓冲区,如果生产缓冲区不在 工作器的管理下,外部传参也传不进来。 {while(!_stop){{std::unique_lockstd::mutex lock(_mutex); 加锁访问缓冲区看看生产缓冲区是否为空,不为空以及工作器被停止了都应该往下继续走_con_condition.wait(lock,[](){return _stop || !_pro_buffer.empty();});_con_buffer.swap(_pro_buffer);//交换 }if(_safe safe::ASYNC_SAFE)_pro_condition.notify_all();_callback(_con_buffer);//处理任务缓冲区内的数据_con_buffer.reset(); 处理完了重置任务缓冲区}}// 双缓冲区bool _stop; safe _safe; Functor _callback;logs::buffer _pro_buffer;logs::buffer _con_buffer;std::mutex _mutex;std::condition_variable _pro_condition;std::condition_variable _con_condition;std::thread _thread;//异步工作器对应的线程}; }; 可以看到当我们定义了Asynclooper这个工作器对象时初始化内部成员的时候就已经创建了一个线程这个线程执行的就是下面这个函数这个函数就负责处理消费缓冲区的数据然后重置消费缓冲区整个过程是在while循环内那外部如何终止呢就用stop()函数控制_stop成员又或者整个对象释放的时候_stop也变成true也就终止了。所以_stop也会被多线程访问也要被保护同样设为原子的性而不是加锁。 这两个函数都有个{}看起来非常奇怪实际上是非常巧妙的巧妙的将锁的生命周期缩小在了{}内如果没有这个{}外部主线程执行push函数和内部这个次线程执行的threadRun函数就是串行的了影响效率。 好像只有一把锁是的免得外部push的时候工作器线程突然去交换。 stop函数作用? 暂停工作器并等待工作线程退出回收这个暂停不是立刻停止而是让工作线程下次while判断从循环中出来可以让外部控制不要再进行落地了所以也会出现工作线程和业务线程(业务线程就是我们定义日志器输出日志消息到任务缓冲区的线程) 我们buffer实现的push接口是直接扩容我们前面说了由外部控制这个外部现在就是工作器。 我们先在工作器内定义了一个枚举类型SAFE表示限制缓冲区的扩容UNSAFE表示不限制。 然后根据_safe成员的类型决定要不要下面这句安全控制。 如果想让缓冲区无限扩容我们就让_safe保存SAFEpush的时候就不会在条件变量下判断可写区域是否足够而是直接调用缓冲区的push函数内部无限扩容。反之工作器对象在push的时候就会在条件变量下等待即便被唤醒也要判断可写区域是否足够如果有一次len大于缓冲区的长度而缓冲区又是有限的此时就会一直阻塞住这不是bug而是我们使用不当是我们自己设置缓冲区有限不能扩容还往缓冲区内部输入超限的数据。 一个工作器对象一个线程? 所以我们的构造函数就需要两个参数一个是回调函数还有一个是枚举类型表示该工作器是否安全。 十二 异步日志器 设计 同步日志器上面刚刚实现完而且我们发现同步日志器只需要复用父类的成员即可不需要自己添加成员但是异步日志器需要需要一个工作器内部定义线程对消费者缓冲区内的数据做处理。最后设计的接口成员如下。 class Async_Logger:public Logger{public:Async_Logger(std::string logger,std::vectorsink::ptr sinks,loglevel::level lev,Formater::ptr formater){;}void log(const char* dst,size_t size);void realog(buffer buf);private:Asynclooper::ptr _looper;}; 实现 class Async_Logger:public Logger{public:Async_Logger(std::string logger,std::vectorsink::ptr sinks,loglevel::level lev,Formater::ptr formater,loopersafe loopsafe):Logger(logger,sinks,lev,formater) 调用基类的构造函数,_looper(std::make_sharedAsynclooper定义一个工作器(std::bind(Async_Logger::realog,this,std::placeholders::_1))){;}void log(const char* dst,size_t size){_looper-push(dst,size);放入任务缓冲区}void realog(buffer buf)实际落地函数,是传给工作器执行的,会将缓冲区数据给sink落地类函数去落地不用加锁因为工作器内就一个线程访问不会有线程安全的问题{if(_sinks.empty()) {_sinks.push_back(Factorysink::Createlogs::Stdoutsink()); }for(auto e : _sinks){e-log(buf.begin(),buf.ReadAbleSize());}}private:Asynclooper::ptr _looper;}; 比较有意思的还是下面这个bind语法首先我们是在调用make_sharedAsynclooper()返回一个智能指针括号内部是在调用构造函数。 异步日志器测试 void test9()//测试异步日志器 {std::shared_ptrlogs::Localloggerbuild localbuild(new(logs::Localloggerbuild));localbuild-buildlevel(logs::loglevel::level::DEBUG);localbuild-buildformater(%d{%H:%S}%m%n);localbuild-buildlogname(日志器1);localbuild-buildloggertype(logs::LoggerType::Asynclooper);localbuild-buildsinklogs::Filesink(./logs/test.log);localbuild-buildsinklogs::Rollsink(./logs/test.log,1024);logs::Logger::ptr lptr localbuild-build();int i 0;int count 0;while(i 5*1024){i 21;std::coutcount countstd::endl;lptr-Debug(__FILE__,__LINE__,日志测试);} } 按理说应该是有5个文件但是我这里测试后只有三个文件。 由于我们是先往文件输入日志后才判断是否会超过容量所以就会出现此时文件里面已经放了1000字节了结果工作线程获得了任务缓冲区的数据这里面也有1000字节直接写入就超限了为什么测试缓冲区的时候是五个文件呢?大家可以去看看上面的测试代码当时是一个个字节写入的所以写入文件时不会超出容量太多就能开辟五个文件我们这里一个文件放了2kb就导致只开了三个文件。 完善建造者类 由于异步日志器内部多了个工作器然后在传参时需要给工作器传回调函数和一个枚举类型所以我们需要对建造者类进行修改。先给建造者父类增加一个成员用来记录枚举类型后面给日志器的构造函数传参。 可是为什么不是把成员定义在下面这个子类里面呢? 因为下面这个是局部的日志器创建后面还有个全局的日志器创建也要用到这个成员。 测试 异步日志器测试bug发现写入数据变少原因:还有部分数据在生产缓冲区但是_stop已经被设为true这部分数据就丢失了。代码修改如下必须把生产缓冲数据处理完才能结束。 十三  日志器管理模块 实现原因 由于我们上面返回的日志器都是局部日志器都只是在某个作用域有效其它函数作用域都无法使用传参太过麻烦所以我们希望有个方法可以让我们创建一个日志器后可以在任意地方使用。所以我们设计出了一个日志管理类这个类内部保存了创建好的日志器可以让我们在任意地方获取。 显然这个管理类必须是个单例模式不然的话我在一个函数内添加了日志器这个日志器被管理类对象保存结果这个管理类对象也是个局部的别的作用域又如何访问这个成员内部的日志器呢。 设计 _loggers是管理所有日志器的数组,应该用mapLoggerManger应该是个单例模式构造函数私有。 实现 class LoggerManager{LoggerManager getinstance()//返回单例的管理者对象{static LoggerManager lm;return lm;}void addlogger(Logger::ptr ptr)//添加一个日志器被管理{std::unique_lockstd::mutex(_mutex);if(haslogger(ptr-name()))//免得覆盖了return;_mp.insert(std::make_pair(ptr-name(),ptr));//添加被管理的日志器}Logger::ptr getlogger(const std::string name){std::unique_lockstd::mutex(_mutex);//防止别人在别人添加的时候获取,导致获取错误数据return _mp[name];}bool haslogger(const std::string name){std::unique_lockstd::mutex(_mutex);if(_mp.count(name))return true;return false; }private:LoggerManager(){std::unique_ptrLocalloggerbuild build(new Localloggerbuild());build-buildlogname(日志器2);_root日志器只需要指定名称,其余的都有默认的初始化值_root build-build(); _mp[_root-name()] _root; 默认日志器也要添加到容器中这样也能通过get获取}std::mutex _mutex;std::mapstd::string,Logger::ptr _mp;//根据日志器名字返回日志器对象Logger::ptr _root;//默认的日志器}; LoggerManager的构造函数内构建默认日志器不能用全局日志器必须用局部日志器。当外部获取LoggerManager的静态对象的时候开始调用管理类的构造函数内部创建了builder对象build函数内部获取LoggerManager的静态对象来添加日志器但是静态对象又没初始化完 建造者完善 因为如果用户想将日志器添加到全局让任何地方都能获取那就得加入到单例管理类对象中被管理而且要先获取单例对象再调用add函数。为了简化我们设计一个全局日志器建造者使得对方调用这个建造者类的时候我们就能顺便把日志器添加到单例对象的管理中。 //全局日志器创建class Globalloggerbuild:public loggerbuild{public:Logger::ptr build()override{assert(!_logger.empty());if(_sinks.empty())buildsinkStdoutsink();if(_formater.get() nullptr)_formater std::make_sharedFormater();Logger::ptr logger;if(_type LoggerType::Asynclooper)logger std::make_sharedAsync_Logger(_logger,_sinks,_limit_level,_formater,_looper_type);elselogger std::make_sharedsync_Logger(_logger,_sinks,_limit_level,_formater);LoggerManager::getinstance().addlogger(logger);return logger;}}; 测试 建造日志器并添加到单例类中并尝试在全局获取。 void test_log() 测试获取全局的日志器 {logs::Logger::ptr manger logs::LoggerManager::getinstance().getlogger(日志器1);int i 0;int count 0;while(i 5*1024){i 21;manger-Debug(__FILE__,__LINE__,日志测试);}} void test11()测试日志管理模块 {std::shared_ptrlogs::Globalloggerbuild localbuild(new(logs::Globalloggerbuild));localbuild-buildlevel(logs::loglevel::level::DEBUG);localbuild-buildformater(%d{%H:%M:%S}[%m]%n);localbuild-buildlogname(日志器1);localbuild-buildloggertype(logs::LoggerType::Asynclooper);localbuild-buildsinklogs::Filesink(./logs/test2.log);localbuild-buildsinklogs::Rollsink(./logs/test2.log,1024);logs::Logger::ptr lptr localbuild-build();//建造日志器test_log(); } 在其它函数内获取日志器来输出可以获取并输出才表示日志器可以在全局获取。由于是异步的最后也是生成了几个文件不足五个但是大小却是足够的。 十四 封装 思想 实现到了这一步我们基本上的代码差不多写完了接下来就是一些小封装实现。我在main.cc测试的时候要使用功能就得包含不少头文件 下面这里用户要先调用管理类的静态成员再获取获取日志器我们应该避免让用户去获取单例的管理者对象而且输出日志每次都要传__FILE____LINE__这两个宏。 所以我们决定提供一些接口和宏函数对日志系统接口进行使用便捷性优化。 1. 提供获取指定日志器的全局接口(避免用户自己操作单例对象) 2. 使用宏函数对日志器的接口进行代理(代理模式) 3.提供宏函数可以直接进行日志的标准输出打印 实现 下面这个封装在log.h中。 下面两个全局接口就可以让用户直接获取日志器而不用获取日志管理对象。 #includelogger.hpp #includestdarg.h namespace logs {Logger::ptr getlogger(const std::string name)//提供全局接口获取日志器{return LoggerManager::getinstance().getlogger(name);}Logger::ptr getrootlogger()//提供全局接口获取默认日志器{return LoggerManager::getinstance().getlogger(root);} } 通过宏代理。 namespace logs {#define Debug(fmt,...) Debug(__FILE__,__LINE__,fmt,##__VA_ARGS__);#define Info(fmt,...) Info(__FILE__,__LINE__,fmt,##__VA_ARGS__);#define War(fmt,...) War(__FILE__,__LINE__,fmt,##__VA_ARGS__);#define Error(fmt,...) Error(__FILE__,__LINE__,fmt,##__VA_ARGS__);#define Fatal(fmt,...) Fatal(__FILE__,__LINE__,fmt,##__VA_ARGS__);#define Off(fmt,...) Off(__FILE__,__LINE__,fmt,##__VA_ARGS__); } 上面代码的意义在于manger日志器获取接口变简易了。 以前 封装后: 而且调用Debug函数也不用传宏了。 void test2_log()//测试获取全局的日志器 {logs::Logger::ptr manger logs::getlogger(日志器1);int i 0;int count 0;while(i 5*1024){i 21;count;manger-Debug(日志测试);}std::coutcountstd::endl; } void test12() {std::shared_ptrlogs::Globalloggerbuild localbuild(new(logs::Globalloggerbuild));localbuild-buildlevel(logs::loglevel::level::DEBUG);localbuild-buildformater(%d{%H:%M:%S}[%f:%l][%m]%n);localbuild-buildlogname(日志器1);localbuild-buildloggertype(logs::LoggerType::Asynclooper);localbuild-buildsinklogs::Filesink(./logs/test2.log);localbuild-buildsinklogs::Stdoutsink();logs::Logger::ptr lptr localbuild-build();//建造日志器并添加到管理对象中test2_log(); } 通过宏直接进行标准输出 namespace logs {后面的这个宏会被替换成上面的,所以这里要特别注意上面的宏名别和下面搞混#define DEBUG(fmt,...) logs::getrootlogger()-Debug(fmt,##__VA_ARGS__);#define INFO(fmt,...) logs::getrootlogger()-Info(fmt,##__VA_ARGS__);#define WAR(fmt,...) logs::getrootlogger()-War(fmt,##__VA_ARGS__);#define ERROR(fmt,...) logs::getrootlogger()-Error(fmt,##__VA_ARGS__);#define FATAL(fmt,...) logs::getrootlogger()-Fatal(fmt,##__VA_ARGS__);#define OFF(fmt,...) logs::getrootlogger()-Off(fmt,##__VA_ARGS__); } 有时候我们自己不想定义日志器不想理会日志消息内部时间行号文件名的排列就可以调用上面的宏会使用默认日志器进行输出日志格式都是用的默认的。 void test2_log()//测试获取全局的日志器 {int i 0;int count 0;while(i 5*1024){i 21;count;DEBUG(日志测试);}std::coutcountstd::endl; } 而且只需要包含一个头文件。 十五 目录梳理 因为我们写代码还要上传到gitte上我们不得写好看点吗不得把代码整理一下。example里面是使用样例我把测试代码和最终封装后的接口使用样例都放在了这里logs内部是我们提供的组件源代码。 用户使用样例 十六 性能测试 测试环境 在什么样的环境下进行了什么测试得出的结果。 #include../logs/log.h #includechrono日志器名称 线程数量 日志数量 日志长度 void bench(const std::string name,size_t thr_count,size_t msg_count,size_t msg_len) {获取指定日志器logs::Logger::ptr logger logs::getlogger(name);if(logger.get() nullptr)return;组织一条日志留了一个空位给\n也就是说一条日志只放msg_len-1个有效字符,留一个放\n,因为\n也是一个字符std::string msg(msg_len-1,A);创建线程std::vectorstd::thread vt;std::vectorint vi; 记录每个线程耗时size_t msg_per_count msg_count / thr_count; 省略了日志余数for(int i 0; i thr_count;i){vt.emplace_back([,i]() 这里是在调用emplace创建线程,所以只需要传线程所需参数即可{ 也就是线程执行函数我们这里传了lambda表达式//计时开始auto begin std::chrono::high_resolution_clock::now();for(int j 0 ; j msg_per_count;j)//写入日志logger-Fatal(%s,msg.c_str());auto end std::chrono::high_resolution_clock::now();std::chrono::durationdouble cost end - begin;//计时结束vi.push_back(cost.count());std::cout线程: i 耗时: cost.count()sstd::endl;});} //必须先回收线程,不然主线程先退出,下面访问vi数组报段错误for(auto e : vt){e.join();}//计算总耗时int max_cost vi[0];for(int i 1; i vi.size();i){if(vi[i] max_cost){max_cost vi[i];}}int size_per_sec (msg_count * msg_len)/(max_cost*1024);//每秒输出日志大小,单位kint msg_per_sec msg_count/max_cost;//每秒输出日志条数std::cout总耗时: max_costsstd::endl;std::cout每秒输出日志大小 size_per_seckstd::endl;std::cout每秒输出日志条数 size_per_sec条std::endl; }注意计算时间是在线程执行函数内不能将线程创建和回收的时间算入其中。 不能用auto。 总耗时考虑的是最大执行时间因为在cpu资源充足的时候多线程是并行的所以总耗时是最大的时间。 同步异步测试代码下面这份代码就是基础的测试代码了后续的多线程和同步异步都是修改一些参数就可以测试了。 void syncbench() {// bench(sync,1,1000000,100);//同步单线程检测bench(sync,3,1000000,100);//同步多线程检测 } int main() {std::shared_ptrlogs::Globalloggerbuild localbuild(new(logs::Globalloggerbuild));//创建一个全局建造者//建造日志器对象成员localbuild-buildlevel(logs::loglevel::level::DEBUG);localbuild-buildformater(%d{%H:%M:%S}[%f:%l][%m]%n);localbuild-buildlogname(sync);localbuild-buildloggertype(logs::LoggerType::sync_Logger);localbuild-buildsinklogs::Filesink(./logs/test2.log);//组装后返回日志器,全局建造者返回的是全局日志器localbuild-build();syncbench();return 0; } 同步单线程 同步多线程 好像是还快了一点。本来以为同步多线程会因为锁冲突而更慢没想到比单线程还快了一点首先可能是线程数量不多冲突影响不大然后就是我用的服务器是两核的可以同时处理多个线程例如一个线程在处理指令另一个线程开始写这样交替进行就比单线程快了。 例如15个线程这个时候线程多反而是累赘了。 异步单线程 非安全模式业务线程会一直往缓冲区(也就是我们定义的vector)写一直扩容直到日志线程来交换此时业务线程就不会等工作线程把数据丢到文件才继续写所以我们下面计算耗时没有把落地的时间算入真考虑的话肯定是不如同步日志器的因为异步是先到内存再到磁盘同步日志器直接写磁盘反而省事了这个耗时我认为应该是表示业务线程完成完写日志任务的时间所以日志线程就不考虑日志落地到磁盘的时间。 void Asyncbench() {bench(Async,1,1000000,100);//异步单线程检测 }int main() {std::shared_ptrlogs::Globalloggerbuild localbuild(new(logs::Globalloggerbuild));//创建一个全局建造者//建造日志器对象成员localbuild-buildlevel(logs::loglevel::level::DEBUG);localbuild-buildformater(%d{%H:%M:%S}[%f:%l][%m]%n);localbuild-buildlogname(Async);localbuild-buildUnsafeAsync();localbuild-buildloggertype(logs::LoggerType::Asynclooper);localbuild-buildsinklogs::Filesink(./logs/test2.log);//组装后返回日志器,全局建造者返回的是全局日志器localbuild-build();Asyncbench();return 0; } 和同步差别不大因为我们都是往内存写我们在操作系统文件章节曾提及往文件写也不是真的写到磁盘而是写到系统在内存的文件缓冲区由os决定什么时候刷新。 异步多线程 6个线程速度加快 14个线程开始变慢。 好了日志系统的项目实现就讲完了对了如果不小心在vscode上删了自己的文件还是可以恢复的百度一下就可以了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/931863.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网站开发后台指什么网站查询器

技术驱动下,现代企业快速发展,产生海量的数据。被称为基础软件三驾马车之一的数据库,一直处于 IT 系统的核心地位,并在技术发展中不断变化。基础数据是“十四五”的重点关注方向,中国数据库正在快速发展崛起&#xff0…

塑料回收技术创新与可持续发展

本文探讨了通过分子级塑料重构和新型化学回收技术实现塑料全生命周期净零碳排放的创新方法,重点介绍了可降解材料开发和混合塑料废物高效处理技术。某中心与能源部门合作推动塑料回收技术革新 某中心加入了美国能源部…

共享掩码:TFHE在打包消息上的自举技术

本文探讨了基于矩阵LWE假设的全同态加密方案,通过引入共享掩码密文格式显著降低密文扩展。研究展示了如何将TFHE类操作扩展到该格式,在布尔场景下打包8条消息可实现51%的性能提升,同时支持在单个密文中应用不同查找…

网站开发任务分解临沂seo公司稳健火星

1. 安装是成功的,但是安装位置,就是用来存放petalinux的文件夹里没有文件 我是照着正点的文档安装的,出现的一个问题就是最后执行文件这里: -d 后面这个文件夹的路径,我看网上的教程也都是跟文档一致的 /opt/pkg/peta…

详细介绍:[论文阅读] (38)基于大模型的威胁情报分析与知识图谱构建论文总结(读书笔记)

详细介绍:[论文阅读] (38)基于大模型的威胁情报分析与知识图谱构建论文总结(读书笔记)pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !importan…

手机搭建免费网站wordpress 模拟登陆

01背包 有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。 第 i件物品的体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出最大价值。 输入格式 第一行两个整数,N…

观澜网站制作做户外的网站

在上一节我们看到了,多人在同一个分支上协作时,很容易出现冲突。即使没有冲突,后push的童鞋不得不先pull,在本地合并,然后才能push成功。 每次合并再push后,分支变成了这样: $ git log --grap…

永康网站建设zjyuxunWordPress推荐版本

密封类和密封成员需要使用 sealed 修饰符,他可以防止当前类被继承或者防止派生类在继承的过程中重写某个方法。 与abstract抽象修饰符类似,sealed 修饰符不仅可用来修饰class,同样也可以修饰类成员。如果sealed关键词用在class上&#xff0c…

免费linux网站空间学做凉菜冷菜的网站

LLaVA:GPT-4V(ision) 的新开源替代品。 LLaVA (https://llava-vl.github.io/,是 Large Language 和Visual A ssistant的缩写)。它是一种很有前景的开源生成式 AI 模型,它复制了 OpenAI GPT-4 在与图像对话方面的一些功…

果女做拍的视频网站wordpress单页主题汉化

在人工智能的浩瀚宇宙中,自然语言处理(NLP)一直是一个充满挑战和机遇的领域。随着技术的发展,我们见证了从传统规则到统计机器学习,再到深度学习和预训练模型的演进。如今,我们站在了大型语言模型&#xff…

打印

View Post打印第一步:权限申请 在module.json5中进行如下配置; "requestPermissions": [{"name": "ohos.permission.PRINT","reason": "$string:permissionsReason&qu…

实用指南:Cursor 工具项目构建指南: Web Vue-Element UI 环境下的 Prompt Rules 约束(new Vue 方式)

实用指南:Cursor 工具项目构建指南: Web Vue-Element UI 环境下的 Prompt Rules 约束(new Vue 方式)pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: b…

完整教程:vue2 项目中 npm run dev 运行98% after emitting CopyPlugin 卡死

完整教程:vue2 项目中 npm run dev 运行98% after emitting CopyPlugin 卡死2025-10-08 19:08 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-…

利用虚拟主机建设企业网站上海si设计公司

最近做用Ajax.AjaxMethod方法的时候,在asp.net的服务器下一切正常,用iis的时候,js中总是cs类找不到,我就郁闷了,折腾了大半天,终于找到错误原因了。因为我发布网站用的是iis7,所以在web.config位…

网站开发的英文书有什么软件安卓应用市场免费下载安装

实体 实体是具有唯一标识的对象,且该标识和对象的属性值分离.即使两个实体的属性完全相同,这两个实体也相同,不能交换使用.由于实体通常对应于现实世界的概念. 是领域模型的中心,因此实体的标识非常重要. 值对象 值对象是主要由其属性值定义的对象.值对象通常不可变,即一旦创建…

广州黄埔区做网站培训机构建设官网公司地址

文章目录一、综述二、常见的回归分析三、对于相关性的理解四、一元线性回归模型五、对于回归系数的解释六、内生性七、四类线性模型回归系数的解释八、对于定性变量的处理——虚拟变量XXX九、下面来看一个实例十、扰动项需要满足的条件十一、异方差十二、多重共线性十三、逐步回…

VsCode 安装 Cline 插件并使用免费模型(例如 DeepSeek) - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

2025球墨铸铁管厂家 TOP 企业品牌推荐排行榜,市政球墨铸铁管、球墨铸铁管件、防腐球墨铸铁管、给水球墨铸铁管推荐这十家公司!

在基础设施建设领域,球墨铸铁管凭借其优异的抗压性能、耐腐蚀特性以及较长的使用寿命,成为供水、排水、燃气输送等工程中的重要建材。然而,当前球墨铸铁管市场并非一片规范,行业内存在不少问题亟待解决。一方面,部…

网站整站html网页设计与制作千年之恋代码

在选择海外IP代理服务时,您将面临一个关键的问题:是选择住宅代理IP还是数据中心代理IP?这两者之间存在着根本性的不同,涉及到性能、隐私和成本等方面的考虑。住宅代理IP通常来自真实的住宅网络连接,更难被检测到。数据…

龙岗网络营销网站制作哪里好做家具厂招聘有哪些网站

android-verticalseekbar——Android可视化SeekBar类库转载于:https://www.cnblogs.com/zhujiabin/p/5706246.html