该模块主要是对前边所以模块的整合(日志等级模块,日志消息模块,日志格式化模块,日志落地模块),向外提供接口完成不同等级日志的输出。当我们需要使⽤⽇志系统打印log的时候, 只需要创建Logger对象,调⽤该对象debug、info、warn、error、fatal等⽅法输出⾃⼰想打印的⽇志即可,⽀持解析可变参数列表和输出格式, 即可以做到像使⽤printf函数⼀样打印⽇志
那么该模块需要哪些成员呢?
1.默认的日志输出等级(等级大于默认日志输出等级的日志才会输出)
2.格式化模块对象:在日志输出之前,对输出信息进行格式化处理是必须要有的
3.落地模块对象数组:输出日志时,日志输出方向是必不可少的,那么为什么我们这里是用数组呢,因为一个日志器可能会同时多个方向输出日志,例如标准输出,文件输出,滚动文件输出同时进行
4.互斥锁:由于我们的日志系统也支持异步日志器,因此可能会多线程访问,因此我们需要锁保证日志器是线程安全的
5.日志器的名字:日志器的名字是唯一标识,方便查找
在实现方面,我们主要分为同步日志器(直接对⽇志消息进⾏输出)和异步日志器(将⽇志消息放⼊缓冲区,由异步线程进⾏输出),两个日志器只是落地方式不同,因此我们可以设计一个Logger基类,该基类完成大部分接口,然后抽象出落地方式接口,派生出同步日志器和异步日志器,由它们自己实现落地方式接口
同步日志器非常简单,直接将内容按照落地方向输出即可,重难点在于异步日志器
异步日志器指的是将格式化后的日志信息放入到一个缓冲区中,由专门的线程将缓冲区中的信息按照落地方式进行输出,这样业务线程就不会因为日志输出的原因阻塞,能够有效提高运行效率。
因此我们的异步日志器主要分为两大块:1.缓冲区 2.异步工作线程处理缓冲区的信息,同时为了进行解耦,所以我们将缓冲区和异步工作线程都单独封装一个类,最后由异步日志器类进行调度实现
那么这个缓冲区应该怎么设计呢?通常情况缓冲区主要是利用队列先进先出的优点的来实现,那么我们这个队列里面要用那种数据结构呢?首先我们要明确,我们用来充当缓冲区的队列尽量不要涉及到频繁的插入和删除操作,毕竟如果写入一个数据就进行插入,读取一个数据就进行删除那效率岂不是太低了
因此我们这里就产生了方案一:环形队列,确定队列的大小然后对空间循环利用,这样就尽量减少了队列的插入和删除操作
但是我们的缓冲区可能会涉及到多线程输入和读取,此时输入线程和输入线程互斥,输入线程和读取线程互斥,读取线程和读取线程互斥,那么就产生了一个问题,我们保证线程安全需要加锁,各个线程直接都要抢这一把锁,那么锁冲突是否过于严重呢?答案是肯定的,那么怎么才能尽量减少锁的冲突?这里我们对缓冲区进行了一个双缓冲区的设计
双缓冲区如上图所示,我们创建两个缓冲区,一个任务写入缓冲区(专门用来写入信息),一个任务处理缓冲区(专门用于输出信息),当任务写入缓冲区满,任务处理缓冲区空的时候,两个缓冲区进行交换,此时输入线程和读取线程的锁冲突的频率就有效的降低了。
那么这个缓冲区类应该如何设计?我们对其成员变量的设计如下:
1. 设计一个存放字符串的缓冲区(这里我们选用vector的数据结构,如果用string,我们每条日志消息都是用'\0'结尾的,而string的大部分接口都是遇到该符号就结束了,因此这里不推荐同string来作为缓冲区)
2. 设计一个writer指针(实际上reader是整形,指向的是vector的下标),用来指向可写区域的初始位置,避免数据写入覆盖
3. 设计一个reader指针(实际上reader是整形,指向的是vector的下标),用来指向读取区域的初始位置,当reader指针和writer指向同一位置时,说明缓冲区没有可以读取的数据
而这个缓冲类主要提供以下接口:
1. 向缓冲区写入数据的接口
2. 获取可读数据地址的接口
3. 获取可读数据长度可写数据长度的接口
4. 移动读写指针的接口
5. 重置读写指针的接口(当缓冲区数据处理完毕时,缓冲区的读写指针需要重置)
6. 提供交换缓冲区的接口(直接交换空间地址)
7. 提供缓冲区的判空接口
讲完缓冲区,就来到了异步日志器的第二个核心部分异步工作线程,根据双缓冲区的设计,异步工作线程的主要功能有两个:1.将任务添加到输入缓冲区 2. 从输出缓冲区中读取任务并且处理,而这个任务处理方式则由上层决定,也就是异步日志器决定
其实由此来看,异步工作线程其实就是一个生产消费模型生产者将任务添加到输入缓冲区,消费者从输出缓冲区中读出任务。
因此,异步工作线程类需要有以下成员变量
1. 双缓冲区(输入缓冲区,输出缓冲区)
2. 互斥锁,用于保证线程安全
3. 条件变量,实现输入缓冲区和输出缓冲区的同步(当输入缓冲区为空时,输出缓冲区会进入休眠状态等待输入缓冲区的唤醒)
4. 回调函数,用于处理输出缓冲区中的任务
5. 异步工作线程类的工作线程
而该类对外提供的接口有下面两个
1. 停止异步工作线程类
2. 添加数据到输入缓冲区
看到这,大家可能在向输出缓冲区的处理呢?先别急,下面就有了,输出缓冲区的处理没必要对外开放,因此和其他一些小接口放到了private中
1. 创建线程
2. 线程入口函数:先交换缓冲区,然后对输出缓冲区数据进行处理,处理后再次交换
在完成缓冲区和异步工作线程后,我们正式在完成异步日志器类,由于上两个模块的完成,异步日志器类仅需重写log,将其数据push入异步工作线程中,然后在写一个处理函数交给异步工作线程即可。
完成代码后,我们发现创建一个logger类相当费劲,因为里面的成员变量需要我们一个一个创建
mjwlog::Formatter::ptr _fptr(new mjwlog::Formatter());
mjwlog::Sink::ptr stdout_ptr = mjwlog::SinkFactory::LogSink();
mjwlog::Sink::ptr file_ptr = mjwlog::SinkFactory::LogSink("./logfile/test.log");
// 以1m为分界线,进行滚动文件输出
mjwlog::Sink::ptr rollfile_ptr = mjwlog::SinkFactory::LogSink("./logfile/test", 1024 * 1024);
mjwlog::Sink::ptr timefile_ptr=mjwlog::SinkFactory::LogSink("./logfile/test",mjwlog::time_seg::SECOND);
std::vector Sinks={file_ptr};
std::string loggername="root";
mjwlog::Logger::ptr logger(new mjwlog::SyncLogger(mjwlog::LogLevel::level::WARN,_fptr,Sinks,loggername));
这样做太麻烦了,因此我们需要完成一个建造者模式,用于logger的创建
建造者模式用来构造日志器类,这样用户就不用一个一个创建日志器的各个模块,这样能够有效简化用户的构造流程以及日志器使用门槛。
建造者模式设计
日志器建造者模式分两个部分,日志器基类和日志器派生类
(用于构建局部日志器和全局日志器)
而日志器基类需要完成下面的三个需求:1.设置日志器类型(同步日志器,异步日志器) 。2.需要设计接口完成日志器各个模块的构造。 3.抽象一个建造接口,由子类完成日志器的建造
日志器派生类的有两个局部日志器和全局日志器,各自完成各自日志器的建造即可
对于日志的输出,理应在任何位置都可以进行,但是我们创建一个日志器后,该日志器只能在局部范围使用,为了突破这种限制,我们专门设计一个单例模式下的日志器管理器,这样以来我们就可以在任何位置通过管理器单例获取指定的日志器进行日志输出。
因此我们的日志器管理器的成员变量设置如下:
1. 设置一个默认的日志器(标准输出)
2. 在设置一个unordered_map里面存储日志器名称和对应日志器键值对
3. 由于用户可能在进程的任意部分从unordered_map中获取日志器,因此需要对其进行加锁
根据需要我们提供下面几个接口:
1. 获取单例对象
2. 向unordered_map中添加日志器
3. 获取指定名称的日志器
4. 获取默认日志器
同时在单例日志器管理器的前提下,我们对于日志建造者类进行继承,继承出一个全局建造者类,该建造者类在建造完日志器后,会直接将该日志器添加到日志器管理器中进行管理,这样以来我们能够在任何地方创建对应日志器并且突破作用域的限制。
日志器头文件
#ifndef _M_LOGGER_H_
#define _M_LOGGER_H_
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "format.hpp"
#include "level.hpp"
#include "message.hpp"
#include "sink.hpp"
#include "buffer.hpp"
#include "athread.hpp"
#include
#include
#include
#include
#include
#include
namespace mjwlog
{
// 日志器基类
class Logger
{
public:
using ptr = std::shared_ptr;
Logger(const LogLevel::level &DefaultLevel,
Formatter::ptr &Format,
std::vector &Sinks,
std::string &LoggerName)
: _DefaultLevel(DefaultLevel),
_Format(Format),
_Sinks(Sinks.begin(), Sinks.end()),
_LoggerName(LoggerName)
{
}
//获取日志器名称,日志器管理器会用到
const std::string& GetLoggername()
{
return _LoggerName;
}
void Debug(const std::string &filename, const size_t &line, const std::string &fmt, ...)
{
// 判断是否需要输出,然后构造日志消息对象,将对象进行格式化,然后log输出
// 1.判断是否大于_DefaultLevel
if (LogLevel::level::DEBUG < _DefaultLevel)
return;
// 2.获取日志信息主体
va_list vl;
va_start(vl, fmt);
char *res;
size_t ret = vasprintf(&res, fmt.c_str(), vl);
if (ret == -1)
{
std::cout << "vasprintf fail!" << std::endl;
return;
}
va_end(vl);
// 3.构建日志信息对象
message msg(line, LogLevel::level::DEBUG, filename, _LoggerName, res);
// 4.用格式化对象对日志信息进行格式化
std::stringstream sti;
{
std::unique_lock lock(_mutex); //_访问_Sinks成员变量,需要加锁保证线程安全
_Format->format(sti, msg);
}
// 5.调用log进行输出
Log(sti.str().c_str(), sti.str().size());
// 调用vasprintf后,系统会在堆上面开辟空间,并且使res指向该空间,因此到最后需要释放
free(res);
}
void Info(const std::string &filename, const size_t &line, const std::string &fmt, ...)
{
if (LogLevel::level::INFO < _DefaultLevel)
return;
va_list vl;
va_start(vl, fmt);
char *res;
size_t ret = vasprintf(&res, fmt.c_str(), vl);
if (ret == -1)
{
std::cout << "vasprintf fail!" << std::endl;
return;
}
va_end(vl);
message msg(line, LogLevel::level::INFO, filename, _LoggerName, res);
std::stringstream sti;
{
std::unique_lock lock(_mutex); //_访问_Sinks成员变量,需要加锁保证线程安全
_Format->format(sti, msg);
}
Log(sti.str().c_str(), sti.str().size());
free(res);
}
void Warn(const std::string &filename, const size_t &line, const std::string &fmt, ...)
{
if (LogLevel::level::WARN < _DefaultLevel)
return;
va_list vl;
va_start(vl, fmt);
char *res;
size_t ret = vasprintf(&res, fmt.c_str(), vl);
if (ret == -1)
{
std::cout << "vasprintf fail!" << std::endl;
return;
}
va_end(vl);
message msg(line, LogLevel::level::WARN, filename, _LoggerName, res);
std::stringstream sti;
{
std::unique_lock lock(_mutex); //_访问_Sinks成员变量,需要加锁保证线程安全
_Format->format(sti, msg);
}
Log(sti.str().c_str(), sti.str().size());
free(res);
}
void Error(const std::string &filename, const size_t &line, const std::string &fmt, ...)
{
if (LogLevel::level::ERROR < _DefaultLevel)
return;
va_list vl;
va_start(vl, fmt);
char *res;
size_t ret = vasprintf(&res, fmt.c_str(), vl);
if (ret == -1)
{
std::cout << "vasprintf fail!" << std::endl;
return;
}
va_end(vl);
message msg(line, LogLevel::level::ERROR, filename, _LoggerName, res);
/* std::stringstream sti;
_Format->format(sti, msg); */
std::stringstream sti;
{
std::unique_lock lock(_mutex); //_访问_Sinks成员变量,需要加锁保证线程安全
_Format->format(sti, msg);
}
Log(sti.str().c_str(), sti.str().size());
free(res);
}
void Fatal(const std::string &filename, const size_t &line, const std::string &fmt, ...)
{
if (LogLevel::level::FATAL < _DefaultLevel)
return;
va_list vl;
va_start(vl, fmt);
char *res;
size_t ret = vasprintf(&res, fmt.c_str(), vl);
if (ret == -1)
{
std::cout << "vasprintf fail!" << std::endl;
return;
}
va_end(vl);
message msg(line, LogLevel::level::FATAL, filename, _LoggerName, res);
/* std::stringstream sti;
_Format->format(sti, msg); */
std::stringstream sti;
{
std::unique_lock lock(_mutex); //_访问_Sinks成员变量,需要加锁保证线程安全
_Format->format(sti, msg);
}
Log(sti.str().c_str(), sti.str().size());
free(res);
}
protected:
// 落地方向抽象接口
virtual void Log(const char *data, size_t len) = 0;
protected:
std::atomic _DefaultLevel; // 默认输出等级,因为level本质为int型,因此可以用atomic构建原子类,保证线程安全
Formatter::ptr _Format; // 日志格式化对象
std::vector _Sinks; // 日志落地模块对象数组
std::mutex _mutex;
std::string _LoggerName;
};
// 同步日志器
class SyncLogger : public Logger
{
public:
SyncLogger(const LogLevel::level &DefaultLevel,
Formatter::ptr &Format,
std::vector &Sinks,
std::string &LoggerName)
: Logger(DefaultLevel, Format, Sinks, LoggerName)
{}
protected:
void Log(const char *data, size_t len)
{
std::unique_lock lock(_mutex); //_访问_Sinks成员变量,需要加锁保证线程安全
if (_Sinks.empty())
return;
for (auto &sink : _Sinks)
{
sink->log(data, len);
}
}
};
//异步日志器
class AsyncLogger : public Logger
{
public:
AsyncLogger(const LogLevel::level &DefaultLevel,
Formatter::ptr &Format,
std::vector &Sinks,
std::string &LoggerName,
mjwlog::BufferMode buffermode)
: Logger(DefaultLevel, Format, Sinks, LoggerName),
_athread(std::make_shared(std::bind(&AsyncLogger::task_deal,this,std::placeholders::_1),buffermode))
{}
protected:
void task_deal(Buffer con_buffer)
{
std::unique_lock lock(_mutex); //_访问_Sinks成员变量,需要加锁保证线程安全
if (_Sinks.empty())
return;
for (auto &sink : _Sinks)
{
sink->log(con_buffer.AbleReadData(), con_buffer.AbleReadSize());
}
}
void Log(const char *data, size_t len)
{
_athread->push(data,len);
}
private:
Athread::ptr _athread;
};
//同步日志器,异步日志器枚举
enum class type
{
SyncLogger=0,
AsyncLogger
};
//建造者基类
class LoggerBuild
{
public:
using ptr=std::shared_ptr;
//type和DefaultLevel以及buffermode给个默认值
LoggerBuild()
:_LoggerType(type::AsyncLogger),
_DefaultLevel(LogLevel::level::DEBUG),
_buffermode(BufferMode::SAFE)
{}
void BuildDefaultLevel(const LogLevel::level& DefaultLevel)
{
_DefaultLevel=DefaultLevel;
}
void BuildFormat(const std::string& pattern)
{
_Format=std::make_shared(pattern);
}
void BuileUnsafeBuffer()
{
_buffermode=BufferMode::UNSAFE;
}
template
void BuildSink(Args &&...args)
{
Sink::ptr pt=std::make_shared(std::forward(args)...);;
_Sinks.push_back(pt);
}
void BuildLoggerName(const std::string& LoggerName)
{
_LoggerName=LoggerName;
}
void BuildLoggerType(const type& LoggerType)
{
_LoggerType=LoggerType;
}
virtual Logger::ptr build()=0;
protected:
BufferMode _buffermode;
type _LoggerType;
LogLevel::level _DefaultLevel;
Formatter::ptr _Format;
std::vector _Sinks;
std::string _LoggerName;
};
//局部日志器派生类
class LocalLoggerBuild:public LoggerBuild
{
public:
Logger::ptr build() override
{
//1.日志器名字不能为空
assert(!_LoggerName.empty());
//2._Format如果为空,我们就用格式化输出模块的默认参数构建
if(!_Format)
{
_Format=std::make_shared();
}
//3._Sinks为空,我们就默认加入一个标准输出的落地方式
if(_Sinks.empty())
{
Sink::ptr pt=std::make_shared();
_Sinks.push_back(pt);
}
//分同步日志器和异步日志期进行构造
if(_LoggerType==type::AsyncLogger)
{
std::make_shared(_DefaultLevel,_Format,_Sinks,_LoggerName,_buffermode);
}
return std::make_shared(_DefaultLevel,_Format,_Sinks,_LoggerName);
}
};
class LoggerManage
{
public:
static LoggerManage& GetInstance()
{
static LoggerManage _eton;
return _eton;
}
//添加日志器
bool AddLogger(Logger::ptr& logger)
{
//加锁
std::unique_lock ul;
//判断日志器是否存在
if(FindLogger(logger->GetLoggername()))
{
return false;
}
_logger_manage.insert(std::make_pair(logger->GetLoggername(),logger));
return true;
}
//获取指定日志器
Logger::ptr GetLogger(const std::string& loggername)
{
std::unique_lock ul;
if(FindLogger(loggername)) return _logger_manage[loggername];
return Logger::ptr();
}
//查找日志器是否存在
bool FindLogger(const std::string& loggername)
{
std::unique_lock ul;
auto it=_logger_manage.find(loggername);
if(it==_logger_manage.end()) return false;
return true;
}
//获取默认日志器
Logger::ptr GetDeafultLogger()
{
std::unique_lock ul;
return _deafult_logger;
}
private:
LoggerManage()
{
LoggerBuild::ptr build(std::make_shared());
build->BuildLoggerName("root");
_deafult_logger=build->build();
_logger_manage[_deafult_logger->GetLoggername()]=_deafult_logger;
}
private:
std::unordered_map _logger_manage;
std::mutex _mutex;
Logger::ptr _deafult_logger;
};
//全局日志器派生类
class GlobalLoggerBuild:public LoggerBuild
{
public:
Logger::ptr build() override
{
//1.日志器名字不能为空
assert(!_LoggerName.empty());
//2._Format如果为空,我们就用格式化输出模块的默认参数构建
if(!_Format)
{
_Format=std::make_shared();
}
//3._Sinks为空,我们就默认加入一个标准输出的落地方式
if(_Sinks.empty())
{
Sink::ptr pt=std::make_shared();
_Sinks.push_back(pt);
}
Logger::ptr logger;
//分同步日志器和异步日志期进行构造
if(_LoggerType==type::AsyncLogger)
{
logger=std::make_shared(_DefaultLevel,_Format,_Sinks,_LoggerName,_buffermode);
}
else
{
logger=std::make_shared(_DefaultLevel,_Format,_Sinks,_LoggerName);
}
LoggerManage::GetInstance().AddLogger(logger);
return logger;
}
};
}
#endif
缓存区头文件
#ifndef _M_BUFFER_H_
#define _M_BUFFER_H_
#include
#include
#include
namespace mjwlog
{
// 异步双缓冲区
class Buffer
{
public:
#define _Default_Buffer_Size (2 * 1024 * 1024) // 缓冲区默认大小
#define _Threshold_Buffer_Size (10 * 1024 * 1024) // 缓冲区阈值
#define _Growth_Buffer_Size (2 * 1024 * 1024) // 超过阈值后线性增长
Buffer()
: _buffer(_Default_Buffer_Size),
_writer(0),
_reader(0)
{
}
// 写入数据
void push(const char *data, size_t len)
{
// 1.判断是否需要扩容
Expansion(len);
// 2.copy数据到buffer
std::copy(data, data + len, _buffer.begin() + _writer);
// 3.移动writer指针
MoveWriter(len);
}
// 获取可读数据地址
const char *AbleReadData()
{
return &_buffer[_reader];
}
// 获取可读数据长度
size_t AbleReadSize()
{
return _writer - _reader;
}
// 获取可写数据长度
size_t AbleWriteSize()
{
return _buffer.size() - _writer;
}
// 移动读写指针
void MoveReader(size_t len)
{
assert((_reader + len) <= _writer);
_reader += len;
}
// 重置读写指针
void ResetPos()
{
_writer = 0;
_reader = 0;
}
// 缓冲区交换接口
void BufferSwap(Buffer &buffer)
{
_buffer.swap(buffer._buffer);
std::swap(_writer, buffer._writer);
std::swap(_reader, buffer._reader);
}
// 判断缓冲区是否为空
bool Empty()
{
return _writer == _reader;
}
private:
void MoveWriter(size_t len)
{
assert((_writer + len) <= _buffer.size());
_writer += len;
}
void Expansion(size_t len)
{
// 这里我们采用循环扩容,因此有可能写入信息很长,扩容一次空间也不够
while (AbleWriteSize() < len)
{
// 扩容策略:当我们buffer的容量小于阈值,双倍扩容;大于时,则线性扩容
if (_buffer.size() < _Threshold_Buffer_Size)
{
_buffer.resize(_buffer.size() * 2);
}
else
{
_buffer.resize(_buffer.size() + _Growth_Buffer_Size);
}
}
}
private:
std::vector _buffer; // 缓冲区
size_t _writer; // 写入指针
size_t _reader; // 读取指针
};
}
#endif
异步工作线程类头文件
#ifndef _M_ASYNT_H_
#define _M_ASYNT_H_
#include "buffer.hpp"
#include
#include
#include
#include
namespace mjwlog
{
// 枚举类,用来选择安全模式,和极限模式
enum class BufferMode
{
SAFE = 1, // 安全模式,缓冲区固定大小,缓冲区满了阻塞
UNSAFE // 非安全模式,主要用于测试日志系统的工作效率
// 缓冲区可以无限扩容,不过可能因为资源耗尽导致关闭
};
using func_t = std::function;
class Athread
{
public:
using ptr = std::shared_ptr;
Athread(func_t deal_task, BufferMode buffermode)
: _buffermode(buffermode),
_stop(false),
_deal_task(deal_task),
_thread(std::thread(&Athread::threadEntry, this))
{
}
~Athread()
{
Stop();
_thread.join();
}
void Stop()
{
_stop = true;
_con_cond.notify_all(); // 唤醒threadEntry进行业务处理
}
void push(const char *data, size_t len)
{
// 1.加锁
std::unique_lock ul(_mutex);
// 2.判断输入缓冲区空间是否足够
// 2.1 安全模式需要条件变量等待,判断空间是否足够
// 2.2 极限模式无需判断直接写入即可
if (_buffermode == BufferMode::SAFE)
{
_pro_cond.wait(ul, [&](){ return _pro_buffer.AbleWriteSize() >= len; });
}
// 3.缓冲区写入数据
_pro_buffer.push(data, len);
// 4.唤醒工作线程
_con_cond.notify_one();
}
private:
void threadEntry() // 线程入口函数,用于处理输出缓冲区
{
while (1)
{
// 1.交换缓冲区
// 局部作用域用于交换缓冲区,该局部作用域目的是,交换完出作用域,锁就自动归还
{
std::unique_lock ul(_mutex);
_con_cond.wait(ul, [&](){ return _stop || !_pro_buffer.Empty(); });
//当业务线程处于停止状态,并且输出缓冲区清空后则break,不然输出缓冲区可能会有数据没有输出就退出了
if(_stop&&_con_buffer.Empty())
{
break;
}
// 交换缓冲区
_con_buffer.BufferSwap(_pro_buffer);
//安全模式下,需要唤醒输入缓冲区
if(_buffermode==BufferMode::SAFE)
{
_pro_cond.notify_all();
}
}
// 2.任务处理函数处理输入缓冲区的信息
_deal_task(_con_buffer);
// 3.重置_con_buffer的读写指针
_con_buffer.ResetPos();
}
}
private:
BufferMode _buffermode;
bool _stop;
Buffer _pro_buffer; // 输出缓冲区(生产)
Buffer _con_buffer; // 输入缓冲区(消费)
std::mutex _mutex;
std::condition_variable _pro_cond; // 生产的条件变量
std::condition_variable _con_cond; // 消费的条件变量
func_t _deal_task; // 任务处理
std::thread _thread;
};
}
#endif