仿Spdlog日志系统项目
前言
本项目实现一个同步异步日志组件,以日志器作为单位,支持同步和异步两种输出方式,以及向不同的方向进行落地。
本项目分为以下几个模块:
- 日志输出等级模块:对日志等级进行划分,以便后续对日志输出进行控制。同时提高枚举等级转字符串功能。
- 日志消息模块:封装一条完整日志所需各项要素。
- 日志格式化模块:根据用户指定输出格式,从消息模块中按序取出对应要素,并进行格式化得到目标字符串。
- 日志落地模块:决定了日志落地的方向。本项目实现了3种:标准输出、指定文件、滚动文件。
- 日志器模块:对上述所有模块进行整合,对外提供不同等级日志的输出接口,⽤⼾通过⽇志器进⾏⽇志的输出,降低用户使用难度。分为局部日志器和全局日志器(需要被管理起来)。包含的成员有:日志限制等级、格式化模块对象,日志落地对象。
- 日志器管理模块:不同的项目组拥有各自的日志器来控制日志输出格式和落地方向,因此本项⽬是⼀个多⽇志器的⽇志系统。管理模块就是对创建的所有全局⽇志器进⾏统⼀管理。并提供⼀个默认⽇志器提供标准输出的⽇志输出。
- 异步线程模块:实现异步日志输出功能。用户只需将输出日志任务放到任务池中即可返回,异步线程进行实际日志落地,以此提供更加高效的非阻塞日志输出。
具体工作流程为:通过日志器调用不同的日志输出接口,在函数内部,先将这条⽇志等级和⽇志内容封装成⼀个⽇志消息对象,在通过⽇志消息格式化模块将⽇志消息进⾏统⼀的格式化处理, 处理完成之后将消息交给LogSink对象进⾏落地输出。
一、 日志输出等级模块
日志提供了 UNKNOW、DEBUG、INFO、 WARN、 ERROR、FATAL、OFF7个等级!
namespace Mylog
{
class LogLevel
{
public:
enum class value
{
UNKNOW = 0,
DEBUG,
INFO,
WARN,
ERROR,
FATAL,
OFF
};
static const char *ToString(LogLevel::value level)
{
switch (level)
{
case LogLevel::value::DEBUG:
return "DEBUG";
case LogLevel::value::INFO:
return "INFO";
case LogLevel::value::WARN:
return "WARN";
case LogLevel::value::ERROR:
return "ERROR";
case LogLevel::value::FATAL:
return "FATAL";
case LogLevel::value::OFF:
return "OFF";
}
return "UNKNOW";
}
};
}
二、 日志消息模块
日志消息模块:封装一条完整日志所需各项要素。包含:⽇志等级、对应的logger name、⽇志源⽂件的位置信息(包括⽂件名和⾏号)、线程ID、间戳信息、具体的⽇志信息
namespace Mylog
{
struct LogMsg
{
time_t _ctime; // 时间戳
LogLevel::value _level; // 日志输出等级
std::thread::id _tid; // 线程
size_t _line; // 行号
std::string _file; // 文件名
std::string _logger; // 日志器名称
std::string _payload; // 有效主体消息
LogMsg(LogLevel::value level, std::string file, size_t line, std::string logger, std::string meg)
: _ctime(Mylog::util::Date::now()), _level(level), _tid(std::this_thread::get_id()),
_file(file), _line(line), _logger(logger), _payload(meg)
{
}
};
}
三、日志格式化模块
整体思路
该模块允许用户设定输出格式,并按照用户指定格式对消息进行格式化得到指定格式字符串。
在该模块中,首先存在两个成员变量_patten和items。其中_patten是用于保存用户设定的输出格式,items是一个格式化子项数组。每一个格式化子项会从消息模块中取出对应的消息要素,数组维护了用户指定消息要素之间的顺序。
在对用户指定输出格式进行解析时,我们会得到一系列特殊字符和普通字符。不同的特殊字符对应特定的消息要素。我们通过解析得到特殊字符或普通字符后,然后构建出对应的子项成员(子项成员会帮我们从消息模块中取出对应的消息要素)保存到子项数组中。
解析完成后,我们只需要遍历子项数组。将所以子项成员取出的消息字符串按序连接即可得到目标格式化字符串!
class Formatter
{
private:
std::string _pattern; // 格式化字串
std::vector<FormatItem::ptr> _items; // 格式化子项数组
};
3.1 格式化字符串规则
这里我们定义一些列特殊字符,这些字符全部以%开头,相关含义如下:
- %d ⽇期
- %T 缩进
- %t 线程id
- %p ⽇志级别
- %c ⽇志器名称
- %f ⽂件名
- %l ⾏号
- %m ⽇志消息
- %n 换⾏
3.2 子项成员
我们定义一些列特殊字符后,每一个特殊字符对应一个子项成员。分别从消息模块中取出对应的消息要素。同时在此基础上抽象出一个子项基类。
/* 抽象格式化子项基类
派生格式化子项子列 -- 消息、时间、日志等级、线程ID、文件名、行号、日志器名称、制表符、换行符、其他
*/
class FormatItem
{
public:
using ptr = std::shared_ptr<FormatItem>;
virtual void format(std::ostream &out, const LogMsg &msg) = 0;
};
class MsgFormatItem : public FormatItem
{
public:
virtual void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._payload;
}
};
class TimeFormatItem : public FormatItem
{
public:
TimeFormatItem(const std::string &fmt = "%H:%M:%S") : _fmt(fmt)
{
}
virtual void format(std::ostream &out, const LogMsg &msg) override
{
time_t t = msg._ctime;
struct tm lt;
localtime_r(&t, <);
char tmp[32] = {
0};
//将时间戳转化位对应的时间结构struct tm, strftime按照指定格式从struct tm中取出对应的数据进行格式化组织
strftime(tmp, sizeof(tmp) - 1, _fmt.c_str(), <);
out << tmp;
}
private:
std::string _fmt;
};
class levelFormatItem : public FormatItem
{
public:
virtual void format(std::ostream &out, const LogMsg &msg) override
{
out << LogLevel::ToString(msg._level);
}
};
class ThreadFormatItem : public FormatItem
{
public:
virtual void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._tid;
}
};
class FileFormatItem : public FormatItem
{
public:
virtual void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._file;
}
};
class LineFormatItem : public FormatItem
{
public:
virtual void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._line;
}
};
class LoggerFormatItem : public FormatItem
{
public:
virtual void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._logger;
}
};
class TabFormatItem : public FormatItem
{
public:
virtual void format(std::ostream &out, const LogMsg &msg) override
{
out << "\t";
}
};
class NlineFormatItem : public FormatItem
{
public:
virtual void format(std::ostream &out, const LogMsg &msg) override
{
out << "\n";
}
};
// 输出原始字符串,
class OtherFormatItem : public FormatItem
{
public:
OtherFormatItem(const std::string &str) : _str(str) {
}
virtual void format(std::ostream &out, const LogMsg &msg) override
{
out << _str;
}
private:
std::string _str;
};
3.3 子项成员生成
后续对用户传入输出格式进行解析后,我们会构造一系列子项成员。所以这里我们将该行为封装为一个函数createItem(),专门用来生成子项成员。
我们约定createItem()函数的传入kv似的两个字符串。其中key表示特殊字符,val表示对应的格式化子项,若不存在则为nullptr。 对于其他子项成员,由于它提取出的消息要素为原始字符串,所以我们规定其他子项(也就是不同字符生成的子项成员)的key为nullptr,key为原始字符串信息。
// 根据不同的格式化字符创建对应的格式化子项
FormatItem::ptr createItem(const std::string &key, const std::string &val)
{
// key:格式化字符, val:格式化子项(对于其他子项,key为空,val为对应内容)
/*
%d 日期 ,包含子格式{%H:%M:%S}
%t 线程id
%c 日志器名称
%f 文件名
%l 行号
%p 日志级别
%T 缩进
%m 日志消息
%n 换行
*/
if (key == "d")
return std::make_shared<TimeFormatItem>(val);
if (key == "t")
return std::make_shared<ThreadFormatItem>();
if (key == "c")
return std::make_shared<LoggerFormatItem>();
if (key == "f")
return std::make_shared<FileFormatItem>();
if (key == "l")
return std::make_shared<LineFormatItem>();
if (key == "p")
return std::make_shared<levelFormatItem>();
if (key == "T")
return std::make_shared<TabFormatItem>();
if (key == "m")
return std::make_shared<MsgFormatItem>();
if (key == "n")
return std::make_shared<NlineFormatItem>();
if (key.empty())
return std::make_shared<OtherFormatItem>(val);
std::cout << "没有对应的格式化字符: %" << key << std::endl;
abort();
}
3.4 对输出格式进行解析
前面所有准备工作已经做好了,解析来就是对用户传入的格式化字符串信息进行解析。解析规则为:
首先所有的特殊字符都是以%开头的,并且如日期%d后可能存在一个子格式,用{}括起来的。所以我们在对用户传入的指定格式进行解析时,之间从开头进行遍历,遇到%说明可能遇到特殊字符。然后判断%后紧跟的字符:
- 如果也为%,表示这个两个字符发送转移为一个普通字符‘%’。然后构造一个其他子项成员保存到数组中,该成员提取到的内容为原始字符串。
- 如果是符号要求的特殊字符,我们构造对应的子项成员,并保存到数组中。
- 所有特殊字符中,日志%d比较特殊,后面可能存在以{}为开始结束的子格式。所以此时我们在向后遍历如果为{表示存在子格式,需要进行提取。子格式同样以%开头,冒号间隔,}结尾。如果有任意一个不满足要求,也认为这是非法的。(%H%M%S 时分秒)
- 初次在外,我们认为用户传入非法格式,解析失败。
我们将解析得到的结果按序保存到一个数组,该数组的成员为一个pair。对应我们所约定的子项成员生成函数createItem()的两个参数。 解析完毕后,我们按照得到的顺序进行统一构造出对应的子项成员,并添加到子项成员数组中。
// 对格式化字符串解析
bool parsePattern()
{
// 1. 对格式化字符串解析
std::vector<std::pair<std::string, std::string>> fmt_order;
ssize_t pos = 0;
std::string key, val;
while (pos < _pattern.size())
{
// 没有出现%, 或者出现%%表示原始字符,直接添加
if (_pattern[pos] != '%')
{
val.push_back(_pattern[pos++]);
continue;
}
// 能走下来pos位置为%
if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%')
{
val.push_back('%');
pos += 2;
continue;
}
// 原始字符串处理完毕
if (!val.empty

最低0.47元/天 解锁文章
9109

被折叠的 条评论
为什么被折叠?



