继上文对日志系统的介绍,并且实现了日志等级、日志消息的定义、以及格式化消息等。由这三个模块就能完整的输出一条消息。但是考虑到日志消息不可能全部都通过显示器展示。本项目针对落地方式,也进行多种编写,以供使用。
消息落地类(简单工厂模式)
消息落地的方式
- 标准输出
- 文件
- 按照大小滚动文件
- 按照时间滚动文件
考虑到如果将消息都放到文件中,查找起来不方便。如果文件过大,就要删除文件,之前的日志消息就会丢失。
针对落地到文件上的不足,博主设计滚动文件。一但到达某个要求之后,就打开一个新文件,将数据往新文件中写。
消息落地设计思路
落地类支持扩展,目前支持落地到标准输出、文件、滚动文件。设计的思想是基类落地类有一个纯虚log函数,具体的落地类继承基类并且重写基类log函数。最后通过简单工厂模式将这三种落地方式组织起来。
工厂模式:根据传入的不同落地类型,生产出指向基类的子类。
面临的问题:不同的落地方式可能会存在不同的参数、不同的类型
为了让工厂模式建造出相应的落地类,传入可变函数和模板参数。
落地的子类
往文件中落地:
设计思想是打开文件,但是如果频繁往文件中写数据就会频繁打开文件,关闭文件。
所以在构造的时候,就将文件的句柄保存
重写log函数,调用文件句柄写入数据。
按照大小滚动文件:
维护文件的当前size,文件最大size:一旦超过最大size就会创建新文件,往新文件写入数据为了便于观察:在创建文件前会先获取文件名,文件名由当前时间+原子递增的序号组成
如 2022-10-10:8:50:20-1 创建新文件后,关闭旧文件,保存新文件的句柄
写入文件前先进行是否创建新文件的判断,再写入。
写入的时候,更新当前文件大小。
按照时间落地文件的设计思路:
- 获取时间戳,对时间戳除当前的gap(如果gap是1minute就是60,1hour就是60*60)
- 因为是按照时间间隔创建新文件,所以处于同一个文件时间除与gap必然是相同的。
- 以时间落地文件的设计和以文件大小落地基本是一致的。
日志器设计(建造者模式)
当我们向通过日志系统来输出日志消息时,只需要创建日志器,调用日志器的debug、info,等接口输出我们想要的内容,同时输入的消息支持可变参数等等。就像printf()格式化的输出日志消息。
博主设计的日志系统支持
- 同步落地(写数据由当前线程进行,可能会阻塞)。
- 异步落地:异步写数据,只需要把数据交给缓冲区。
所谓的同步日志还是异步日志器都是基于普通日志器。因此先设计日志器Logger基类,继承出同步SyncLogger和AsyncLogger。
日志器的设计还必须包含消息的保存,利用格式化器对消息进行格式化,基于不同落地方式进行落地消息。这一系列的整合才能构建完整的日志器。所以日志器的创建是比较复杂的(用户并不知道、或者少创建了某个模块)借助建造者模式更加简洁优雅的构建日志器。
日志器的成员
- 日志器应该包含日志器名称
- 默认输出等级
- 互斥锁(设计多线程访问)
- 落地方式
- 格式化器
日志器的基类
抽象基类,抽象出debug、info、warning等接口,并且实现接口。接口的设置就是组织可变参数,调用格式化输出器转化成具体消息,至于落地方式,就交给派生类具体实现。
日志器支持多种落地方式,因此落地方式存储在数组中保存。将来具体的日志器类会重写log函数,根据同步或者异步,不同处理。
class Logger;
using LoggerPtr = std::shared_ptr<Logger>;
class Logger
{
public:
Logger(const std::string &logger_name, LogLevel::value level,
const FormatterPtr &format, std::vector<LogSinkPtr> sinks)
: _logger_name(logger_name), _format(format), _limit_level(level), _sinks(sinks.begin(), sinks.end())
{
}
virtual void Dubug(const std::string &file, size_t line, const char *fmt, ...);
virtual void Info(const std::string &file, size_t line, const char *fmt, ...);
virtual void Warning(const std::string &file, size_t line, const char *fmt, ...);
virtual void Fatal(const std::string &file, size_t line, const char *fmt, ...);
protected:
virtual void log(const char *data, size_t len) = 0;
void serialize(LogLevel::value level, const std::string &file, size_t line, const char *data)
{
LogMsg lg(level, file, line, _logger_name, data);
// 数据格式化,并放到流中
std::stringstream ss;
_format->format(ss, lg);
log(ss.str().c_str(), ss.str().size());
}
protected:
std::mutex _mutex;
std::atomic<LogLevel::value> _limit_level;
std::string _logger_name;
std::vector<LogSinkPtr> _sinks; // 可能存在多种落地方式
FormatterPtr _format;
};
落地出同步日志器
同步互斥器就是当前线程进行写操作,设计多线程访问,需要加锁保护
class SyncLogger : public Logger
{
public:
SyncLogger(const std::string &logger_name, LogLevel::value level,
const FormatterPtr &formate, std::vector<LogSinkPtr> sinks)
: Logger(logger_name, level, formate, sinks)
{
}
void log(const char *data, size_t len) override
{
std::unique_lock<std::mutex> lock(_mutex);
if (_sinks.empty())
return;
for (auto &sink : _sinks)
{
sink->log(data, len);
}
}
};
落地异步日志器
异步日志器就是将组织好的消息,将给内存缓冲区。就不管了。后续会详细介绍异步工作线程的原理。
class AsyncLogger : public Logger
{
public:
AsyncLogger(const std::string &logger_name, LogLevel::value level,
const FormatterPtr &formate, std::vector<LogSinkPtr> sinks, AsyncSafeType safe_type)
: Logger(logger_name, level, formate, sinks)
{
_pasync = std::make_shared<AsyncLoop>(std::bind(&AsyncLogger::func, this, std::placeholders::_1), safe_type);
}
void log(const char *data, size_t len)
{
// std::unique_lock<std::mutex> lock(_mutex); push是线程安全的
// 写数据往缓冲区放数据即可
_pasync->Push(data, len);
}
利用建造者模式创建日志器
建造者模式的一般步骤:抽象设备父类,具体设备子类,抽象零件父类,具体零件子类,如果涉及到顺序考虑用到指挥者构建。
抽象建造者的基类,建造日志器应该包含日志器名称、默认输出等级、格式化器、日志器类型(同步异步),落地方式
注意:
- 落地方式是通过工厂模式建造的,是一个模板可变参数。
- 对外提供build接口构建日志器。
// 建造者模式
// 抽象接口类
// 派生出具体的接口类
class LoggerBuilde
{
public:
LoggerBuilde()
: _type(LogType::SYNC), _limit_level(LogLevel::value::Dubug), _safe_type(AsyncSafeType::Async_Safe)
{
}
void BuildType(LogType type) { _type = type; }
void BuildName(std::string log_name) { _log_name = log_name; }
void EnableUnSafe() { _safe_type = AsyncSafeType::Async_Unsafe; }
void BuildLimit(LogLevel::value limit_level) { _limit_level = limit_level; }
void BuildFormat(const std::string &foamat) { _formatter = std::make_shared<Formatter>(foamat); }
template <typename SinkType, typename... Args>
void BuildSink(Args &&...args)
{
LogSinkPtr psink = SinkFoctory::CreateSink<SinkType>(std::forward<Args>(args)...);
_sinks.push_back(psink);
}
virtual LoggerPtr build() = 0;
protected:
AsyncSafeType _safe_type;
LogType _type;
std::string _log_name;
LogLevel::value _limit_level;
FormatterPtr _formatter;
std::vector<LogSinkPtr> _sinks;
};
建造者模式基类
后续想派生出局部日志器,只要继承父类,重写build接口既可。
缓冲区的设计
一旦有数据需要落地时,当前线程阻塞进行IO写。这是非常消耗时间的事情,所以为了提高主线程的速度。实际的落地消息采用的是异步写。即由主线程只负责将数据格式化出来,组织好一条数据。实际上的文件打开和关闭和关闭交给子线程,不再阻塞当前线程。
待写数据是被放到主线程和子线程的共享位置(一块内存上)。常用的缓冲区是队列,考虑到STL的队列底层是链表,会频繁删节点和new节点,放弃了STL的队列。转而采用vector,实现缓冲区。
双缓冲区的由来:
线程往缓冲区上放数据,可能存在并发访问。线程和线程之间的竞争,需要加锁保护。同时生产者和消费者之间也会竞争缓冲区的资源。放数据和取数据也就需要加锁。频繁的申请锁和释放锁也是降低效率的原因。
因此采用双缓冲区。缓冲区分为写缓冲区和读缓冲区。写缓冲区由生产者所有,读缓冲区由消费者所有。只有当读缓冲区无数据并且写缓冲区有数据时,交换缓冲区才加锁保护。
设计思路
缓冲区是可读可写的,所以维护读指针和写指针。缓冲区的内容是一条格式化好的数据。
对外提供的接口
- push( )生产者放数据---支持放多条消息
- Front( ) 获取一条头部消息
- Writeable( )获取可写的空间
- Readable( )获取可读的空间
- moveRead() 移动读指针
- moveWrite()移动写指针
- 扩容
- 缓冲区满
- 缓冲区为空
要解决的问题:当缓冲区满了,如何处理?
1.阻塞,等待工作线程交换走写缓冲区 2.扩容
博主实现的缓冲区支持阻塞和扩容机制,阻塞机制没什么好说的,缓冲区满了就不写数据,返回fasle。阻塞交给上层来实现。
详细说一下扩容机制。
一般来说,实际的日志系统是不会用到扩容机制的,因为扩容是有风险的,一般扩二倍。会导致无限的申请内存。但是相对于极限测试,博主还是设计了扩容机制。
扩容的设计:
一定可用空间<len,就进行扩容。扩容分为快速扩和慢速扩。
- 快速扩容:每次扩原来空间的2倍。
- 慢速扩容:空间达到某个阈值的时候,扩一个增长量+lenth.
#define BUFFER_DEFAULT_SIZE 1024 * 1024 * 1 // 缓冲区起始默认10M
#define BUFFER_INCREAMENT_SIZE 1* 1024 * 1024 // 低速增长速度默认1M
#define BUFFER_THRESHOULD_VALUE 3 * 1024 * 1024 // 阈值默认40M
namespace ns_logger
{
class Buffer
{
public:
Buffer(size_t capacity = BUFFER_DEFAULT_SIZE)
: _buffer(capacity), _write_index(0), _read_idex(0) {}
void Push(const char *data, size_t len)
{
// 扩容机制
EnsureEnoughSpace(len);
std::copy(data, data + len, &_buffer[_write_index]);
_write_index += len;
}
void Pop(size_t len)
{
_read_idex += len;
assert(_read_idex <= _write_index);
}
const char* GetFront() { return _buffer[_read_idex].c_str(); }
bool Empty() { return _write_index == _read_idex; }
// 可读的空间
size_t Readable()
{
return _write_index - _read_idex;
}
size_t Writeable()
{
return _buffer.size() - _write_index;
}
void ReadMove(size_t len)
{
assert(_read_idex+len<=_write_index);
_read_idex+=len;
}
void WriteMove(size_t len)
{
assert(_write_index+len<_buffer.size());
_write_index+=len;
}
void Swap(Buffer &buffer)
{
_buffer.swap(buffer._buffer);
std::swap(_read_idex, buffer._read_idex);
std::swap(_write_index, buffer._write_index);
}
void ReSet(){
_read_idex=_write_index=0;
}
private:
void EnsureEnoughSpace(size_t len)
{
// 检查是否扩容
if (len + _write_index < _buffer.size())
return;
// 扩容的大小
size_t newsize;
if (_buffer.size() <BUFFER_THRESHOULD_VALUE)
{
newsize = _buffer.size() * 2 + len;
}
// 低速扩容
else
{
newsize = _buffer.size() + len + BUFFER_INCREAMENT_SIZE;
}
_buffer.resize(newsize);
}
private:
std::vector<std::string> _buffer;
size_t _read_idex;
size_t _write_index;
};
};
本篇主要介绍日志系统消息的落地简单工厂模式的使用、日志器的整合建造者模式的使用
另外了解异步工作机制,以及双缓冲区的设计。缓冲区的扩容机制
下一篇将进行异步工作器的设计。