您现在的位置是:首页 >技术教程 >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 _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设置数据;