您现在的位置是:首页 >技术教程 >Log库和配置系统结构网站首页技术教程

Log库和配置系统结构

刘欢明 2023-05-12 20:15:03
简介主要是实现这两个函数,ListAllMember呢,是将root node中的所有节点都加入到一个list中,而且名字也有讲究,ConfigVar的name就root node展开的树中对应该ConfigVar节点的路径,路径名字的格式已经在代码中给出了,根据这个名字我们可以给每个ConfigVar设置数据;然后是ConfigVar了,这个类的话,是一个模板,存储不同类型的信息,对应不同的配置需要,比如存储一个vector或者map。LogFormat:存储格式信息,并根据LogEvent生成信息。

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 _WIN32
	int 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 lower
			std::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();
				else
					LU_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设置数据;

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。