基于多设计模式下的同步&异步⽇志系统

目录

1.项目介绍

2.整体框架设计

3.⽇志输出格式化类设计

4.⽇志落地(LogSink)类设计

5.⽇志器类(Logger)设计(建造者模式)

6.双缓冲区异步任务处理器(AsyncLooper)设计

7.⽇志宏&全局接⼝设计(代理模式)

8.性能测试

9.扩展

10.参考资料


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 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"
pattern = "[%d{%H:%M:%S}] %m%n"
items = {
 {OtherFormatItem(), "["},
 {TimeFormatItem(), "%H:%M:%S"},
 {OtherFormatItem(), "]"},
 {MsgFormatItem (), ""},
 {NLineFormatItem (), ""}
}
LogMsg msg = {
 size_t _line = 22;
 size_t _ctime = 12345678;
 std::thread::id _tid = 0x12345678;
 std::string _name = "logger";
 std::string _file = "main.cpp";
 std::string _payload = "创建套接字失败";
 LogLevel::value _level = ERROR;
};
格式化的过程其实就是按次序从Msg中取出需要的数据进⾏字符串的连接的过程。
最终组织出来的格式化消息: "[22:32:54] 创建套接字失败\n"
代码实现:
#ifndef __M_FMT_H__
#define __M_FMT_H__
#include "util.hpp"
#include "message.hpp"
#include "level.hpp"
#include 
#include 
#include 
namespace bitlog{
class FormatItem{
 public:
 using ptr = std::shared_ptr;
 virtual ~FormatItem() {}
 virtual void format(std::ostream &os, const LogMsg &msg) = 0;
};
class MsgFormatItem : public FormatItem {
 public:
 MsgFormatItem(const std::string &str = ""){}
 virtual void format(std::ostream &os, const LogMsg &msg) {
 os << msg._payload;
 }
};
class LevelFormatItem : public FormatItem {
 public:
 LevelFormatItem(const std::string &str = ""){}
 virtual void format(std::ostream &os, const LogMsg &msg) {
 os << LogLevel::toString(msg._level);
 }
};
class NameFormatItem : public FormatItem {
 public:
 NameFormatItem(const std::string &str = ""){}
 virtual void format(std::ostream &os, const LogMsg &msg) {
 os << msg._name;
 }
};
class ThreadFormatItem : public FormatItem {
 public:
 ThreadFormatItem(const std::string &str = ""){}
 virtual void format(std::ostream &os, const LogMsg &msg) {
 os << msg._tid;
 }
};
class TimeFormatItem : public FormatItem {
 private:
 std::string _format;
 public:
 TimeFormatItem(const std::string &format = "%H:%M:%S"):_format(format){
 if (format.empty()) _format = "%H:%M:%S";
 }
 virtual void format(std::ostream &os, const LogMsg &msg) {
 time_t t = msg._ctime;
 struct tm lt;
 localtime_r(&t, <);

 char tmp[128];
 strftime(tmp, 127, _format.c_str(), <);
 os << tmp;
 }
};
class CFileFormatItem : public FormatItem {
 public:
 CFileFormatItem(const std::string &str = ""){}
 virtual void format(std::ostream &os, const LogMsg &msg) {
 os << msg._file;
 }
};
class CLineFormatItem : public FormatItem {
 public:
 CLineFormatItem(const std::string &str = ""){}
 virtual void format(std::ostream &os, const LogMsg &msg) {
 os << msg._line;
 }
};
class TabFormatItem : public FormatItem {
 public:
 TabFormatItem(const std::string &str = ""){}
 virtual void format(std::ostream &os, const LogMsg &msg) {
 os << "\t";
 }
};
class NLineFormatItem : public FormatItem {
 public:
 NLineFormatItem(const std::string &str = ""){}
 virtual void format(std::ostream &os, const LogMsg &msg) {
 os << "\n";
 }
};
class OtherFormatItem : public FormatItem {
 private:
 std::string _str;
 public:
 OtherFormatItem(const std::string &str = ""):_str(str){}
 virtual void format(std::ostream &os, const LogMsg &msg) {
 os << _str;
 }
};
class Formatter {
 public:
 using ptr = std::shared_ptr;
 /*
 %d ⽇期
 %T 缩进
 %t 线程id
 %p ⽇志级别
 %c ⽇志器名称
 %f ⽂件名
 %l ⾏号
 %m ⽇志消息
 %n 换⾏
 */
 Formatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%p][%c]
[%f:%l] %m%n"):
 _pattern(pattern){
 assert(parsePattern());
 }
 const std::string pattern() { return _pattern; }
 std::string format(const LogMsg &msg) {
 std::stringstream ss;
 for (auto &it : _items) {
 it->format(ss, msg);
 }
 return ss.str();
 }
 std::ostream& format(std::ostream &os, const LogMsg &msg) {
 for (auto &it : _items) {
 it->format(os, msg);
 }
 return os;
 }
 FormatItem::ptr createItem(const std::string &fc, const std::string 
&subfmt) {
 if (fc == "m") return FormatItem::ptr(new MsgFormatItem(subfmt));
 if (fc == "p") return FormatItem::ptr(new LevelFormatItem(subfmt));
 if (fc == "c") return FormatItem::ptr(new NameFormatItem(subfmt));
 if (fc == "t") return FormatItem::ptr(new
ThreadFormatItem(subfmt));
 if (fc == "n") return FormatItem::ptr(new NLineFormatItem(subfmt));
 if (fc == "d") return FormatItem::ptr(new TimeFormatItem(subfmt));
 if (fc == "f") return FormatItem::ptr(new CFileFormatItem(subfmt));
 if (fc == "l") return FormatItem::ptr(new CLineFormatItem(subfmt));
 if (fc == "T") return FormatItem::ptr(new TabFormatItem(subfmt));
 return FormatItem::ptr();
 }
 //pattern解析
 bool parsePattern() {
 //std::string _pattern 
="sg{}fsg%d{%H:%M:%S}%Tsdf%t%T[%p]%T[%c]%T%f:%l%T%m%n";
 //std::cout << _pattern << std::endl;
 //每个要素分为三部分:
 // 格式化字符 : %d %T %p...
 // 对应的输出⼦格式 : {%H:%M:%S}
 // 对应数据的类型 : 0-表⽰原始字符串,也就是⾮格式化字符,1-表⽰格式化数据
类型
 // 默认格式 "%d{%H:%M:%S}%T%t%T[%p]%T[%c]%T%f:%l%T%m%n"
 std::vector> arry;
 std::string format_key;//存放%后的格式化字符
 std::string format_val;//存放格式化字符后边 {} 中的⼦格式字符串
 std::string string_row;//存放原始的⾮格式化字符
 bool sub_format_error = false;
 int pos = 0;
 while (pos < _pattern.size()) {
 if (_pattern[pos] != '%') {
 string_row.append(1, _pattern[pos++]);
 continue;
 }
 if (pos+1 < _pattern.size() && _pattern[pos+1] == '%') {
 string_row.append(1, '%');
 pos += 2;
 continue;
 }
 if (string_row.empty() == false) {
 arry.push_back(std::make_tuple(string_row, "", 0));
 string_row.clear();
 }
 //当前位置是%字符位置
 pos += 1;//pos指向格式化字符位置
 if (pos < _pattern.size() && isalpha(_pattern[pos])) {
 format_key = _pattern[pos];//保存格式化字符
 }else {
 std::cout << &_pattern[pos-1] << "位置附近格式错误!\n";
 return false;
 }
 //pos指向格式化字符的下⼀个位置,判断是否包含有⼦格式 %d{%Y-%m-%d}
 pos += 1;
 if (pos < _pattern.size() && _pattern[pos] == '{') {
 sub_format_error = true;
 pos += 1;//pos指向花括号下⼀个字符处
 while(pos < _pattern.size()) {
 if (_pattern[pos] == '}') {
 sub_format_error = false;
 pos += 1;//让pos指向}的下⼀个字符处
 break;
 }
 format_val.append(1, _pattern[pos++]);
 }
 }
 arry.push_back(std::make_tuple(format_key, format_val, 1));
 format_key.clear();
 format_val.clear();
 }
 if (sub_format_error) {
 std::cout << "{}对应出错\n";
 return false;
 }
 if (string_row.empty() == false) 
 arry.push_back(std::make_tuple(string_row, "", 0));
 if (format_key.empty() == false) 
 arry.push_back(std::make_tuple(format_key, format_val, 1));
 for (auto &it : arry) {
 if (std::get<2>(it) == 0) {
 FormatItem::ptr fi(new OtherFormatItem(std::get<0>(it)));
 _items.push_back(fi);
 }else {
 FormatItem::ptr fi = createItem(std::get<0>(it), 
std::get<1>(it));
 if (fi.get() == nullptr) {
 std::cout <<"没有对应的格式化字符: %"<(it) << 
std::endl;
 return false;
 }
 _items.push_back(fi);
 }
 }
 return true;
 }
 private:
 std::string _pattern;
 std::vector _items;
};
}
#endif

4.⽇志落地(LogSink)类设计

⽇志落地类主要负责落地⽇志消息到⽬的地。
它主要包括以下内容:
Formatter⽇志格式化器:主要是负责格式化⽇志消息,
mutex互斥锁:保证多线程⽇志落地过程中的线程安全,避免出现交叉输出的情况。
这个类⽀持可扩展,其成员函数log设置为纯虚函数,当我们需要增加⼀个log输出⽬标, 可以增加⼀个类继承⾃该类并重写log⽅法实现具体的落地⽇志逻辑。
⽬前实现了三个不同⽅向上的⽇志落地:
标准输出:StdoutSink
固定⽂件:FileSink
滚动⽂件:RollSink
滚动⽇志⽂件输出的必要性:
由于机器磁盘空间有限, 我们不可能⼀直⽆限地向⼀个⽂件中增加数据   如果⼀个⽇志⽂件体积太⼤,⼀⽅⾯是不好打开,另⼀⽅⾯是即时打开了由于包含数据巨
⼤,也不利于查找我们需要的信息
所以实际开发中会对单个⽇志⽂件的⼤⼩也会做⼀些控制,即当⼤⼩超过某个⼤⼩时(如
1GB),我们就重新创建⼀个新的⽇志⽂件来滚动写⽇志。 对于那些过期的⽇志, ⼤部分
企业内部都有专⻔的运维⼈员去定时清理过期的⽇志,或者设置系统定时任务,定时清理过
期⽇志。
⽇志⽂件的滚动思想:
⽇志⽂件滚动的条件有两个:⽂件⼤⼩ 和 时间。我们可以选择:
⽇志⽂件在⼤于 1GB 的时候会更换新的⽂件
每天定点滚动⼀个⽇志⽂件
本项⽬基于⽂件⼤⼩的判断滚动⽣成新的⽂件
#ifndef __M_SINK_H__
#define __M_SINK_H__
#include "util.hpp"
#include "message.hpp"
#include "formatter.hpp"
#include 
#include 
namespace bitlog{
class LogSink {
 public:
 using ptr = std::shared_ptr;
 LogSink() {}
 virtual ~LogSink() {}
 virtual void log(const char *data, size_t len) = 0;
};
class StdoutSink : public LogSink {
 public:
 using ptr = std::shared_ptr;
 StdoutSink() = default;
 void log(const char *data, size_t len) {
 std::cout.write(data, len);
 }
};
class FileSink : public LogSink {
 public:
 using ptr = std::shared_ptr;
 FileSink(const std::string &filename):_filename(filename) {
 util::file::create_directory(util::file::path(filename));
 _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 RollSink : public LogSink {
 public:
 using ptr = std::shared_ptr;
 RollSink(const std::string &basename, size_t max_fsize):
 _basename(basename), _max_fsize(max_fsize), _cur_fsize(0){
 util::file::create_directory(util::file::path(basename));
 }
 void log(const char *data, size_t len) {
 initLogFile();
 _ofs.write(data, len);
 if (_ofs.good() == false) {
 std::cout << "⽇志输出⽂件失败!\n";
 }
 _cur_fsize += len;
 }
 private:
 void initLogFile() {
 if (_ofs.is_open() == false || _cur_fsize >= _max_fsize) {
 _ofs.close();
 std::string name = createFilename();
 _ofs.open(name, std::ios::binary | std::ios::app);
 assert(_ofs.is_open());
 _cur_fsize = 0;
 return;
 }
 return;
 } 
 std::string createFilename() {
 time_t t = time(NULL);
 struct tm lt;
 localtime_r(&t, <);
 std::stringstream ss;
 ss << _basename;
 ss << lt.tm_year + 1900;
 ss << lt.tm_mon + 1;
 ss << lt.tm_mday;
 ss << lt.tm_hour;
 ss << lt.tm_min;
 ss << lt.tm_sec;
 ss << ".log";
 return ss.str();
 }
 private:
 std::string _basename;
 std::ofstream _ofs;
 size_t _max_fsize;
 size_t _cur_fsize;
};
class SinkFactory {
 public:
 template
 static LogSink::ptr create(Args &&...args) {
 return std::make_shared(std::forward(args)...);
 }
};
}
#endif

5.⽇志器类(Logger)设计(建造者模式)

⽇志器主要是⽤来和前端交互, 当我们需要使⽤⽇志系统打印log的时候, 只需要创建Logger对象,
调⽤该对象debug、info、warn、error、fatal等⽅法输出⾃⼰想打印的⽇志即可,⽀持解析可变参数
列表和输出格式, 即可以做到像使⽤printf函数⼀样打印⽇志。
当前⽇志系统⽀持同步⽇志 & 异步⽇志两种模式,两个不同的⽇志器唯⼀不同的地⽅在于他们在⽇志
的落地⽅式上有所不同:
同步⽇志器:直接对⽇志消息进⾏输出。
异步⽇志器:将⽇志消息放⼊缓冲区,由异步线程进⾏输出。
因此⽇志器类在设计的时候先设计出⼀个Logger基类,在Logger基类的基础上,继承出SyncLogger同
步⽇志器和AsyncLogger异步⽇志器。
且因为⽇志器模块是对前边多个模块的整合,想要创建⼀个⽇志器,需要设置⽇志器名称,设置⽇志输出等级,设置⽇志器类型,设置⽇志输出格式,设置落地⽅向,且落地⽅向有可能存在多个,整个⽇志器的创建过程较为复杂,为了保持良好的代码⻛格,编写出优雅的代码,因此⽇志器的创建这⾥采⽤了建造者模式来进⾏创建。
#ifndef __M_LOG_H__
#define __M_LOG_H__
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "formatter.hpp"
#include "sink.hpp"
#include "looper.hpp"
#include 
#include 
#include 
#include 
#include 
#include 
namespace bitlog{
class Logger {
 public:
 enum class Type {
 LOGGER_SYNC = 0,
 LOGGER_ASYNC
 };
 using ptr = std::shared_ptr; 
 Logger(const std::string &name, 
 Formatter::ptr formatter,
 std::vector &sinks, 
 LogLevel::value level = LogLevel::value::DEBUG): 
 _name(name), _level(level), _formatter(formatter),
 _sinks(sinks.begin(), sinks.end()){
 }
 std::string loggerName() { return _name; }
 LogLevel::value loggerLevel() { return _level; }
 void debug(const char *file, size_t line, const char *fmt, ...) {
 if (shouldLog(LogLevel::value::DEBUG) == false) {
 return ;
 }
 va_list al;
 va_start(al, fmt);
 log(LogLevel::value::DEBUG, file, line, fmt, al);
 va_end(al);
 }
 void info(const char *file, size_t line, const char *fmt, ...) {
 if (shouldLog(LogLevel::value::INFO) == false) return ;
 va_list al;
 va_start(al, fmt);
 log(LogLevel::value::INFO, file, line, fmt, al);
 va_end(al);
 }
 void warn(const char *file, size_t line, const char *fmt, ...) {
 if (shouldLog(LogLevel::value::WARN) == false) return ;
 va_list al;
 va_start(al, fmt);
 log(LogLevel::value::WARN, file, line, fmt, al);
 va_end(al);
 }
 void error(const char *file, size_t line, const char *fmt, ...) {
 if (shouldLog(LogLevel::value::ERROR) == false) return ;
 va_list al;
 va_start(al, fmt);
 log(LogLevel::value::ERROR, file, line, fmt, al);
 va_end(al);
 }
 void fatal(const char *file, size_t line, const char *fmt, ...) {
 if (shouldLog(LogLevel::value::FATAL) == false) return ;
 va_list al;
 va_start(al, fmt);
 log(LogLevel::value::FATAL, file, line, fmt, al);
 va_end(al);
 }
 public:
 class Builder {
 public:
 using ptr = std::shared_ptr;
 Builder():_level(LogLevel::value::DEBUG), 
 _logger_type(Logger::Type::LOGGER_SYNC) {}
 void buildLoggerName(const std::string &name) { _logger_name = 
name; }
 void buildLoggerLevel(LogLevel::value level) { _level = level; 
}
 void buildLoggerType(Logger::Type type) { _logger_type = type; 
}
 void buildFormatter(const std::string pattern) { 
 _formatter = std::make_shared(pattern); 
 }
 void buildFormatter(const Formatter::ptr &formatter) { 
 _formatter = formatter; 
 }
 template
 void buildSink(Args &&...args) { 
 auto sink = SinkFactory::create
(std::forward(args)...);
 _sinks.push_back(sink); 
 }
 virtual Logger::ptr build() = 0;
 protected:
 Logger::Type _logger_type;
 std::string _logger_name;
 LogLevel::value _level;
 Formatter::ptr _formatter;
 std::vector _sinks;
 };
 protected:
 bool shouldLog(LogLevel::value level) { return level >= _level; }
 void log(LogLevel::value level,const char *file, 
 size_t line, const char *fmt, va_list al) {
 char *buf;
 std::string msg;
 int len = vasprintf(&buf, fmt, al);
 if (len < 0) {
 msg = "格式化⽇志消息失败!!";
 }else {
 msg.assign(buf, len);
 free(buf);
 }
 //LogMsg(name, file, line, payload, level)
 LogMsg lm(_name, file, line, std::move(msg), level);
 std::stringstream ss;
 _formatter->format(ss, lm);
 logIt(std::move(ss.str()));
 }
 virtual void logIt(const std::string &msg) = 0;
 protected:
 std::mutex _mutex;
 std::string _name;
 Formatter::ptr _formatter;
 std::atomic _level;
 std::vector _sinks;
};
class SyncLogger : public Logger {
 public:
 using ptr = std::shared_ptr;
 SyncLogger(const std::string &name, 
 Formatter::ptr formatter,
 std::vector &sinks, 
 LogLevel::value level = LogLevel::value::DEBUG): 
 Logger(name, formatter, sinks, level){ 
 std::cout << LogLevel::toString(level)<<" 同步⽇志器: "<< name<< "创
建成功...\n";
 }
 private:
 virtual void logIt(const std::string &msg) {
 std::unique_lock lock(_mutex);
 if (_sinks.empty()) { return ; }
 for (auto &it : _sinks) {
 it->log(msg.c_str(), msg.size());
 }
 }
};
class LocalLoggerBuilder: public Logger::Builder {
 public:
 virtual Logger::ptr build() {
 if (_logger_name.empty()) {
 std::cout << "⽇志器名称不能为空!!";
 abort();
 }
 if (_formatter.get() == nullptr) {
 std::cout<<"当前⽇志器:" << _logger_name;
 std::cout<<" 未检测到⽇志格式,默认设置为: ";
 std::cout<<" %d{%H:%M:%S}%T%t%T[%p]%T[%c]%T%f:%l%T%m%n\n";
 _formatter = std::make_shared();
 }
 if (_sinks.empty()) {
 std::cout<<"当前⽇志器:"<<_logger_name<<" 未检测到落地⽅向,默认为
标准输出!\n";
 _sinks.push_back(std::make_shared());
 }
 Logger::ptr lp;
 if (_logger_type == Logger::Type::LOGGER_ASYNC) {
 lp = std::make_shared(_logger_name,_formatter, 
_sinks, _level);
 }else {
 lp = std::make_shared(_logger_name, _formatter, 
_sinks, _level);
 }
 return lp;
 }
};
}
#endif

6.双缓冲区异步任务处理器(AsyncLooper)设计

设计思想:异步处理线程 + 数据池
使⽤者将需要完成的任务添加到任务池中,由异步线程来完成任务的实际执⾏操作。
任务池的设计思想:双缓冲区阻塞数据池
优势:避免了空间的频繁申请释放,且尽可能的减少了⽣产者与消费者之间锁冲突的概率,提⾼了任务处理效率。
在任务池的设计中,有很多备选⽅案,⽐如循环队列等等,但是不管是哪⼀种都会涉及到锁冲突的情况,因为在⽣产者与消费者模型中,任何两个⻆⾊之间都具有互斥关系,因此每⼀次的任务添加与取出都有可能涉及锁的冲突,⽽双缓冲区不同,双缓冲区是处理器将⼀个缓冲区中的任务全部处理完毕后,然后交换两个缓冲区,重新对新的缓冲区中的任务进⾏处理,虽然同时多线程写⼊也会冲突,但是冲突并不会像每次只处理⼀条的时候频繁(减少了⽣产者与消费者之间的锁冲突),且不涉及到空间的频繁申请释放所带来的消耗。
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
namespace bitlog{
#define BUFFER_DEFAULT_SIZE (1*1024*1024)
#define BUFFER_INCREMENT_SIZE (1*1024*1024)
#define BUFFER_THRESHOLD_SIZE (10*1024*1024)
class Buffer {
 public:
 Buffer(): _reader_idx(0), _writer_idx(0), _v(BUFFER_DEFAULT_SIZE){}
 bool empty() { return _reader_idx == _writer_idx; }
 size_t readAbleSize() { return _writer_idx - _reader_idx; }
 size_t writeAbleSize() { return _v.size() - _writer_idx; }
 void reset() { _reader_idx = _writer_idx = 0; }
 void swap(Buffer &buf) {
 _v.swap(buf._v);
 std::swap(_reader_idx, buf._reader_idx);
 std::swap(_writer_idx, buf._writer_idx);
 }
 void push(const char *data, size_t len) { 
 assert(len <= writeAbleSize());
 ensureEnoughSpace(len);
 std::copy(data, data+len, &_v[_writer_idx]);
 _writer_idx += len;
 }
 const char*begin() { return &_v[_reader_idx]; }
 void pop(size_t len) { 
 _reader_idx += len; 
 assert(_reader_idx <= _writer_idx);
 }
 protected:
 void ensureEnoughSpace(size_t len) {
 if (len <= writeAbleSize()) return;
 /*每次增⼤1M⼤⼩*/
 size_t new_capacity;
 if (_v.size() < BUFFER_THRESHOLD_SIZE)
 new_capacity = _v.size() * 2 + len;
 else
 new_capacity = _v.size() + BUFFER_INCREMENT_SIZE + len;
 _v.resize(new_capacity);
 }
 private:
 size_t _reader_idx;
 size_t _writer_idx;
 std::vector _v;
};
}
#ifndef __M_LOOP_H__
#define __M_LOOP_H__
#include "util.hpp"
#include 
#include 
#include 
#include 
#include 
#include 
#include "buffer.hpp"
namespace bitlog{
 class AsyncLooper {
 public:
 using Functor = std::function;
 using ptr = std::shared_ptr;
 AsyncLooper(const Functor &cb): _running(true), 
_looper_callback(cb),
 _thread(std::thread(&AsyncLooper::worker_loop, this)) {
 }
 ~AsyncLooper() { stop(); }
 void stop(){ 
 _running = false; 
 _pop_cond.notify_all();
 _thread.join();
 }
 void push(const std::string &msg){
 if (_running == false) return;
 {
 std::unique_lock lock(_mutex);
 _push_cond.wait(lock, 
 [&]{ return _tasks_push.writeAbleSize() >= msg.size(); 
});
 _tasks_push.push(msg.c_str(), msg.size());
 }
 _pop_cond.notify_all();
 }
 private:
 void worker_loop(){
 while(1){
 {
 std::unique_lock lock(_mutex);
 if (_running == false && _tasks_push.empty()) { 
return; }
 _pop_cond.wait(lock,[&]{ return
!_tasks_push.empty()||!_running; });
 _tasks_push.swap(_tasks_pop);
 }
 _push_cond.notify_all();
 _looper_callback(_tasks_pop);
 _tasks_pop.reset();
 }
 return;
 }
 private:
 Functor _looper_callback;
 private:
 std::mutex _mutex;
 std::atomic _running;
 std::condition_variable _push_cond;
 std::condition_variable _pop_cond;
 Buffer _tasks_push;
 Buffer _tasks_pop;
 std::thread _thread;
 };
}
#endif

7.⽇志宏&全局接⼝设计(代理模式)

提供全局的⽇志器获取接⼝。
使⽤代理模式通过全局函数或宏函数来代理Logger类的log、debug、info、warn、error、fatal等接
⼝,以便于控制源码⽂件名称和⾏号的输出控制,简化⽤⼾操作。
当仅需标准输出⽇志的时候可以通过主⽇志器来打印⽇志。 且操作时只需要通过宏函数直接进⾏输出 即可。
#ifndef __M_BIT_H__
#define __M_BIT_H__
#include "logger.hpp"
namespace bitlog {
 Logger::ptr getLogger(const std::string &name) {
 return loggerManager::getInstance().getLogger(name);
 }
 Logger::ptr rootLogger() {
 return loggerManager::getInstance().rootLogger();
 }
 
