? ? ? ? 日志系統的功能也就是將一條消息格式化后寫入到指定位置,這個指定位置一般是文件,顯示器,支持拓展到數據庫和服務器,后面我們就知道如何實現拓展的了,支持不同的寫入方式(同步異步),同步:業務線程自己寫到文件中,也就是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,不能是pos+1,當我們要創建 ./test/test2的時候,可是最后一次find返回-1,此時如果substr(0,pos+1)就會什么都截取不了。
功能測試
測試結果:
二 日志等級類模塊
? ? ? ? 模塊實現
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是c++11新推出的,它的區別在于內部定義的宏是有作用域的,要使用必須: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::cout<<logs::loglevel::to_String(logs::loglevel::level::DEBUG)<<std::endl;std::cout<<logs::loglevel::to_String(logs::loglevel::level::ERROR)<<std::endl;std::cout<<logs::loglevel::to_String(logs::loglevel::level::FATAL)<<std::endl;std::cout<<logs::loglevel::to_String(logs::loglevel::level::INFO)<<std::endl;std::cout<<logs::loglevel::to_String(logs::loglevel::level::UNKNOW)<<std::endl;std::cout<<logs::loglevel::to_String(logs::loglevel::level::WARN)<<std::endl;std::cout<<logs::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_ptr<Format>; c++11支持的取別名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{out<<logs::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數組中,顯然這個格式我們可以指定out<<arr;}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{out<<lsg._logger;}};消息主體處理類class PayloadFormat:public Format{public:void format(std::ostream& out,const logs::LogMsg& lsg)override{out<<lsg._payload;}};文件名處理類class FileFormat:public Format{public:void format(std::ostream& out,const logs::LogMsg& lsg)override{out<<lsg._file;}};線程id處理類class TidFormat:public Format{public:void format(std::ostream& out,const logs::LogMsg& lsg)override{out<<lsg._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::vector<Format::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::vector<std::pair<std::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[i+1] == '%')//是普通字符{val.push_back(_format[i]);i+=2;//跳過%continue;}else 解析%d{if(val.size())// 先push先前的普通字符到vss數組中vss.push_back(std::make_pair(key,val));val = ""; 然后清空val 例子:ab%dif(i+1 == _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 = pos+1;//跳過"%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_shared<TimeFormat>(val);if(key == "t") return std::make_shared<TidFormat>();if(key == "c") return std::make_shared<LoggerFormat>();if(key == "f") return std::make_shared<FileFormat>();if(key == "l") return std::make_shared<LinelFormat>();if(key == "p") return std::make_shared<LevelFormat>();if(key == "m") return std::make_shared<PayloadFormat>();if(key == "n") return std::make_shared<NlineFormat>();if(key == "T") return std::make_shared<TableFormat>();if(key == "") return std::make_shared<OtherFormat>(val);//例如遇到%gstd::cout<<"%之后的格式字符輸入錯誤:% "<<key<<std::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_ptr<Formater>;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::vector<Format::ptr> _vf;//保存格式處理函數,后續按順序調用};
};
????????功能測試
void test3()//測試日志消息類和格式化類
{定義了一條日志logs::LogMsg msg(logs::loglevel::level::DEBUG,__FILE__,"日志器1","測試日志",__LINE__);給日志定格式 logs::Formater ft;std::cout<<ft.format(msg); Formater類內的format函數會執行格式化子項函數,并且將結果返回,我們直接打印看一看
}
????????沒有給Formater ft傳入格式時,結果如下,用了缺省格式。
? ? ? ? 如果傳了,結果如下,此時我們可以發現,只有我們輸入了對應的格式字符,日志消息才會輸出對應的部分,具體原因大家可以結合先前代碼理解。
????????經過前面幾步,一條日志已經被制作出來了,難道日志做出來就是直接cout嗎?如何控制輸出方向呢?就由接下來的落地類來實現了
五?日志落地類
? ? ? ? 模塊實現
? ? ? 將日志消息輸出到指定位置,支持拓展日志消息落地到不同位置,如何支持拓展的呢?后面我們就知道了。下面這幾個落地位置是我們自己實現的。
1?標準輸出 2?指定文件 3?滾動文件(也就是按大小和時間切換新文件保存)?
如果我們是只實現一個類內函數的話,那顯然在后續增加落地方向的時候就要修改類內函數,這顯然是不符合開閉原則的,而下面這種抽象出基類,在派生實現落地邏輯的設計,后續增加落地方向可以不修改原來代碼,而是直接添加一個新的子類,符合開閉原則。
namespace logs
{class sink{public:智能指針類型取別名,因為后續要使用智能指針來管理sink的子類對象using ptr = std::shared_ptr<sink>;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_year+1900; sst<< st.tm_mon+1;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:template<typename T,typename ...Args>static std::shared_ptr<T> Create(Args&& ...args) {return std::make_shared<T>(std::forward<Args>(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::Create<logs::Stdoutsink>(); sptr->log( ret.c_str(),ret.size());auto sptr2 = logs::Factorysink::Create<logs::Filesink>("./logs/test.log");sptr2->log( ret.c_str(),ret.size());auto sptr3 = logs::Factorysink::Create<logs::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_year+1900;sst<< st.tm_mon+1;sst<< st.tm_mday;sst<<st.tm_hour;sst<<st.tm_min;sst<<st.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::Create<RollBytimesink>("./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::vector<sink::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::atomic<loglevel::level> _limit_level; std::vector<sink::ptr> _sinks;//保存了該日志器的落地方向};
};
? ? ? 日志器的名稱是唯一標識的,作用后提,后面我們是可以將日志器統一管理的,而日志器名就是日志器對象的唯一標識。這個限制等級是經常要被訪問的,在目前的接口來看幾乎不對其進行修改,應該不用擔心線程安全的問題,如果后面擔心后面會修改,就設為原子性,懶得加鎖了,因為如果一個線程執行要申請很多鎖,就比較容易沖突,執行比較慢。
???????? 所以日志器內部的Debug函數負責構建Debug日志,Info函數構建一條info級別的日志,然后子類實現log來決定這條日志的去向。為啥不直接在父類內實現呢?
????????為什么要子類來實現呢? 因為我們要實現同步日志器和異步日志器,這兩個日志器的區別在于落地方式的不同,注意不是落地方向,同步日志器是直接輸出,而異步日志器是交給緩沖區,由其它線程去輸出,但是日志的構建大家都是一樣的,所以抽象出基類共同使用。抽象基類還有個好處是?可以用基類指針對子類日志器管理和操作,這也得后面提了。
抽象類內部實現
????????看著多,但只要了解了一個Debug函數,其它幾個都是幾乎一模一樣的。
namespace logs
{class Logger{protected:Logger(std::string logger,std::vector<sink::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::atomic<loglevel::level> _limit_level;std::vector<sink::ptr> _sinks;//保存了該日志器的落地方向};
};
子類同步日志器實現
????????各個sink直接調用log函數。
class sync_Logger:public Logger{public:sync_Logger(std::string& logger,std::vector<sink::ptr> sinks,loglevel::level lev,Formater::ptr& formater):Logger(logger,sinks,lev,formater) 這是在調用基類的構造函數{;}void log(const char* dst,size_t size){std::unique_lock<std::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_shared<Formater>(parttern);} void buildloggertype(LoggerType& type){_type = type;}template<typename T,typename ...Args>void buildsink(Args&& ...args){_sinks.push_back(std::make_shared<T>(std::forward<Args>(args)...));}virtual Logger::ptr build() = 0;std::mutex _mutex;LoggerType _type;Formater::ptr _formater;std::string _logger;//日志器名稱std::atomic<loglevel::level> _limit_level;std::vector<sink::ptr> _sinks;//保存了該日志器的落地方向};
? ????????根據類型返回同步和異步日志器,這些都是局部的日志器,都是只能定義的地方使用,其它作用域要使用得通過傳參等方式,比較麻煩,全局的則是任意地方都可以獲取日志器對象來輸出日志。
enum class LoggerType{Asynclooper,sync_Logger}; 根據枚舉類型返回不同的日志器對象class Localloggerbuild:public loggerbuild{Logger::ptr build()override{if(_type == LoggerType::Asynclooper) 異步,這個return可以在實現完異步日志器再補全return std::make_shared<Async_Logger>(_logger,_sinks,_limit_level,_formater);return std::make_shared<sync_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::vector<char> _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,dst+len,&_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::vector<char> _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::function<void(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_lock<std::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_lock<std::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保存SAFE,push的時候就不會在條件變量下判斷可寫區域是否足夠,而是直接調用緩沖區的push函數,內部無限擴容。反之,工作器對象在push的時候就會在條件變量下等待,即便被喚醒,也要判斷可寫區域是否足夠,如果有一次len大于緩沖區的長度,而緩沖區又是有限的,此時就會一直阻塞住,這不是bug,而是我們使用不當,是我們自己設置緩沖區有限,不能擴容,還往緩沖區內部輸入超限的數據。
????????一個工作器對象一個線程?
????????所以我們的構造函數就需要兩個參數,一個是回調函數,還有一個是枚舉類型表示該工作器是否安全。
十二 異步日志器
????????設計
? ? ? ? 同步日志器上面剛剛實現完,而且我們發現同步日志器只需要復用父類的成員即可,不需要自己添加成員,但是異步日志器需要,需要一個工作器,內部定義線程,對消費者緩沖區內的數據做處理。最后設計的接口成員如下。
class Async_Logger:public Logger{public:Async_Logger(std::string& logger,std::vector<sink::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::vector<sink::ptr> sinks,loglevel::level lev,Formater::ptr& formater,loopersafe loopsafe):Logger(logger,sinks,lev,formater) 調用基類的構造函數,_looper(std::make_shared<Asynclooper>定義一個工作器(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::Create<logs::Stdoutsink>()); }for(auto e : _sinks){e->log(buf.begin(),buf.ReadAbleSize());}}private:Asynclooper::ptr _looper;};
????????比較有意思的還是下面這個bind語法,首先我們是在調用make_shared<Asynclooper>()返回一個智能指針,括號內部是在調用構造函數。
異步日志器測試
void test9()//測試異步日志器
{std::shared_ptr<logs::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->buildsink<logs::Filesink>("./logs/test.log");localbuild->buildsink<logs::Rollsink>("./logs/test.log",1024);logs::Logger::ptr lptr = localbuild->build();int i = 0;int count = 0;while(i < 5*1024){i += 21;std::cout<<"count "<<count++<<std::endl;lptr->Debug(__FILE__,__LINE__,"日志測試");}
}
????????按理說應該是有5個文件,但是我這里測試后只有三個文件。
????????由于我們是先往文件輸入日志后才判斷是否會超過容量,所以就會出現此時文件里面已經放了1000字節了,結果工作線程獲得了任務緩沖區的數據,這里面也有1000字節,直接寫入,就超限了,為什么測試緩沖區的時候是五個文件呢?大家可以去看看上面的測試代碼,當時是一個個字節寫入的,所以寫入文件時不會超出容量太多,就能開辟五個文件,我們這里一個文件放了2kb,就導致只開了三個文件。
完善建造者類
? ? ? ? 由于異步日志器內部多了個工作器,然后在傳參時需要給工作器傳回調函數和一個枚舉類型,所以我們需要對建造者類進行修改。先給建造者父類增加一個成員,用來記錄枚舉類型,后面給日志器的構造函數傳參。
????????可是為什么不是把成員定義在下面這個子類里面呢? 因為下面這個是局部的日志器創建,后面還有個全局的日志器創建,也要用到這個成員。
測試
????????異步日志器測試bug,發現寫入數據變少,原因:還有部分數據在生產緩沖區,但是_stop已經被設為true,這部分數據就丟失了。代碼修改如下,必須把生產緩沖數據處理完才能結束。
十三? 日志器管理模塊
? ? ? ? 實現原因
? ? ? ? 由于我們上面返回的日志器都是局部日志器,都只是在某個作用域有效,其它函數作用域都無法使用,傳參太過麻煩,所以我們希望有個方法可以讓我們創建一個日志器后可以在任意地方使用。所以我們設計出了一個日志管理類,這個類內部保存了創建好的日志器,可以讓我們在任意地方獲取。
????????顯然這個管理類必須是個單例模式,不然的話我在一個函數內添加了日志器,這個日志器被管理類對象保存,結果這個管理類對象也是個局部的,別的作用域又如何訪問這個成員內部的日志器呢。
設計
_loggers是管理所有日志器的數組,應該用map,LoggerManger應該是個單例模式,構造函數私有。
實現
class LoggerManager{LoggerManager& getinstance()//返回單例的管理者對象{static LoggerManager lm;return lm;}void addlogger(Logger::ptr ptr)//添加一個日志器被管理{std::unique_lock<std::mutex>(_mutex);if(haslogger(ptr->name()))//免得覆蓋了return;_mp.insert(std::make_pair(ptr->name(),ptr));//添加被管理的日志器}Logger::ptr getlogger(const std::string& name){std::unique_lock<std::mutex>(_mutex);//防止別人在別人添加的時候獲取,導致獲取錯誤數據return _mp[name];}bool haslogger(const std::string& name){std::unique_lock<std::mutex>(_mutex);if(_mp.count(name))return true;return false; }private:LoggerManager(){std::unique_ptr<Localloggerbuild> build(new Localloggerbuild());build->buildlogname("日志器2");_root日志器只需要指定名稱,其余的都有默認的初始化值_root = build->build(); _mp[_root->name()] = _root; 默認日志器也要添加到容器中,這樣也能通過get獲取}std::mutex _mutex;std::map<std::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())buildsink<Stdoutsink>();if(_formater.get() == nullptr)_formater = std::make_shared<Formater>();Logger::ptr logger;if(_type == LoggerType::Asynclooper)logger = std::make_shared<Async_Logger>(_logger,_sinks,_limit_level,_formater,_looper_type);elselogger = std::make_shared<sync_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_ptr<logs::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->buildsink<logs::Filesink>("./logs/test2.log");localbuild->buildsink<logs::Rollsink>("./logs/test2.log",1024);logs::Logger::ptr lptr = localbuild->build();//建造日志器test_log();
}
????????在其它函數內獲取日志器來輸出,可以獲取并輸出才表示日志器可以在全局獲取。由于是異步的,最后也是生成了幾個文件,不足五個,但是大小卻是足夠的。
十四?封裝
思想
? ? ? ? 實現到了這一步,我們基本上的代碼差不多寫完了,接下來就是一些小封裝實現。我在main.cc測試的時候,要使用功能就得包含不少頭文件,
? ? ? ? ?下面這里用戶要先調用管理類的靜態成員,再獲取獲取日志器,我們應該避免讓用戶去獲取單例的管理者對象,而且輸出日志每次都要傳__FILE__,__LINE__這兩個宏。
????????所以我們決定提供一些接口和宏函數,對日志系統接口進行使用便捷性優化。
1.?提供獲取指定日志器的全局接口(避免用戶自己操作單例對象)
2.?使用宏函數對日志器的接口進行代理(代理模式)
3.提供宏函數,可以直接進行日志的標準輸出打印
實現
????????下面這個封裝在log.h中。
????????下面兩個全局接口就可以讓用戶直接獲取日志器,而不用獲取日志管理對象。
#include"logger.hpp"
#include<stdarg.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::cout<<count<<std::endl;
}
void test12()
{std::shared_ptr<logs::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->buildsink<logs::Filesink>("./logs/test2.log");localbuild->buildsink<logs::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::cout<<count<<std::endl;
}
而且只需要包含一個頭文件。
十五?目錄梳理
????????因為我們寫代碼還要上傳到gitte上,我們不得寫好看點嗎,不得把代碼整理一下。example里面是使用樣例,我把測試代碼和最終封裝后的接口使用樣例都放在了這里,logs內部是我們提供的組件源代碼。
用戶使用樣例
十六?性能測試
? ? ?
測試環境:
在什么樣的環境下,進行了什么測試,得出的結果。
#include"../logs/log.h"
#include<chrono>日志器名稱 線程數量 日志數量 日志長度
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::vector<std::thread> vt;std::vector<int> 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::duration<double> cost = end - begin;//計時結束vi.push_back(cost.count());std::cout<<"線程: "<<i<<" 耗時: "<<cost.count()<<"s"<<std::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_cost<<"s"<<std::endl;std::cout<<"每秒輸出日志大小 "<<size_per_sec<<"k"<<std::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_ptr<logs::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->buildsink<logs::Filesink>("./logs/test2.log");//組裝后返回日志器,全局建造者返回的是全局日志器localbuild->build();syncbench();return 0;
}
同步單線程
????????同步多線程
????????好像是還快了一點。本來以為同步多線程會因為鎖沖突而更慢,沒想到比單線程還快了一點,首先可能是線程數量不多,沖突影響不大,然后就是我用的服務器是兩核的,可以同時處理多個線程,例如一個線程在處理指令,另一個線程開始寫,這樣交替進行就比單線程快了。
例如15個線程,這個時候線程多反而是累贅了。
異步單線程
????????非安全模式業務線程會一直往緩沖區(也就是我們定義的vector)寫,一直擴容,直到日志線程來交換,此時業務線程就不會等工作線程把數據丟到文件才繼續寫,所以我們下面計算耗時沒有把落地的時間算入,真考慮的話肯定是不如同步日志器的,因為異步是先到內存,再到磁盤,同步日志器直接寫磁盤反而省事了,這個耗時我認為應該是表示業務線程完成完寫日志任務的時間,所以日志線程就不考慮日志落地到磁盤的時間。
void Asyncbench()
{bench("Async",1,1000000,100);//異步單線程檢測
}int main()
{std::shared_ptr<logs::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->buildsink<logs::Filesink>("./logs/test2.log");//組裝后返回日志器,全局建造者返回的是全局日志器localbuild->build();Asyncbench();return 0;
}
和同步差別不大,因為我們都是往內存寫,我們在操作系統文件章節曾提及,往文件寫也不是真的寫到磁盤,而是寫到系統在內存的文件緩沖區,由os決定什么時候刷新。
異步多線程
6個線程,速度加快
14個線程,開始變慢。
好了,日志系統的項目實現就講完了,對了如果不小心在vscode上刪了自己的文件還是可以恢復的,百度一下就可以了。
????????