前面的文章中我们讲述了日志系统项目的前置知识点,再本文中我们将开始日志项目的细节实现。
日志系统框架设计
本项目实现的是一个多日志器日志系统,主要实现的功能是让程序员能够轻松的将程序运行日志信息落地到指定的位置,且支持同步与异步两种方式的日志落地方式。
项目的框架设计将项目分为以下几个模块来实现。
模块划分
-
日志等级模块:对输出日志的等级进行划分,以便于控制日志的输出,并提供等级枚举转字符串功能。
- OFF:关闭
- DEBUG:调试,调试时的关键信息输出。
- INFO:提示,普通的提示型日志信息。
- WARN:警告,不影响运行,但是需要注意一下的日志。
- ERROR:错误,程序运行出现错误的日志。
- FATAL:致命,一般是代码异常导致程序无法继续推进运行的日志。
-
日志消息模块:中间存储日志输出所需的各项要素信息
- 时间:描述本条日志的输出时间。
- 线程ID:描述本条日志是哪个线程输出的。
- 日志等级:描述本条日志的等级。
- 日志数据:本条日志的有效载荷数据。
- 日志文件名:描述本条日志在哪个源码文件中输出的。
- 日志行号:描述本条日志在源码文件的哪一行输出的。
-
日志消息格式化模块:设置日志输出格式,并提供对日志消息进行格式化功能。
- 系统的默认日志输出格式:%d{%H:%M:%S}%T[9%t]%T[%p]%T[%c]%T%f:%1%T%m%no
->13:26:32 [2343223321] [FATAL] [root] main.c:76 套接字创建失败\n - %d{%H:%M:%S}:表示日期时间,花括号中的内容表示日期时间的格式。
- %T:表示制表符缩进。
- %t:表示线程ID。
- %p:表示日志级别。
- %c:表示日志器名称,不同的开发组可以创建自己的日志器进行日志输出,小组之间互不影响。
- %f:表示日志输出时的源代码文件名。
- %l:表示日志输出时的源代码行号。
- %m:表示给与的日志有效载荷数据。
- %n:表示换行。
- 设计思想:设计不同的子类,不同的子类从日志消息中取出不同的数据进行处理。
- 系统的默认日志输出格式:%d{%H:%M:%S}%T[9%t]%T[%p]%T[%c]%T%f:%1%T%m%no
-
日志消息落地模块∶决定了日志的落地方向,可以是标准输出,也可以是日志文件,也可以滚动文件输出…
- 标准输出:表示将日志进行标准输出的打印。
- 日志文件输出:表示将日志写入指定的文件末尾。
- 滚动文件输出:当前以文件大小进行控制,当一个日志文件大小达到指定大小,则切换下一个文件进行输出
- 后期,也可以扩展远程日志输出,创建客户端,将日志消息发送给远程的日志分析服务器。
- 设计思想:设计不同的子类,不同的子类控制不同的日志落地方向。
-
日志器模块:
- 此模块是对以上几个模块的整合模块,用户通过日志器进行日志的输出,有效降低用户的使用难度。
- 包含有:日志消息落地模块对象,日志消息格式化模块对象,日志输出等级
-
日志器管理模块:
- 为了降低项目开发的日志耦合,不同的项目组可以有自己的日志器来控制输出格式以及落地方向,因此本项目是一个多日志器的日志系统。
- 管理模块就是对创建的所有日志器进行统一管理。并提供一个默认日志器提供标准输出的日志输出。
-
异步线程模块:
- 实现对日志的异步输出功能,用户只需要将输出日志任务放入任务池,异步线程负责日志的落地输出功能,以此提供更加高效的非阻塞日志输出。
模块关系图
代码设计
实用类设计
完成一些零碎的功能接口,以便于后面会用到。
- 获取系统时间信息
- 判断文件是否存在
- 获取文件所在路径
- 创建目录
/*实用工具类的实现:
1. 获取系统时间
2. 判断文件是否存在
3. 获取文件所在目录
4. 创建目录
*/
namespace zyqlog
{
namespace util
{
class Date
{
public:
static size_t now() // 获取当前的系统时间
{
return (size_t)time(nullptr);
}
};
class File
{
public:
static bool exists(const std::string &pathname) // 判断当前的文件是否存在
{
// return (access(pathname.c_str(), F_OK) == 0); // Linux下的接口
struct stat st;
if (stat(pathname.c_str(), &st) < 0)
{
return false;
}
return true;
}
static std::string path(const std::string &pathname) // 获取当前的文件路径
{
// ./abc/a.txt
size_t pos = pathname.find_last_of("/\\"); // 从文件路径最后的一个/或者\\开始获取文件路径
if (pos == std::string::npos) return ".";
return pathname.substr(0, pos + 1);
}
static void createDirectory(const std::string &pathname)
{
// ./abc/bcd/a.txt
size_t pos = 0, idx = 0;
while (idx < pathname.size())
{
pos = pathname.find_first_of("/\\", idx); // 从文件路径开始处的/或者\\开始获取文件路径
if (pos == std::string::npos) // 获取到的结果如果已到文件末尾则说明传入的整个路径名都是需要创建的目录,直接进行创建
{
mkdir(pathname.c_str(), 0777);
}
std::string parent_dir = pathname.substr(0, pos + 1); // 将获取到的每级目录进行截取
if (parent_dir == "." || parent_dir == "..") // 如果截取的目录是当前目录或者是上一级目录则继续进行截取。
{
idx = pos + 1;
continue;
}
if (exists(parent_dir) == true) // 如果目录已经存在则继续进行截取
{
idx = pos + 1;
continue;
}
mkdir(parent_dir.c_str(), 0777); // 创建截取得到的未创建目录
idx = pos + 1;
}
}
};
}
}
/*test*/
std::cout << zyqlog::util::Date().now() << std::endl;
std::string pathname = "./abc/bcd/a.txt";
zyqlog::util::File().createDirectory(zyqlog::util::File::path(pathname));
日志等级类设计
日志等级共分为7个等级,分别为:
- OFF 关闭所有日志输出
- DRBUG 进行debug时候打印日志的等级
- INFO 打印一些用户提示信息
- WARN 打印警告信息
- ERROR 打印错误信息
- FATAL 打印致命信息-导致程序崩溃的信息
/*
1. 定义枚举类,枚举出日志等级
2. 提供转换接口,将枚举转换为对应的字符串
*/
namespace zyqlog
{
class LogLevel
{
private:
public:
enum class value // 枚举类实现不同的日志等级
{
UNKNOW = 0,
DEBUG,
INFO,
WARNING,
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::WARNING:
return "WARNING";
case LogLevel::value::ERROR:
return "ERROR";
case LogLevel::value::FATAL:
return "FATAL";
case LogLevel::value::OFF:
return "OFF";
}
return "UNKNOW";
}
};
}
日志消息类设计
/*
定义日志消息类,进行日志中间信息的存储:
1. 日志的输出时间--用于过滤日志输出时间
2. 日志等级--用于进行日志过滤分析
3. 源文件名称
4. 源代码行号--用于定位出现错误的代码的位置
5. 线程ID--用于过滤出错的线程
6. 日志主体消息
7. 日志器名称--支持多日志器同时使用
*/
namespace zyqlog
{
struct LogMsg
{
time_t _ctime; // 日志产生的时间戳
LogLevel::value _level; // 日志等级
std::thread::id _tid; // 线程ID
size_t _line; // 行号
std::string _file; // 源码文件名
std::string _logger; // 日志器名称
std::string _payload; // 有效消息数据
LogMsg() {}
LogMsg(LogLevel::value level, size_t line, const std::string file, const std::string logger, const std::string msg)
: _ctime(util::Date::now())
, _level(level)
, _line(line)
, _tid(std::this_thread::get_id())
, _file(file)
, _logger(logger)
, _payload(msg)
{}
};
}
日志格式化输出类
日志格式化(Formatter)类主要负责格式化日志消息。其主要包含以下内容
- pattern成员:保存日志输出的格式字符串
- %d 日期
- %T 缩进
- %t 线程id
- %p 日志级别
- %c 日志器名称
- %f 文件名
- %l 行号
- %m 日志消息
- %n 换行
- std::vector< FormatItem::ptr > items成员:用于按序保存格式化字符串对应的子格式化对象
FormatItem类主要负责日志消息子项的获取及格式化。其包含以下子类 - MsgFormatItem:表示要从LogMsg中取出有效日志数据
- LevelFormatItem:表示要从LogMsg中取出日志等级
- ThreadFormatItem:表示要从LogMsg中取出线程ID
- TimeFormatItem:表示要从LogMsg中取出时间戳并按照指定格式进行格式化
- LineFormatItem:表示要从LogMsg中取出源码所在行号
- TabFormatItem:表示⼀个制表符缩进
- NLineFormatItem:表示⼀个换行
- OtherFormatItem:表示非格式化的原始字符串
格式化的过程其实就是按次序从Msg中取出需要的数据进行字符串的连接的过程。
// 设计思想:
// 1. 抽象一个格式化子项基类
// 2. 基于基类, 派生出格式化子项子类
// 在父类中定义父类指针数组,指向不同格式化子类对象
namespace zyqlog
{
// 抽象格式化子项基类
class FormatItem
{
public:
using ptr = std::shared_ptr<FormatItem>;
virtual void format(std::ostream &out, const LogMsg &msg) = 0;
};
// 派生类格式化子项子类--消息,等级,时间,文件名,行号,线程ID,日志器名,制表符,换行吗,其他
class MsgFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._payload;
}
};
class LevelFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << LogLevel::toString(msg._level);
}
};
class TimeFormatItem : public FormatItem
{
public:
TimeFormatItem(const std::string &fmt = "%H:%M:%S") :_time_fmt(fmt) {}
void format(std::ostream &out, const LogMsg &msg) override
{
struct tm t;
localtime_r(&msg._ctime, &t);
char tmp[32] = {0};
strftime(tmp, 31, _time_fmt.c_str(), &t); // strftime()函数根据格式字符串将给定的日期和时间从给定的日历时间转换为以空结尾的多字节字符串。
out << tmp;
}
private:
std::string _time_fmt; // %H:%M:%S
};
class FileFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._file;
}
};
class LineFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._line;
}
};
class ThreadFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._tid;
}
};
class LoggerFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._logger;
}
};
class TabFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << "\t";
}
};
class NLineFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << "\n";
}
};
class OtherFormatItem : public FormatItem
{
public:
OtherFormatItem(const std::string &str) :_str(str) {}
void format(std::ostream &out, const LogMsg &msg) override
{
out << _str;
}
private:
std::string _str;
};
/*
%d 表示日期 包含子格式{%H:%M:%S}
%t 表示线程ID
%c 表示日志器名称
%f 表示源码文件名
%l 表示源码行号
%p 表示日志级别
%T 表示制表符缩进
%m 日志消息
%n 表示换行
*/
class ForMatter
{
public:
using ptr = std::shared_ptr<ForMatter>;
ForMatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n") : _pattern(pattern)
{
assert(parsePattern()); // 对格式化规则字符串进行解析
}
// 对msg进行格式化
void format(std::ostream &out, LogMsg &msg)
{
for(auto &item : _items)
{
item->format(out, msg);
}
}
std::string format(LogMsg &msg)
{
std::stringstream ss;
format(ss, msg);
return ss.str();
}
private:
// 对格式化规则字符串进行解析
bool parsePattern()
{
// 1. 对格式化规则字符串进行解析
// abcd[ % d {%H:%M:%S} ][ %t][%c][%f:%l][%p]%T%m%n
std::vector<std::pair<std::string, std::string>> fmt_order;
size_t pos = 0;
std::string key, val;
while (pos < _pattern.size())
{
// 1. 处理原始字符串--判断是否是%,不是就是原始字符
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())
{
fmt_order.push_back(std::make_pair("", val));
val.clear();
}
//代表%后面是一个格式化字符,格式化字符的处理
pos += 1; // pos指向格式化字符的位置
if (pos == _pattern.size())
{
std::cerr << "%之后没有对应的字符!\n";
return false;
}
key = _pattern[pos]; // 确定key格式化字符的位置
// 此时pos指向格式化字符后的位置
pos += 1;
if (pos < _pattern.size() && _pattern[pos] == '{')
{
pos += 1; // pos指向子规则的起始位置
while (pos < _pattern.size() && _pattern[pos] != '}')
{
val.push_back(_pattern[pos++]);
}
// 走到末尾跳出循环,则代表没有遇到},代表格式是错误的
if (pos == _pattern.size())
{
std::cerr << "子规则{}匹配出错!\n";
return false;
}
pos += 1; // pos指向}位置,向后走一步,到了下一步的位置
}
fmt_order.push_back(std::make_pair(key, val));
key.clear();
val.clear();
}
/*
这个处理的过程以 abcd[%d{%H:%M:%S}][ %t][%c][%f:%l][%p]%T%m%n 为例子进行解析
key = nullptr,val = abcd[
key = d,val = %H:%M:%S
key = nullptr,val = ][
...
得到数组内容之后,根据数组内容,创建格式化子项对象,添加到items成员数组中。
*/
// 2. 根据解析得到的数据初始化格式化子项数组成员
for (auto &it : fmt_order)
{
_items.push_back(createItem(it.first, it.second));
}
return true;
}
// 根据不同格式化字符创建不同的格式化子项对象
FormatItem::ptr createItem(const std::string &key, const std::string &val)
{
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::cerr << "没有对应的格式化字符: %" << key << std::endl;
abort();
return FormatItem::ptr();
}
private:
std::string _pattern; // 格式化规则字符串
std::vector<FormatItem::ptr> _items;
};
}