目录
- 项目系统
- 开发环境
- 核心技术
- 日志系统介绍
- 为什么需要日志系统?
- 日志系统框架设计
- 日志系统模块划分
- 代码实现
- 通用工具实现
- 日志等级模块实现
- 日志消息模块实现
- 格式化模块实现
- 落地模块实现
- 日志器模块
- 同步日志器
- 异步日志器
- 缓冲区实现
- 异步工作器实现
- 回归异步日志器模块
- 建造者模式
- 日志器管理模块实现
- 日志器模块具体实现
- 整合模块
- 总的头文件
- 日志系统使用样例:
项目系统
本项目主要实现一个日志系统,其核心功能是:将一条日志消息按照指定格式和输出等级写入到指定位置;
本日志系统,主要支持一下几个功能:
- 支持多级别日志消息的输出,比如:现在日志消息有A、B、C、D这几个等级,如果我们想让B等级以上的日志消息输出出来,那么我们就只需要将日志的输出等级设置为B就好了,那么低于这个等级的C、D等级的日志就不会输出出来;
- 支持同步写日志和异步写日志,刚才我们不是说本日志系统的核心是 ‘‘将一条日志消息写入到指定位置’’ 嘛,怎么写入?这里就支持两种写入方式,同步写入和异步写入;
- 支持多个落地方向,比如:支持将日志消息落地到标准输出、指定文件、滚动文件、数据库、网络服务器等等…
- 支持多线程使用同一个日志器写日志,也就是我们的日志系统是线程安全的!
- 除了上述我们指定的落地方向之外,我们的日志系统还支持扩展用户自定义落地方向…
开发环境
Centos 7.6
vscode/vim/VS2019
g++/gdb
makefile
核心技术
继承和多态的应用
C++11(多线程、auto、智能指针、右值引用、互斥锁等)
双缓冲区
生产者消费者模型
设计模式
可变参数
日志系统介绍
为什么需要日志系统?
- ⽣产环境的产品为了保证其稳定性及安全性是不允许开发⼈员附加调试器去排查问题,可以借助⽇
志系统来打印⼀些⽇志帮助开发⼈员解决问题- 上线客⼾端的产品出现bug⽆法复现并解决,可以借助⽇志系统打印⽇志并上传到服务端帮助开发⼈员进⾏分析
- 对于⼀些⾼频操作(如定时器、⼼跳包)在少量调试次数下可能⽆法触发我们想要的⾏为,通过断
点的暂停⽅式,我们不得不重复操作⼏⼗次、上百次甚⾄更多,导致排查问题效率是⾮常低下,可
以借助打印⽇志的⽅式查问题- 在分布式、多线程/多进程代码中,出现bug⽐较难以定位,可以借助⽇志系统打印log帮助定位
bug- 帮助⾸次接触项⽬代码的新开发⼈员理解代码的运⾏流程
日志系统框架设计
本项目实现的是一个多日志器的日志系统,说白了也就是多种类型的日志器,程序员可以通过使用我们的日志器来将指定日志消息按照指定格式和输出等级写入到指定位置!
程序员再使用我们日志器的时候只需要初始化好日志器的格式化方式、日志输出等级、日志的实际落地方向、以及日志器的类型(同步写日志/异步写日志)就可以了,之后就可以使用我们的日志器输出日志消息了;
日志系统模块划分
首先,我们在来重复一下,本日志系统的核心功能:
将一条日志消息按照指定格式化和输出等级写入到指定文件
由此根据本项目的核心功能,我们可以完成日志系统的各个模块的肢解:
1. 日志消息模块
我们的日志消息不可能就是一个简单的字符串吧,根据C++面向对象的思想,我们需要将用户传递给我们的日志消息包装成一个一个日志对象,该日志对象中包含:该日志消息所在的文件、所产生的行号、所发生的时间、那个线程产生的这条日志消息、本条日志消息的等级、以及本条日志消息的主体(也就是用户告诉我们的日志消息)等这些信息,基于C++的OO思想,我们需要将这西基本日志消息打包起来,这样更符合我们C++程序员对于面向对象的思想的理解、也是我们的程序代码更具有封装性、独立性、健壮性!
2. 格式化模块
我们最终写入到"文件"中的日志消息都是一个一个的字符串,而不是一个个的日志对象,因此我们需要一个专门将日志对象格式化成指定字符串的模块;我们只需要给该格式化类在初始化的时候指定好我们想要的格式化方式,那么在后面的操作中,我们只需要给该格式化类传递一个日志对象就能帮我们转换出一个指定格式的日志字符串!
3. 日志等级模块
由于我们的日志系统是需要支持日志输出等级的限制的,因此我们需要单独拎出来一个日志等级的模块,该模块规定了我们的日志消息具有那些等级!
4. 落地模块
现在格式化好的日志消息字符串已经有了,那么我们就应该将这些格式化字符串写入到指定"文件"中去,那么指定"文件"有那些?
根据我们刚开始所说,我们支持将日志消息落地到:标准输出、指定文件、滚动文件等这几个方向,因此我们在落地模块中针对标准输出我们可以封装一个落地方向、针对指定文件我们又可以封装一个落地方向、针对滚动文件我们又可以封装一个落地方向,但是这些落地方向,都需要提供一个同样的接口,也就是落地接口,用户只要通过该接口就能将格式化日志信息落地到指定的落地方向;
5.写入方式模块(日志器模块)
上面几个模块中,格式化日志字符串准备好了、落地方向也准备好了,最终我们要完成的就是将格式化信息“写入”到实际的落地方向中,可是怎么写啊?
根据我们目前的情况,我们
写日志有两种情况:1、同步写;2、异步写;
这两种写入方式,也就是两种不同的日志器:同步日志器、异步日志器;
该模块是对于:格式化模块、落地模块、日志消息模块的整合;
最终与用户直接交互的也是该模块!
用户可以通过改模块将日志消息通过指定接口输出到指定文件;
就比如:用户选择了同步日志器模块,那么用户只需要通过该模块提供的接口就能将用户想要输出的日志信息,通过该日志器以同步写入的方式落地到指定"文件",异步日志器也是一样的!
以上是几个大模块,当然不排除在实际开发过程中需要我们在细分几个小模块出来!
以下是几个模块交互的简图:
代码实现
注意我们所实现的日志系统是能在Linux和Windows平台下都能跑的,因此在某些地方我们可能需要使用到条件编译;
__inux__
:表示在Linux环境下;
_WIN32
:表示在Windows环境下;
通用工具实现
在具体开始设计各个模块之间,我们需要先设计一些通用工具,因为在后续模块的设计中,我们需要使用这些工具:
我们需要设计那些工具?
1、获取当前时间戳;
2、获取文件目录;(比如:给你一个/a/b/c/t.txt,我们要获取的目录就是:/a/b/c/)
3、判断路径是否存在;
4、创建一个目录;(不是创建一个普通文件,给你一个/a/b/c/t.txt路径你要创建出/a/b/c/目录)
基于上述操作,我们设计的代码如下,这里的设计我们不讲具体实现细节,只讲大概思想:
//util.hpp
#pragma once
#include <iostream>
#include <ctime>
#include <string>
#include <sys/stat.h>
#ifdef _WIN32
#include<direct.h>
#elif __linux__
#include <sys/stat.h>
#endif // _WIN32
// 这个文件中包含一些通用工具
//
namespace MySpace
{
class util
{
public:
// 1、获取当前时间戳
static time_t getCurTime()
{
return time(nullptr);
}
// 2、获取文件目录
static std::string getDirectory(const std::string& pathname)
{
int pos = pathname.find_last_of("/\\");
if (pos == std::string::npos)
return std::string("./");
return std::string(pathname.begin(), pathname.begin() + pos + 1);
}
// 3、判断路径是否存在
static bool isExist(const std::string& pathname)
{
struct stat buf;
return !(stat(pathname.c_str(), &buf) < 0);
}
// 4、创建一个目录
static bool createDirectory(const std::string& pathname)
{
if (pathname.empty())
return false;
if (isExist(pathname))
return true;
int pos = 0;
while (pos < pathname.size())
{
pos = pathname.find_first_of("/\\", pos);
if (pos == std::string::npos)
{
//mkdir(pathname.c_str(), 0777);
#ifdef _WIN32
if (_mkdir(pathname.c_str()) == -1)
return false;
#elif __linux__
;
if (mkdir(pathname.c_str(), 0777) == -1)
return false;
#endif
break;
}
// 0~pos+1的需要创建
std::string cur = std::string(pathname.begin(), pathname.begin() + pos + 1);
if (isExist(cur))
{
pos++;
continue;
}
#ifdef _WIN32
if (_mkdir(cur.c_str()) == -1)
return false;
#elif __linux__
;
if(mkdir(cur.c_str(),0777)==-1)
return false;
#endif
pos += 1;
}
return true;
}
};
}
日志等级模块实现
在该模块中我们需要规定各种日志等级,因此我们可以采用枚举来实现,同时我们需要提供一个将这些枚举常量转换为字符串的函数,因为最后我们在使用的时候是需要将这些日志等级转换成字符串的!
而我们的日志等级有:
DEBUG:调试日志信息;
INFO:正常日志信息;
WARNING:警告日志信息;
Err:错误日志信息;
FATAL:致命日志信息;
UNKNOWN:未知日志信息;
OFF:关闭日志信息//当日志的输出等级为OFF的时候,低于这个等级以下的日志信息都无法被输出!
转换接口:
levelToString();//将日志等级转换为字符串;
代码实现:
//level.hpp
#pragma once
// 日志等级设置模块
#include <iostream>
#include <string>
namespace MySpace
{
enum class LogLevel
{
DEBUG = 0,
INFO,
WARNING,
Err,
FATAL,
UNKNOWN,
OFF
};
// 再提供一个函数将枚举等级转换为字符串
std::string levelToString(MySpace::LogLevel lev)
{
switch (lev)
{
case MySpace::LogLevel::DEBUG:
return std::string("DEBUG");
case MySpace::LogLevel::INFO:
return std::string("INFO");
case MySpace::LogLevel::WARNING:
return std::string("WARNING");
case MySpace::LogLevel::Err:
return std::string("Err");
case MySpace::LogLevel::FATAL:
return std::string("FATAL");
case MySpace::LogLevel::OFF:
return std::string("OFF");
default:
return std::string("UNKNOWN");
}
}
}
日志消息模块实现
该模块比较简单,也就是设计一个日志消息类,该日志消息类中主要包含以下几个要素信息:
- 日志消息所产生时间;
- 日志消息所产生源文件;
- 日志信息产生的行号;
- 产生日志的线程号;
- 日志等级;
- 日志器名称;(当前日志消息是通过那个日志器输出的!)
- 日志的主体信息
代码实现:
//logMes.hpp
#pragma once
#include <iostream>
#ifdef __linux__
#include <pthread.h>
#endif
#include <string>
#include <thread>
#include "level.hpp"
/*
日志消息模块:
功能:封装一条日志信息!
*/
namespace MySpace
{
class LogMes
{
public:
time_t _tm; // 日志消息所产生的时间
std::string _fileName; // 日志信息所产生的源文件
int _line; // 日志信息产生的行号
std::thread::id _tid; // 产生日志的线程号
MySpace::LogLevel _lev; // 日志等级
std::string _loggerName; // 日志器名称
std::string _payload; // 日志的主体信息
LogMes() = default;
LogMes(const std::string &filename, int line, MySpace::LogLevel lev, const std::string &loggerName, const std::string &payLoad)
: _tm(time(nullptr)), _fileName(filename), _line(line), _lev(lev), _tid(std::this_thread::get_id()), _loggerName(loggerName), _payload(payLoad)
{
}
~LogMes() {}
};
}
格式化模块实现
该模块的主要功能就是将日志对象格式化成特定字符串;
同时提供用户自定义格式化字符串的功能;
因此,在该模块中我们自定义一些格式化字符(注意与C语言的标准格式化字符区别,这里定义的字符只针对我们本日志系统生效!):
注意如果我们想要根据明确的指定每个元素的打印格式的话,我们可以在主格式化字符后面跟上{子格式化字符}的方式来进一步指明具体打印格式,比如:
%d格式字符表示我们要从日志消息中取出时间这个元素,可是如果直接取出来的话,那么打印的就是时间戳,这样打印出来的格式明显不好看,因此我们需要进一步细化时间的打印格式,比如我们想按照:"某时某分某秒 "这样的格式打印时间,那么我在指定话%d格式化字符的时候就必须像这样指明:
%d{%H:%M:%S}这样:%d表示从日志器对象中取出时间元素,{}表示按照花括号里面的格式打印指定元素;
就比如现在有一个日志对象:
对于格式化字串为:[%d{%H:%M:%S}] %m%n
则该日志对象对应的格式化信息为:[22:32:54] 创建套接字失败\n
因此我们的格式化类可以按照一下方向设计:
管理的成员:
格式化字符串;
提供接口:
format();//接收外部的日志对象,然后根据格式化字符串将日志对象转换成指定字符串;
为此,格式化类的大致结构如下:
现在的问题就变成了,我们应该如何去编写这个format格式化字符函数?
首先format的功能是干什么?
就是根据格式化字符从日志对象中拿出指定元素来并且按照格式化字符的顺序来进行排列组合!
说白了也就是按照格式化字符串中格式化字符的顺序来从日志对象中拿取指定元素;
因此这里有一个思路:
1、便分析格式化字符串边从日志对象中获取元素;
具体思想就是:我们可以边分析格式化字符串,然后从日志对象中获取数据:
就比如:%d%m%c%n
我们首先分析到%d字符串,那么我们此时就从日志对象中取出时间元素来插入一个临时字符串中;
接着,我们又分析到%m字符串,那么我们此时就从日志对象中取出日志主体消息元素来插入到临时字符串中;
紧接着,我们有分析到%c字符串,那么表示我们此时需要从日志对象中取出一个日志器名称元素来插入到临时字符串中;
最后分析到%n字符串时,表示我们需要向零时字符串中插入一个’\n’;
最后我们给外部返回这个临时字符串就好了!
上面的方法可以吗?当然可以!没有任何问题!
可是似乎效率上有一点浪费,就比如如果我用同一个格式化类格式化多个日志对象,那么我们就需要每次都对格式化字符串做一次遍历分析,有点划不来,因为这个格式化字符串是一样的啊,我们每格式化一个日志对象就要对一格式化字符串做一次分析,还是同一个格式化字符串,这不就是一种浪费吗?为此我们需要想一种比较高效的格式化方法!
唉,实际上格式化的过程不就是按照某一种顺序从日志对象中拿数据然后插入到临时字符串中吗?
这个“顺序”哪里来?
不就是通过分析格式化字符串而来吗?
要是我们能够“保存”一下这个“取值顺序”,那么以后我们再格式化日志对象的时候,我们就只需要遍历这个顺序,去日志对象中取元素,不就完成了日志对象的格式化了吗?而不用每次都花大量的精力去遍历分析格式化字符串!你品,你细品,是不是很有道理?
那么具体应该怎么做呢?
在C++中万物皆可对象化,我们使用一个个对象来形成对应的取值:
比如:
%d
字符对应一个子格式化对象TimeFormatItem,该格式化对象表示从日志对象中获取时间元素!
%t
字符对应一个子格式化对象ThreadFormatItem,该格式化对象表示从日志对象中获取线程ID!
%p
字符对应一个子格式化对象LevelFormatItem,该格式化对象表示从日志对象中获取⽇志等级!
%c
字符对应一个子格式化对象NameFormatItem,该格式化对象表示从日志对象中获取⽇志器名称!
%f
字符对应一个子格式化对象CFileFormatItem,该格式化对象表示从日志对象中获取源码所在⽂件名!
%l
字符对应一个子格式化对象CLineFormatItem,该格式化对象表示从日志对象中获取源码所在⾏号!
%m
字符对应一个子格式化对象MsgFormatItem,该格式化对象表示从日志对象中获取有效⽇志数据!
%n
字符对应一个子格式化对象NLineFormatItem,该格式化对象表示需要提供一个’\n’换行!
%T
字符对应一个子格式化对象TabFormatItem,该格式化对象表示需要提供一个tab缩进!
其它非格式化字符
对应一个子格式化对象OtherFormatItem,该格式化对象表示原样输出非格式化字符;我们再来举个简单例子来理解一下我们想要干什么:
就比如现在有一个格式化字符串:[%d%m%n]
现在我们分析到:“[“非格式化字符串,我们就会形成一个OtherFormatItem 对象;
然后分析到%d:形成一个TimeFormatItem对象;
分析到%m:形成一个MsgFormatItem对象;
分析到%n:形成一个NLineFormatItem对象;
分析到非格式化字符:”]”,形成一个OtherFormatItem 对象;
然后我们利用一个容器将这些对象给保存起来,也就是将这些"顺序"保存起来,然后我们在格式化日志对象的是否只需要遍历这个容器,就能得到指定格式的字符串!
为此这个子格式化对象必须提供一个接口:
该接口具备从日志对象中获取指定元素的能力,并且能够将获取到的指定元素转换为字符串返回给外部!这样外部就能直接将得到的字符串插入进临时字符串!
同时为了更好的利用容器来管理这些子格式化类,我们可以考虑使用继承的体系来设计这些子格式化类,这样的话我们在利用容器管理这些类的时候就能利用父类指针或引用来管理这些子格式化对象了!
为此我们重新设计一下Formater类:
管理成员:
1、格式化字符串;
2、vector数组;//用来存放“取值顺序”
提供接口:
format();//用于暴露给外部提供格式化的接口;
parsePattern();//不暴露给外部,用来专门进行格式化字符串分析,并形成对应子格式化对象的接口;
createItem();//不暴露给外部,专门用来创建格式化子对象的!
父类:子格式类FormatItem
提供虚接口:
format();//功能从格式化对象中取出指定元素并格式化成字符串返回给外部!
具体从日志对象中取出什么由具体的派生对象来决定:
//format.hpp
#pragma once
/*
格式化模块:
功能:将一个日志对象格式化成一个字符串
接口设计:
1、构造时告诉格式化对象格式化字符串;
2、format接口:给这个接口传递一个日志对象,将其转换成格式化字符串
格式化接口设计:本质就是按照一定顺序从日志对象中取出对应元素来插入字符串中!
比如:%d%m%t 就是告诉我们先从日志对象中取出时间元素、再取出消息主体元素、再取出线程id元素;
要是我们能够将这个顺序保存起来,那么后面再格式化日志对象的时候,我们只需要遍历这个顺序从日志对象中取出对应元素即可!
因此格式化类的属性包含两个:
1、string :保存格式化字符串;
2、一个容器:保存日志对象的取值“顺序”!
如何得到这个顺序?
分析格式化字符串->得到一个格式化字符,创建一个子格式化对象,该子格式化对象表示从日志对象中获取特定元素;
比如:%d%m%t;
1、形成%d的子格式化对象,该格式化对象表示从日志对象中获取时间元素; A
2、形成%m的子格式化对象,该格式化对象表示从日志对象中获取主体消息元素; B
3、形成%t的子格式化对象,该格式化对象表示从日志对象中获取线程id元素; C
我们得将分析出来的ABC对象保存起来,然后后面再格式化日志对像的时候就不用再分析格式化字符串,而是直接遍历ABC三个对象按照一定顺序从日志对象中获取元素了
得用一个公共类型来接收ABC三个对象,考虑继承(利用父类指针或引用来接收这些对象)
这样顺序如何保存的问题得以解决!元素也获取到了!
但是最后我们是要的字符串啊!
因此父类提供一个虚函数,该虚函数专门从日志对象中获取指定元素,然后转换成字符串的接口!
*/
#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <sstream>
#include <time.h>
#include <unordered_set>
#include "logMes.hpp"
#include "util.hpp"
namespace MySpace
{
// 格式化类
class Formater
{
private:
// 子格式化对象的公共父类,写出来是为了方便保存"顺序",同时也方便后续使用统一接口输出...
class FormatItem
{
public:
virtual ~FormatItem() {};
// 公共接口,从日志对象中获取指定元素,并格式化成字符串
virtual std::string format(const MySpace::LogMes&) = 0;
};
// 开始派生...
//%m对应的子格式化对象
class MsgFormatItem : public FormatItem
{
public:
// 从日志对象中获取主体消息
std::string format(const MySpace::LogMes& mes) override
{
return mes._payload;
}
};
//%p对应的子格式化对象
class LevelFormatItem : public FormatItem
{
public:
// 从日志对象中获取日志等级元素,并转换为字符串
std::string format(const MySpace::LogMes& mes) override
{
return levelToString(mes._lev);
}
};
//%c对应的子格式化对象
class NameFormatItem : public FormatItem
{
public:
// 从日志对象获取日志器名称
std::string format(const MySpace::LogMes& mes) override
{
return mes._loggerName;
}
};
//%t对应的子格式化对象
class ThreadFormatItem : public FormatItem
{
public:
// 从日志对象获取线程id元素
std::string format(const MySpace::LogMes& mes) override
{
std::stringstream fstr;
fstr << mes._tid;
return fstr.str();
}
};
//%d对应的子格式化对象
class TimeFormatItem : public FormatItem
{
private:
std::string _childFmt;
public:
TimeFormatItem(const std::string& Cfmt = "%H:%M:%S") : _childFmt(Cfmt) {}
// 从日志对象获取时间元素,并按照%d字符对应的子格式转换为字符串
// eg: %d -> %H:%M:%S
std::string format(const MySpace::LogMes& mes) override
{
time_t t = mes._tm;
char buffer[1024];
struct tm tmp;
// 将时间戳现转换为时间结构体
#ifdef _WIN32
localtime_s(&tmp, &t);
#elif __linux__
localtime_r(&t,&tmp);
#endif
// 将时间结构体转换为格式化字符串
strftime(buffer, sizeof(buffer), _childFmt.c_str(), &tmp);
return buffer;
}
};
//%f对应的子格式化对象
class CFileFormatItem : public FormatItem
{
public:
// 从mes对象中获取文件名
std::string format(const MySpace::LogMes& mes) override
{
return mes._fileName;
}
};
//%l对应的子格式化对象
class CLineFormatItem : public FormatItem
{
public:
// 从mes对象中获取行号
std::string format(const MySpace::LogMes& mes) override
{
return std::to_string(mes._line);
}
};
//%T对应的子格式
class TabFormatItem : public FormatItem
{
public:
// 输出tab
std::string format(const MySpace::LogMes& mes) override
{
return " ";
}
};
//%n对应的子格式化对象
class NLineFormatItem : public FormatItem
{
public:
// 输出换行
std::string format(const MySpace::LogMes& mes) override
{
return "\n";
}
};
// 非格式化字对应的符子格式化对象(原样输出)
class OtherFormatItem : public FormatItem
{
public:
OtherFormatItem(const std::string& str) : _oldStr(str) {}
// 原样输出非格式化字符
std::string format(const MySpace::LogMes& mes) override
{
return _oldStr;
}
private:
std::string _oldStr;
};
private:
// 分析格式化字符串的工作
bool parsePattern()
{
char key = -1;
std::string val;
int pos = 0;
while (pos < _fmt.size())
{
val.clear();
while (pos < _fmt.size() && _fmt[pos] != '%')
val += _fmt[pos++];
key = -1;
_items.push_back(createItem(key, val));
//没有遇到格式化字符
if(pos>=_fmt.size())
return true;
//%后面没有字符了,错误格式化字符串
if (pos + 1 >= _fmt.size())
return false;
if (_fmt[pos + 1] == '%')
{
key = -1, val = "%";
_items.push_back(createItem(key, val));
pos += 2;
continue;
}
// 检查是不是合法格式字符
key = _fmt[pos + 1];
auto it = _fmtSet.find(key);
//%x这种错误格式化字符
if (it == _fmtSet.end())
return false;
if (pos + 2 >= _fmt.size() || _fmt[pos + 2] != '{')
{
val = "";
pos += 2;
}
else
{
int cur = pos + 3;
while (cur < _fmt.size() && _fmt[cur] != '}')
cur++;
// 没找到'}'
if (cur >= _fmt.size())
return false;
// 找到了'}'
val = std::string(_fmt.begin() + pos + 3, _fmt.begin() + cur);
pos = cur + 1;
}
_items.push_back(createItem(key, val));
}
return true;
}
// 根据格式化字符创建对应的格式化对象 %d{%H:%M:%S}可能会出现这种情况:key=%d,val=%H:%M:%S
std::shared_ptr<FormatItem> createItem(const char& key, const std::string& val)
{
switch (key)
{
case 'd':
if (val == "")
return std::make_shared<TimeFormatItem>();
else
return std::make_shared<TimeFormatItem>(val);
case 'T':
return std::make_shared<TabFormatItem>();
case 't':
return std::make_shared<ThreadFormatItem>();
case 'p':
return std::make_shared<LevelFormatItem>();
case 'c':
return std::make_shared<NameFormatItem>();
case 'f':
return std::make_shared<CFileFormatItem>();
case 'l':
return std::make_shared<CLineFormatItem>();
case 'm':
return std::make_shared<MsgFormatItem>();
case 'n':
return std::make_shared<NLineFormatItem>();
default:
return std::make_shared<OtherFormatItem>(val);
}
}
public:
Formater(const std::string& fmt = "[%d{%H:%M:%S}]%T[%t]%T[%p]%T[%c]%T[%f:%l]%T%m%n") : _fmt(fmt)
{
// 分析格式化字符串,形成"取值顺序",方便后续格式化操作
if (!parsePattern())
throw std::string("格式化字符串错误!");
}
// 将日志对象格式化的接口
std::string format(const MySpace::LogMes& mes)
{
std::string ret; // 记录格式化字符串
for (auto& e : _items)
{
ret += e->format(mes);
}
return ret;
}
static std::unordered_set<char> _fmtSet; // 用来记录格式化字符,所有formater类都要共享该集合,为避免每次都创建,故设为静态
private:
std::string _fmt; // 格式化字符串
std::vector<std::shared_ptr<FormatItem>> _items; // 用来记录取值"顺序"
};
std::unordered_set<char> Formater::_fmtSet = { 'd', 'T', 't', 'p', 'c', 'f', 'l', 'm', 'n' };
}
落地模块实现
该模块的主要任务是,完成日志信息的实际落地,也就是用户给该模块一个格式化好的日志信息,该模块直接将其落地当指定"文件";
本日志系统提供:标准输出、指定文件、滚动文件这几个落地方向
同时也支持扩展其它落地方向,比如:网络服务器、数据库
对于该模块的实现我们打算采用继承+工厂模式的方式来实现;
因此我们需要首先抽象出一个:抽象落地方向;
该抽象落地方向提供一个虚接口:outLog();用户通过该接口,来完成实际的落地;
具体实现如下:
sink.hpp
#pragma once
/*
日志落地模块
每一个类对应一个落地方向
落地方向:
1、标砖输出
2、指定文件
3、滚动文件(以大小滚动、以时间滚动...)
目前就这样...
扩展:网络服务器、数据库
设计思想:为了方便扩展
可以采用继承的设计层次
1、先抽象出一个抽象基类(该基类有一个接口专门实现具体落地的)
2、根据基类派生出具体落地方向...(如果要扩展的话,那么可以继承基类,重写落地接口)
3、采用工厂模式来创建落地方向,封装创建细节,让用户使用起来更爽!
*/
#include<iostream>
#include<string>
#include<fstream>
#include<time.h>
#include"util.hpp"
#include"sock.hpp"
namespace MySpace
{
class Sink
{
public:
virtual ~Sink() {};
//具体落地接口
virtual bool outLog(const char*,int ) = 0;
};
//落地方向是标准输出
class StdoutSink :public Sink
{
public:
bool outLog(const char*str,int len)override
{
std::cout.write(str,len);
//std::string str1(str,str+len);
//std::cout << str1 << std::endl;
bool flaf = !std::cout.fail();
return flaf;
}
};
//落地方向是一个具体文件
class FileSink :public Sink
{
private:
std::string _fileName;
std::ofstream _of;//利用文件流管理文件
public:
FileSink(const std::string& filename) :_fileName(filename) {
//先保文件所在目录存在
MySpace::util::createDirectory(MySpace::util::getDirectory(_fileName));
//追加写的方式打开文件,文件不存在则创建
_of.open(_fileName, std::ios::out | std::ios::app);
}
//析构的时候关闭文件
~FileSink() { _of.close(); }
bool outLog(const char* str, int len)override
{
_of.write(str,len);
return !_of.fail();
}
};
//落地方向是滚动文件(以大小为间隔)
class RollSinkBySize :public Sink
{
private:
std::string _baseName;//滚动文件的基本名称
std::ofstream _of;//文件流来管理一个文件
size_t _curSize;//当前文件大小
const size_t _MaxSize;//单个文件总大小
size_t _count;//创建的文件的个数
std::string createFileName()
{
std::string ret(_baseName);
ret += "-v";
ret += std::to_string(_count++);
ret += ".log";
return ret;
}
public:
enum class FileSize :size_t
{
Small_Size = 512 * 1024,//512k
Middle_Size = 1 * 1024 * 1024,//1M
Big_Size = 10 * 1024 * 1024//10M
};
RollSinkBySize(const std::string& baseName, FileSize MaxSzie = (FileSize::Small_Size))
:_baseName(baseName), _MaxSize(static_cast<size_t>(MaxSzie)), _curSize(0), _count(1)
{
//先保证文件所在路径存在
MySpace::util::createDirectory(MySpace::util::getDirectory(_baseName));
//根据基本文件名+_v[count]的方式来形成新文件名
std::string fileName = createFileName();
_of.open(fileName, std::ios::out | std::ios::app);
}
bool outLog(const char* str, int len)override
{
//该换文件了
if (_curSize >= _MaxSize)
{
std::string ret = createFileName();
_of.close();
_curSize = 0;
_of.open(ret, std::ios::out | std::ios::app);
}
_of .write (str,len);
_curSize += len;
return !_of.fail();
}
};
//落地方向:滚动文件(通过过时间)
class RollSinkByTime :public Sink
{
private:
std::string _baseName;//滚动文件的基本名称
std::ofstream _of;//文件流来管理一个文件
size_t _timeGap;//时间间隔
time_t _createTime;//文件创建时间
std::string createFileName()
{
//扩展名+xxxx-xx-xx_H:M:S.log
time_t curTime = time(nullptr);
struct tm tmp;
#ifdef _WIN32
localtime_s(&tmp, &curTime);
#elif __linux__
localtime_r(&curTime, &tmp);
#endif // _WIN32
char buffer[128];
snprintf(buffer, sizeof(buffer), "%04d%02d%02d%02d%02d%02d.log", tmp.tm_year + 1900, tmp.tm_mon + 1, tmp.tm_mday, tmp.tm_hour, tmp.tm_min, tmp.tm_sec);
std::string ret(_baseName);
ret += buffer;
return ret;
}
public:
enum class TimeGap
{
Samll_Time = 1,
Middle_Time = 3600,
Big_Time = 24 * 3600
};
RollSinkByTime(const std::string& baseName, TimeGap tm = TimeGap::Samll_Time) :_baseName(baseName), _timeGap(static_cast<size_t> (tm))
{
// 先保证文件所在路径存在
MySpace::util::createDirectory(MySpace::util::getDirectory(_baseName));
std::string fileName = createFileName();
_of.open(fileName, std::ios::out | std::ios::app);
}
bool outLog(const char*str,int len)override
{
//大于设置时间间隔
if (time(nullptr) - _createTime >= _timeGap)
{
std::string ret = createFileName();
_of.close();
_of.open(ret, std::ios::out | std::ios::app);
}
_of .write( str,len);
return !_of.fail();
}
};
//扩展1:网络服务器
//扩展细节:可以考虑使用多路复用...
//采用工厂模式,封装落地对象创建细节
class SinkFactory
{
public:
template<class T, class ...Args>
static std::shared_ptr<T> createSink(Args...args)
{
return std::make_shared<T>(args...);
}
};
}
这样的话,我们就可以利用工厂对象创建出指定落地方向,然后利用outLog接口完成指定输出:
日志器模块
该模块是针对于前几个模块的整合,也是直接面向客户所使用的,对于该模块的实现,我们基于:继承+建造者设计模式来实现;
因此我们需要抽象出一个日志器抽象基类;
该基类提供的接口如下:
1、 debug();//站在用户的角度来说就是我只需要调用该接口就能完成debug版本日志消息的输出;但是站在日志器的角度来看就是,我日志器通过该接口先收集用户的日志信息,然后根据输出等级来决定是不是要实际落地这条消息,如果当前消息高于输出等级限制,那么我们会利用该日志消息封装一个更为详细的一个日志对象,然后利用日志器的格式化对象格式化成指定字符串,最后在完成实际落地!
同理对于,info、warning、error、fatal、unknown这几个接口的实现也是一样的;
2、 该基类还需要提供一个完成实际落地的接口log();来完成实际落地,该接口根据不同的日志器采用不同的实现,因此该接口必须是虚接口;
管理成员:
格式化对象;
vector数组,用来存放各种落地方向;
日志输出限制等级;
互斥锁;保证日志器是线程安全的;
日志器名称;(用于唯一标识一个日志器)
为此。基类日志器代码如下:
同步日志器
基于抽象基类所作的工作,同步日志器所做的工作就很简单,同步日志器只需要重写log()接口完成实际落地就可以了:
同步日志器的落地方式也是比较简单的就是直接向落地方向中写入就好:
为此同步日志器的设计代码如下:
异步日志器
异步日志器呢就比较复杂一点了,为什么呢?
因为异步日志器与同步日志器不同,异步日志器不会完成日志消息的实际落地,异步日志器是将自己的日志消息落地到一个“缓冲区”中,由异步线程完成从缓冲区中取出数据来完成实际落地!
下面我们来具体聊一聊异步日志器的工作流程:
首先,我们的工作线程落地不在向实际方向落地而是向“缓冲区”中落地,由我们的异步线程来从缓冲区中取出数据来完成实际落地;
因此这是一个典型的生产者消费者模型,在这个过程中我们需要维护,生产者&生产者 生产者&消费者之间的互斥关系,因此我们需要一把锁 同时需要两个条件变量分别用来维护生产者消费者之间的同步关系,当缓冲区满了,我们需要阻塞生产者线程,让消费者线程来;当缓冲区为空了,我们需要阻塞消费者线程,让生产者来进行生产活动!
这些都没问题!可是在实际开发中,锁与锁的冲突无疑是比较严重的,因为这把锁每时每刻除了有生产者和生产者在竞争也有生产者和消费者在竞争,竞争是比较激烈的,这样的话就会导致整个异步日志器的工作效率比较低,为此为了减少锁的冲突,提高效率,我们可以采用双缓冲区的作法:
双缓冲区的工作流程是这样的,工作线程呢现在将数据全部放进生产缓冲区中,而异步线程只从消费缓冲区中拿数据,只有当消费缓冲区中的数据没了的时候我们才交换两个缓冲区,这时候我们消费者才需要去申请锁,而在一般情况下,我们生产者和消费者都不是关心的同一个缓冲区,我们自然也就不必每次都去申请锁,这样就减少了生产者和消费者之间的锁冲突,提高了整体工作效率!
当然,如果在消费缓冲区为空了同时生产缓冲区也为空了,那么就没有交换的必要了,我们就阻塞异步线程;
如果生产缓冲区被打满了,那么我们也就可以阻塞生产者,让消费者继续消费一会!
缓冲区实现
上面交代了异步日志器工作流程,最为异步日志器必不可少的一环,我们先来实现缓冲区;
该缓冲区采用面向字节流的设计思想,不需要设计成为一个个string类型的缓冲区,因为设计成string类型的话,异步线程在取的时候就只能取一个落地一个,会增加IO次数,降低IO效率;同时异步线程在取string字符串的时候,需要自己先定义一个string缓冲区在来取,这又是一次没必要拷贝!为此我们的缓冲区采用面向字节流的思想!用户可以自主决定一次性从缓冲区中读取多少字节的数据!
该缓冲区提供的功能:
write();用户只需要调用write接口就能向缓冲区中写入数据;
begin();返回可读位置的指针,注意这里并没有实现read()接口,如果实现该接口的话,那么上层在读取的时候必须先定义一个缓冲区来读取,这势必会造成一次不必要的拷贝!
AbleReadSize();可读数据长度;
AbleWriteSize();可写数据长度;
moveRead();移动读指针;
moveWrite();移动写指针;
reset();将读写指针置0;(交换缓冲区之前,需要对消费缓冲区做的动作)
swap();//交换两个缓冲区;
管理的成员:
vector
读指针
写指针
为此缓冲区具体实现如下:
//buffer.hpp
#pragma once
/*
异步缓冲区:
功能:暂时接收工作线程发送的日志信息
提供接口:
write接口:外界向缓冲区插入数据
begin();//可写位置指针
AbleWriteSize();//可写空间
AbleReadSize();//可读空间
moveRead()/moveWrite();//读写指针移动
reset();//复位
swap();//交换
成员:
vector<char>
read_index;
write_index;
*/
#include <iostream>
#include <vector>
#include <cassert>
namespace MySpace
{
#define DEFAULT_BUFFER_SIZE 10 * 1024 * 1024 // 10M
#define DEFAULT_BUFFER_THRESHOLD 80 * 1024 * 1024 // 80M//阈值
#define DEFAULT_BUFFER_INCREMENT 10 * 1024 * 1024 // 10M//增量
class Buffer
{
public:
Buffer(size_t cap = DEFAULT_BUFFER_SIZE) : _q(cap) {}
bool write(const char *str, int len)
{
// 如果可写空间不够?
//有两种作法:1、扩容;
//2、由外部直接在插入执前做插入判断,如果实际插入的大小<=AbleWriteSize()那么就直接插入即可;如果大于了,那么由外部进行阻塞!
//这里的话我们缓冲区提供的是扩容操作
//但是实际会不会扩容完全由用户控制
//如果用户做插入检查,那么就不会发生扩容
//如果用户不做插入检查,那么就会发生扩容
EableEnoughSpace(len);
// 进行拷贝
std::copy(str, str + len, _q.begin() + _write_index);
// 更新write指针
moveWrite(len);
return true;
}
const char *begin()
{
return &_q[_read_index];
}
// 可写空间
size_t AbleWriteSize()
{
return _q.size() - _write_index;
}
// 可读空间大小
size_t AbleReadSize()
{
return _write_index - _read_index;
}
void swap(Buffer &bf)
{
_q.swap(bf._q);
std::swap(_write_index, bf._write_index);
std::swap(_read_index, bf._read_index);
}
void reset()
{
_write_index = _read_index = 0;
}
void moveRead(size_t len)
{
assert(len + _read_index <= _write_index);
_read_index += len;
}
bool empty()
{
return _write_index == _read_index;
}
private:
void moveWrite(size_t len)
{
assert(_write_index + len <= _q.size());
_write_index += len;
}
void EableEnoughSpace(int len)
{
if (len > AbleWriteSize())
{
//扩容也是有讲究的
//在没到达阈值之前快速扩;
//达到阈值过后线性扩!
size_t newSize = _q.size() < DEFAULT_BUFFER_THRESHOLD ? 2 * _q.size() : _q.size() + DEFAULT_BUFFER_INCREMENT;
_q.resize(newSize);
}
}
std::vector<char> _q;
size_t _read_index = 0;
size_t _write_index = 0;
};
} // namespace MySpace
异步工作器实现
异步工作器模块实际上就是双缓冲区与异步线程的整合;
工作线程通过与异步线程交互来完成异步日志器的工作输出;
异步工作器提供的功能:
push:工作线程通过该接口向缓冲区添加数据;
stop();工作线程可以通过调用该接口来停止异步工作器的工作;
管理成员:
双缓冲区;
互斥锁;
两个条件变量;
_stop;停止标志
回调函数(也就是实际处理逻辑,消费数据怎么处理,由外部决定)
异步工作线程
具体实现代码如下:
//looper.hpp
#pragma once
#include "buffer.hpp"
/*
异步工作器模块:
功能:异步处理主线程的日志消息,完成日志消息的实际落地
提供接口:
stop();//停止异步工作器
push();//方便工作线程向异步工作器提供数据
成员:
1、双缓冲区(生&消
2、互斥锁(保护生产者&生产者 消费者&生产者)
3、双条件变量(双缓冲区都为空,则阻塞异步线程;若生产缓冲区不够了,阻塞生产者)
4、异步线程
5、停止标志
6、实际处理逻辑(具体如何实现交给外部来决定,实现解耦)
*/
#include <atomic>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
namespace MySpace
{
using func_t = std::function<void(Buffer &)>;
class AsyncLooper
{
public:
AsyncLooper(const func_t &f, bool safe = true) : _work(f), _stop(false), _t(&AsyncLooper::threadRuning, this), _safe(safe)
{
}
~AsyncLooper()
{
stop();
}
void stop()
{
_stop = true;
_conCond.notify_all();
_t.join();
}
void push(const char *str, int len)
{
if (_stop)
return;
{
std::unique_lock<std::mutex> locker(_mux);
// 唤醒条件:或者实际写入的长度小于可写入的长度
// 安全模式下执行
if (_safe)
{
_proCond.wait(locker, [&]() -> bool
{ return len <= _proBuf.AbleWriteSize(); });
}
// 非安全模式下直接插入,不考虑空间问题,少了会自动扩容
_proBuf.write(str, len);
}
// 唤醒消费者,有数据来消费了
_conCond.notify_all();
}
private:
void threadRuning()
{
while (true)
{
// 尝试交换
if (_conBuf.empty())
{
std::unique_lock<std::mutex> locker(_mux);
// 唤醒条件:生产者队列不为空或者_stop标志被置true
// 阻塞条件:生产者队列为空并且_stop置false;
_conCond.wait(locker, [&]() -> bool
{ return _proBuf.empty() == false || _stop; });
if (_stop && _proBuf.empty())
break;
// 生产者缓冲区有数据被唤醒
_conBuf.reset();
_conBuf.swap(_proBuf);
}
_proCond.notify_all();
_work(_conBuf);
}
_proCond.notify_all();
}
bool _safe = true;
Buffer _proBuf; // 生产缓冲区
Buffer _conBuf; // 消费缓冲区
std::mutex _mux;
std::condition_variable _proCond; // 生产者条件队列
std::condition_variable _conCond; // 消费者条件队列
std::atomic<bool> _stop; // 停止标志
std::thread _t; // 异步线程
func_t _work; // 实际处理逻辑
};
} // nam;espace
在该异步工作器模块中,我们提供了两种模式,安全模式和非安全模式;
处于非安全模式下的生产者线程在向缓冲区插入数据的时候不会发生扩容;
而处于安全模式下的生产者线程在向缓冲区插入数据的时候不会发生扩容,因为会被阻塞!
回归异步日志器模块
现在准备工作已经做完了,我们只需要在异步日志器中包含一个异步工作器模块就行了,之后所有工作都交给工作线程和异步工作器这两个模块来完成,因此异步工作器的log()落地函数实际是将信息落地到缓冲区!
同时异步日志器需要给异步工作器提供一个实际的工作函数来给异步工作器提供实际的工作处理:
为此异步日志器模块设计如下:
建造者模式
现在同步日志器与异步日志器都已经建立好了,但是每次使用之前都需要我们自己将日志器所需要的零件构造好,很是麻烦,为此我们可以采用兼着做模式来帮我们完成零件构造的过程;
首先抽象出一个抽象建造者:
该建造者可以帮我们建造任何零件;
同时该建造者还具有根据零件完成组装的功能,这个根据具体的建造者建有关系,因此我们需要提供一个组装接口,该接口是虚接口:
日志器管理模块实现
上面的功能已经差不多将日志系统的大厦建立起来了,可是上面建立的日志器只能在当前作用域使用,要是在其它作用域使用的话,那么就需要我们来传参完成,使用起来非常不方便!
可不可以设计一种管理器,该管理器可以将所创建出来的日志器都管理起来,同时这个管理器是全局唯一的,哪里都可以使用,我们只需要通过向该管理器告知日志器名称就能返回给我们指定日志器;答案是可以的!
该日志器管理类:
1、管理已创建的日志器
2、可以同通过该管理器在全局任意地方根据日志器名称获取日志器
3、该管理器默认提供一个标准输出的同步日志器
4、该管理器为单例对象,全局只有一个管理器对象
提供接口:
getInstance();//获取日志管理器对象
addLogger();//添加日志器
isExists();//日志器是否存在
getLogger();//获取日志器
getRootLogger();//获取默认日志器
管理成员;
unordered_map<string ,shared_ptr> _loggers;
mutex保证日志管理器对象的线程安全
默认日志器对象 (同步标输出)
该管理器类设计如下:
可是这样还不够方便,我们通过LocalLoggerBuilder建造出来的日志器需要我们自己手动添加进管理器很麻烦,为此我们可以专门设计一个建造者,该建造者会自动将建造出来的日志器添加进日志器管理对象中:
该日志器建造者具体实现如下:
日志器模块具体实现
至此日志器模块算是结束了,接下来附上整个日志器模块的代码:
//logger.hpp
#pragma once
/*
日志器模块或者说写入方式模块
日志器是直接暴露给用户的
功能:完成用户交给日志器的具体日志打印工作
设计思想:
为此站在用户的角度来说就是
我只要将日志消息传递给日志器,日志器就能帮助我们完成日志的输出
因此日志器要提供一些接口来供用户传入日志消息
而站在日志器的角度来说,我通过上诉接口获取到了用户的日志信息,日志器要做的工作就是
根据用户传进来的日志信息封装一个日志对象
然后利用格式化类对象格式化日志对象
然后再使用一个落地接口完成最终落地!
一个日志器的落地方向可能不止一种,为此我们需要一个容器来保存落地方向
日志器也需要知道如何格式化对象,为此他需要一个格式化对象
多线程情况下,有可能多个线程使用同一个日志器对同一个落地方向进行输出,会造成线程安全,需要一把锁
日治器也需要一个限制等级来维护日志器正对那些等级的日志进行实际落地,那些日志不进行输出;
日志器也需要一个名字来标识唯一日志器
如果具体细分的话,日志器分为:同步日志器、异步日志器
为此为了方便管理日志器,可以采用继承的设计层次
*/
#include "util.hpp"
#include "level.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "looper.hpp"
#include <mutex>
#include <stdarg.h>
#include <unordered_map>
namespace MySpace
{
// 先抽象出一个基类日志器
class Logger
{
public:
virtual ~Logger() {}
enum class LoggerType
{
LOGGER_SYNC, // 同步日志器
LOGGER_ASYNC // 异步日志器
};
// 提供日志器基本功能
public:
Logger(const MySpace::Formater& fmt, const std::vector<std::shared_ptr<MySpace::Sink>>& sinks, const std::string name,
MySpace::LogLevel lev = LogLevel::DEBUG, LoggerType type = LoggerType::LOGGER_SYNC)
: _fmter(fmt), _sinks(sinks), _levelLimits(lev), _mux(), _loggerName(name)
{
if (_sinks.empty())
_sinks.push_back(std::make_shared<StdoutSink>());
}
const std::string& name()
{
return _loggerName;
}
// 站在用户的角度,是我只要想给接口提供日志信息就能完成debug等级的输出
void debug(const std::string& fileName, size_t line, const char* fmat, ...)
{
// 站在日志器的角度来说,我收到了用户的日志信息,我需要将其加工成日志对象,然后格式化,然后完成实际落地
// 0、先判断是否大于>=当前限制等级
// 1、提取出日志消息主体;
// 2、构造日志对象
// 3、格式化
va_list ap;
va_start(ap, fmat);
std::string str;
// 完成0~3的工作
if (!forwardRoll(fileName, line, LogLevel::DEBUG, str, fmat, ap))
{
va_end(ap);
return;
}
va_end(ap);
// 4、实际落地
log(str);
}
void info(const std::string& fileName, size_t line, const char* fmat, ...)
{
va_list ap;
va_start(ap, fmat);
std::string str;
// 完成0~3的工作
if (!forwardRoll(fileName, line, LogLevel::INFO, str, fmat, ap))
{
va_end(ap);
return;
}
va_end(ap);
// 4、实际落地
log(str);
}
void warning(const std::string& fileName, size_t line, const char* fmat, ...)
{
va_list ap;
va_start(ap, fmat);
std::string str;
// 完成0~3的工作
if (!forwardRoll(fileName, line, LogLevel::WARNING, str, fmat, ap))
{
va_end(ap);
return;
}
va_end(ap);
// 4、实际落地
log(str);
}
void error(const std::string& fileName, size_t line, const char* fmat, ...)
{
va_list ap;
va_start(ap, fmat);
std::string str;
// 完成0~3的工作
if (!forwardRoll(fileName, line, LogLevel::Err, str, fmat, ap))
{
va_end(ap);
return;
}
va_end(ap);
// 4、实际落地
log(str);
}
void fatal(const std::string& fileName, size_t line, const char* fmat, ...)
{
va_list ap;
va_start(ap, fmat);
std::string str;
// 完成0~3的工作
if (!forwardRoll(fileName, line, LogLevel::FATAL, str, fmat, ap))
{
va_end(ap);
return;
}
va_end(ap);
// 4、实际落地
log(str);
}
void unknown(const std::string& fileName, size_t line, const char* fmat, ...)
{
va_list ap;
va_start(ap, fmat);
std::string str;
// 完成0~3的工作
if (!forwardRoll(fileName, line, LogLevel::UNKNOWN, str, fmat, ap))
{
va_end(ap);
return;
}
va_end(ap);
// 4、实际落地
log(str);
}
private:
bool forwardRoll(const std::string& fileName, size_t line, LogLevel lev, std::string& outBuffer, const char* fmat, va_list& ap)
{
if (lev < _levelLimits)
return false;
// 提取有效载荷消息
char buffer[4096];
vsnprintf(buffer, sizeof(buffer), fmat, ap);
// 构建日志对象
LogMes mes(fileName, line, lev, _loggerName, buffer);
// 格式化
outBuffer = _fmter.format(mes);
return true;
}
protected:
// 完成实际落地
// 根据不同类型日志器完成不同的落地操作
virtual void log(const std::string& str) = 0;
MySpace::Formater _fmter; // 用来记录当前日志器的格式化格式
std::vector<std::shared_ptr<MySpace::Sink>> _sinks; // 记录实际落地方向
std::mutex _mux; // 保护日志器的线程安全
MySpace::LogLevel _levelLimits; // 日志限制输出等级
std::string _loggerName; // 日志器名称
LoggerType _type;
};
// 具体派生出一个同步日志器
class SyncLogger : public Logger
{
public:
SyncLogger(const MySpace::Formater& fmt, const std::vector<std::shared_ptr<MySpace::Sink>>& sinks, const std::string name,
MySpace::LogLevel lev = LogLevel::DEBUG) : Logger(fmt, sinks, name, lev, LoggerType::LOGGER_SYNC) {}
virtual void log(const std::string& str) override
{
std::lock_guard<std::mutex> locker(_mux);
for (auto& e : _sinks)
e->outLog(str.c_str(), str.length());
}
};
// 具体派生出一个异步日器
class AsyncLogger : public Logger
{
public:
AsyncLogger(const MySpace::Formater& fmt, const std::vector<std::shared_ptr<MySpace::Sink>>& sinks, const std::string name,
MySpace::LogLevel lev = LogLevel::DEBUG, bool safe = true) : Logger(fmt, sinks, name, lev, LoggerType::LOGGER_SYNC), _looper(std::bind(&AsyncLogger::realLog, this, std::placeholders::_1), safe)
{
}
virtual void log(const std::string& str) override
{
_looper.push(str.c_str(), str.size());
}
private:
void realLog(Buffer& bf)
{
for (auto& e : _sinks)
e->outLog(bf.begin(), bf.AbleReadSize());
bf.moveRead(bf.AbleReadSize());
}
MySpace::AsyncLooper _looper;
};
// 创建日志器所需要的零件
class Builder
{
public:
virtual ~Builder() {}
void buildLoggerName(const std::string name)
{
_loggerName = name;
}
void buildLoggerType(Logger::LoggerType type = Logger::LoggerType::LOGGER_SYNC)
{
_type = type;
}
void buildFormater(const std::string& fmt = "")
{
if (fmt == "")
_fmt = Formater();
else
_fmt = Formater(fmt);
}
void buildLevelLimits(LogLevel lev = LogLevel::DEBUG)
{
_levelLimits = lev;
}
// 建造异步建造器时时所使用
void EnableSafe(bool flag)
{
_safe = flag;
}
template <class T, class... Args>
void buildSink(Args... args)
{
_sinks.push_back(SinkFactory::createSink<T>(args...));
}
// 根据零件组合一个具体对象
virtual std::shared_ptr<Logger> build() = 0;
protected:
bool _safe = true; // 异步日志器建造需要(默认安全)
std::string _loggerName;
Logger::LoggerType _type;
Formater _fmt;
MySpace::LogLevel _levelLimits;
std::vector<std::shared_ptr<MySpace::Sink>> _sinks;
};
// 局部日志器建造
class LocalLoggerBuilder : public Builder
{
public:
virtual std::shared_ptr<Logger> build() override
{
std::shared_ptr< std::vector<std::shared_ptr<MySpace::Sink>>> sp(&_sinks, [](std::vector < std::shared_ptr<MySpace::Sink>>*ptr)->void {
ptr->clear();
});
if (_type == Logger::LoggerType::LOGGER_SYNC)
return std::make_shared<SyncLogger>(_fmt, _sinks, _loggerName, _levelLimits);
else
return std::make_shared<AsyncLogger>(_fmt, _sinks, _loggerName, _levelLimits, _safe);
}
};
/*
创建一个日志器管理类:
该类提供的作用:
1、管理已创建的日志器
2、可以同通过该管理器在全局任意地方根据日志器名称获取日志器
3、该管理器默认提供一个标标准输出的日志器
4、该管理器为单例对象,全局只有一个管理器对象
提供接口:
getInstance();//获取日志管理器对象
addLogger();//添加日志器
isExists();//日志器是否存在
getLogger();//获取日志器
getRootLogger();//获取默认日志器
管理成员;
unordered_map<string ,shared_ptr<Logger>> _loggers;
mutex保证日志管理器对象的线程安全
默认日志器对象 (同步标输出)
*/
class loggerManager
{
public:
static loggerManager& getInstance()
{
static loggerManager lo;
return lo;
}
void addLogger(std::shared_ptr<Logger>& ptr)
{
if (isExists(ptr->name()))
return;
_loggers[ptr->name()] = ptr;
}
bool isExists(const std::string& name)
{
auto it = _loggers.find(name);
if (it == _loggers.end())
return false;
return true;
}
std::shared_ptr<Logger> getLogger(const std::string& name)
{
if (isExists(name))
return _loggers[name];
return nullptr;
}
std::shared_ptr<Logger> getRootLogger()
{
return _root;
}
private:
loggerManager()
{
LocalLoggerBuilder lo;
lo.buildFormater();
lo.buildLevelLimits();
lo.buildLoggerName("root");
lo.buildLoggerType();
lo.buildSink<StdoutSink>();
_root = lo.build();
std::string name("root");
_loggers[name] = _root;
}
loggerManager(const loggerManager&) = delete;
loggerManager(loggerManager&&) = delete;
std::unordered_map<std::string, std::shared_ptr<Logger>> _loggers;
std::mutex _mux;
std::shared_ptr<Logger> _root; // 默认日志器
};
// 全局日志器建造者//改日志器建造者是配合日志器管理对象来使用的
// 与局部日志器建造者的唯一区别就是多了一步将构建出来的日志器添加进日志器管理对象的步骤
// 省去了用户需要手动添加的过程
class GobalLoggerBuilder : public Builder
{
public:
virtual std::shared_ptr<Logger> build() override
{
std::shared_ptr< std::vector<std::shared_ptr<MySpace::Sink>>> sp(&_sinks, [](std::vector < std::shared_ptr<MySpace::Sink>>* ptr)->void {
ptr->clear();
});
std::shared_ptr<Logger> it(nullptr);
if (_type == Logger::LoggerType::LOGGER_SYNC)
it = std::make_shared<SyncLogger>(_fmt, _sinks, _loggerName, _levelLimits);
else
it = std::make_shared<AsyncLogger>(_fmt, _sinks, _loggerName, _levelLimits, _safe);
//_sinks.clear();
loggerManager::getInstance().addLogger(it);
return it;
}
};
}
整合模块
为了进一步优化用户的使用体验,我们将对使用接口进行进一步封装:
封装代码:
//logEncapsulation.hpp
#pragma once
#include <iostream>
#include <memory>
#include <stdio.h>
#include <unistd.h>
#include "util.hpp"
#include "level.hpp"
#include "logMes.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "sock.hpp"
#include "logger.hpp"
#include "buffer.hpp"
// #include "looper.hpp"
/*
为了更好的用户体验,我们可以在进行一层封装
1. 封装一个全局函数,专门用来获取日志器;
2. 封装一个全局函数,专门用来获取默认日志器
封装一层日志输出宏
预期:logger->debug(fmt,....); 也就是不用我们用户再自己输入__FILE__,__LINE__了
针对默认日志器单独一套输出宏
*/
namespace MySpace
{
// 获取日志器
std::shared_ptr<Logger> getLogger(const std::string &name)
{
return loggerManager::getInstance().getLogger(name);
}
// 获取默认日志器
std::shared_ptr<Logger> getRootLogger()
{
return loggerManager::getInstance().getRootLogger();
}
// 预期使用:logger->Debug(fmt,...); 实际:logger->debug(__FILE,__LINE__,fmt,...)
#define Debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define Info(fmt, ...) info(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define Warning(fmt, ...) warning(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define Error(fmt, ...) error(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define Fatal(fmt, ...) fatal(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define Unknown(fmt, ...) unknown(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
// 针对默认日志器单独一套:
// 只要使用LOGD("%s",测试);//就是利用标砖输出日志器输出
#define LOGD(fmt, ...) MySpace::getRootLogger()->debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOGI(fmt, ...) MySpace::getRootLogger()->info(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOGW(fmt, ...) MySpace::getRootLogger()->warning(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOGE(fmt, ...) MySpace::getRootLogger()->error(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOGF(fmt, ...) MySpace::getRootLogger()->fatal(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOGU(fmt, ...) MySpace::getRootLogger()->unknown(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
// 针对普通日志器也一套:
// 只要使用LOG_DEBUG("sync","%s","测试...");就是使用指定日志器进行日志输出
#define LOG_DEBUG(name, fmt, ...) MySpace::getLogger(name)->debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_INFO(name, fmt, ...) MySpace::getLogger(name)->info(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_WARN(name, fmt, ...) MySpace::getLogger(name)->warning(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_ERROR(name, fmt, ...) MySpace::getLogger(name)->error(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_FATAL(name, fmt, ...) MySpace::getLogger(name)->fatal(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define LOG_UNKNOWN(name, fmt, ...) MySpace::getLogger(name)->unknown(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
} // namespace MySpace
总的头文件
//log.hpp
#pragma once
#include <iostream>
#include <memory>
#include <stdio.h>
#ifdef __linux__
#include <unistd.h>
#endif
#include "util.hpp"
#include "level.hpp"
#include "logMes.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "sock.hpp"
#include "logger.hpp"
#include "buffer.hpp"
#include "logEncapsulation.hpp"
日志系统使用样例: