本期我们接着上一篇的日志内容,接着实现日志的其他模块
相关代码上传至作者个人gitee:同步_异步日志: 本项⽬主要实现⼀个⽇志系统, 其主要⽀持以下功能: 1、⽀持多级别⽇志消息 2、⽀持同步⽇志和异步⽇志 3、⽀持可靠写⼊⽇志到控制台、⽂件以及滚动⽂件中 4、⽀持多线程程序并发写⽇志 5、⽀持扩展不同的⽇志落地⽬标地
目录
日志落地模块
实现目标
功能:将格式化完成后的日志消息字符串,输出到指定的位置
扩展:支持同时将日志落地到不同的位置
位置分类:
-
标准输出
-
指定文件(事后进行日志分析)
-
滚动文件(文件按照时间/大小进行滚动切换)
扩展:支持落地方向的扩展:
用户可以自己编写一个新的落地模块,将日志进行其他方向的落地
实现思想
-
抽象出落地模块类
-
不同落地方向从基类进行派生
-
使用工厂模式进行创建与表示的分离
日志落地实现
Sink.hpp
cpp
/*
日志落地模块的实现
1、抽象落地基类
2、派生子类(根据不同的落地方向进行派生)
3、使用工厂模式进行创建与表示的分离
*/
#pragma once
#include"util.hpp"
#include <string>
#include <cassert>
#include <memory>
#include <iostream>
#include <fstream>
namespace Logger
{
class LogSink
{
public:
using SPtr = std::shared_ptr<LogSink>;
LogSink() = default;
virtual ~LogSink() = default;
virtual void log(const std::string& message) = 0; // 纯虚函数,要求派生类实现
};
//标准输出落地
class StdoutSink : public LogSink
{
public:
void log(const std::string& message) override;
};
//指定文件落地
class FileSink : public LogSink
{
public:
FileSink(const std::string& filename);
void log(const std::string& message) override;
private:
std::string filename_;
std::ofstream file_;
};
//滚动文件落地
class RollingFileSink : public LogSink
{
public:
RollingFileSink(const std::string& basefilename, size_t maxfilesize);
//输入前检查文件大小,超过就切换文件
void log(const std::string& message) override;
private:
std::string CreateNewFile();//进行大小判断,创建新文件
private:
//基础文件名+扩展文件名=当前文件名
//扩展文件名以时间生成
//滚动文件的命名规则:基础文件名_时间戳.log
std::string basefilename_;//基础文件名
std::ofstream file_;
size_t maxfilesize_;//最大文件大小,超过就要切换文件
size_t currentfilesize_;//当前文件大小
};
}
Sink.cpp
cpp
#include"sink.hpp"
namespace Logger
{
void StdoutSink::log(const std::string& message)
{
std::cout << message << std::endl;
}
FileSink::FileSink(const std::string& filename) : filename_(filename)
{
//创建日志所在的目录
util::File::CreateDirectory(util::File::GetPath(filename_));
//创建并打开日志文件
file_.open(filename_, std::ios::binary|std::ios::app);
assert(file_.is_open());
}
void FileSink::log(const std::string& message)
{
file_.write(message.c_str(), message.size());
assert(file_.good());
file_.flush();
}
RollingFileSink::RollingFileSink(const std::string& basefilename, size_t maxfilesize)
: basefilename_(basefilename), maxfilesize_(maxfilesize), currentfilesize_(0)
{
//创建日志所在的目录
util::File::CreateDirectory(util::File::GetPath(basefilename));
//创建并打开日志文件
file_.open(basefilename_, std::ios::binary|std::ios::app);
assert(file_.is_open());
}
void RollingFileSink::log(const std::string& message)
{
if(currentfilesize_>=maxfilesize_)
{
file_.close();
std::string newfilename = CreateNewFile();
currentfilesize_=0;
}
//检查当前文件大小,超过就切换文件
if (currentfilesize_ + message.size() > maxfilesize_)
{
file_.close();
std::string newfilename = CreateNewFile();
currentfilesize_ = 0;
}
file_.write(message.c_str(), message.size());
assert(file_.good());
file_.flush();
currentfilesize_ += message.size();
}
std::string RollingFileSink::CreateNewFile()
{
//获取当前时间
std::string now = util::Date::NowAsString("%Y年%m月%d日 %H时%M分%S秒");
//生成新文件名
std::string newfilename = basefilename_ + "_" + now;
//打开新文件
file_.open(newfilename, std::ios::binary|std::ios::app);
assert(file_.is_open());
return newfilename;
}
}
日志工厂类实现
cpp
template<typename SinkType, typename... Args>
class SinkFactory
{
public:
static LogSink::SPtr Create(Args&&... args)
{
return std::make_shared<SinkType>(std::forward<Args>(args)...);
}
};
拓展
sink.hpp
cpp
/*
扩展一个以时间作为入职文件滚动切换类型的日志落地模块
1. 以时间进行文件滚动,实际上是以时间段进行滚动
实现思想:以当前系统时间,取模时间段大小,可以得到当前时间段是第几个时间段
time(nullptr) % 60 == 10 当前就是第n个60s
*/
// 代码片段
enum class TimeGap
{
GapHour ,
GapMinute ,
GapSecond ,
GapDay ,
};
class TimeRollingFileSink : public LogSink
{
public:
TimeRollingFileSink(const std::string& basefilename, TimeGap gaptype);
void log(const std::string& message) override;
private:
std::string CreateNewFile();
private:
std::string basefilename_;
std::ofstream file_;
size_t gap_size_;//时间段大小,单位秒
size_t cur_gap_;//第几个时间片
};
sink.cpp
cpp
TimeRollingFileSink::TimeRollingFileSink(const std::string& basefilename, TimeGap gaptype)
: basefilename_(basefilename)
{
switch (gaptype)
{
case TimeGap::GapHour:
gap_size_ = 3600;
break;
case TimeGap::GapMinute:
gap_size_ = 60;
break;
case TimeGap::GapSecond:
gap_size_ = 1;
break;
case TimeGap::GapDay:
gap_size_ = 3600*24;
default:
break;
}
cur_gap_ = util::Date::NowAsTimeT() / gap_size_;//获取当前为第几个时间片
std::string newfilename = CreateNewFile();
util::File::CreateDirectory(util::File::GetPath(newfilename));
file_.open(newfilename, std::ios::binary|std::ios::app);
assert(file_.is_open());
}
void TimeRollingFileSink::log(const std::string& message)
{
size_t now_gap = util::Date::NowAsTimeT() / gap_size_;
if (now_gap != cur_gap_)
{
file_.close();
std::string newfilename = CreateNewFile();
cur_gap_ = now_gap;
}
file_.write(message.c_str(), message.size());
assert(file_.good());
file_.flush();
}
std::string TimeRollingFileSink::CreateNewFile()
{
//获取当前时间
std::string now = util::Date::NowAsString("%Y年%m月%d日 %H时%M分%S秒");
//生成新文件名
std::string newfilename = basefilename_ + "_" + now;
//打开新文件
file_.open(newfilename, std::ios::binary|std::ios::app);
assert(file_.is_open());
return newfilename;
}
日志器模块
功能:对前边所有模块进行整合,向外提供接口完成不同等级日志的输出
管理的成员:
1.格式化模块对象
2.落地模块对象数组(一个日志器可能会向多个位置进行日志输出)
3.默认的日志输出限制等级(大于等于限制等级的日志才能输出)
4.互斥锁(保证日志输出过程是线程安全的,不会出现交叉日志)
5.日志器名称(日志器的唯一标识,以便于查找)
提供的操作:
debug等级日志的输出操作(分别会封装日志消息LogMsg--各个接口日志等级不同)
info等级日志的输出操作I
warn等级日志的输出操作
error等级日志的输出操作
fatal等级日志的输出操作
当前日志系统支持同步日志 & 异步日志两种模式,两个不同的日志器唯一不同的地方在于他们在日志的落地方向上有所不同:
同步日志器:直接对日志消息进行输出。
异步日志器:将日志消息放入缓冲区,由异步线程进行输出。
因此日志器类在设计的时候先设计出一个Logger基类,在Logger基类的基础上,继承出SyncLogger同步日志器和AsyncLogger异步日志器。
因为日志器模块是对前边多个模块的整合,想要创建一个日志器,需要设置日志器名称,设置日志输出等级,设置日志器类型,设置日志输出格式,设置落地方向,且落地方向有可能存在多个,整个日志器的创建过程较为复杂,为了保持良好的代码风格,编写出优雅的代码,因此日志器的创建这里采用了构建器模式来进行创建。
实现思路:
-
抽象Logger基类(派生出同步日志器类 & 异步日志器类)
-
因为两种不同的日志器,只有落地方式不同,因此将落地操作给抽象出来
不同的日志器调用各自的落地操作进行日志落地
模块关联中使用基类指针对子类日志器对象进行日志管理和操作
日志器框架
cpp
/*
日志器模块:
1. 抽象日志器基类
2. 派生出不同的子类(同步日志器类 & 异步日志器类)
*/
#pragma once
#include "sink.hpp"
#include "util.hpp"
#include "Level.hpp"
#include"format.hpp"
#include<mutex>
#include<atomic>
#include<cstdarg>
namespace Logger
{
class Logger
{
public:
using Ptr=std::shared_ptr<Logger>;
Logger(const std::string& logger_name,
LogLevel::Value level,
Formatter::SPtr&formatter,
const std::vector<LogSink::SPtr>& sinks);
//完成日志消息格式化,得到格式化后的消息,随后进行落地输出
void Debug(const std::string&file,size_t line,const std::string& fmt,...);
void Info(const std::string&file,size_t line,const std::string& fmt,...);
void Warn(const std::string&file,size_t line,const std::string& fmt,...);
void Error(const std::string&file,size_t line,const std::string& fmt,...);
void Fatal(const std::string&file,size_t line,const std::string& fmt,...);
protected:
//对日志消息进行格式化
void serialize(LogLevel::Value level,const std::string&file,size_t line,char*msg);
//抽象接口完成实际的落地输出,由派生类实现
virtual void Log(const char*msg,size_t len) = 0;
protected:
std::mutex mutex_;//保证多线程安全
std::atomic<LogLevel::Value> level_;//保证多线程原子访问
std::string logger_name_;
std::vector<LogSink::SPtr> sinks_;
Formatter::SPtr formatter_;
};
//同步日志器类,直接在调用线程中完成日志落地输出
class SyncLogger : public Logger
{
public:
SyncLogger(const std::string& logger_name,
LogLevel::Value level,
Formatter::SPtr&formatter,
const std::vector<LogSink::SPtr>& sinks);
protected:
void Log(const char*msg,size_t len) override;
};
//异步日志器类,使用一个独立的线程来完成日志落地输出
class AsyncLogger : public Logger
{
public:
AsyncLogger(const std::string& logger_name,
LogLevel::Value level,
Formatter::SPtr&formatter,
const std::vector<LogSink::SPtr>& sinks);
protected:
void Log(const char*msg,size_t len) override;
};
}
基类实现
基类的实现是这样的
cpp
Logger::Logger(const std::string& logger_name,
LogLevel::Value level,
Formatter::SPtr&formatter,
const std::vector<LogSink::SPtr>& sinks)
: logger_name_(logger_name), level_(level),
formatter_(formatter), sinks_(sinks.begin(), sinks.end())
{
}
//完成日志消息格式化,得到格式化后的消息,随后进行落地输出
void Logger::Debug(const std::string&file,size_t line,const std::string& fmt,...)
{
//通过传入的参数构造出一个日志消息对象,进行日志的格式化,最终落地
//1. 判断当前的日志是否达到了输出等级
if (LogLevel::Value::DEBUG < 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 failed!!\n";
return;
}
va_end(ap); //将ap指针置空
//序列化日志消息格式
serialize(LogLevel::Value::DEBUG,file,line,res);
free(res); //释放vasprintf分配的内存
}
void Logger::Info(const std::string&file,size_t line,const std::string& fmt,...)
{
//通过传入的参数构造出一个日志消息对象,进行日志的格式化,最终落地
//1. 判断当前的日志是否达到了输出等级
if (LogLevel::Value::INFO < 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 failed!!\n";
return;
}
va_end(ap); //将ap指针置空
//序列化日志消息格式
serialize(LogLevel::Value::INFO,file,line,res);
free(res); //释放vasprintf分配的内存
}
void Logger::Warn(const std::string&file,size_t line,const std::string& fmt,...)
{
//通过传入的参数构造出一个日志消息对象,进行日志的格式化,最终落地
//1. 判断当前的日志是否达到了输出等级
if (LogLevel::Value::WARNING < 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 failed!!\n";
return;
}
va_end(ap); //将ap指针置空
//序列化日志消息格式
serialize(LogLevel::Value::WARNING,file,line,res);
free(res); //释放vasprintf分配的内存
}
void Logger::Error(const std::string&file,size_t line,const std::string& fmt,...)
{
//通过传入的参数构造出一个日志消息对象,进行日志的格式化,最终落地
//1. 判断当前的日志是否达到了输出等级
if (LogLevel::Value::ERROR < 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 failed!!\n";
return;
}
va_end(ap); //将ap指针置空
//序列化日志消息格式
serialize(LogLevel::Value::INFO,file,line,res);
free(res); //释放vasprintf分配的内存
}
void Logger::Fatal(const std::string&file,size_t line,const std::string& fmt,...)
{
//通过传入的参数构造出一个日志消息对象,进行日志的格式化,最终落地
//1. 判断当前的日志是否达到了输出等级
if (LogLevel::Value::FATAL < 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 failed!!\n";
return;
}
va_end(ap); //将ap指针置空
//序列化日志消息格式
serialize(LogLevel::Value::FATAL,file,line,res);
free(res); //释放vasprintf分配的内存
}
void Logger::serialize(LogLevel::Value level,const std::string&file,size_t line,char*str)
{
//对日志消息进行格式化,得到格式化后的消息
//构造LogMsg对象
Message msg(level, line, file, str, logger_name_);
//通过格式化工具对LogMsg进行格式化,得到格式化后的日志字符串
std::stringstream ss;
formatter_->Format(ss, msg);
std::string formatted_msg = ss.str();
//进行日志落地
Log(ss.str().c_str(),ss.str().size());
}
同步日志
Logger.hpp
cpp
//同步日志器类,直接在调用线程中完成日志落地输出
class SyncLogger : public Logger
{
public:
SyncLogger(const std::string& logger_name,
LogLevel::Value level,
Formatter::SPtr&formatter,
const std::vector<LogSink::SPtr>& sinks);
protected:
void Log(std::string msg) override;
};
Logger.cpp
cpp
SyncLogger::SyncLogger(const std::string& logger_name,
LogLevel::Value level,
Formatter::SPtr&formatter,
const std::vector<LogSink::SPtr>& sinks)
: Logger(logger_name, level, formatter, sinks) {}
void SyncLogger::Log(std::string msg)
{
//直接在调用线程中完成日志落地输出
std::lock_guard<std::mutex> lock(mutex_);
for (const auto& sink : sinks_)
{
sink->log(msg);
}
}
日志器建造者类
Logger.hpp
cpp
class LoggerBuilder
{
public:
LoggerBuilder();
LoggerBuilder& BuildLoggerType(LoggerType type);
LoggerBuilder& BuildLoggerName(const std::string& logger_name);
LoggerBuilder& BuildLogLevel(LogLevel::Value level);
LoggerBuilder& BuildFormatter(const std::string& pattern);
template<typename T,typename... Args>
LoggerBuilder& BuildSinks(Args&&... args)
{
LogSink::SPtr psink = SinkFactory<T, Args...>::Create(std::forward<Args>(args)...);
sinks_.push_back(psink);
return *this;
}
virtual Logger::Ptr build()=0;//建造日志器的接口,由派生类实现
protected:
LoggerType type_;
std::string logger_name_;
std::atomic<LogLevel::Value> level_;
Formatter::SPtr formatter_;
std::vector<LogSink::SPtr> sinks_;
};
/* 2. 派生出具体的建造者类---局部日志器的建造者 & 全局的日志器建造者(后边添加了全局单例管理器之后,
将日志器添加全局管理器中,用户就不需要关心日志器的生命周期了,直接通过全局管理器获取日志器实例即可)*/
class LocalLoggerBuilder : public LoggerBuilder
{
public:
Logger::Ptr build() override;
};
// 全局日志器的建造者,建造的日志器会添加到全局日志器管理器中
class GlobalLoggerBuilder : public LoggerBuilder
{
public:
Logger::Ptr build() override;
};
Logger.cpp
cpp
LoggerBuilder::LoggerBuilder()
: type_(LoggerType::SYNC), level_(LogLevel::Value::DEBUG)
{}
LoggerBuilder& LoggerBuilder::BuildLoggerType(LoggerType type)
{
type_ = type;
return *this;
}
LoggerBuilder& LoggerBuilder::BuildLoggerName(const std::string& logger_name)
{
logger_name_ = logger_name;
return *this;
}
LoggerBuilder& LoggerBuilder::BuildLogLevel(LogLevel::Value level)
{
level_ = level;
return *this;
}
LoggerBuilder& LoggerBuilder::BuildFormatter(const std::string& pattern)
{
formatter_ = std::make_shared<Formatter>(pattern);
return *this;
}
Logger::Ptr LocalLoggerBuilder::build()
{
assert(logger_name_ .empty() == false);
if(formatter_ .get() == nullptr)
{
formatter_ = std::make_shared<Formatter>("%d %p %m%n");
}
if(sinks_.empty())
{
BuildSinks<StdoutSink>();
}
if(type_ == LoggerType::SYNC)
{
return std::make_shared<SyncLogger>(logger_name_, level_, formatter_, sinks_);
}
else
{
return std::make_shared<AsyncLogger>(logger_name_, level_, formatter_, sinks_);
}
}
Logger::Ptr GlobalLoggerBuilder::build()
{
// 创建日志器实例(与LocalLoggerBuilder相同的逻辑)
Logger::Ptr logger;
if(type_ == LoggerType::SYNC)
{
logger = std::make_shared<SyncLogger>(logger_name_, level_, formatter_, sinks_);
}
else
{
logger = std::make_shared<AsyncLogger>(logger_name_, level_, formatter_, sinks_);
}
// 全局日志器的特殊处理:将日志器添加到全局管理器
// 这里可以预留接口,后续实现全局日志器管理器
// LoggerManager::GetInstance().RegisterLogger(logger_name_, logger);
// 目前先直接返回日志器,后续可以扩展为全局单例管理
return logger;
}
异步日志
实现思路
为了避免因为写日志的过程阻塞,导致业务线程在写日志的时候影响效率,因此异步的思想就是不让业务线程进行日志的实际落地操作,而是将日志消息放到缓冲区(一块指定的内存)中。因此有一个专门的异步线程,去针对缓冲区中的数据进行处理(实际的落地操作)
问题:这个缓冲区的操作会涉及到多线程,因此缓冲区的操作必须保证线程安全
对于线程安全的实现,可以对于缓冲区的读写加锁
因为写日志操作,在实际开发中,并不会分配太多资源,所以工作线程只需要一个日志器有一个就行
涉及到的锁冲突:生产者与生产者的互斥 & 生产者与消费者的互斥
问题:锁冲突较为严重,因为所有线程之间都存在互斥关系
解决方案:双缓冲区

单个缓冲区的进一步设计:
设计一个缓冲区:直接存放格式化后的日志消息字符串
好处:
1.减少了LogMsg对象频繁的构造的消耗
2.可以针对缓冲区中的日志消息,一次性进行IO操作,减少IO次数,提高效率
框架设计
缓冲区类的设计:
1.管理一个存放字符串数据的缓冲区(使用vector进行空间管理)
2.当前的写入数据位置的指针(指向可写区域的起始位置,避免数据的写入覆盖)
3.当前的读取数据位置的指针(指向可读数据区域的起始位置,当读取指针与写入指针指向相同位置表示数据取完了)
提供的操作:
-
向缓冲区中写入数据
-
获取可读数据起始地址的接口
-
获取可读数据长度的接口
-
移动读写位置的接口
-
初始化缓冲区的操作(将读写位置初始化--将一个缓冲区所有数据处理完毕之后)
-
提供交换缓冲区的操作(交换空间地址,并不交换空间数据)
异步缓冲区设计
buffer.hpp
cpp
/*
异步日志缓冲区:
1. 向缓冲区中写入数据
2. 获取可读数据起始地址的接口
3. 获取可读数据长度的接口
4. 移动读写位置的接口
5. 初始化缓冲区的操作(将读写位置初始化--将一个缓冲区所有数据处理完毕之后)
6. 提供交换缓冲区的操作(交换空间地址,并不交换空间数据)
*/
#pragma once
#include"util.hpp"
#include<vector>
#include<mutex>
namespace Logger
{
const size_t MAX_BUFFER_SIZE = 10*1024 * 1024; // 10MB缓冲区大小
const size_t THRESHOLD_SIZE = 80*1024 * 1024; // 80MB缓冲区大小阈值
const size_t INCREASE_BUFFER_SIZE = 10*1024 * 1024; // 10MB扩容大小
class LogBuffer
{
public:
LogBuffer();
// 向缓冲区写入数据
void push(const char *data, size_t len);
// 返回可写入数据的长度
size_t writeAbleSize();
// 返回可读数据的起始地址
const char* begin();
// 返回可读数据的长度
size_t readAbleSize();
// 对读指针进行向后偏移操作
void moveReader(size_t len);
// 重置读写位置,初始化缓冲区
void reset();
// 对Buffer实现交换操作
void Swap(LogBuffer &buffer);
// 判断缓冲区是否为空
bool empty();
private:
//对空间扩容
void EnsureEnoughSize(size_t len);
// 对写指针进行向后偏移操作
void moveWriter(size_t len);
private:
std::vector<char> buffer_;
size_t read_ptr_;//读取指针,指向当前可读取的日志消息的起始位置
size_t write_ptr_;//写入指针,指向当前可写入的日志消息的起始位置
};
}
buffer.cpp
cpp
#include"buffer.hpp"
#include<assert.h>
namespace Logger
{
LogBuffer::LogBuffer()
:buffer_(MAX_BUFFER_SIZE),read_ptr_(0),write_ptr_(0)
{}
void LogBuffer::push(const char *data, size_t len)
{
//检查是否有足够的空间写入数据,空间不够可以:1、扩容2、阻塞
//考虑空间不够就扩容
EnsureEnoughSize(len);
//数据拷贝进缓冲区
std::copy(data, data + len, buffer_.begin() + write_ptr_);
//写指针向后偏移len个字节
moveWriter(len);
}
size_t LogBuffer::writeAbleSize()
{
//仅仅针对固定缓冲区,返回可写入数据的长度
return buffer_.size() - write_ptr_;
}
size_t LogBuffer::readAbleSize()
{
return write_ptr_ - read_ptr_;
}
void LogBuffer::EnsureEnoughSize(size_t len)
{
if(len <= writeAbleSize())
{
return; // 无需扩容
}
// 保存当前有效数据
size_t current_data_size = readAbleSize();
std::vector<char> temp_buffer(buffer_.begin() + read_ptr_, buffer_.begin() + write_ptr_);
size_t newsize = 0;
if(len < THRESHOLD_SIZE)
{
newsize = buffer_.size() * 2 + len; // 小于阈值翻倍增长
}
else
{
newsize = buffer_.size() + len; // 大于阈值按需增长
}
// 扩容并恢复数据到缓冲区开头
buffer_.resize(newsize);
if (current_data_size > 0)
{
std::copy(temp_buffer.begin(), temp_buffer.end(), buffer_.begin());
}
// 调整指针位置
read_ptr_ = 0;
write_ptr_ = current_data_size;
}
const char* LogBuffer::begin()
{
return buffer_.data() + read_ptr_;
}
void LogBuffer::moveReader(size_t len)
{
assert(len <= readAbleSize());
read_ptr_ += len;
}
void LogBuffer::reset()
{
read_ptr_ = 0;
write_ptr_ = 0;
}
bool LogBuffer::empty()
{
return read_ptr_ == write_ptr_;
}
void LogBuffer::moveWriter(size_t len)
{
assert(len <= writeAbleSize());
write_ptr_ += len;
}
void LogBuffer::Swap(LogBuffer &buffer)
{
buffer_.swap(buffer.buffer_);
std::swap(read_ptr_, buffer.read_ptr_);
std::swap(write_ptr_, buffer.write_ptr_);
}
}
异步工作器设计
异步工作使用双缓冲区思想
外界将任务数据,添加到输入缓冲区中,
异步线程对处理缓冲区中的数据进行处理,若处理缓冲区中没有数据了则交换缓冲区
实现:
管理的成员:
-
双缓冲区(生产、消费)
-
互斥锁 // 保证线程安全
-
条件变量-生产&消费(生产缓冲区没有数据,处理完消费缓冲区数据后就休眠)
-
回调函数(针对缓冲区中数据的处理接口-外界传入一个函数,告诉异步工作器数据该如何处理)
提供的操作:
-
停止异步工作器
-
添加数据到缓冲区
私有操作:
创建线程,线程入口函数中,交换缓冲区,对消费缓冲区数据使用回调函数进行处理,处理完后再次交换
looper.hpp
cpp
/*
异步工作器设计:
1. 双缓冲区(生产、消费)
2. 互斥锁 // 保证线程安全
3. 条件变量-生产&消费(生产缓冲区没有数据,处理完消费缓冲区数据后就休眠)
4. 回调函数(针对缓冲区中数据的处理接口-外界传入一个函数,告诉异步工作器数据该如何处理)
提供的操作:
1. 停止异步工作器
2. 添加数据到缓冲区
私有操作:
创建线程,线程入口函数中,交换缓冲区,对消费缓冲区数据使用回调函数进行处理,处理完后再次交换
*/
#pragma once
#include"buffer.hpp"
#include<functional>
#include<memory>
#include<condition_variable>
#include<mutex>
#include<thread>
namespace Logger
{
using Functor=std::function<void(LogBuffer&)>;
class AsyncLooper
{
public:
enum class LooperType
{
ASYNC_SAFE, // 满了就阻塞,避免资源耗尽
ASYNC_UNSAFE, // 不考虑资源耗尽,无限扩容,仅用于测试
};
using Ptr=std::shared_ptr<AsyncLooper>;
AsyncLooper();
~AsyncLooper();
void Push(const char* data,size_t len);
void Stop();
private:
void ThreadEntry();//线程入口函数
private:
LooperType type_;// 工作器类型
Functor callback_;//回调函数,处理缓冲区数据的接口
std::atomic<bool> stop_flag_;// 停止标志,控制工作线程的运行
LogBuffer pro_buffer_;// 生产缓冲区
LogBuffer con_buffer_;// 消费缓冲区
std::mutex mutex_;// 互斥锁,保护缓冲区的访问
std::condition_variable cond_con_;// 条件变量,生产者和消费者的同步
std::condition_variable cond_pro_;// 条件变量,生产者和消费者的同步
std::thread worker_thread_;// 工作线程
};
}
looper.cpp
cpp
#include"looper.hpp"
namespace Logger
{
AsyncLooper::AsyncLooper()
:stop_flag_(false),worker_thread_(std::thread(&AsyncLooper::ThreadEntry,this)),
type_(AsyncLooper::LooperType::ASYNC_SAFE)
{}
void AsyncLooper::Push(const char* data,size_t len)
{
//1、无限扩容------非安全,只用于测试
std::unique_lock<std::mutex> lock(mutex_);
// 条件变量空值,若缓冲区剩余空间大小大于数据长度,则可以添加数据
if(type_==AsyncLooper::LooperType::ASYNC_SAFE)
cond_pro_.wait(lock, [&]() { return pro_buffer_.writeAbleSize() >= len; });
// 能够走下来代表满足了条件,可以向缓冲区添加数据
pro_buffer_.push(data, len);
// 唤醒消费者对缓冲区中的数据进行处理
cond_con_.notify_one();
//2、固定大小缓冲区,直接阻塞
}
void AsyncLooper::Stop()
{
stop_flag_ = true;//退出标记为true,通知工作线程退出
cond_con_.notify_all();//唤醒所有的线程
worker_thread_.join();//等待工作线程退出
}
void AsyncLooper::ThreadEntry()
{
while (!stop_flag_)
{
//1. 判断生产缓冲区有没有数据,有则交换,无则阻塞
{
std::unique_lock<std::mutex> lock(mutex_);
//若推出前唤醒或者有数据被唤醒,则返回真继续运行,否则返回假,继续阻塞
cond_con_.wait(lock, [&]() { return !pro_buffer_.empty()||stop_flag_; });
con_buffer_.Swap(pro_buffer_);
//2. 唤醒生产者
if(type_==AsyncLooper::LooperType::ASYNC_SAFE)
cond_pro_.notify_all();
}
//3. 被唤醒后,对消费缓冲区进行数据处理
callback_(con_buffer_);
//4. 初始化消费缓冲区
con_buffer_.reset();
}
}
AsyncLooper::~AsyncLooper()
{
Stop();
}
}
异步日志器模块设计
- 继承于Logger日志器类
对于写日志操作进行函数重写(不再将数据直接写入文件,而是通过异步消息处理器,放到缓冲区中)
- 通过异步消息处理器,进行日志数据的实际落地
管理的成员:
- 异步工作器(异步消息处理器)
完成后,完善日志器建造者,进行异步日志器安全模式的选择,提供异步日志器的创建
Logger.hpp
cpp
//异步日志器类,使用一个独立的线程来完成日志落地输出
class AsyncLogger : public Logger
{
public:
AsyncLogger(const std::string& logger_name,
LogLevel::Value level,
Formatter::SPtr&formatter,
AsyncLooper::LooperType type,
const std::vector<LogSink::SPtr>& sinks);
void Log(std::string msg) override;//数据写入缓冲区
//将缓冲区的数据落地输出
void Flush(LogBuffer& buffer);
private:
AsyncLooper::Ptr looper_;//异步工作器,负责处理缓冲区数据并完成日志落地输出
};
class LoggerBuilder
{
public:
LoggerBuilder();
LoggerBuilder& BuildLoggerType(LoggerType type);
LoggerBuilder& BuildLoggerName(const std::string& logger_name);
LoggerBuilder& BuildLogLevel(LogLevel::Value level);
LoggerBuilder& BuildFormatter(const std::string& pattern);
LoggerBuilder& BuildAsyncLooperType(AsyncLooper::LooperType type);
void BuildEnableUnsafe();
template<typename T,typename... Args>
LoggerBuilder& BuildSinks(Args&&... args)
{
LogSink::SPtr psink = SinkFactory<T, Args...>::Create(std::forward<Args>(args)...);
sinks_.push_back(psink);
return *this;
}
virtual Logger::Ptr build()=0;//建造日志器的接口,由派生类实现
protected:
LoggerType type_;
std::string logger_name_;
std::atomic<LogLevel::Value> level_;
Formatter::SPtr formatter_;
AsyncLooper::LooperType looper_type_;
std::vector<LogSink::SPtr> sinks_;
};
Logger.cpp
cpp
AsyncLogger::AsyncLogger(const std::string& logger_name,
LogLevel::Value level,
Formatter::SPtr&formatter,
AsyncLooper::LooperType type,
const std::vector<LogSink::SPtr>& sinks)
: Logger(logger_name, level, formatter, sinks),
looper_(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::Flush,this,std::placeholders::_1)))
{}
void AsyncLogger::Log(std::string msg)
{
looper_->Push(msg);
}
void AsyncLogger::Flush(LogBuffer& buffer)
{
if(sinks_.empty()) return;
// 1. 先将缓冲区数据转换为std::string,确保所有落地器都能收到完整数据
std::string log_data(buffer.begin(), buffer.readAbleSize());
// 2. 遍历所有落地器,输出相同的完整数据
for (const auto& sink : sinks_)
{
sink->log(log_data);
}
// 3. 在所有落地器处理完成后,一次性移动读取指针
buffer.moveReader(buffer.readAbleSize());
}
class LoggerBuilder
{
public:
LoggerBuilder();
LoggerBuilder& BuildLoggerType(LoggerType type);
LoggerBuilder& BuildLoggerName(const std::string& logger_name);
LoggerBuilder& BuildLogLevel(LogLevel::Value level);
LoggerBuilder& BuildFormatter(const std::string& pattern);
LoggerBuilder& BuildAsyncLooperType(AsyncLooper::LooperType type);
void BuildEnableUnsafe();
template<typename T,typename... Args>
LoggerBuilder& BuildSinks(Args&&... args)
{
LogSink::SPtr psink = SinkFactory<T, Args...>::Create(std::forward<Args>(args)...);
sinks_.push_back(psink);
return *this;
}
virtual Logger::Ptr build()=0;//建造日志器的接口,由派生类实现
protected:
LoggerType type_;
std::string logger_name_;
std::atomic<LogLevel::Value> level_;
Formatter::SPtr formatter_;
AsyncLooper::LooperType looper_type_;
std::vector<LogSink::SPtr> sinks_;
};
本期内容就到这里了,喜欢请点个赞谢谢
封面图自取:
