目录
1.项目介绍
本项⽬主要实现⼀个⽇志系统, 其主要⽀持以下功能:
•
⽀持多级别⽇志消息
•
⽀持同步⽇志和异步⽇志
•
⽀持可靠写⼊⽇志到控制台、⽂件以及滚动⽂件中
•
⽀持多线程程序并发写⽇志
•
⽀持扩展不同的⽇志落地⽬标地
2.整体框架设计
本项⽬实现的是⼀个多⽇志器⽇志系统,主要实现的功能是让程序员能够轻松的将程序运⾏⽇志信息落地到指定的位置,且⽀持同步与异步两种⽅式的⽇志落地⽅式。主要分为:
1.日志等级模块:对输出⽇志的等级进⾏划分,以便于控制⽇志的输出,并提供等级枚举转字符串功
能。
◦
OFF:关闭
◦
DEBUG:调试,调试时的关键信息输出。
◦
INFO:提⽰,普通的提⽰型⽇志信息。
◦
WARN:警告,不影响运⾏,但是需要注意⼀下的⽇志。
◦
ERROR:错误,程序运⾏出现错误的⽇志
◦
FATAL:致命,⼀般是代码异常导致程序⽆法继续推进运⾏的⽇志
2.日志消息模块:中间存储⽇志输出所需的各项要素信息
◦
时间:描述本条⽇志的输出时间。
◦
线程ID:描述本条⽇志是哪个线程输出的。
◦
日志等级:描述本条⽇志的等级。
◦
日志数据:本条⽇志的有效载荷数据。
◦
日志⽂件名:描述本条⽇志在哪个源码⽂件中输出的。
◦
日志⾏号:描述本条⽇志在源码⽂件的哪⼀⾏输出的。
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:表⽰换⾏
◦
设计思想:设计不同的⼦类,不同的⼦类从⽇志消息中取出不同的数据进⾏处理。
4.
⽇志消息落地模块:决定了⽇志的落地⽅向,可以是标准输出,也可以是⽇志⽂件,也可以滚动⽂
件输出....
◦
标准输出:表⽰将⽇志进⾏标准输出的打印。
◦
⽇志⽂件输出:表⽰将⽇志写⼊指定的⽂件末尾。
◦
滚动⽂件输出:当前以⽂件⼤⼩进⾏控制,当⼀个⽇志⽂件⼤⼩达到指定⼤⼩,则切换下⼀个
⽂件进⾏输出
◦
后期,也可以扩展远程⽇志输出,创建客⼾端,将⽇志消息发送给远程的⽇志分析服务器。
◦
设计思想:设计不同的⼦类,不同的⼦类控制不同的⽇志落地⽅向。
5.
⽇志器模块:
◦
此模块是对以上⼏个模块的整合模块,⽤⼾通过⽇志器进⾏⽇志的输出,有效降低⽤⼾的使⽤
难度。
◦
包含有:⽇志消息落地模块对象,⽇志消息格式化模块对象,⽇志输出等级。
6.
⽇志器管理模块:
◦
为了降低项⽬开发的⽇志耦合,不同的项⽬组可以有⾃⼰的⽇志器来控制输出格式以及落地⽅
向,因此本项⽬是⼀个多⽇志器的⽇志系统。
◦
管理模块就是对创建的所有⽇志器进⾏统⼀管理。并提供⼀个默认⽇志器提供标准输出的⽇志
输出。
7.
异步线程模块:
◦
实现对⽇志的异步输出功能,⽤⼾只需要将输出⽇志任务放⼊任务池,异步线程负责⽇志的落
地输出功能,以此提供更加⾼效的⾮阻塞⽇志输出。
3.⽇志输出格式化类设计
⽇志格式化(Formatter)类主要负责格式化⽇志消息。其主要包含以下内容
•
pattern成员:保存⽇志输出的格式字符串。
◦
%d ⽇期
◦
%T 缩进
◦
%t 线程id
◦
%p ⽇志级别
◦
%c ⽇志器名称
◦
%f ⽂件名
◦
%l ⾏号
◦
%m ⽇志消息
◦
%n 换⾏
•
std::vector<FormatItem::ptr> items成员:⽤于按序保存格式化字符串对应的⼦格式化对象。
FormatItem类主要负责⽇志消息⼦项的获取及格式化。其包含以下⼦类
•
MsgFormatItem :表⽰要从LogMsg中取出有效⽇志数据
•
LevelFormatItem :表⽰要从LogMsg中取出⽇志等级
•
NameFormatItem :表⽰要从LogMsg中取出⽇志器名称
•
ThreadFormatItem :表⽰要从LogMsg中取出线程ID
•
TimeFormatItem :表⽰要从LogMsg中取出时间戳并按照指定格式进⾏格式化
•
CFileFormatItem :表⽰要从LogMsg中取出源码所在⽂件名
•
CLineFormatItem :表⽰要从LogMsg中取出源码所在⾏号
•
TabFormatItem :表⽰⼀个制表符缩进
•
NLineFormatItem :表⽰⼀个换⾏
•
OtherFormatItem :表⽰⾮格式化的原始字符串
⽰例:"[%d{%H:%M:%S}] %m%n"
/*
定义日志消息类,进行日志中间消息的存储:
1.日志输出时间: 用于过滤日志输出时间
2.日志等级 : 用于进行日志过滤分析
3.源代码文件名称
4.源代码行号 用于定位出现错误的代码位置
5.线程ID 用于过滤出错的线程
6.日志消息主体
7.日志器名称 (当前支持日志器的同时使用)
*/
#pragma once
#include<iostream>
#include<string>
#include<thread>
#include"util.hpp"
#include"level.hpp"
namespace mylog
{
struct LogMsg
{
using ptr = std::shared_ptr<LogMsg>;
size_t _line; // ⾏号
size_t _ctime; // 时间
std::thread::id _tid; // 线程ID
std::string _logger; // ⽇志器名称
std::string _file; // ⽂件名
std::string _payload; // ⽇志消息
LogLevel::value _level; // ⽇志等级
LogMsg() {}
LogMsg(LogLevel::value level,
size_t line,std::string file,
std::string name,
std::string payload) :
_logger(name),
_file(file),
_payload(payload),
_level(level),
_line(line),
_ctime(util::Date::getTime()),
_tid(std::this_thread::get_id())
{
}
};
}
格式化的过程其实就是按次序从Msg中取出需要的数据进⾏字符串的连接的过程。
最终组织出来的格式化消息: "[22:32:54] 创建套接字失败\n"
代码实现:
#pragma once
#include<ctime>
#include<vector>
#include<cassert>
#include<sstream>
#include<memory.h>
#include <tuple>
#include"level.hpp"
#include"message.hpp"
//抽象格式化子项基类
//派生格式化子项子类-消息 等级 文件名 时间 行号 线程id 日志器名称 制表符 换行 其他
namespace mylog
{
class FormatItem{
public:
using ptr=std::shared_ptr<FormatItem>;
virtual void format(std::ostream &out,LogMsg &msg)=0;
virtual ~FormatItem(){}
};
class LeveFormatItem :public FormatItem
{
public:
void format(std::ostream &out,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,LogMsg &msg)override{
struct tm tl;
time_t t=msg._ctime;
localtime_r(&t,&tl);
char temp[32]={0};
strftime(temp,31,_time_fmt.c_str(),&tl);
out<<temp;
}
private:
std::string _time_fmt;//%H %M %S
};
class FileFormatItem :public FormatItem
{
public:
void format(std::ostream &out,LogMsg &msg)override{
out<<msg._file;
}
};
class LineFormatItem :public FormatItem
{
public:
void format(std::ostream &out,LogMsg &msg)override{
out<<msg._line;
}
};
class ThreadFormatItem :public FormatItem
{
public:
void format(std::ostream &out,LogMsg &msg)override{
out<<msg._tid;
}
};
class LoggerFormatItem :public FormatItem
{
public:
void format(std::ostream &out,LogMsg &msg)override{
out<<msg._logger;
}
};
//newline 换行
class NlineFormatItem :public FormatItem
{
public:
void format(std::ostream &out,LogMsg &msg)override{
out<<"\n";
}
};
class OtherFormatItem :public FormatItem
{
public:
OtherFormatItem(const std::string &str)
:_str(str)
{
}
void format(std::ostream &out,LogMsg &msg)override{
out<<_str;
}
private:
std::string _str;
};
class TabFormatItem : public FormatItem
{
public:
TabFormatItem(const std::string &str = "") {}
void format(std::ostream &os, LogMsg &msg)
{
os <<"\t";
}
};
class MsgFormatItem : public FormatItem
{
public:
MsgFormatItem(const std::string &str = "") {}
void format(std::ostream &os, LogMsg &msg)
{
os << msg._payload;
}
};
/*
%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:_itmes)
{
item->format(out,msg);
}
}
std::string format(LogMsg&msg)
{
std::stringstream ss;
format(ss,msg);
return ss.str();
}
private:
//对格式化规则的字符串进行解析
bool parsePattern(){
//1.对格式化规则字符串进行解析
std::vector<std::pair<std::string,std::string>>fmt_oredr;
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;
}
//此时 pos位置是格式化字符
if (!val.empty())
{
fmt_oredr.push_back(std::make_pair("", val));
val.clear();
}
//这时候是格式化字符的处理
pos+=1;
if(pos==_pattern.size())
{
std::cout<<"%之后没有对应的格式化字符\n";
return false;
}
key=_pattern[pos];
//这时候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::cout<<"子格式{}匹配出错!\n";
return false;
}
pos+=1;//此时pos指向的'}'加+1跳过};
}
fmt_oredr.push_back(std::make_pair(key,val));
key.clear();
val.clear();
}
//2.根据解析得到的数据初始化格式化子项数组成员
for(auto& it:fmt_oredr)
{
_itmes.push_back(createItem(it.first,it.second));
}
return true;
}
/*
%d 表示日期 包含子格式("%H %M %S ")
%t 表示线程id
%c 表示日志器名称
%f 表示源码文件名
%l 表示源码行号
%p 表示日志级别
%T 表示制表符缩进
%m 表示主体消息
%n 表示换行
*/
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<LeveFormatItem>();
}
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();
}
private:
std::string _pattern;//格式化规则字符串
std::vector<FormatItem::ptr>_itmes;
};
}
4.⽇志落地(LogSink)类设计
⽇志落地类主要负责落地⽇志消息到⽬的地。
它主要包括以下内容:
•
Formatter⽇志格式化器:主要是负责格式化⽇志消息,
•
mutex互斥锁:保证多线程⽇志落地过程中的线程安全,避免出现交叉输出的情况。
这个类⽀持可扩展,其成员函数log设置为纯虚函数,当我们需要增加⼀个log输出⽬标, 可以增加⼀个类继承⾃该类并重写log⽅法实现具体的落地⽇志逻辑。
⽬前实现了三个不同⽅向上的⽇志落地:
•
标准输出:StdoutSink
•
固定⽂件:FileSink
•
滚动⽂件:RollSink
◦
滚动⽇志⽂件输出的必要性:
▪
由于机器磁盘空间有限, 我们不可能⼀直⽆限地向⼀个⽂件中增加数据
如果⼀个⽇志⽂件体积太⼤,⼀⽅⾯是不好打开,另⼀⽅⾯是即时打开了由于包含数据巨
⼤,也不利于查找我们需要的信息
▪
所以实际开发中会对单个⽇志⽂件的⼤⼩也会做⼀些控制,即当⼤⼩超过某个⼤⼩时(如
1GB),我们就重新创建⼀个新的⽇志⽂件来滚动写⽇志。 对于那些过期的⽇志, ⼤部分
企业内部都有专⻔的运维⼈员去定时清理过期的⽇志,或者设置系统定时任务,定时清理过
期⽇志。
◦
⽇志⽂件的滚动思想:
⽇志⽂件滚动的条件有两个:⽂件⼤⼩ 和 时间。我们可以选择:
▪
⽇志⽂件在⼤于 1GB 的时候会更换新的⽂件
▪
每天定点滚动⼀个⽇志⽂件
本项⽬基于⽂件⼤⼩的判断滚动⽣成新的⽂件
/*
日志落地模块的实现
1.抽象落地基类
2.派生子类(根据不同的落地反向进行派生)
3.使用工厂模式进行创建与表示的分离
*/
#pragma once
#include "util.hpp"
#include "message.hpp"
#include "format.hpp"
#include<sstream>
#include <memory>
#include <mutex>
#include <fstream>
namespace mylog{
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:
using ptr = std::shared_ptr<StdoutSink>;
// 将日志消息写入到标准输出
// cout是默认作为\0截止的,这里就不采用了
void log(const char *data, size_t len)
{
std::cout.write(data, len);
}
};
// 落地方向向:指定文件
class FileSink : public LogSink
{
public:
using ptr = std::shared_ptr<FileSink>;
// 构造时传入文件名,并打开文件,将操作句柄管理起来
FileSink(const std::string &filename) : _filename(filename)
{
// 1.创建日志文件所在的目录
mylog::util::File::createDirectory(mylog::util::File::path(filename));
// 2.创建并打开目录下的日志文件
_ofs.open(_filename, std::ios::binary | std::ios::app); // 追加写
assert(_ofs.is_open());
}
// 将日志消息写入到指定文件中
const std::string &file() { return _filename; }
void log(const char *data, size_t len)
{
_ofs.write((const char *)data, len);
if (_ofs.good() == false)
{
std::cout << "日志输出文件失败!\n";
}
}
private:
std::string _filename;
std::ofstream _ofs;
};
// 落地方向 :滚动文件(以大小进行滚动)
class RollBySizeSink : public LogSink
{
public:
using ptr = std::shared_ptr<RollBySizeSink>;
// 构造时传入文件名 并且打开文件,将操作句柄管理起来
RollBySizeSink(const std::string &basename, size_t max_fsize) :
_basename(basename), _max_fsize(max_fsize), _cur_fsize(0),_name_count(0)
{
std::string pathname = createNewFile();
// 1.创建日志文件所在的目录
mylog::util::File::createDirectory(mylog::util::File::path(pathname));
// 2.创建并打开目录下的日志文件
_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 = mylog::util::Date::getTime();
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 -20230101.log
size_t _name_count;
std::ofstream _ofs;
size_t _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)...);
}
};
}
5.⽇志器类(Logger)设计(建造者模式)
⽇志器主要是⽤来和前端交互, 当我们需要使⽤⽇志系统打印log的时候, 只需要创建Logger对象,
调⽤该对象debug、info、warn、error、fatal等⽅法输出⾃⼰想打印的⽇志即可,⽀持解析可变参数
列表和输出格式, 即可以做到像使⽤printf函数⼀样打印⽇志。
当前⽇志系统⽀持同步⽇志 & 异步⽇志两种模式,两个不同的⽇志器唯⼀不同的地⽅在于他们在⽇志
的落地⽅式上有所不同:
同步⽇志器:直接对⽇志消息进⾏输出。
异步⽇志器:将⽇志消息放⼊缓冲区,由异步线程进⾏输出。
因此⽇志器类在设计的时候先设计出⼀个Logger基类,在Logger基类的基础上,继承出SyncLogger同
步⽇志器和AsyncLogger异步⽇志器。
且因为⽇志器模块是对前边多个模块的整合,想要创建⼀个⽇志器,需要设置⽇志器名称,设置⽇志输出等级,设置⽇志器类型,设置⽇志输出格式,设置落地⽅向,且落地⽅向有可能存在多个,整个⽇志器的创建过程较为复杂,为了保持良好的代码⻛格,编写出优雅的代码,因此⽇志器的创建这⾥采⽤了建造者模式来进⾏创建。
/*
日志落地模块的实现
1.抽象落地基类
2.派生子类(根据不同的落地反向进行派生)
3.使用工厂模式进行创建与表示的分离
*/
#pragma once
#include "util.hpp"
#include "message.hpp"
#include "format.hpp"
#include<sstream>
#include <memory>
#include <mutex>
#include <fstream>
namespace mylog{
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:
using ptr = std::shared_ptr<StdoutSink>;
// 将日志消息写入到标准输出
// cout是默认作为\0截止的,这里就不采用了
void log(const char *data, size_t len)
{
std::cout.write(data, len);
}
};
// 落地方向向:指定文件
class FileSink : public LogSink
{
public:
using ptr = std::shared_ptr<FileSink>;
// 构造时传入文件名,并打开文件,将操作句柄管理起来
FileSink(const std::string &filename) : _filename(filename)
{
// 1.创建日志文件所在的目录
mylog::util::File::createDirectory(mylog::util::File::path(filename));
// 2.创建并打开目录下的日志文件
_ofs.open(_filename, std::ios::binary | std::ios::app); // 追加写
assert(_ofs.is_open());
}
// 将日志消息写入到指定文件中
const std::string &file() { return _filename; }
void log(const char *data, size_t len)
{
_ofs.write((const char *)data, len);
if (_ofs.good() == false)
{
std::cout << "日志输出文件失败!\n";
}
}
private:
std::string _filename;
std::ofstream _ofs;
};
// 落地方向 :滚动文件(以大小进行滚动)
class RollBySizeSink : public LogSink
{
public:
using ptr = std::shared_ptr<RollBySizeSink>;
// 构造时传入文件名 并且打开文件,将操作句柄管理起来
RollBySizeSink(const std::string &basename, size_t max_fsize) :
_basename(basename), _max_fsize(max_fsize), _cur_fsize(0),_name_count(0)
{
std::string pathname = createNewFile();
// 1.创建日志文件所在的目录
mylog::util::File::createDirectory(mylog::util::File::path(pathname));
// 2.创建并打开目录下的日志文件
_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 = mylog::util::Date::getTime();
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 -20230101.log
size_t _name_count;
std::ofstream _ofs;
size_t _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)...);
}
};
}
6.双缓冲区异步任务处理器(AsyncLooper)设计
设计思想:异步处理线程 + 数据池
使⽤者将需要完成的任务添加到任务池中,由异步线程来完成任务的实际执⾏操作。
任务池的设计思想:双缓冲区阻塞数据池
优势:避免了空间的频繁申请释放,且尽可能的减少了⽣产者与消费者之间锁冲突的概率,提⾼了任务处理效率。
在任务池的设计中,有很多备选⽅案,⽐如循环队列等等,但是不管是哪⼀种都会涉及到锁冲突的情况,因为在⽣产者与消费者模型中,任何两个⻆⾊之间都具有互斥关系,因此每⼀次的任务添加与取出都有可能涉及锁的冲突,⽽双缓冲区不同,双缓冲区是处理器将⼀个缓冲区中的任务全部处理完毕后,然后交换两个缓冲区,重新对新的缓冲区中的任务进⾏处理,虽然同时多线程写⼊也会冲突,但是冲突并不会像每次只处理⼀条的时候频繁(减少了⽣产者与消费者之间的锁冲突),且不涉及到空间的频繁申请释放所带来的消耗。
/*实现异步工作器*/
#pragma once
#include"buffer.hpp"
#include<mutex>
#include<thread>
#include<condition_variable>
#include<functional>
#include<memory>
#include<atomic>
namespace mylog{
using Functor =std::function<void(Buffer &)>;
enum class AsyncType {
ASYNC_SAFE,//安全状态,表示缓冲区满了则阻塞,避免资源耗尽的风险
ASUNC_UNSAFE//不考虑资源耗尽的问题,无限扩容,用于测试
};
class AsyncLooper{
public:
using ptr=std::shared_ptr<AsyncLooper>;
AsyncLooper(const Functor&cb,AsyncType loop_tye=AsyncType::ASYNC_SAFE)
:_stop(false),
_looper_type(loop_tye),
_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_pro.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);
//唤醒生产者
if(_looper_type==AsyncType::ASYNC_SAFE)
_cond_pro.notify_all();
}
//2.被唤醒后,对消费缓冲区进行数据处理
_callBack(_con_buf);
//3.初始化消费缓冲区
_con_buf.reset();
}
}
AsyncType _looper_type;
Functor _callBack;//回调函数
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 ;//异步工作器对应的工作线程
};
}
7.⽇志宏&全局接⼝设计(代理模式)
提供全局的⽇志器获取接⼝。
使⽤代理模式通过全局函数或宏函数来代理Logger类的log、debug、info、warn、error、fatal等接
⼝,以便于控制源码⽂件名称和⾏号的输出控制,简化⽤⼾操作。
当仅需标准输出⽇志的时候可以通过主⽇志器来打印⽇志。 且操作时只需要通过宏函数直接进⾏输出 即可。
#pragma once
#include"logger.hpp"
namespace mylog{
//1.提供获取指定日志器的全局接口
Logger::ptr getLogger(const std::string &name){
return mylog::LoggerManger::getInstance().getLogger(name);
}
Logger::ptr rootLogger(){
return mylog::LoggerManger::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,...) mylog::rootLogger()->debug(fmt,##__VA_ARGS__)
#define INFO(fmt,...) mylog::rootLogger()->info(fmt,##__VA_ARGS__)
#define WARN(fmt,...) mylog::rootLogger()->warn(fmt,##__VA_ARGS__)
#define ERROR(fmt,...) mylog::rootLogger()->error(fmt,##__VA_ARGS__)
#define FATAL(fmt,...) mylog::rootLogger()->fatal(fmt,##__VA_ARGS__)
}
8.性能测试
下⾯对⽇志系统做⼀个性能测试,测试⼀下平均每秒能打印多少条⽇志消息到⽂件。
主要的测试⽅法是:每秒能打印⽇志数 = 打印⽇志条数 / 总的打印⽇志消耗时间
主要测试要素:同步/异步 & 单线程/多线程
•
100w+条指定⻓度的⽇志输出所耗时间
•
每秒可以输出多少条⽇志
•
每秒可以输出多少MB⽇志
测试环境:
•
CPU:AMD Ryzen 7 5800H with Radeon Graphics 3.20 GHz
•
RAM:16G DDR4 3200
•
ROM:512G-SSD
•
OS:ubuntu-22.04TLS虚拟机(2CPU核⼼/4G内存)
#include "../MyLog/mylog.hpp"
#include<vector>
#include<thread>
#include<chrono>
void bench(const std::string &logger_name,size_t thr_count,size_t msg_count,size_t msg_len)
{
//1.获取日志器
mylog::Logger::ptr logger=mylog::getLogger(logger_name);
if(logger.get()==nullptr){
std::cout << "no logger " << logger_name << std::endl;
return ;
}
std::cout<<"测试日志:"<<msg_count<<"条,总大小:"<<(msg_count+msg_len)/1024<<"KB\n";
//2.组织指定长度的日志器
std::string msg(msg_len-1,'A');//少一个字节,是为了给末尾的时候添加换行
//3.创建指定数量的线程
std::vector<std::thread> threads;
std::vector<double>cost_arry(thr_count);
size_t msg_per_thr=msg_count/thr_count;//总日志数量/线程数量就是每个线程要输出的日志数量
for(int i=0;i<thr_count;i++)
{
threads.emplace_back([&,i](){
//4.线程函数内部开始计时
auto start=std::chrono::high_resolution_clock::now();
//5.开始循环写日志
for(int j=0;j<msg_per_thr;j++)
{
logger->fatal("%s",msg.c_str());
}
//6.线程函数内部结束计时
auto end=std::chrono::high_resolution_clock::now();
std::chrono::duration<double> cost=end-start;
cost_arry[i]=cost.count();
std::cout<<"线程"<<i+1<<":"<<"\t输出日志数量:"<<msg_per_thr<<",耗时:"<<cost.count()<<"s"<<std::endl;
});
}
for(int i=0;i<thr_count;i++){
threads[i].join();
}
//7.计算总耗时 在多线程中,线程并发执行因此耗时时间最高的那个就是总时间
double max_cost=cost_arry[0];
for(int i=0;i<thr_count;i++){
max_cost= max_cost<cost_arry[i]?cost_arry[i]:max_cost;
}
size_t msg_per_sec=msg_count/max_cost;
size_t size_per_sec=(msg_count*msg_len)/(max_cost*1024);
//8.进行输出打印
std::cout<<"每秒输出日志数量: "<<msg_per_sec<<"条\n";
std::cout<<"每秒输出日志大小:"<<size_per_sec<<"KB\n";
}
void sync_bench(){
std::unique_ptr<mylog::LoggerBuilder>builder(new mylog::GlobalLoggerBulid());
builder->bulidLoggerName("sync_logger");
builder->bulidLoggerFormatter("%m%n");
builder->bulidLoggerType(mylog::LoggerType::LOGGER_SYNC);
builder->bulidSink<mylog::FileSink>("./logfile/sync.log");
builder->bulid();
bench("sync_logger",3,1000000,100);
}
void async_bench(){
std::unique_ptr<mylog::LoggerBuilder>builder(new mylog::GlobalLoggerBulid());
builder->bulidLoggerName("async_logger");
builder->bulidLoggerFormatter("%m%n");
builder->bulidLoggerType(mylog::LoggerType::LOGGER_ASYNC);
builder->buildEnableUnSafeAsync();//开启非安全模式
builder->bulidSink<mylog::FileSink>("./logfile/async.log");
builder->bulid();
bench("async_logger",3,1000000,100);
}
int main()
{
async_bench();
std:: cout<<"###############\n";
sync_bench();
return 0;
}
在单线程情况下,异步效率看起来还没有同步⾼,这个我们得了解,现在的IO操作在⽤⼾态都会有缓冲区进⾏缓冲区,因此我们当前测试⽤例看起来的同步其实⼤多时候也是在操作内存,只有在缓冲区 满了才会涉及到阻塞写磁盘操作,⽽异步单线程效率看起来低,也有⼀个很重要的原因就是单线程同步操作中不存在锁冲突,⽽单线程异步⽇志操作存在⼤量的锁冲突,因此性能也会有⼀定的降低。
但是,我们也要看到限制同步⽇志效率的最⼤原因是磁盘性能,打⽇志的线程多少并⽆明显区别,线 程多了反⽽会降低,因为增加了磁盘的读写争抢,⽽对于异步⽇志的限制,并⾮磁盘的性能,⽽是cpu的处理性能,打⽇志并不会因为落地⽽阻塞,因此在多线程打⽇志的情况下性能有了显著的提⾼。
9.扩展
•
丰富sink类型:
◦
⽀持按⼩时按天滚动⽂件
◦
⽀持将log通过⽹络传输落地到⽇志服务器(tcp/udp)
◦
⽀持在控制台通过⽇志等级渲染不同颜⾊输出⽅便定位
◦
⽀持落地⽇志到数据库
◦
⽀持配置服务器地址,将⽇志落地到远程服务器
•
实现⽇志服务器负责存储⽇志并提供检索、分析、展⽰等功能
10.参考资料
https://www.imangodoc.com/174918.html
https://blog.csdn.net/w1014074794/article/details/125074038
https://zhuanlan.zhihu.com/p/472569975
https://zhuanlan.zhihu.com/p/460476053
https://gitee.com/davidditao/DDlog
https://www.cnblogs.com/ailumiyana/p/9519614.html
https://gitee.com/lqk1949/plog/
https://www.cnblogs.com/horacle/p/15494358.html
https://blog.csdn.net/qq_29220369/article/details/127314390