> 文章列表 > Log库和配置系统结构

Log库和配置系统结构

Log库和配置系统结构

Log库:

类关系

首先有3个大类:LogEvent、LogAppender、Logger、LogFormat;

关系如下:

        

Logger:具体log的实现

LogAppender:将Log信息传输到不同的目的地,根据不同的需求派生出不同的类

LogFormat:存储格式信息,并根据LogEvent生成信息

log Event:存储具体的log信息,如内容、时间戳、线程号等等

类实现:

LogFormat:格式由%[ item ]{ [data] }构成

首先对于每一个item,我的方法是通过策略模式来解决,每一个item都继承自FormatItem,比如输出时间信息的TimeFormatItem,输出线程号的ThreadIDFormatItem,每一种不同的信息都用不同的策略;

然后将格式字符串解析出来,根据不同的格式生成不同的策略,最后在接收LogEvent的时候从中提取信息到每一个Item中,并生成最后的Log信息

		class FormatItem {public:using Ptr = std::shared_ptr<FormatItem>;virtual ~FormatItem(){}//os存储最后的字符串virtual void format(std::ostream& os, Logger*, LogLevel, LogEvent::Ptr) = 0;};

比如%d %m解析为输出时间和内容;

LogEvent

LogEvent就是一个存储信息的地方,在这一方面没什么好说的,一堆变量,我们可以让LogEvent支持c风格的printf,这样我们可以将处理后的字符串作为内容:

	class LogEvent {public://"xxxx %s" ,"string" 的形式会在void Format(const char* fmt, va_list al, bool isStart = false);进行,因为va_list是char*,//所以设计一个Flag标记,防止错误载入struct Flag{};using Ptr = std::shared_ptr<LogEvent>;LogEvent(std::shared_ptr<Logger> logger, LogLevel level, const char* file, int32_t m_line, uint32_t elapse, uint32_t thread_id, uint32_t fiber_id, uint64_t time);const std::string GetContent() { return m_Content.str(); }std::stringstream& GetStream() { return m_Content; }uint32_t GetElapse() { return m_elaspe; }uint64_t GetTime() { return m_time; }uint32_t GetThreadID() { return m_ThreadId; }uint32_t GetLine() { return m_Line; }uint32_t GetFiberID() { return m_FiberId; }const char* GetFile() { return m_file; }LogLevel GetLevel() { return m_Level; }std::shared_ptr<Logger>& GetLogger() { return m_Logger; }void Format(const char* fmt, ...);private:void Format(const char* fmt, Flag ,va_list al, bool isStart = false);const char* m_file = nullptr;	//文件名std::stringstream m_Content;			//内容int32_t m_Line = 0;				//行号uint32_t m_ThreadId = 0;		//线程号uint32_t m_FiberId = 0;			//协程号uint64_t m_time = 0;			//时间戳uint32_t m_elaspe = 0;			//从重新开始到目前的时间std::shared_ptr<Logger> m_Logger;LogLevel m_Level;friend class LogFormatter;};

支持c风格的格式化:

	void LogEvent::Format(const char* fmt, ...) {va_list al;va_start(al, fmt);Format(fmt, Flag{},al, true);va_end(al);}void LogEvent::Format(const char* fmt, Flag ,va_list al,bool isStart) {char* buf = nullptr;int len = vasprintf(&buf, fmt, al);if (len != -1) {m_Content << buf;free(buf);}}

注意,如果是Windows的话vasprint要自己去实现一下:

#ifdef _WIN32int vasprintf(char strp, const char* fmt, va_list ap){va_list ap_copy;va_copy(ap_copy, ap);int len = vsnprintf(NULL, 0, fmt, ap);if (len < 0) {return -1;}*strp = (char*)malloc(len + 1);if (*strp == NULL) {return -1;}len = vsnprintf(*strp, len + 1, fmt, ap_copy);va_end(ap_copy);return len;}int asprintf(char ptr, const char* format, ...) {va_list ap;int ret;*ptr = NULL;va_start(ap, format);ret = vasprintf(ptr, format, ap);va_end(ap);return ret;}
#endif

LogAppender

这个的话,这个类有很多种实现方法,具体的思路就是持有一个LogFormat,然后给它一个LogEvent,将生成出来的字符串加入到目的地,如文件或者控制台;

Logger

这个也非常好弄,他就是一个LogAppender的集合,将一个LogEvent分发给不同的LogAppender,实现一个信息分发到不同目的地;

End

也可以写一个Manger类,来管理所有的Logger,这个结构呢,就是怎么解析字符串,然后生成不同的item比较难,其他都是顺水推舟了;

Config系统:

使用了YAML-CPP库

Config系统的话,有3大类和两大操作:

ConfigVarBase、ConfigVar<T>:public ConfigVarBase、Config

两大操作:FormString、ToString

ConfigVarBase

ConfigVarBase就一个接口,没什么好说的,看代码:

	class ConfigVarBase {public:using Ptr = std::shared_ptr<ConfigVarBase>;ConfigVarBase(const std::string& name,const std::string& description = ""):m_Name(name), m_Description(description) {//To lowerstd::transform(name.begin(), name.end(), m_Name.begin(), ::tolower);}virtual ~ConfigVarBase(){}const std::string& GetName()const { return m_Name; };const std::string& GetDescription()const {return m_Description;};virtual std::string ToString() = 0;virtual bool FromString(const std::string& val) = 0;protected:std::string m_Name;std::string m_Description;};

ConfigVar<T>

然后是ConfigVar<T>了,这个类的话,是一个模板,存储不同类型的信息,对应不同的配置需要,比如存储一个vector或者map。

我们会有一个ToString和FromString的操作,这俩呢,就是序列化和反序列话,将信息存储到文件中,然后从文件在加载信息,一段信息经过两次操作不能改变;

	template<class T,class FormSting_ = LexicalCast<std::string, T>,class ToString_ = LexicalCast<T, std::string>>class ConfigVar :public ConfigVarBase {FormSting_ formStr;ToString_ toStr;public:using Ptr = std::shared_ptr<ConfigVar<T>>;ConfigVar(const std::string& name, const T& defaultVal, const std::string& description = ""):ConfigVarBase(name, description), m_Val(defaultVal){}std::string ToString() override {try {return toStr(m_Val);}catch (std::exception& e) {LU_LOG_ERROR(LU_LOG_ROOT()) << "ConfigVar::ToString expection" <<e.what() << " convert :" << typeid(m_Val).name() << " to string";}}const T& GetValue() { return m_Val; }static std::string GetTypeName() { return typeid(T).name(); }virtual bool FromString(const std::string& val) override {try {m_Val = formStr(val);return true;}catch (std::exception& e) {LU_LOG_ERROR(LU_LOG_ROOT()) << "ConfigVar::ToString expection" <<e.what() << " convert :" << "string to "<< typeid(m_Val).name();return false;}}private:T m_Val;};

这边我们用LexicalCast将ToString和FormString分离到类外去操作,这样可以增加代码的可读性和可拓展性;

Config

这个类是最主要的一个类,作用是管理ConfigVar和读取文件,并根据读取的信息设置配置;

	class Config {public:using Ptr = std::shared_ptr<Config>;template<class T>static typename ConfigVar<T>::Ptr LookUp(const std::string& name, const T& defaultValue, const std::string& description) {typename ConfigVar<T>::Ptr config = LookUp<T>(name);if (config) {return config;}if (name.find_first_not_of("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM._0123456789") != std::string::npos) {LU_LOG_ERROR(LU_LOG_ROOT()) << "lookup name invaild :" << name;throw std::invalid_argument(name);}typename ConfigVar<T>::Ptr v(new ConfigVar<T>(name, defaultValue, description));map.emplace(name, v);return std::dynamic_pointer_cast<ConfigVar<T>>(map.at(name));}template<class T>static typename ConfigVar<T>::Ptr LookUp(const std::string& name) {auto it = map.find(name);if (it != map.end()) {ConfigVar<T>::Ptr res = std::dynamic_pointer_cast<ConfigVar<T>>(it->second);if (!res) LU_LOG_ERROR(LU_LOG_ROOT()) << "Lookup name :" << name << " exist but type not is " << ConfigVar<T>::GetTypeName() << "  " << it->second->ToString();elseLU_LOG_INFO(LU_LOG_ROOT()) << "Lookup name :" << name << " exist";return res;}LU_LOG_ERROR(LU_LOG_ROOT()) << "Lookup name :" << name << " not exist";return nullptr;}static ConfigVarBase::Ptr LookupBase(const std::string& name);static void LoadFormYaml(const YAML::Node& root);private:static std::unordered_map<std::string, ConfigVarBase::Ptr> map;};

这边的话主要是看LoadFormYaml这个方法,这个方法是接受一个Node节点,这个节点是存储被读取文件的整个信息,读取数据后根据数据设置map中的ConfigVar:

	static void ListAllMember(const std::string& prefix,const YAML::Node& node, std::list<std::pair<std::string, const YAML::Node>>& output) {if (prefix.find_first_not_of("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM._0123456789") != std::string::npos) {LU_LOG_ERROR(LU_LOG_ROOT()) << "Congig invaild name " << prefix << " : " << node;return;}/ Scalar是直接读取字符串形式的数据,* 构建     x*		  / \\*      x.y   x.z*       |*      x.y.u*/output.push_back({ prefix,node });if (node.IsMap()) {for (auto& t : node) {ListAllMember(prefix.empty()?t.first.Scalar() : prefix + "." + t.first.Scalar(),t.second, output);}}}void Config::LoadFormYaml(const YAML::Node& root) {std::list<std::pair<std::string, const YAML::Node>> allNodes;ListAllMember("",root, allNodes);for (auto& it : allNodes) {std::string key = it.first;if (key.empty())continue;std::transform(key.begin(), key.end(), key.begin(), ::tolower);ConfigVarBase::Ptr var = LookupBase(key);if (var) {if (it.second.IsScalar()) {var->FromString(it.second.Scalar());}else {std::stringstream ss;ss << it.second;var->FromString(ss.str());}}}}

主要是实现这两个函数,ListAllMember呢,是将root node中的所有节点都加入到一个list中,而且名字也有讲究,ConfigVar的name就root node展开的树中对应该ConfigVar节点的路径,路径名字的格式已经在代码中给出了,根据这个名字我们可以给每个ConfigVar设置数据;