一、简介
1.1 项目介绍
本项目主要实现一个日志系统, 它可以根据不同的级别、配置和策略,以同步或异步的方式,将日志信息可靠地写入控制台文件或滚动文件中,同时支持多线程并发写日志和扩展不同的日志落地目标地模式。其主要支持以下功能:
• 支持多级别日志消息
• 支持同步日志和异步日志
• 支持可靠写入日志到控制台、文件以及滚动文件中
• 支持多线程程序并发写日志
•支持扩展不同的日志落地目标地
1.2开发环境
• CentOS 7
• vscode/vim
• g++/gdb
• Makefile
1.3 核心技术
• 类层次设计(继承和多态的应用)
• C++11(多线程、auto、智能指针、右值引用等)
• 双缓冲区
• 生产消费模型
• 多线程
• 设计模式(单例、工厂、代理、模板等)
二、日志系统介绍
2.1 日志系统的目的
- ⽣产环境的产品为了保证其稳定性及安全性是不允许开发⼈员附加调试器去排查问题,可以借助⽇志系统来打印⼀些⽇志帮助开发⼈员解决问题
- 上线客⼾端的产品出现bug⽆法复现并解决,可以借助⽇志系统打印⽇志并上传到服务端帮助开发⼈员进⾏分析
- 对于⼀些⾼频操作(如定时器、⼼跳包)在少量调试次数下可能⽆法触发我们想要的⾏为,通过断点的暂停⽅式,我们不得不重复操作⼏⼗次、上百次甚⾄更多,导致排查问题效率是⾮常低下,可以借助打印⽇志的⽅式查问题
- 在分布式、多线程/多进程代码中,出现bug⽐较难以定位,可以借助⽇志系统打印log帮助定位bug帮助
- 帮助⾸次接触项⽬代码的新开发⼈员理解代码的运⾏流程
2.2 日志系统的技术实现
日志系统的实现技术主要包括三种
- 利用printf、std::cout等输出函数将⽇志信息打印到控制台
- 同步写日志
- 异步写日志
2.2.1 同步写日志
同步⽇志是指当输出⽇志时,必须等待⽇志输出语句执⾏完毕后,才能执⾏后⾯的业务逻辑语句,⽇志输出语句与程序的业务逻辑语句在同⼀个线程运⾏。每次调⽤⼀次写⽇志API就对应⼀次系统调⽤write写⽇志⽂件。
同步日志在高并发的场景下的缺点:
- 同步写日志的方式会存在大量的日志打印陷入等量的write系统调用,有一定系统开销。
- 打印日志的进程附带了大量同步的磁盘IO,会影响程序性能
2.2.2 异步写日志
异步日志是指在进行日志输出时,日志输出语句与业务逻辑语句并不是在同一个线程中运行,而是有专门的线程用于进行日志输出操作。业务线程只需要将日志放到一个内存缓冲区中不用等待即可继续执行后续业务逻辑(作为日志的生产者),而日志的落地操作交给单独的日志线程去完成(作为日志的消费者), 这是一个典型的生产-消费模型。
异步日志的好处是即使日志没有真的地完成输出也不会影响程序的主业务,可以提高程序的性能:
• 主线程调用日志打印接口成为非阻塞操作
• 同步的磁盘IO从主线程中剥离出来交给单独的线程完成
三、日志系统项目框架设计
3.1 模块划分
3.1.1 日志等级模块
等级模块主要是根据信息的等级进行分类,以便于控制⽇志的输出,并提供等级枚举转字符串功能,通常有以下几类
- OFF:关闭
- DEBUG:调试,调试时的关键信息输出。
- INFO:提⽰,普通的提⽰型日志信息。
- WARN:警告,不影响运⾏,但是需要注意⼀下的⽇志。
- ERROR:错误,程序运行出现错误的⽇志
- FATAL:致命,⼀般是代码异常导致程序⽆法继续推进运⾏的⽇志
3.1.2 日志消息模块
将日志消息包装成一个一个日志对象,该对像中间存储日志输出所需的各项要素信息
- 时间:描述本条日志的输出时间。
- 线程ID:描述本条日志是哪个线程输出的。
- 日志等级:描述本条日志的等级。
- 日志数据:本条日志的有效载荷数据。
- 日志文件名:描述本条日志在哪个源码文件中输出的。
- 日志行号:描述本条日志在源码文件的哪一行输出的。
3.1.3 消息格式化模块
按照指定的格式对日志信息关键要素进行组织,最终获得我们想要的日志信息格式进行输出,例如:
系统的默认⽇志输出格式:%d{%H:%M:%S}%T[%t]%T[%p]%T[%c]%T%f:%l%T%m%n
-> 13:26:32 [2343223321] [FATAL] [root] main.c:76 套接字创建失败\n
- %d{%H:%M:%S}:表⽰⽇期时间,花括号中的内容表⽰⽇期时间的格式。
- %T:表⽰制表符缩进。
- %t:表⽰线程ID
- %p:表⽰⽇志级别
- %c:表⽰⽇志器名称,不同的开发组可以创建⾃⼰的⽇志器进⾏⽇志输出,⼩组之间互不影响。
- %f:表⽰⽇志输出时的源代码⽂件名。
- %l:表⽰⽇志输出时的源代码⾏号。
- %m:表⽰给与的⽇志有效载荷数据
- %n:表⽰换⾏
设计思想:设计不同的⼦类,不同的⼦类从⽇志消息中取出不同的数据进⾏处理。
3.1.4 日志消息落地模块
该模块决定了日志最后的落地方向,例如标准输出,日志文件,滚动文件输出等。
- 标准输出:表⽰将⽇志进⾏标准输出的打印。
- ⽇志⽂件输出:表⽰将⽇志写⼊指定的⽂件末尾。
- 滚动⽂件输出:当前以⽂件⼤⼩进⾏控制,当⼀个⽇志⽂件⼤⼩达到指定⼤⼩,则切换下⼀个⽂件进⾏输出
后期可以扩展远程日志输出,创建客户端,将日志消息发送给远程的日志分析服务器。
3.1.5 日志器模块
日志器模块主要是对上面几个模块的整合,因此用户可以直接用日志器进行日志的输出,可以降低用户使用日志系统的上手难度。
包含:日志信息落地模块,日志信息格式化模块,日志等级模块
3.1.6⽇志器管理模块:
为了降低项⽬开发的⽇志耦合,不同的项⽬组可以有⾃⼰的⽇志器来控制输出格式以及落地⽅向,因此本项⽬是⼀个多⽇志器的⽇志系统。
管理模块就是对创建的所有⽇志器进⾏统⼀管理。并提供⼀个默认⽇志器提供标准输出的⽇志输出。
3.1.7 异步线程模块
实现对日志信息的异步输出功能,用户将所需要输出的日志信息放入任务池中,会有专门的异步线程进行日志信息的落地工作
3.2各模块关系图
四、代码实现
4.1实用工具类设计
⼀些零碎的功能接⼝,以便于项⽬中编写过程中使用。包括:
- 获取系统时间
- 判断⽂件是否存在
- 获取⽂件的所在⽬录路径
- 创建⽬录
- 判断文件是否存在一般使用stat接口,获取文件属性,通过获取文件属性成功与否判断文件是否存在
- 创建目录要根据目录结构依次进行,创建目录我们可以调用系统接口mkdir()
代码如下
#ifndef __M_UTIL_H__
#define __M_UTIL_H__
/*实用工具类实现
1.获取系统时间
2.获取文件大小
3.创建目录
4.获取文件所在目录
*/
#include <iostream>
#include <ctime>
#include<unistd.h>
#include<sys/stat.h>
namespace viclog
{
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);
struct stat st;
if(stat(pathname.c_str(),&st)<0)
{
return false;
}
return true;
}
//获取文件所在目录
static std::string path(const std::string &pathname)
{
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)
{
size_t pos=0,index=0;
while(index<pathname.size())
{
pos=pathname.find_first_of("/\\",index);
if(pos==std::string::npos) mkdir(pathname.c_str(),0777);
std::string parent_dir=pathname.substr(0,pos+1);
//if(parent_dir=="."||parent_dir=="..") continue;
if(exists(parent_dir)==true)
{
index=pos+1;
continue;
}
mkdir(parent_dir.c_str(),0777);
index=pos+1;
}
}
};
}
}
#endif
4.2日志等级类设计
我们将日志等级封装成一个类,定义出日志系统所包含的所以日志等级:
- OFF 关闭所有⽇志输出
- DRBUG 进⾏debug时候打印⽇志的等级
- INFO 打印⼀些⽤⼾提⽰信息
- WARN 打印警告信息
- ERROR 打印错误信
- FATAL 打印致命信息- 导致程序崩溃的信息
设计思想:
- 每一个项目都会设置默认日志输出等级,只有日志等级大于默认等级才会输出。
- 提供一个接口,将对应等级的枚举转换成字符串
/*
1.枚举日志等级
2.接口:枚举->字符串
*/
#ifndef __M_LEVEL_H__
#define __M_LEVEL_H__
namespace viclog
{
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";
}
};
}
#endif
4.3日志消息类设计
⽇志消息类主要是封装⼀条完整的⽇志消息所需的内容
- 日志的输出时间 : 用于过滤日志输出时间
- 日志等级: 用于进行日志过滤分析
- 源文件名: 用于定位
- 源代码行号:用于定位
- 线性ID: 用于过滤出错线程
- 日志消息主体
- 日志器名称
/*
1.日志消息类,进行日志中间消息的存储
*/
#ifndef __M_MSG_H__
#define __M_MSG_H__
#include"level.hpp"
#include"util.hpp"
#include<iostream>
#include<string>
#include<thread>
namespace viclog
{
struct LogMsg
{
time_t _ctime;//日志产生的时间戳
LogLevel::value _level;//日志等级
size_t _line;//行号
std::thread::id _tid;//线程id
std::string _file;//文件名
std::string _logger;//日志器名字
std::string _paylaod;//有效载荷
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),
_paylaod(msg)
{}
};
}
#endif
4.4 日志输出格式化类设计
通过对日志消息进行格式化,组织称为指定格式的字符串。
在该模块中我们自定义一些格式化字符:
- %d 日期
- %T 缩进
- %t 线程id
- %p 日志级别
- %c 日志器名称
- %f 文件名
- %l 行号
- %m 日志消息
- %n 换行
在主格式化字符后面跟上{子格式化字符}的方式来进一步指明具体打印格式,比如:%d{%H:%M:%S}这样:%d表示从日志器对象中取出时间元素,按照:"某时某分某秒 "这样的格式打印时间
设计思想:
-
抽象一个格式化子项基类
-
派生出不同的格式化子项类:
主体消息子项、日志等级子项、时间子项、文件名子项、行号子项、日志器名称子项、线程ID子项、制表符子项、换行子项、其他信息子项。
-
在父类中定义父类指针的数组,指向不同的格式化子项子类对象。
比如现有一个格式化字符串:[%d%m%n]
1.我们分析**'['
为非格式化字符串,我们就会形成一个其他信息子项对象**;然后分析到**%d
形成一个时间子项对象**;分析到**%m
:形成一个主体消息子项对象**;分析到**%n
:形成一个换行子项对象**;分析到非格式化字符**'['
形成一个其他信息子项 对象**。2.然后我们利用一个容器将这些对象给保存起来。然后我们在格式化日志对象的时候只需要遍历这个容器,就能得到指定格式的字符串!
格式化子项类(FormatItem):
FormatItem类主要负责日志消息子项的获取及格式化。
提供虚接口format():从日志消息中取出指定的元素,追加到一块内存空间中。
其包含以下子类
MsgFormatItem :表示要从LogMsg中取出有效日志数据
LevelFormatItem :表示要从LogMsg中取出日志等级
NameFormatItem :表示要从LogMsg中取出日志器名称
ThreadFormatItem :表示要从LogMsg中取出线程ID
TimeFormatItem :表示要从LogMsg中取出时间戳并按照指定格式进行格式化
FileFormatItem :表示要从LogMsg中取出源码所在文件名
LineFormatItem :表示要从LogMsg中取出源码所在行号
TabFormatItem :表示一个制表符缩进
NLineFormatItem :表示一个换行
OtherFormatItem :表示非格式化的原始字符串
// 抽象格式化子项基类
class FormatItem
{
public:
using ptr = std::shared_ptr<FormatItem>;
virtual void format(std::ostream &out, const LogMsg &msg) = 0;
};
// 派生格式化子项子类
// 消息、
class MsgFormatItem : public FormatItem
{
public:
void format(std::ostream &out,const LogMsg &msg)
{
out << msg._paylaod;
}
};
// 等级、
class LevelFormatItem : public FormatItem
{
public:
void format(std::ostream &out,const LogMsg &msg)
{
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)
{
struct tm t;
localtime_r(&msg._ctime, &t);
char tmp[32] = {0};
strftime(tmp, 31, _time_fmt.c_str(), &t);
out << tmp;
}
private:
std::string _time_fmt;
};
// 文件名、
class FileFormatItem : public FormatItem
{
public:
void format(std::ostream &out,const LogMsg &msg)
{
out << msg._file;
}
};
// 行号、
class LineFormatItem : public FormatItem
{
public:
void format(std::ostream &out,const LogMsg &msg)
{
out << msg._line;
}
};
// 线程id、
class ThreadFormatItem : public FormatItem
{
public:
void format(std::ostream &out,const LogMsg &msg)
{
out << msg._tid;
}
};
// 日志器名、
class LoggerFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg)
{
out << msg._logger;
}
};
// 制表符、
class TabFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg)
{
out << "\t";
}
};
// 换行、
class NLineFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg)
{
out << "\n";
}
};
// 其他
class OtherFormatItem : public FormatItem
{
public:
OtherFormatItem(const std::string &str) : _str(str)
{
}
void format(std::ostream &out, const LogMsg &msg)
{
out << _str;
}
private:
std::string _str;
};
日志格式化类(Formatter)
管理成员:
- pattern成员:保存日志输出的格式字符串。
- std::vectorFormatItem::ptr items成员:用于按序保存格式化字符串对应的子格式化对象。
提供接口:
- format();//用于暴露给外部提供格式化的接口;
- parsePattern();//不暴露给外部,用来专门进行格式化字符串分析,并形成对应子格式化对象的接口
- createItem();//不暴露给外部,专门用来创建格式化子对象的
对格式化规则字符串解析的思想:通过解析规则字符串来初始化格式化子项数组
while(){
//1.处理原始字符串
//2.原始字符串处理结束后,遇到%就处理一个格式化字符
}
在处理过程中,我们需要将处理得到的信息保存下来,(既要保存子项的类型还要保存该子项对应的内容,使用
std::vector<std::pair<std::string, std::string>>
)
然后根据得到的数组内容创建对应的格式化子项对象,添加到item成员数组中。
/*
%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(paresPattern());
}
// 对msg进行格式化(按照对格式化规则字符串解析的顺序逐个的从msg中取出对应的信息)
void format(std::ostream &out, const LogMsg &msg)
{
for (auto &item : _items)
{
item->format(out, msg);
}
}
std::string format(const LogMsg &msg)
{
std::stringstream ss;
format(ss, msg);
return ss.str();
}
private:
// 对格式化规则字符串解析
bool paresPattern()
{
// 1.对格式化规则字符串解析
// asd[%d{%H:%M%S}][%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.1判断是否是% 不是就是原始字符
if (_pattern[pos] != '%')
{
val.push_back(_pattern[pos++]);
continue;
}
// 1.2pos位置是% 判断是否是%% 双%表示一个原始%字符
if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%')
{
val.push_back('%');
pos += 2;
continue;
}
// 1.3原始字符处理完毕 原始字符串添加
if(val.empty()==false)
{
fmt_order.push_back(std::make_pair("", val));
val.clear();
}
// 1.4格式化字符处理
pos += 1;
if(pos==_pattern.size()){
std::cout<<"%d之后没有对应格式化字符\n";
}
key = _pattern[pos];
pos+=1;
// 1.4.1 pos指向格式化字符后的位置 判断是否有子规则{};
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::cout<<"子规则{}去配出错!\n";
return false;//没有遇到} 格式出错了
}
pos+=1;
}
fmt_order.push_back(std::make_pair(key,val));
key.clear();
val.clear();
}
// 2.根据解析结果初始化格式化子项数组_items成员
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=="")
return std::make_shared<OtherFormatItem>(val);
std::cout<<"没有对应格式化字符:%"<<key<<std::endl;
abort();
return FormatIt::ptr();
}
private:
std::string _pattern; // 格式化字规则符串
std::vector<FormatItem::ptr> _items;
};
4.5日志落地类设计(简单工厂模式)
日志落地类主要负责落地日志消息到指定位置。
设计思想:
- 抽象出落地模块基类(该抽象落地方向提供一个虚接口:log();用户通过该接口,来完成实际的落地;)
- 不同的落地方式从基类派生
- 使用工厂模式进行创建表示分离
落地方式分类:
-
标准输出(StdoutSink)
-
固定文件(FileSink)
-
滚动文件(RollBySizeSink)按大小滚动
滚动日志文件输出的必要性:
由于机器磁盘空间有限, 我们不可能一直无限地向一个文件中增加数据
如果一个日志文件体积太大,一方面是不好打开,另一方面是即时打开了由于包含数据巨大,也不利于查找我们需要的信息
实际开发中会对单个日志文件的大小也会做一些控制,即当大小超过某个大小时(如1GB),我们就重新创建一个新的日志文件来滚动写日志。 对于那些过期的日志, 大部分企业内部都有专门的运维人员去定时清理过期的日志,或者设置系统定时任务,定时清理过期日志。
日志文件的滚动思想:
- 日志文件在大于 1GB 的时候会更换新的文件
- 每天定点滚动一个日志文件
日志落地类主要包括以下内容:
- Formatter日志格式化器:主要是负责格式化日志消息
- mutex互斥锁:保证多线程日志落地过程中的线程安全,避免出现交叉输出的情况。
/*日志器落地模块
1.抽象落地基类
2.根据不同的落地方式派生子类
3.工厂模式
*/
#ifndef __M_SINK_H__
#define __M_SINK_H__
#include"util.hpp"
#include<memory>
#include<fstream>
#include<cassert>
#include<sstream>
namespace viclog
{
class LogSink
{
public:
using ptr=std::shared_ptr<LogSink>;
LogSink(){}
virtual ~LogSink(){}
virtual void log(const char* data,size_t len)=0;
};
//落地方向:标准输出
class StdoutSink:public LogSink
{
public:
void log(const char* data,size_t len)
{
std::cout.write(data,len);
}
};
//落地方向:指定文件
class FileSink:public LogSink
{
public:
//构造时传入文件名并打开文件,将操作句柄管理起来
FileSink(const std::string& pathname):_pathname(pathname)
{
//创建日志文件所在目录
util::File::createDirectory(util::File::path(pathname));
//打开文件
_ofs.open(_pathname,std::ios::binary|std::ios::app);
assert(_ofs.is_open());
}
void log(const char* data,size_t len)
{
_ofs.write(data,len);
assert(_ofs.good());
}
private:
std::string _pathname;
std::ofstream _ofs;//输出文件的操作句柄
};
//落地方向:滚动文件(按大小滚动)
class RollBySizeSink:public LogSink
{
public:
//构造时传入文件名并打开文件,将操作句柄管理起来
RollBySizeSink(const std::string& basename,size_t max_size)
:_basename(basename),_max_fsize(max_size),_cur_fsize(0),_name_count(0)
{
std::string pathname=createNewFile();
//创建日志文件所在目录
util::File::createDirectory(util::File::path(pathname));
//打开文件
_ofs.open(pathname,std::ios::binary|std::ios::app);
assert(_ofs.is_open());
}
void log(const char* data,size_t len)
{
if(_cur_fsize>=_max_fsize)
{
//超过指定大小创建新文件,关闭原打开的文件,打开新文件
std::string pathname=createNewFile();
_ofs.close();
_ofs.open(pathname,std::ios::binary|std::ios::app);
assert(_ofs.is_open());
_cur_fsize=0;
}
_ofs.write(data,len);
assert(_ofs.good());
_cur_fsize+=len;
}
private:
//超过指定大小时创建新文件
std::string createNewFile()
{
//获取系统时间,以系统时间创建文件扩展名
time_t t=util::Date::now();
struct tm lt;
localtime_r(&t,<);
std::stringstream filename;
filename<<_basename;
filename<<lt.tm_year+1900;
filename<<lt.tm_mon+1;
filename<<lt.tm_mday;
filename<<lt.tm_hour;
filename<<lt.tm_min;
filename<<lt.tm_sec;
filename<<"-";
filename<<_name_count++;
filename<<".log";
return filename.str();
}
private:
std::string _basename;//基础文件名(通过基础文件名+扩展文件名(按时间生产)组成当前实际输出文件名)
//./log/base--->./log/base-20200922089.log
size_t _name_count;
std::ofstream _ofs;//输出文件的操作句柄
size_t _max_fsize;//超过max_fsize则切换文件
size_t _cur_fsize;//记录当前文件大小
};
//简单工厂模式
class SinkFactory
{
public:
template<class SinkType,class ...Args>
static LogSink::ptr create(Args &&...args)
{
return std::make_shared<SinkType>(std::forward<Args>(args)...);
}
};
}
#endif
补充:工厂模式
工厂模式是一种创建型设计模式, 它提供了一种创建对象的最佳方式。在工厂模式中,我们创建对象时不会对上层暴露创建逻辑,而是通过使用一个共同结构来指向新创建的对象,以此实现创建-使用的分离。
简单工厂模式: 简单工厂模式实现由一个工厂对象通过类型决定创建出来指定产品类的实例。这个模式的结构和管理产品对象的方式十分简单, 但是它的扩展性非常差,当我们需要新增产品的时候,就需要去修改工厂类新增一个类型的产品创建逻辑,违背了开闭原则。
工厂方法模式: 在简单工厂模式下新增多个工厂,多个产品,每个产品对应一个工厂。工厂方法模式每次增加一个产品时,都需要增加一个具体产品类和工厂类,这会使得系统中类的个数成倍增加,在一定程度上增加了系统的耦合度。
抽象工厂模式: 工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,我们可以考虑将一些相关的产品组成一个产品族(位于不同产品等级结构中功能相关联的产品组成的家族),由同一个工厂来统一生产,这就是抽象工厂模式的基本思想。
4.6日志器类设计
该模块主要是对前边所以模块的整合(日志等级模块,日志消息模块,日志格式化模块,日志落地模块),向外提供接口完成不同等级日志的输出。当我们需要使用日志系统打印log的时候, 只需要创建Logger对象,调用该对象debug、info、warn、error、fatal等方法输出自己想打印的日志即可,支持解析可变参数列表和输出格式, 即可以做到像使用printf函数一样打印日志。
设计思想:
- 抽象Logger基类(派生出同步日志器类&异步日志器类)
- 两种不同的日志器只有落地方式不同,因此将落地方式抽象出来,不同的日志器调用各自的落地操作。
当前日志系统支持同步日志 & 异步日志两种模式,两个不同的日志器唯一不同的地方在于他们在日志的落地方式上有所不同,因此我们可以设计一个Logger基类,该基类完成大部分接口,然后抽象出落地方式接口,派生出同步日志器和异步日志器,由它们自己实现落地方式接口
同步日志器:直接对日志消息进行输出。
异步日志器:将日志消息放入缓冲区,由异步线程进行输出。
管理成员:
- 格式化模块对象
- 落地模块对象数组(一个日志器可能回想多个位置进行日志输出)
- 默认至日输出等级限制
- 互斥锁
- 日志器名称(日志器的标识,方便查找)
提供接口:
- debug等级的日志输出操作
- info等级的日志输出操作
- warn等级的日志输出操作
- error等级的日志输出操作
- fatal等级的日志输出操作
且因为日志器模块是对前边多个模块的整合,想要创建一个日志器,需要设置日志器名称,设置日志
输出等级,设置日志器类型,设置日志输出格式,设置落地方向,且落地方向有可能存在多个,整个
日志器的创建过程较为复杂,为了保持良好的代码风格,编写出优雅的代码,因此日志器的创建这里
采用了建造者模式来进行创建。
基类日志器代码:
class Logger
{
public:
using ptr = std::shared_ptr<Logger>;
Logger(const std::string &logger_name, LogLevel::value level, Formatter::ptr &formatter, std::vector<LogSink::ptr> sinks)
: _logger_name(logger_name), _limit_level(level), _formatter(formatter), _sinks(sinks)
{}
const std::string& name()
{
return _logger_name;
}
// 以下接口完成日志消息对象构造构成并进行格式化,得到格式化后的日志消息字符串--落地输出
void debug(const std::string &file, size_t line, const std::string &fmt, ...)
{
// 通过传入的参数构造消息对象,进行日志消息格式化
// 1.判断是否达到输出等级
if (LogLevel::value::DEBUG < _limit_level)
{
return;
}
// 2.对fmt格式化字符和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf fail?\n";
return;
}
va_end(ap);
// 3.构造LogMsg对象
// 4.通过格式化工具对LogMsg进行格式化,得到格式化后的字符串
// 5.日志落地
serialize(LogLevel::value::DEBUG, file, line, res);
free(res);
}
void info(const std::string &file, size_t line, const std::string &fmt, ...)
{
// 通过传入的参数构造消息对象,进行日志消息格式化
// 1.判断是否达到输出等级
if (LogLevel::value::INFO < _limit_level)
{
return;
}
// 2.对fmt格式化字符和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf fail?\n";
return;
}
va_end(ap);
// 3.构造LogMsg对象
// 4.通过格式化工具对LogMsg进行格式化,得到格式化后的字符串
// 5.日志落地
serialize(LogLevel::value::INFO, file, line, res);
free(res);
}
void warn(const std::string &file, size_t line, const std::string &fmt, ...)
{
// 通过传入的参数构造消息对象,进行日志消息格式化
// 1.判断是否达到输出等级
if (LogLevel::value::WARN < _limit_level)
{
return;
}
// 2.对fmt格式化字符和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf fail?\n";
return;
}
va_end(ap);
// 3.构造LogMsg对象
// 4.通过格式化工具对LogMsg进行格式化,得到格式化后的字符串
// 5.日志落地
serialize(LogLevel::value::WARN, file, line, res);
free(res);
}
void error(const std::string &file, size_t line, const std::string &fmt, ...)
{
// 通过传入的参数构造消息对象,进行日志消息格式化
// 1.判断是否达到输出等级
if (LogLevel::value::ERROR < _limit_level)
{
return;
}
// 2.对fmt格式化字符和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf fail?\n";
return;
}
va_end(ap);
// 3.构造LogMsg对象
// 4.通过格式化工具对LogMsg进行格式化,得到格式化后的字符串
// 5.日志落地
serialize(LogLevel::value::ERROR, file, line, res);
free(res);
}
void fatal(const std::string &file, size_t line, const std::string &fmt, ...)
{
// 通过传入的参数构造消息对象,进行日志消息格式化
// 1.判断是否达到输出等级
if (LogLevel::value::FATAL < _limit_level)
{
return;
}
// 2.对fmt格式化字符和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf fail?\n";
return;
}
va_end(ap);
// 3.构造LogMsg对象
// 4.通过格式化工具对LogMsg进行格式化,得到格式化后的字符串
// 5.日志落地
serialize(LogLevel::value::FATAL, file, line, res);
free(res);
}
protected:
void serialize(LogLevel::value level, const std::string &file, size_t line, char *str)
{
// 3.构造LogMsg对象
LogMsg msg(level, line, file, _logger_name, str);
// 4.通过格式化工具对LogMsg进行格式化,得到格式化后的字符串
std::stringstream ss;
_formatter->format(ss, msg);
// 5.日志落地
log(ss.str().c_str(), ss.str().size());
}
// 抽象接口完成实际的落地
virtual void log(const char *data, size_t len) = 0;
protected:
std::mutex _mutex;
std::string _logger_name;
std::atomic<LogLevel::value> _limit_level; // 原子变量
Formatter::ptr _formatter;
std::vector<LogSink::ptr> _sinks;
};
4.6.1同步日志器
同步日志器只需要重写log()接口完成实际落地就可以了
class SyncLogger : public Logger
{
public:
SyncLogger(const std::string &logger_name, LogLevel::value level, Formatter::ptr &formatter, std::vector<LogSink::ptr>& sinks)
: Logger(logger_name, level, formatter, sinks)
{}
protected:
void log(const char *data, size_t len)
{
std::unique_lock<std::mutex> lock(_mutex);
if (_sinks.empty())
return;
for (auto &sink : _sinks)
{
sink->log(data, len);
}
}
};
4.6.2异步日志器
异步日志器是将自己的日志消息落地到一个“缓冲区”中,由异步线程完成从缓冲区中取出数据来完成实际落地!
因此我们的异步日志器主要分为两大块:
1.实现一个线程安全的缓冲区
2.创建一个异步工作线程,负责缓冲区中的日志落地,
缓冲区设计:
缓冲区最好不涉及空间的频繁申请释放,可以想到用环形队列的方式,提前将空间申请好,然后循环利用。但是环形队列的缓冲区存在两个问题。
问题一:这个环形队列缓冲区涉及多线程,因此需要对缓冲区的读写加锁。但是开实际开发中,日志操作不会分配太多的资源,工作线程只需要一个(一个消费者,多个生产者)涉及的锁冲突:生产者&生产者 生产者&消费者
问题二:锁冲突较为严重,因为所有线程之间都存在互斥关系
采用双缓冲区阻塞数据池的设计:1.减少空间频繁申请释放 2.减少生产者消费者锁冲突次数
只有在交换时,生产者和消费者才会产生一次锁冲突
单个缓冲区进一步设计:
直接存放格式化后的日志消息字符串
好处:
- 减少LogMsg对象的频繁构造的消耗
- 可以对缓冲区中的日志消息一次性进行IO操作。
管理成员:
- 管理一个存放字符串数据的缓冲区(使用vector进行空间管理)
- 当前写入位置的指针(指向可写区域的起始位置,避免数据的写入覆盖)
- 当前读取位置的指针(指向可读区域的起始位置,当读指针和写指针位置相同时:数据读取完毕)
提供操作:
- 向缓冲区写数据
- 获取可读数据起始地址的接口
- 获取可读数据长度的接口
- 移动读写位置的接口
- 初始化缓冲区的操作(交换之前将读写位置初始化)
- 交换缓冲区的操作(交换空间地址,而不是交换数据)
/*异步日志的缓冲区*/
#pragma once
#include <vector>
namespace viclog
{
#define DEFAULT_BUFFER_SIZE (100 * 1024 * 1024)
#define THRESHOLD_BUFFER_SIZE (80*1024*1024)
#define INCREMENT_BUFFER_SIZE (10*1024*1024)
class Buffer
{
public:
Buffer():_buffer(DEFAULT_BUFFER_SIZE),_reader_idx(0),_writer_idx(0) {}
// 向缓冲区写
void push(const char *data, size_t len)
{
//1.缓冲器剩余空间是否满足
//1(1)固定大小 超过直接返回
// if(len>writeAbleSize()) return ;
//1(2)动态空间 不够扩容(用于极限测试)
ensureEnoughSize(len);
//2.数据拷贝到缓冲区
std::copy(data,data+len,&_buffer[_writer_idx]);
//3.写入位置向后偏移
moveWriter(len);
}
// 获取可读数据的起始地址
const char *begin()
{
return &_buffer[_reader_idx];
}
// 获取可读数据的长度
size_t readAbleSize()
{
return (_writer_idx-_reader_idx);
}
// 获取可写数据的长度
size_t writeAbleSize()
{
//针对固定大小缓冲区提供的接口
return (_buffer.size()-_writer_idx);
}
// 移动读写位置(读写指针向后偏移)
void moveReader(size_t len)
{
assert(len<=readAbleSize());
_reader_idx+=len;
}
// 初始化缓冲区 重置读写位置
void reset()
{
_writer_idx=0;
_reader_idx=0;
}
// 交换缓冲区
void swap( Buffer &buffer)
{
_buffer.swap(buffer._buffer);
std::swap(_reader_idx,buffer._reader_idx);
std::swap(_writer_idx,buffer._writer_idx);
}
// 判断缓冲区是否为空
bool empty()
{
return (_reader_idx==_writer_idx);
}
private:
void moveWriter(size_t len)
{
assert(_writer_idx+len<=_buffer.size());
_writer_idx+=len;
}
//扩容
void ensureEnoughSize(size_t len)
{
if(len<=writeAbleSize()) return ;
size_t new_size;
//小于阈值翻倍
if(_buffer.size()<THRESHOLD_BUFFER_SIZE)
{
new_size=_buffer.size()*2+len;
}
else//大于阈值线性
{
new_size=_buffer.size()+INCREMENT_BUFFER_SIZE+len;
}
_buffer.resize(new_size);
}
private:
std::vector<char> _buffer;
size_t _reader_idx; // 当前可读位置
size_t _writer_idx; // 当前可写位置
};
}
异步工作器(异步工作线程)
异步工作器使用双缓冲区思想,外界将任务添加到输入缓冲区。异步线程对处理缓冲区中的数据进行处理,如果处理缓冲区中没有数据了则交换缓冲区
管理成员:
- 双缓冲区
- 互斥锁
- 条件变量
- 回调函数(实际处理逻辑,消费数据怎么处理,由外部决定)
- _stop;停止标志
提供操作:
- 停止异步工作器
- 添加数据到缓冲区
- 私有操作:
创建线程、线程入口函数、交换缓冲区、对消费缓冲区数据使用回调,处理完后再次交换
/*
异步工作器
*/
#include "buffer.hpp"
#include <mutex>
#include <condition_variable>
#include <functional>
#include <memory>
#include <thread>
#include <atomic>
#include<unordered_map>
namespace viclog
{
using Func_t = std::function<void(Buffer &)>;
enum class AsyncType
{
ASYNC_SAFE, // 安全状态,缓冲区满则阻塞
ASYNC_UNSAFE // 无限扩容(常用于测试)
};
class AsyncLooper
{
public:
using ptr = std::shared_ptr<AsyncLooper>;
AsyncLooper(const Func_t &cb, AsyncType looper_type = AsyncType::ASYNC_SAFE) : _looper_type(looper_type),
_stop(false),
_thread(std::thread(&AsyncLooper::threadEntry, this)),
_callBack(cb)
{
}
~AsyncLooper() { stop(); }
void stop()
{
_stop = true;
_cond_con.notify_all(); // 唤醒所有工作线程
_thread.join(); // 等待线程退出
}
void push(const char *data, size_t len)
{
// 1.无限扩容(用于测试)2.固定大小(满了阻塞)
std::unique_lock<std::mutex> lock(_mutex);
// 条件变量控制,缓冲区空间大于数据长度则可以添加
if (_looper_type == AsyncType::ASYNC_SAFE)
_cond_con.wait(lock, [&]()
{ return _pro_buf.writeAbleSize() >= len; });
_pro_buf.push(data, len);
// 唤醒一个消费者
_cond_con.notify_one();
}
private:
void threadEntry() // 线程入口函数
{
// 调用回调函数对消费缓冲区的数据进行处理 处理完后初始化缓冲区,交换缓冲区
while (1)
{
// 1.判断生产缓冲区有无数据,有:交换到消费缓冲区 无:阻塞
{
std::unique_lock<std::mutex> lock(_mutex);
if (_stop && _pro_buf.empty())
break;
// 有数据或者退出前被唤醒,则返回真继续向下运行,否则重新陷入休眠
_cond_con.wait(lock, [&]()
{ return _stop || !_pro_buf.empty(); });
_con_buf.swap(_pro_buf);
// 2.唤醒生产者(生产者也可能因为缓冲区满而阻塞)
if (_looper_type == AsyncType::ASYNC_SAFE)
_cond_pro.notify_all();
}
// 3.在被唤醒后对消费缓冲区进行数据处理
_callBack(_con_buf);
// 4.初始化消费缓冲区 重置消费缓冲区
_con_buf.reset();
}
}
private:
AsyncType _looper_type;
std::atomic<bool> _stop; // 工作停止标志
// 生产消费缓冲区
Buffer _pro_buf;
Buffer _con_buf;
std::mutex _mutex;
// 生产消费的条件变量
std::condition_variable _cond_pro;
std::condition_variable _cond_con;
std::thread _thread; // 异步工作器对应的线程
Func_t _callBack; // 对缓冲区数据进行处理的回调,有异步工作器使用者传入
};
}
异步日志器
-
继承于Logger日志器类
对于写日志操作进行函数重写(通过异步工作器将数据放入缓冲区)
-
通过异步工作器,进行日志的实际落地
管理成员:
- 异步工作器
提供操作:
- 重写log函数主要实现将日志数据加入异步队列缓冲区中
- 为异步消息处理器设置的回调函数,完成日志的实际落地工作。
class AsyncLogger : public Logger
{
public:
AsyncLogger(const std::string &logger_name,
LogLevel::value level,
Formatter::ptr &formatter,
std::vector<LogSink::ptr>& sinks,
AsyncType looper_type):
Logger(logger_name, level, formatter, sinks),
_looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::realLog,this,std::placeholders::_1),looper_type))
{}
protected:
void log(const char *data, size_t len)
{
_looper->push(data,len);
}
void realLog(Buffer& buf)//实际落地函数将缓冲区数据落地
{
if (_sinks.empty())
return;
for (auto &sink : _sinks)
{
sink->log(buf.begin(), buf.readAbleSize());
}
}
private:
AsyncLooper::ptr _looper;
};
4.6.3 建造者模式
采用建造者模式来建造日志器 简化用户使用
-
抽象日志建造者类(完成日志器所需零部件的构造&日志器的构造)
-
将不同的日志器创建放到同一个建造者类中完成
-
派生出具体的建造者类 局部日志器建造者& 全局日志器建造者、
enum class LoggerType
{
LOGGER_SYNC,
LOGGER_ASYNC,
};
class LoggerBuilder
{
public:
LoggerBuilder():
_logger_type(LoggerType::LOGGER_SYNC),
_limit_level(LogLevel::value::DEBUG),
_looper_type(AsyncType::ASYNC_SAFE)
{}
void buildLoggerType(LoggerType type){ _logger_type=type;}
void buildEnableUnSafeAsync(){ _looper_type=AsyncType::ASYNC_UNSAFE;}
void buildLoggerName(const std::string &name){_logger_name=name;}
void buildLoggerLevel(LogLevel::value level){_limit_level=level;}
void buildFormatter(const std::string pattern="[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n")
{
_formatter=std::make_shared<Formatter>(pattern);
}
template <class SinkType, class... Args>
void buildSink(Args &&...args)
{
LogSink::ptr psink=SinkFactory::create<SinkType>(std::forward<Args>(args)...);
_sinks.push_back(psink);
}
virtual Logger::ptr build() = 0;
protected:
AsyncType _looper_type;
LoggerType _logger_type;
std::string _logger_name;
std::atomic<LogLevel::value> _limit_level; // 原子变量
Formatter::ptr _formatter;
std::vector<LogSink::ptr> _sinks;
};
class LocalLoggerBuilder : public LoggerBuilder
{
public:
Logger::ptr build()
{
assert(_logger_name.empty()==false);
if(_formatter.get()==nullptr)
{
_formatter=std::make_shared<Formatter>();
}
if(_sinks.empty())
{
buildSink<StdoutSink>();
}
if(_logger_type==LoggerType::LOGGER_SYNC)
{
return std::make_shared<SyncLogger>(_logger_name,_limit_level,_formatter,_sinks);
}
else
{
return std::make_shared<AsyncLogger>(_logger_name,_limit_level,_formatter,_sinks,_looper_type);
}
}
};
4.7日志器管理类设计(单例模式)
我们希望能够在任意位置都可以进⾏日志输出,但是当我们创建了⼀个⽇志器之后,就会受到⽇志器所在作⽤域的访问属性限制。因此,为了突破访问区域的限制,我们创建⼀个⽇志器管理类,且这个类是⼀个单例类,这样的话,我们就可以在任意位置来通过管理器单例获取到指定的⽇志器来进⾏⽇志输出了。
作用:对所有创建的日志器进行管理,可以在程序任意位置,获取单例对象,获取其中的日志器进行日志输出。
管理成员:
- 默认日志器(在单例管理创建时,默认创建一个日志器用于标准输出打印)
- 所管理的日志器数组
- 互斥锁
提供操作:
- 添加日志器
- 判断是否管理了指定名字的日志器
- 获取日志器名称
- 获取默认日志器
class LoggerManager
{
public:
static LoggerManager &getInstance()
{
//c++11针对静态局部变量,编译器在编译层面实现了线程安全
//在静态局部变量没有构造完成前,其他线程进入会阻塞
static LoggerManager eton;
return eton;
}
void addLogger(Logger::ptr& logger)
{
if (hasLogger(logger->name())) return;
std::unique_lock<std::mutex> lock(_mutex);
_loggers.insert(std::make_pair(logger->name(),logger));
}
bool hasLogger(const std::string& name)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it=_loggers.find(name);
if(it==_loggers.end())
{
return false;
}
return true;
}
Logger::ptr getLogger(const std::string& name)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it=_loggers.find(name);
if(it==_loggers.end())
{
return Logger::ptr();
}
return it->second;
}
Logger::ptr rootLogger()
{
return _root_logger;
}
private:
LoggerManager() {
std::unique_ptr<viclog::LoggerBuilder> builder(new viclog::LocalLoggerBuilder());
builder->buildLoggerName("root");
_root_logger=builder->build();
_loggers.insert(std::make_pair("root",_root_logger));
}
private:
std::mutex _mutex;
Logger::ptr _root_logger;
std::unordered_map<std::string,Logger::ptr> _loggers;
};
完成单例日志器管理器后,我们通过LocalLoggerBuilder建造出来的日志器需要手动添加进管理器很麻烦。我们对于日志建造者类进行继承,继承出一个全局建造者类,该建造者类在建造完日志器后,会直接将该日志器添加到日志器管理器中进行管理,这样以来我们能够在任何地方创建对应日志器并且突破作用域的限制。
//全局日志器建造者
//在局部日志器基础数添加一个功能:将日志器添加到单例对象中
class GlobalLoggerBuilder : public LoggerBuilder
{
public:
Logger::ptr build()
{
assert(_logger_name.empty()==false);
if(_formatter.get()==nullptr)
{
_formatter=std::make_shared<Formatter>();
}
if(_sinks.empty())
{
buildSink<StdoutSink>();
}
Logger::ptr logger;
if(_logger_type==LoggerType::LOGGER_SYNC)
{
logger =std::make_shared<SyncLogger>(_logger_name,_limit_level,_formatter,_sinks);
}
else
{
logger= std::make_shared<AsyncLogger>(_logger_name,_limit_level,_formatter,_sinks,_looper_type);
}
LoggerManager::getInstance().addLogger(logger);
return logger;
}
};
}
4.8日志宏&全局接口设计
提供全局的日志器获取接口。使用代理模式通过全局函数或宏函数来代理Logger类的log、debug、info、warn、error、fatal等接口,以便于控制源码文件名称和行号的输出控制,简化用户操作。在viclog.h中其中用宏函数向用户提供一些日志系统的简易接口
- 提供获取指定日志器的全局接口
- 使用宏函数对日志器的接口进行代理(代理模式)
- 提供直接通过标准输出日志器打印的宏函数
#ifndef __M_VICLOG_H__
#define __M_VICLOG_H__
#include"logger.hpp"
namespace viclog
{
//1.提供获取指定日志器的全局接口
Logger::ptr getLogger(const std::string& name)
{
return viclog::LoggerManager::getInstance().getLogger(name);
}
Logger::ptr rootLogger()
{
return viclog::LoggerManager::getInstance().rootLogger();
}
//2.使用宏函数对日志器的接口进行代理(代理模式)
#define debug(fmt,...) debug(__FILE__,__LINE__,fmt,##__VA_ARGS__)
#define info(fmt,...) info(__FILE__,__LINE__,fmt,##__VA_ARGS__)
#define warn(fmt,...) warn(__FILE__,__LINE__,fmt,##__VA_ARGS__)
#define error(fmt,...) error(__FILE__,__LINE__,fmt,##__VA_ARGS__)
#define fatal(fmt,...) fatal(__FILE__,__LINE__,fmt,##__VA_ARGS__)
//3.提供直接通过标准输出日志器打印的宏函数
#define DEBUG(fmt,...) viclog::rootLogger()->debug(fmt,##__VA_ARGS__)
#define INFO(fmt,...) viclog::rootLogger()->info(fmt,##__VA_ARGS__)
#define WARN(fmt,...) viclog::rootLogger()->warn(fmt,##__VA_ARGS__)
#define ERROR(fmt,...) viclog::rootLogger()->error(fmt,##__VA_ARGS__)
#define FATAL(fmt,...) viclog::rootLogger()->fatal(fmt,##__VA_ARGS__)
}
#endif
五、测试
5.1 功能测试
测试一个日志器中包含有所有的落地方向,观察是否每个方向都正常落地,分别测试同步方式和异步方式落地后数据是否正常。
测试样例
5.2性能测试
测试环境:
- 腾讯云服务器
- CPU:2核
- 内存:2G
- 硬盘:系统盘 40G
- 系统:CentOS 7.6 64bit
主要的测试方法是:
每秒能打印日志数 = 打印日志条数 / 总的打印日志消耗时间
主要测试要素:同步/异步 & 单线程/多线程
- 100w+条指定长度的日志输出所耗时间
- 每秒可以输出多少条日志
- 每秒可以输出多少MB日志
测试代码:
同步日志的单线程:
同步日志的多线程:
异步日志的单线程:
异步日志的多线程:
5.3总结
- 单线程情况下,异步日志器效率没有同步日志器高,原因:
IO操作在用户态都会有缓冲区进行缓冲区,因此我们当前测试用例看起来的同步其实大多时候也是在操作内存,只有在缓冲区满了才会涉及到阻塞写磁盘操作,而异步单线程效率低,很重要的原因就是单线程同步操作中不存在锁冲突,而单线程异步日志操作存在大量的锁冲突,因此性能也会有一定的降低。
- 多线程情况下,发现同步日志器性能不但没有提升而且还下降了,而异步日志器性能则有有效的提升,原因:
同步日志器要将日志亲自写入到磁盘中,因此其性能上限收到磁盘读写速度的限制,即使多线程也难以提升,反而因为锁冲突导致性能下降;而异步日志器上限则是CPU的处理性能,日志输出时不会因为落地而阻塞,因此多线程下有了显著提升。