 #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__)
 
 #define LOG_DEBUG(logger, fmt, ...) (logger)->debug(fmt, ##__VA_ARGS__)
 #define LOG_INFO(logger, fmt, ...) (logger)->info(fmt, ##__VA_ARGS__)
 #define LOG_WARN(logger, fmt, ...) (logger)->warn(fmt, ##__VA_ARGS__)
 #define LOG_ERROR(logger, fmt, ...) (logger)->error(fmt, ##__VA_ARGS__)
 #define LOG_FATAL(logger, fmt, ...) (logger)->fatal(fmt, ##__VA_ARGS__)
 #define LOGD(fmt, ...) LOG_DEBUG(bitlog::rootLogger(), fmt, ##__VA_ARGS__)
 #define LOGI(fmt, ...) LOG_INFO(bitlog::rootLogger(), fmt, ##__VA_ARGS__)
 #define LOGW(fmt, ...) LOG_WARN(bitlog::rootLogger(), fmt, ##__VA_ARGS__)
 #define LOGE(fmt, ...) LOG_ERROR(bitlog::rootLogger(), fmt, ##__VA_ARGS__)
 #define LOGF(fmt, ...) LOG_FATAL(bitlog::rootLogger(), fmt, ##__VA_ARGS__)
}#endif

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内存)
#ifndef __M_BENCH_H__
#define __M_BENCH_H__
#include "bitlog.h"
#include 
namespace bitlog {
void bench(const std::string &loger_name, size_t thread_num, 
 size_t msglen, size_t msg_count)
{
 Logger::ptr lp = getLogger(loger_name);
 if (lp.get() == nullptr) return;
 std::string msg(msglen, '1');
 size_t msg_count_per_thread = msg_count / thread_num;
 std::vector cost_time(thread_num);
 std::vector threads;
 std::cout << "输⼊线程数量: " << thread_num << std::endl;
 std::cout << "输出⽇志数量: " << msg_count << std::endl;
 std::cout << "输出⽇志⼤⼩: " << msglen * msg_count / 1024 << "KB" << 
std::endl;
 for (int i = 0; i < thread_num; i++) {
 threads.emplace_back([&, i](){
 auto start = std::chrono::high_resolution_clock::now();
 for(size_t j = 0; j < msg_count_per_thread; j++) {
 lp->fatal("%s", msg.c_str());
 }
 auto end = std::chrono::high_resolution_clock::now();
 auto cost=std::chrono::duration_cast>
(end-start);
 cost_time[i] = cost.count();
 auto avg = msg_count_per_thread / cost_time[i];
 std::cout << "线程" << i << "耗时: " << cost.count() << "s";
 std::cout << " 平均:" << (size_t)avg << "/s\n";
 });
 }
 for(auto &thr : threads) {
 thr.join();
 }
 double max_cost = 0;
 for (auto cost : cost_time) max_cost = max_cost < cost ? cost : max_cost;
 std::cout << "总消耗时间: " << max_cost << std::endl;
 std::cout << "平均每秒输出: " << (size_t)(msg_count / max_cost) << std::endl;
}
}
#endif
#include "bitlog.h"
#include "bench.h"
#include 
void sync_bench_thread_log(size_t thread_count, size_t msg_count, size_t
msglen)
{
 static int num = 1;
 std::string logger_name = "sync_bench_logger" + std::to_string(num++);
 LOGI("************************************************");
 LOGI("同步⽇志测试: %d threads, %d messages", thread_count, msg_count);
 bitlog::GlobalLoggerBuilder::ptr lbp(new bitlog::GlobalLoggerBuilder);
 lbp->buildLoggerName(logger_name);
 lbp->buildFormatter("%m%n");
 lbp->buildSink("./logs/sync.log");
 lbp->buildLoggerType(bitlog::Logger::Type::LOGGER_SYNC);
 lbp->build(); 
 bitlog::bench(logger_name, thread_count, msglen, msg_count);
 LOGI("************************************************");
}
void async_bench_thread_log(size_t thread_count, size_t msg_count, size_t
msglen)
{
 static int num = 1;
 std::string logger_name = "async_bench_logger" + std::to_string(num++);
 LOGI("************************************************");
 LOGI("异步⽇志测试: %d threads, %d messages", thread_count, msg_count);
 bitlog::GlobalLoggerBuilder::ptr lbp(new bitlog::GlobalLoggerBuilder);
 lbp->buildLoggerName(logger_name);
 lbp->buildFormatter("%m");
 lbp->buildSink("./logs/async.log");
 lbp->buildLoggerType(bitlog::Logger::Type::LOGGER_ASYNC);
 lbp->build(); 
 bitlog::bench(logger_name, thread_count, msglen, msg_count);
 LOGI("************************************************");
}
void bench_test() {
 // 同步写⽇志
 sync_bench_thread_log(1, 1000000, 100);
 sync_bench_thread_log(5, 1000000, 100);
 /*异步⽇志输出,为了避免因为等待落地影响时间所以⽇志数量降低为⼩于缓冲区⼤⼩进⾏测试*/
 async_bench_thread_log(1, 100000, 100);
 async_bench_thread_log(5, 100000, 100);
}
int main(int argc, char *argv[])
{
 bench_test();
 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

你可能感兴趣的:(设计模式)