日志项目2

【C++ - 基于多设计模式下的同步&异步日志系统】

文章目录

  • 【C++ - 基于多设计模式下的同步&异步日志系统】
    • 项目开发
      • 7. 日志系统框架设计
        • 7.1 模块划分
        • 7.2 模块关系图
      • 8. 代码设计
        • 8.1 实用类设计
        • 8.2 日志等级类设计
        • 8.3 日志消息类设计
        • 8.4 日志输出格式化类设计
        • 8.5 日志落地(LogSink)类设计(简单工厂模式)
        • 8.6 日志器类(Logger)设计(建造者模式)
          • 建造者模式的使用
        • 8.7 双缓冲区异步任务处理器(AsyncLooper)设计
          • 8.7.1 异步缓冲区类设计
          • 8.7.2 异步工作器类设计
        • 8.8 异步日志器(AsyncLogger)设计
        • 8.9 单例日志器管理类设计
        • 8.10 日志宏&全局接口设计(代理模式)
      • 9. 测试
        • 9.1 功能测试
        • 9.2 扩展测试
        • 9.3 性能测试
          • 9.3.1 测试环境
          • 9.3.2 测试方法
          • 9.3.3 测试结果
            • (1) 单线程同步日志
            • (2) 多线程同步日志
            • (3) 单线程异步日志
            • (4) 多线程异步日志
          • 9.3.4 结论
      • 10. 扩展

项目开发

7. 日志系统框架设计

作用:将一条消息,进行格式化成为指定格式的字符串后,写入到指定位置

分析:

  1. 日志要写入指定位置(标准输出,指定文件,滚动文件…)

    • 日志系统需要支持将日志消息落地到不同位置 — 多落地方向
  2. 日志写入指定位置,支持不同的写入方式(同步,异步)

    • 同步:业务线程自己负责日志的写入(流程简单,但是有可能会因为阻塞导致效率降低)
    • 异步:业务线程将日志放入缓冲区内存,让其他异步线程负责将日志写入指定位置
  3. 日志输出以日志器为单位,支持多日志器(即不同的项目组有不同的输出策略)

    • 日志器的管理

项目的框架设计将项目分为以下几个模块来实现。

7.1 模块划分
  • 日志等级模块:对输出日志的等级进行划分,以便于控制日志的输出,并提供等级枚举转字符串功能。

    • OFF:关闭
    • DEBUG:调试,调试时的关键信息输出。
    • INFO:提示,普通的提示型日志信息。
    • WARN:警告,不影响运行,但是需要注意一下的日志。
    • ERROR:错误,程序运行出现错误的日志。
    • FATAL:致命,一般是代码异常导致程序无法继续推进运行的日志。
  • 日志消息模块:中间存储日志输出所需的各项要素信息

    • 时间:描述本条日志的输出时间。
    • 线程ID:描述本条日志是哪个线程输出的。
    • 日志等级:描述本条日志的等级。
    • 日志数据:本条日志的有效载荷数据。
    • 日志文件名:描述本条日志在哪个源码文件中输出的。
    • 日志行号:描述本条日志在源码文件的哪一行输出的。
  • 日志消息格式化模块:设置日志输出格式,并提供对日志消息进行格式化功能。

    • 系统的默认日志输出格式:

      %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:表示换行

    • 设计思想:设计不同的子类,不同的子类从日志消息中取出不同的数据进行处理。

  • 日志消息落地模块:决定了日志的落地方向,可以是标准输出,也可以是日志文件,也可以滚动文件输出…

    • 标准输出:表示将日志进行标准输出的打印。
    • 日志文件输出:表示将日志写入指定的文件末尾。
    • 滚动文件输出:当前以文件大小进行控制,当一个日志文件大小达到指定大小,则切换下一个文件进行输出
    • 后期,也可以扩展远程日志输出,创建客户端,将日志消息发送给远程的日志分析服务器。
    • 设计思想:设计不同的子类,不同的子类控制不同的日志落地方向。
  • 日志器模块

    • 此模块是对以上几个模块的整合模块,用户通过日志器进行日志的输出,有效降低用户的使用难度。
    • 包含有:日志消息落地模块对象,日志消息格式化模块对象,日志输出等级
  • 日志器管理模块

    • 为了降低项目开发的日志耦合,不同的项目组可以有自己的日志器来控制输出格式以及落地方向,因此本项目是一个多日志器的日志系统。
    • 管理模块就是对创建的所有日志器进行统一管理。并提供一个默认日志器提供标准输出的日志输出。
  • 异步线程模块

    • 实现对日志的异步输出功能,用户只需要将输出日志任务放入任务池,异步线程负责日志的落地输出功能,以此提供更加高效的非阻塞日志输出。
7.2 模块关系图

在这里插入图片描述

8. 代码设计

8.1 实用类设计

提前完成一些零碎的功能接口,以便于项目中会用到。

  • 获取系统时间

  • 判断文件是否存在

  • 获取文件的所在目录路径

  • 创建目录

实用类设计在util.hpp

/*实用工具类的实现:1. 获取系统时间2. 判断文件是否存在3. 获取文件所在的目录路径4. 创建目录
*/#pragma once
#include<iostream>
#include<ctime>
#include<string>
#include<sys/types.h>
#include <sys/stat.h>
#include <unistd.h>namespace yjlog
{namespace util{class Date{public:static time_t now(){return time(nullptr);}};class File{public:static bool exists(const std::string&pathname)      // 判断文件是否存在{       struct stat st;if(stat(pathname.c_str(),&st)<0)return false;return true;}static std::string path(const std::string&pathname)   // 获取文件所在路径{// ./abc/a.txtsize_t pos=pathname.find_last_of("/\\");if(pos==std::string::npos)return ".";return pathname.substr(0,pos+1);       // 截取0~最后一个'\'位置的字符}static void createDirectory(const std::string&pathname)     // 创建目录{// ./abc/bcd/a.txtsize_t pos=0,idx=0;      // pos标记'/'的位置, idx标记下次查找的起始位置while (pos < pathname.size()){pos = pathname.find_first_of("/\\",idx);if (pos == std::string::npos) // 不存在父级目录mkdir(pathname.c_str(), 0777);std::string parent_dir = pathname.substr(0, pos + 1); // 把'/'也截取上if (exists(parent_dir)==true)       // 存在父级目录{idx=pos+1;continue;}mkdir(parent_dir.c_str(),0777);    // 不存在父级目录idx=pos+1;}}};}
}
8.2 日志等级类设计

日志等级类主要功能:(1) 定义出日志系统所包含的所有日志等级 (2) 提供一个接口,将对应等级的枚举,转换成一个对应的字符串

日志等级总共分为7个等级,分别为:

  • UNKNOW 未知错误
  • DEBUG 进行debug时候打印日志的等级
  • INFO 打印一些用户提示信息
  • WARN 打印警告信息
  • ERROR 打印错误信息
  • FATAL 打印致命信息- 导致程序崩溃的信息
  • OFF 关闭所有日志输出

注:每一个项目中都会设置一个默认的日志输出等级,只有输出的日志等级大于等于默认限制等级的时候才可以进行输出

日志等级类设计在level.hpp中:

/*日志等级类的实现:1. 定义等级类, 枚举出日志等级2. 提供转换接口, 将日志转换成对应字符串
*/
#pragma oncenamespace yjlog
{class LogLevel{public:enum class Value   // 枚举类{UNKNOW=0,DEBUG,INFO,WARN,ERROR,FATAL,OFF       // 关闭等级};static const char*toString(yjlog::LogLevel::Value level){switch(level){case LogLevel::Value::DEBUG: return "DEBUG";case LogLevel::Value::INFO: return "INFO";case LogLevel::Value::WARN: return "WARN";case LogLevel::Value::ERROR: return "ERROR";case LogLevel::Value::FATAL: return "FATAL";case LogLevel::Value::OFF: return "OFF";}return "UNKNOW";}};
}
8.3 日志消息类设计

日志消息类主要是封装一条完整的日志消息所需的内容,其中包括日志等级、对应的logger name、打印日志源文件的位置信息(包括文件名和行号)、线程ID、时间戳信息、具体的日志信息等内容。

日志消息类设计在message.hpp中:

/*日志消息类的实现:  定义日志消息类, 进行日志中间信息的存储1.  日志输出时间       用于过滤日志输出时间2.  日志等级           用于进行日志过滤分析3.  源文件名称4.  源代码行号         用于定位出现错误的代码位置5.  线程ID             用于过滤出错的线程6.  日志主体消息7.  日志器名称 (当前支持多日志器的同时使用)
*/#pragma once
#include"util.hpp"
#include"level.hpp"
#include<thread>namespace yjlog
{struct LogMsg{time_t _ctime;             // 时间LogLevel::Value _level;    // 日志等级size_t _line;              // 行号std::thread::id _tid;      // 线程IDstd::string _file;         // 源文件名称std::string _logger;       // 日志器名称std::string _payload;      // 日志主体消息LogMsg(LogLevel::Value level, size_t line,const std::string& file, const std::string& logger,const std::string&msg):_ctime(util::Date::now()),_level(level),_line(line),_tid(std::this_thread::get_id()),_file(file),_logger(logger),_payload(msg){}};
}
8.4 日志输出格式化类设计

日志格式化模块,主要负责对日志消息进行格式化,组织成为指定格式的字符串。

(1) 日志格式化(Formatter)类主要负责格式化日志消息

  • 格式化字符串(_pattern):来控制日志的输出格式,让日志系统进行日志格式化更加灵活方便
    • %d 日期
    • %T 缩进
    • %t 线程id
    • %p 日志级别
    • %c 日志器名称
    • %f 文件名
    • %l 行号
    • %m 日志消息
    • %n 换行
  • 格式化子项数组(vector<FormatItem::ptr>_items):不同的格式化子项,会从日志消息中取出指定的元素,转换成为字符串

(2) FormatItem类主要负责日志消息子项的获取及格式化。其包含以下子类:

  • MsgFormatItem :表示要从LogMsg中取出有效日志数据
  • LevelFormatItem :表示要从LogMsg中取出日志等级
  • TimeFormatItem :表示要从LogMsg中取出时间戳并按照指定格式进行格式化
  • FileFormatItem :表示要从LogMsg中取出源码所在文件名
  • LineFormatItem:表示要从LogMsg中取出源码所在行号
  • ThreadFormatItem :表示要从LogMsg中取出线程ID
  • LoggerFormatItem :表示要从LogMsg中取出日志器名称
  • TabFormatItem :表示一个制表符缩进
  • NLineFormatItem :表示一个换行
  • OtherFormatItem :表示非格式化的原始字符串

举例:

在这里插入图片描述

具体实现方式是:定义一个抽象格式化子项父类,派生格式化子项子类,vector<FormatItem::ptr>_items,此格式化子项数组中存储父类的智能指针,当返回各种类型的子类的智能指针类型的数据时,使用父类就能够访问到各个子项子类函数(继承多态的思想)

日志输出格式化类设计在format.hpp中:

/*日志输出格式化类的实现: 对消息进行格式化, 组织成为指定格式的字符串(从日志消息中取出指定的元素, 追加到一块内存中)%d    日期%T    缩进%t    线程id%p    日志级别%c    日志器名称%f    文件名%l    行号%m    日志消息%n    换行
默认格式:[%d{%H:%M:%S}][%t][%c][%p][%f:%l]%T%m%n
举例:   [2003-08-16 12:38:26][1234567][root][FATAL][main.c:89]  创建套接字失败…\n
设计思想:1. 抽象格式化子项基类2. 基于基类, 派生出不同的格式化子项子类
*/#pragma once
#include"level.hpp"
#include"message.hpp"
#include"util.hpp"
#include<vector>
#include<sstream>
#include<cstdlib>
#include<cassert>namespace yjlog
{class FormatItem{public:using ptr=std::shared_ptr<FormatItem>;virtual void format(std::ostream&out, LogMsg&msg)=0;};class MsgFormatItem:public FormatItem    // 日志消息格式化子项{public:virtual void format(std::ostream&out, LogMsg&msg){out<<msg._payload;}};class TimeFormatItem:public FormatItem   // 日期格式化子项{public:TimeFormatItem(const std::string &time_fmt="%H:%M:%S"):_time_fmt(time_fmt){}virtual void format(std::ostream&out, LogMsg&msg){struct tm t;localtime_r(&msg._ctime,&t);               // 将生成的时间戳放入t结构体中char tmp[32]={0};                          // 存放格式化日期的字符串strftime(tmp,31,_time_fmt.c_str(),&t);     // 根据_time_fmt结构化规则,将tm结构体中的日期放入tmp中out<<tmp;}private:std::string _time_fmt;       // 给定日期格式};class LevelFormatItem:public FormatItem    // 日志等级格式化子项{public:virtual void format(std::ostream&out, LogMsg&msg){out<<LogLevel::toString(msg._level);}};class FileFormatItem:public FormatItem    // 文件名格式化子项{public:virtual void format(std::ostream&out, LogMsg&msg){out<<msg._file;}};class LineFormatItem:public FormatItem    // 行号格式化子项{public:virtual void format(std::ostream&out, LogMsg&msg){out<<msg._line;}};class ThreadFormatItem:public FormatItem    // 线程id格式化子项{public:virtual void format(std::ostream&out, LogMsg&msg){out<<msg._tid;}};class LoggerFormatItem:public FormatItem    // 日志器等级格式化子项{public:virtual void format(std::ostream&out, LogMsg&msg){out<<msg._logger;}};class TabFormatItem:public FormatItem       // 缩进格式化子项{public:virtual void format(std::ostream&out, LogMsg&msg){out<<"\t";}};class NLineFormatItem:public FormatItem     // 换行格式化子项{public:virtual void format(std::ostream&out, LogMsg&msg){out<<"\n";}};class OtherFormatItem:public FormatItem     // 其他格式化子项{public:OtherFormatItem(const std::string&str):_str(str){}// abcdefg[%d{%H}]virtual void format(std::ostream&out, LogMsg&msg){out<<_str;}private:std::string _str;};// 格式化子项类class Formatter{public:Formatter(const std::string&pattern="[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"):_pattern(pattern){assert(parsePattern());}// 对msg进行格式化void format(std::ostream&out,LogMsg&msg)   // 按照解析顺序逐个从msg中取出对应信息并对其进行格式化{for(auto&item:_items){item->format(out,msg);  // 对格式化子项数组中每个成员调用其format函数进行输出打印}}std::string format(LogMsg&msg){std::stringstream ss;        // stringstream类型转换format(ss,msg);return ss.str();}// 对格式化规则字符串进行解析(即把_pattern字符串的内容解析到_items中)bool parsePattern(){// abcde%%[%d{%H:%M:%S}][%p]%T%m%n// 1. 对格式化规则字符串进行解析std::vector<std::pair<std::string, std::string>> fmt_order;std::string key;   // 存放%后的格式化字符std::string val;   // 存放非格式化字符及格式化字符后{}中的子格式化字符size_t pos=0;while(pos<_pattern.size()){// 不是原始的格式化字符if(_pattern[pos]!='%')  {val.push_back(_pattern[pos++]);continue;}// 走下来代表pos位置是'%'字符, 需要判断是否是'%%'的情况if( pos+1<_pattern.size() && _pattern[pos+1]=='%'){val.push_back('%');pos+=2;             // 要跳过两个'%%'字符continue;}// 避免刚开始就出现格式化字符,添加key为空值的情况  %d%mif (!val.empty()){fmt_order.push_back(std::make_pair("", val)); // 添加非格式化字符val.clear();}// 这时pos走到%位置, 对格式化字符进行处理pos+=1;     // 这里代表pos走到'%'后面的格式化字符if (pos == _pattern.size()){std::cout<<"%之后没有对应的格式化字符\n";return false;}key = _pattern[pos];pos+=1;     // 这时候走到格式化字符后面的位置// 要判断是否存在{}中的子格式化字符if(pos<_pattern.size() && _pattern[pos]=='{'){pos+=1;     // pos指向'{'后面的位置while(pos<_pattern.size() && _pattern[pos]!='}')   // 截取{}中的子格式化字符{val.push_back(_pattern[pos++]);}// 走到末尾没有找到'}'字符, 则{}出现匹配错误if (pos == _pattern.size()){std::cout << "子规则{}匹配出错\n";return false;}pos += 1;      // 此时pos指向'}'位置, +1指向'}'后面的位置}fmt_order.push_back(std::make_pair(key,val));val.clear();key.clear();}// 2. 根据解析得到的数据初始化格式化子项数组成员for(auto&it:fmt_order){_items.push_back(createItem(it.first,it.second));}return true;}private:// 根据不同的格式化字符创建不同的格式化子项对象FormatItem::ptr createItem(const std::string &key, const std::string &val){//  abcdefg[%d{%H}]if(key=="d")  return std::make_shared<TimeFormatItem>(val);if(key=="t")  return std::make_shared<ThreadFormatItem>();if(key=="c")  return std::make_shared<LoggerFormatItem>();if(key=="p")  return std::make_shared<LevelFormatItem>();if(key=="f")  return std::make_shared<FileFormatItem>();if(key=="l")  return std::make_shared<LineFormatItem>();if(key=="m")  return std::make_shared<MsgFormatItem>();if(key=="n")  return std::make_shared<NLineFormatItem>();if(key=="T")  return std::make_shared<TabFormatItem>();if(key.empty())  return std::make_shared<OtherFormatItem>(val);std::cout<<"没有对应格式化字符: %"<<key<<std::endl;abort();return FormatItem::ptr();}private:std::string _pattern;                  // 格式化规则字符串std::vector<FormatItem::ptr> _items;   // 格式化子项数组};
}

难点讲解:parsePattern()

实现思路:

  1. 对格式化规则字符串进行解析。

    定义两个字符串key:存放%后的格式化字符; val: 存放非格式化字符及格式化字符后{}中的子格式化字符。循环解析,将每次解析的结果,以k,v的键值对形式放入fmt_order格式化解析结果数组中

规则:

  • 不以%起始的字符串都是原始字符串,处理思想:不是%,则一直向后走,直到遇到%,则是原始字符串的结束

  • 遇到%,看跟随其后的这个字符是否是%,若是就把’%%‘解析成单独’%'字符;不是则代表后面字符是格式化字符

  • 在格式化字符后面,有没有{,若有{,则{之后,{之前的数据是格式化字符的子格式;即{}之间的字符是格式化字符的子格式

  • 注意判断{}规则不匹配和%后不出现格式化字符的情况

  1. 根据解析得到的数据初始化格式化子项数组成员

调用createItem(),将fmt_order格式化解析结果数组中的成员放入格式化子项数组中

字符串解析过程举例:

在这里插入图片描述

代码实现:

// 对格式化规则字符串进行解析(即把_pattern字符串的内容解析到_items中)
bool parsePattern()
{// abcde%%[%d{%H:%M:%S}][%p]%T%m%n// 1. 对格式化规则字符串进行解析std::vector<std::pair<std::string, std::string>> fmt_order;std::string key; // 存放%后的格式化字符std::string val; // 存放非格式化字符及格式化字符后{}中的子格式化字符size_t pos = 0;while (pos < _pattern.size()){// 不是原始的格式化字符if (_pattern[pos] != '%'){val.push_back(_pattern[pos++]);continue;}// 走下来代表pos位置是'%'字符, 需要判断是否是'%%'的情况if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%'){val.push_back('%');pos += 2; // 要跳过两个'%%'字符continue;}// 避免刚开始就出现格式化字符,添加key为空值的情况  %d%mif (!val.empty()){fmt_order.push_back(std::make_pair("", val)); // 添加非格式化字符val.clear();}// 这时pos走到%位置, 对格式化字符进行处理pos += 1; // 这里代表pos走到'%'后面的格式化字符if (pos == _pattern.size()){std::cout << "%之后没有对应的格式化字符\n";return false;}key = _pattern[pos];pos += 1; // 这时候走到格式化字符后面的位置// 要判断是否存在{}中的子格式化字符if (pos < _pattern.size() && _pattern[pos] == '{'){pos += 1;                                             // pos指向'{'后面的位置while (pos < _pattern.size() && _pattern[pos] != '}') // 截取{}中的子格式化字符{val.push_back(_pattern[pos++]);}// 走到末尾没有找到'}'字符, 则{}出现匹配错误if (pos == _pattern.size()){std::cout << "子规则{}匹配出错\n";return false;}pos += 1; // 此时pos指向'}'位置, +1指向'}'后面的位置}fmt_order.push_back(std::make_pair(key, val));val.clear();key.clear();}// 2. 根据解析得到的数据初始化格式化子项数组成员for (auto &it : fmt_order){_items.push_back(createItem(it.first, it.second));}return true;
}

代码测试:

int main()
{yjlog::LogMsg msg(yjlog::LogLevel::Value::INFO,53,"main.cc","root","格式化测试...");yjlog::Formatter fmt("abcd%%[%d{%H:%M:%S}][%t][%p][%c][%f:%l]%T%m%n");std::string str=fmt.format(msg);std::cout<<str<<std::endl;
}

运行结果:
在这里插入图片描述

8.5 日志落地(LogSink)类设计(简单工厂模式)
  • 功能:将格式化完成后的日志消息字符串,输出到指定的位置

  • 扩展:

    • 支持同时将日志落地到不同的位置

      • 位置分类:

        1. 标准输出

        2. 指定文件(事后进行日志分析)

        3. 滚动文件(文件按照时间/大小进行滚动切换)

    • 支持落地方向的扩展

      • 用户可以自己编写一个新的落地模块,将日志进行其他方向的落地
  • 实现思想:

    1. 抽象出落地模块类
    2. 不同落地方向从基类进行派生
    3. 使用工厂模式进行创建与表示的分离

日志输出格式化类设计在sink.hpp中:

/*日志落地类的实现: 将格式化完成后的日志消息字符串, 输出大指定的位置1. 抽象出落地基类2. 不同落地方向从基类进行派生: <1> 标准输出  <2> 指定文件(事后进行日志分析)   <3> 滚动文件(文件按照时间/大小进行滚动切换)3. 使用工厂模式进行创建与分离
*/#pragma once
#include<iostream>
#include<memory>
#include<string>
#include<fstream>
#include<cassert>
#include<sstream>
#include"util.hpp"namespace yjlog
{class LogSink{public:using ptr=std::shared_ptr<LogSink>;virtual ~LogSink(){};virtual void log(const char*data, size_t len)=0; // data: 字符串起始位置 len: 长度};// 落地方向: 标准输出class StdoutSink:public LogSink{public:// 将日志消息写到标准输出virtual void log(const char*data, size_t len){std::cout.write(data,len);}};// 落地方向: 指定文件(在文件方向时中 每一次写文件都直接写入(不是写一次就打开一次), 所以构造函数时打开文件)class FileSink:public LogSink{public:// 构造时传入文件名, 并打开文件, 将文件操作句柄管理起来FileSink(const std::string&pathname):_pathname(pathname){// 1. 创建日志文件所在的目录util::File::createDirectory(util::File::path(_pathname));// 2. 创建并打开日志文件_ofs.open(_pathname,std::ofstream::binary | std::ofstream::app);   // 二进制打开方式并追加assert(_ofs.is_open());    // 判断文件是否被打开}// 将日志消息写到指定文件virtual void log(const char*data, size_t len){_ofs.write(data,len);assert(_ofs.good());     // 判断上面的写入是否出错}private:std::string _pathname;   // 文件路径名, 如./abc/a.txtstd::ofstream _ofs;};// 落地方向: 滚动文件:以文件大小进行滚动切换class RollBySizeSink:public LogSink{public:// 构造时传入文件名, 并打开文件, 将文件操作句柄管理起来RollBySizeSink(std::string basename, size_t max_size):_basename(basename),_max_fsize(max_size),_cur_fsize(0),_name_count(0){std::string pathname=createNewFile();     // 真正的文件名: 文件基础名 + 扩展名// 1. 创建日志文件所在的目录util::File::createDirectory(util::File::path(pathname));// 2. 创建并打开日志文件_ofs.open(pathname,std::ofstream::binary | std::ofstream::app);assert(_ofs.is_open());}// 将日志消息写到滚动文件中---写入前判断文件大小, 超过了最大大小就要切换文件virtual void log(const char*data, size_t len){if(_cur_fsize>=_max_fsize){_ofs.close();      // 关闭原来的文件(否则会文件描述符泄露)std::string pathname=createNewFile();_ofs.open(pathname, std::ofstream::binary | std::ofstream::app);assert(_ofs.is_open());_cur_fsize=0;      // 每次切换完后, 当前文件大小清0}_ofs.write(data,len);assert(_ofs.good());_cur_fsize+=len;}private:// 进行大小判断, 超过指定大小则创建新文件std::string createNewFile(){// 获取系统时间, 以时间构造扩展名time_t t=util::Date::now();struct tm lt;localtime_r(&t,&lt);std::stringstream filename;filename<<_basename;filename<<lt.tm_year+1900;filename<<lt.tm_mon+1;filename<<lt.tm_mday;filename<<lt.tm_hour;filename<<lt.tm_min;filename<<lt.tm_sec;filename<<"-";filename<<_name_count++;filename<<".log";return filename.str();}private:// 通过基础文件名 + 扩展文件名(以时间生成)组成一个实际的当前输出文件名std::string _basename;   // ./logs/base-    ->  ./logs/base/-20020702153124.logstd::ofstream _ofs;size_t _max_fsize;          // 记录文件最大大小, 当前文件超过了这个大小就要切换文件size_t _cur_fsize;          // 记录当前文件已经写入的数据大小size_t _name_count;         // 名称计数器, 防止1s内出现的文件比较多};// 工厂模式: 通过模板参数类型SinkType来创建对象, 问题: 各个构造函数参数不同, 通过不定参函数来解决class SinkFactory{public:template<class SinkType, class...Args>        // 函数模板static LogSink::ptr create(Args&&...args){return std::make_shared<SinkType>(std::forward<Args>(args)...);}};
}

要点:

  1. 关于滚动文件类中,切换文件注意点:
  • 每次切换完文件后,需要将当前大小_cur_fsize清0
  • 每次向滚动文件中写入时,当前大小加上写入大小,即_cur_fsize+=len
  • 类中增加名称计数器(_name_count), 创建新文件时文件名后添加计数器。防止1s内产生大量文件名, 出现重复,无法及时创建大量文件

未添加计数器前:
在这里插入图片描述

添加计数器后:

在这里插入图片描述

  1. 使用简单工厂模式组织上面各个类时,我们实现成函数模板 + 可变参数列表形式,便于我们在不修改此类源码的基础上增添新的落地方向

在这里插入图片描述

代码测试:

#include"sink.hpp"int main()
{// 日志落地测试yjlog::LogMsg msg(yjlog::LogLevel::Value::INFO,53,"main.cc","root","格式化测试...");yjlog::Formatter fmt;std::string str=fmt.format(msg);yjlog::LogSink::ptr stdout_lsp=yjlog::SinkFactory::create<yjlog::StdoutSink>();yjlog::LogSink::ptr file_lsp=yjlog::SinkFactory::create<yjlog::FileSink>		("./logfile/test.log");yjlog::LogSink::ptr roll_lsp=yjlog::SinkFactory::create<yjlog::RollBySizeSink>("./logfile/roll-",1024*1024);stdout_lsp->log(str.c_str(),str.size());file_lsp->log(str.c_str(),str.size());size_t cursize=0;size_t i=0;while(cursize<1024*1024*10)    // 向文件写入10MB数据{std::string tmp=str+std::to_string(i++);roll_lsp->log(tmp.c_str(),tmp.size());cursize+=tmp.size();}
}

测试结果:

  1. 标准输出:

在这里插入图片描述

  1. 指定文件:

在这里插入图片描述

  1. 滚动文件:

在这里插入图片描述

补充

一个以时间作为日志文件滚动切换的日志落地模块(滚动文件以时间段进行切换)

实现思想:

  • 以当前系统时间来取模时间段大小,得到当前时间段是第几个时间段(_cur_gap)

  • 每次以当前系统时间取模,判断与当前文件时间段是否一致,不一致则代表不是一个时间段

	// 落地方向: 滚动文件: 以时间进行滚动切换/*实现思想:以当前系统时间段来取模, 得到当前时间段是第几个时间段每次以系统时间取模, 判断与当前文件时间段是否一致, 不一致则代表不是一个时间段*/enum class TimeGap{GAP_SECOND,GAP_MINUTE,GAP_HOUR,GAP_DAY};class RollByTimeSink:public LogSink{public:// 构造时传入文件名, 并打开文件, 将文件操作句柄管理起来RollByTimeSink(const std::string&basename, TimeGap gap_type):_basename(basename){switch(gap_type){case TimeGap::GAP_SECOND: _gap_size=1; break;case TimeGap::GAP_MINUTE: _gap_size=60; break;case TimeGap::GAP_HOUR: _gap_size=3600; break;case TimeGap::GAP_DAY: _gap_size=3600*24; break;}// 注意: 任何数取模1都是0_cur_gap =_gap_size == 1?util::Date::now():_cur_gap%util::Date::now();    // 获取当前时间段// 0. 创建新文件std::string filename=createNewFile();// 1. 创建日志文件所在的目录util::File::createDirectory(util::File::path(filename));// // 2. 创建并打开日志文件_ofs.open(filename,std::ofstream::binary | std::ofstream::app);   // 二进制打开方式并追加assert(_ofs.is_open());    // 判断文件是否被打开}// 将日志消息写到滚动文件中virtual void log(const char*data, size_t len){time_t cur=util::Date::now();if(cur%_gap_size != _cur_gap){_ofs.close();      // 关闭原来的文件// 0. 创建新文件std::string filename = createNewFile();// 1. 创建日志文件所在的目录util::File::createDirectory(util::File::path(filename));// // 2. 创建并打开日志文件_ofs.open(filename, std::ofstream::binary | std::ofstream::app);  // 二进制打开方式并追加assert(_ofs.is_open());    // 判断文件是否被打开}_ofs.write(data,len);assert(_ofs.good());     // 判断上面的写入是否出错}private:// 进行大小判断, 超过指定大小则创建新文件std::string createNewFile(){// 获取系统时间, 以时间构造扩展名time_t t=util::Date::now();struct tm lt;localtime_r(&t,&lt);std::stringstream filename;filename<<_basename;filename<<lt.tm_year+1900;filename<<lt.tm_mon+1;filename<<lt.tm_mday;filename<<lt.tm_hour;filename<<lt.tm_min;filename<<lt.tm_sec;filename<<".log";return filename.str();}private:std::string _basename;   // 文件路径名, 如./abc/a.txtstd::ofstream _ofs;size_t _cur_gap;         // 当前是第几个时间段size_t _gap_size;        // 时间段的大小};

代码测试:

int main()
{// 日志落地测试yjlog::LogMsg msg(yjlog::LogLevel::Value::INFO,53,"main.cc","root","格式化测试...");yjlog::Formatter fmt;std::string str=fmt.format(msg);yjlog::LogSink::ptr time_lsp=yjlog::SinkFactory::create<yjlog::RollByTimeSink>("./logfile/roll-",yjlog::TimeGap::GAP_SECOND);time_t old=yjlog::util::Date::now();while(yjlog::util::Date::now() < old+5)    {time_lsp->log(str.c_str(),str.size());}
}

测试结果:

在这里插入图片描述

8.6 日志器类(Logger)设计(建造者模式)
  • 功能:对前边所有模块进行整合,向外提供接口完成不同等级日志的输出

  • 管理的成员:

    1. 格式化模块对象
    2. 落地模块数组(一个日志器可能会向多个位置进行日志输出)
    3. 默认的日志输出限制等级(大于等于限制等级的日志才能输出
    4. 互斥锁(保证日志输出过程是线程安全的,不会出现交叉日志)
    5. 日志器名称(日志器的唯一标识,便于查找)
  • 提供的操作:分别会封装日志消息LogMsg — 各个接口日志等级不同

    • debug等级日志的输出操作
    • info等级日志的输出操作
    • warn等级日志的输出操作
    • error等级日志的输出操作
    • fatal等级日志的输出操作
  • 实现:

    1. 抽象Logger基类(派生出同步日志器类 && 异步日志器类)
    2. 有两种不同的日志器,只有落地方向不同,因此将落地操作给抽象出来。即不同的日志器调用各自的落地操作进行日志落地
    3. 模块关联中使用基类指针对子类日志器对象进行日志管理和操作

日志器类设计在logger.hpp中:

/*1. 抽象日志器基类2. 派生出不同的子类 (同步日志器  & 异步日志器)
*/#pragma once
#include"util.hpp"
#include"level.hpp"
#include"format.hpp"
#include"sink.hpp"
#include<cstdarg>
#include<cstdlib>
#include<atomic>
#include<mutex>namespace yjlog
{class Logger{public:using ptr=std::shared_ptr<Logger>;Logger(const std::string&logger_name, LogLevel::Value level, Formatter::ptr&formatter, std::vector<LogSink::ptr> &sinks):_logger_name(logger_name),_limit_level(level),_formatter(formatter),_sinks(sinks.begin(),sinks.end()){}// 完成构造日志对象过程并进行格式化, 得到格式化后的日志消息字符串---然后进行落地输出void debug(const std::string &file,size_t line,const std::string&fmt,...)  // 根据fmt取出后边的不定参数据,组织成消息{// 通过传入的参数构造出一个日志消息对象, 进行日志的格式化, 最终落地// 1. 判断当前日志是否达到输出等级if(LogLevel::Value::DEBUG < _limit_level)return;// 2. 对fmt格式化字符串和不定参进行字符串组织, 得到日志消息的字符串va_list ap;va_start(ap,fmt);char*res;int ret=vasprintf(&res,fmt.c_str(),ap); // 通过ap指针根据fmt组织好数据后放入res中if(ret==-1){std::cout<<"vasprintf failed!\n";return;}va_end(ap);      // 将ap指针置空// 3. 格式化处理serialize(LogLevel::Value::DEBUG, file,line,res);// 4. 释放空间free(res);}void info(const std::string &file,size_t line,const std::string&fmt,...){// 通过传入的参数构造出一个日志消息对象, 进行日志的格式化, 最终落地// 1. 判断当前日志是否达到输出等级if(LogLevel::Value::INFO < _limit_level)return;// 2. 对fmt格式化字符串和不定参进行字符串组织, 得到日志消息的字符串va_list ap;va_start(ap,fmt);char*res;int ret=vasprintf(&res,fmt.c_str(),ap); // 通过ap指针根据fmt组织好数据后放入res中if(ret==-1){std::cout<<"vasprintf failed!\n";return;}va_end(ap);      // 将ap指针置空// 3. 格式化处理serialize(LogLevel::Value::INFO, file,line,res);// 4. 释放空间free(res);}void warn(const std::string &file,size_t line,const std::string&fmt,...){// 通过传入的参数构造出一个日志消息对象, 进行日志的格式化, 最终落地// 1. 判断当前日志是否达到输出等级if(LogLevel::Value::WARN < _limit_level)return;// 2. 对fmt格式化字符串和不定参进行字符串组织, 得到日志消息的字符串va_list ap;va_start(ap,fmt);char*res;int ret=vasprintf(&res,fmt.c_str(),ap); // 通过ap指针根据fmt组织好数据后放入res中if(ret==-1){std::cout<<"vasprintf failed!\n";return;}va_end(ap);      // 将ap指针置空// 3. 格式化处理serialize(LogLevel::Value::WARN, file,line,res);// 4. 释放空间free(res);}void error(const std::string &file,size_t line,const std::string&fmt,...){// 通过传入的参数构造出一个日志消息对象, 进行日志的格式化, 最终落地// 1. 判断当前日志是否达到输出等级if(LogLevel::Value::ERROR < _limit_level)return;// 2. 对fmt格式化字符串和不定参进行字符串组织, 得到日志消息的字符串va_list ap;va_start(ap,fmt);char*res;int ret=vasprintf(&res,fmt.c_str(),ap); // 通过ap指针根据fmt组织好数据后放入res中if(ret==-1){std::cout<<"vasprintf failed!\n";return;}va_end(ap);      // 将ap指针置空// 3. 格式化处理serialize(LogLevel::Value::ERROR, file,line,res);// 4. 释放空间free(res);}void fatal(const std::string &file,size_t line,const std::string&fmt,...){// 通过传入的参数构造出一个日志消息对象, 进行日志的格式化, 最终落地// 1. 判断当前日志是否达到输出等级if(LogLevel::Value::FATAL < _limit_level)return;// 2. 对fmt格式化字符串和不定参进行字符串组织, 得到日志消息的字符串va_list ap;va_start(ap,fmt);char*res;int ret=vasprintf(&res,fmt.c_str(),ap); // 通过ap指针根据fmt组织好数据后放入res中if(ret==-1){std::cout<<"vasprintf failed!\n";return;}va_end(ap);      // 将ap指针置空// 3. 格式化处理serialize(LogLevel::Value::FATAL, file,line,res);// 4. 释放空间free(res);} protected:void serialize(LogLevel::Value level, const std::string &file,size_t line,char*str){// 3. 构造LogMsg对象LogMsg msg(level,line,file,_logger_name,str);// 4. 通过格式化工具对LogMsg进行格式化, 得到格式化后的日志字符串std::stringstream ss;_formatter->format(ss,msg);// 5. 进行日志落地log(ss.str().c_str(),ss.str().size());}// 抽象接口完成实际的落地输出 --- 不同的日志器会有不同的实际落地方式virtual void log(const char*data,size_t len)=0;protected:std::mutex _mtx;          // 保证日志落地时线程安全问题std::string _logger_name;std::atomic<LogLevel::Value> _limit_level;Formatter::ptr _formatter;std::vector<LogSink::ptr> _sinks;      // 日志落地方向数组};class SyncLogger : public Logger{public:SyncLogger(const std::string &logger_name, LogLevel::Value level,Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks):Logger(logger_name,level,formatter,sinks){}protected:// 同步日志器, 是将日志直接通过落地模块句柄进行日志落地virtual void log(const char *data, size_t len){std::unique_lock<std::mutex> lock(_mtx);if (_sinks.empty())return;for (auto &sink : _sinks){sink->log(data, len);}}};
}

代码测试:

#include"util.hpp"
#include"level.hpp"
#include"message.hpp"
#include"format.hpp"
#include"sink.hpp"
#include"logger.hpp"int main()
{// 同步写日志测试std::string logger_name="sync_logger";yjlog::LogLevel::Value limit=yjlog::LogLevel::Value::WARN;yjlog::Formatter::ptr fmt(new yjlog::Formatter("[%d{%H:%M:%S}][%c][%f:%l][%p]%T%m%n"));yjlog::LogSink::ptr stdout_lsp=yjlog::SinkFactory::create<yjlog::StdoutSink>();yjlog::LogSink::ptr file_lsp=yjlog::SinkFactory::create<yjlog::FileSink>("./logfile/test.log");yjlog::LogSink::ptr roll_lsp=yjlog::SinkFactory::create<yjlog::RollBySizeSink>("./logfile/roll-",1024*1024);std::vector<yjlog::LogSink::ptr> sinks={stdout_lsp,file_lsp,roll_lsp};yjlog::Logger::ptr logger(new yjlog::SyncLogger(logger_name,limit,fmt,sinks));logger->debug(__FILE__,__LINE__,"%s","测试日志");logger->info(__FILE__,__LINE__,"%s","测试日志");logger->warn(__FILE__,__LINE__,"%s","测试日志");logger->error(__FILE__,__LINE__,"%s","测试日志");logger->fatal(__FILE__,__LINE__,"%s","测试日志");size_t cursize=0,i=0;while(cursize<1024*1024*10)    // 向文件写入10MB数据{logger->fatal(__FILE__,__LINE__,"测试日志-%d",i++);cursize+=20;}
}

测试结果:

  1. 标准输出:

在这里插入图片描述

  1. 滚动文件和指定文件

在这里插入图片描述
在这里插入图片描述

建造者模式的使用

上面日志器类的测试中,我们是直接让用户去构造日志器比较复杂。下面在日志器中使用建造者模式,将不同类型的日志器的创建放入到一个日志器建造者类中完成

实现思路:

  1. 抽象一个日志器建造者类 (完成日志器对象所需零部件的构建 & 日志器的构建)
  2. 派生出具体的建造者类—局部日志的建造者 & 全局日志的建造者 (后边添加了全局单例管理器之后, 将日志器添加全局管理)
    /*使用建造者模式来建造日志器, 而不要让用户直接去构造日志器, 简化用户的使用复杂度*/// 1. 抽象一个日志器建造者类 (完成日志器对象所需零部件的构建 & 日志器的构建)//  <1> 设置日志器类型//  <2> 将不同类型日志器的创建放入到一个日志器建造者类中完成enum class LoggerType{LOGGER_SYNC,LOGGER_ASYNC};class LoggerBuilder{public:LoggerBuilder():_logger_type(LoggerType::LOGGER_SYNC),_limit_level(LogLevel::Value::DEBUG){}void buildLoggerType(LoggerType logger_type=LoggerType::LOGGER_SYNC){_logger_type =logger_type;}void buildLoggername(const std::string&logger_name){_logger_name=logger_name;}void buildLoggerLevel(LogLevel::Value level){_limit_level=level;}void buildFormatter(const std::string&pattern) // 让用户设定格式, 我们自己完成对象的实例化{_formatter=std::make_shared<Formatter>(pattern);}// 存在多个方向的落地方式---你提供日志器类型, 我来创建具体日志落地器(函数模板)template<class SinkType, class...Args>void buildSink(Args &&...args){LogSink::ptr psink=SinkFactory::create<SinkType>(std::forward<Args>(args)...);_sinks.push_back(psink);}virtual Logger::ptr build()=0;protected:LoggerType _logger_type;std::string _logger_name;                   // 日志器名称LogLevel::Value _limit_level;               // 限制等级Formatter::ptr _formatter;                  // 格式化对象std::vector<yjlog::LogSink::ptr> _sinks;    // 日志落地方向数组};// 2. 派生出具体的建造者类---局部日志的建造者  & 全局日志的建造者 (后边添加了全局单例管理器之后, 将日志器添加全局管理)class LocalLoggerBuilder:public LoggerBuilder{public:virtual Logger::ptr build(){assert(!_logger_name.empty());      // 必须要有日志器名称if(_formatter.get() == nullptr)     // 空, 就构造默认的_formatter=std::make_shared<Formatter>();if(_sinks.empty())buildSink<StdoutSink>();if(_logger_type==yjlog::LoggerType::LOGGER_ASYNC){}return 	std::make_shared<SyncLogger> 	(_logger_name,_limit_level,_formatter,_sinks);}};

测试代码:

#include"logger.hpp"int main()
{// 建造者模式日志器类测试(logger.hpp)std::unique_ptr<yjlog::LoggerBuilder> builder(new yjlog::LocalLoggerBuilder());builder->buildLoggername("sync_logger");builder->buildLoggerLevel(yjlog::LogLevel::Value::WARN);builder->buildFormatter("%m%n");builder->buildLoggerType(yjlog::LoggerType::LOGGER_SYNC);builder->buildSink<yjlog::FileSink>("./logfile/test.log");builder->buildSink<yjlog::StdoutSink>();yjlog::Logger::ptr logger=builder->build();logger->debug(__FILE__,__LINE__,"%s", "测试日志");logger->info(__FILE__,__LINE__,"%s", "测试日志");logger->warn(__FILE__,__LINE__,"%s", "测试日志");logger->error(__FILE__,__LINE__,"%s", "测试日志");logger->fatal(__FILE__,__LINE__,"%s", "测试日志");size_t cursize=0,count=0;  while(cursize<1024*1024*10)    // 一共写入10M数据{logger->fatal(__FILE__,__LINE__,"测试日志-%d", count++);cursize+=50;}
}

是否使用建造者模式对比:

在这里插入图片描述

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

思想:避免写日志的过程阻塞,导致业务线程在写日志的时候影响效率。异步的思想:不让业务线程进行日志的实际落地操作,而是将日志消息放到缓冲区(一块指定内存)中,接下来有一个专门的异步线程,去针对缓冲区的数据进行处理(实际的落地操作)

在这里插入图片描述

实现:

  1. 实现一个线程安全的缓冲区
  2. 创建一个异步工作线程,专门负责缓冲区中日志消息的落地操作
8.7.1 异步缓冲区类设计

缓冲区设计思想提出

  • 使用队列,缓存日志消息,逐条处理

    • 问题:直接使用STL中的队列,此队列使用链表实现。使用此队列必然会涉及到频繁地添加释放节点,会造成空间的频繁申请与释放,降低效率
    • 优化:使用一个环形队列,提前将空间申请好,对空间循环利用
  • 同时这个缓冲区的操作,会涉及到多线程,必须要保证线程安全

    • 线程安全实现:对于缓冲区的读写加锁。在实际开发中,写日志操作并不会分配太多资源,所以工作线程只需要一个日志器就行。涉及到的锁冲突:生产者与生产者互斥生产者与消费者互斥
    • 问题:设计成环形队列,锁冲突严重,生产者与生产者,生产者与消费者存在互斥关系
    • 解决方案:双缓冲区
    • 优点:虽然同时多线程写入也会冲突,但是减少了生产者与消费者之间的锁冲突。在异步线程将任务处理缓冲区中的任务处理完毕后,交换缓冲区只有在交换的时候,生产者与消费者才会产生一次锁冲突

在这里插入图片描述

单个缓冲区的设计

  • 思想:不存放LogMsg对象,直接存放格式化后的日志消息字符串

    • 好处:
      • 减少了LogMsg对象频繁地构造的消耗
      • 可以针对缓冲区中的日志消息,一次性进行IO操作,减少IO次数,提高效率
  • 具体设计:

    1. 管理一个存放字符串数据的缓冲区(使用vector进行空间管理)
    2. 当前的写入数据位置的指针(指向可写区域的起始位置,避免数据的写入覆盖)
    3. 当前的读取数据位置的指针(指向读取数据区域的起始位置,当读取指针与写入指针指向相同位置表示据读取完了
  • 提供操作:

    1. 向缓冲区中写入数据
    2. 获取可读数据起始地址的接口(不提供读取数据的接口,因为读取相当于把数据拷贝一次)
    3. 获取可读写数据长度的接口
    4. 移动读写位置的接口
    5. 初始化缓冲区的操作(将一个缓冲区所有数据处理完毕后,把读写位置初始化
    6. 提供交换缓冲区的操作(交换空间地址,并不交换空间数据

可读写数据长度的计算:

在这里插入图片描述

异步缓冲区类设计在buffer.hpp中:

/*异步缓冲区类的实现:为了避免因为写日志的过程阻塞, 导致业务线程在写日志的时候影响效率因此提出异步的思想: 不让业务线程进行日志的实际落地, 而是将日志消息放到缓冲区(一块特定的内存)中,接下来有一个专门的异步线程, 去针对缓冲区中的数据进行处理(实际的落地操作)实现:   1. 一个线程安全的缓冲区(vector的双缓冲区)   2. 创建一个异步工作线程, 专门负责缓冲区中日志消息的落地操作
*/
#pragma once
#include<vector>
#include<cassert>#define DEFAULT_BUFFER_SIZE (1*1024*1024)      // 缓冲区的初始容量
#define THRESHOLD_BUFFER_SIZE (8*1024*1024)    // 扩容阈值
#define INCREMENT_BUFFER_SIZE (1*1024*1024)    // 线性增长扩容namespace yjlog
{class Buffer{public:Buffer():_buffer(DEFAULT_BUFFER_SIZE),_reader_idx(0),_writer_idx(0){}// 向缓冲区写入数据void push(const char*data, size_t len){// 0. 考虑缓冲区剩余空间不足的情况: (1) 直接返回  (2) 扩容// (1) 固定大小, 直接返回// if(len>writeAbleSize())//     return;// (2) 动态空间, 用于极限测试 --- 扩容ensureEnoughSize(len);// 1. 将数据拷贝进缓冲区std::copy(data,data+len,&_buffer[_writer_idx]);// 2. 当前写入位置向后偏移moveWriter(len);}   // 返回缓冲区数据的起始地址const char*begin(){return &_buffer[_reader_idx];}           // 返回可读数据的长度size_t readAbleSize(){// 当前设计的缓冲区是双缓冲区, 处理完就交换, 不存在空间循环利用return (_writer_idx-_reader_idx);  }//  返回可写数据的长度size_t writeAbleSize(){// 对于扩容: 不存在可写空间大小, 因为总是可写// 因此这个接口针对固定大小缓冲区提供return (_buffer.size()-_writer_idx);}// 对读写指针向后偏移操作void moveWriter(size_t len){assert(_writer_idx+len<=_buffer.size());_writer_idx+=len;}void moveReader(size_t len){assert(len<=readAbleSize());_reader_idx+=len;}// 重置读写位置, 初始化缓冲区void reset(){_writer_idx=0;      // 缓冲区所有空间都是空闲的_reader_idx=0;      // 与_writer_idx相等表示没有数据可读}// 对Buffer实现交换操作void swap(Buffer&buffer){_buffer.swap(buffer._buffer);std::swap(_reader_idx,buffer._reader_idx);std::swap(_writer_idx,buffer._writer_idx);}// 判断缓冲区是否为空bool empty(){return (_reader_idx == _writer_idx);}private:   // 对空间进行扩容void ensureEnoughSize(size_t len){if(len<=writeAbleSize())   // 不需要扩容return;// 这里扩容时new_size后+len原因: 防止2倍扩容或线性扩容后出现: len > 当前容量size_t new_size=0;if(_buffer.size()<THRESHOLD_BUFFER_SIZE)            // 小于阈值, 翻倍增长new_size=_buffer.size()*2+len;else                                                // 否则线性增长new_size=_buffer.size()+INCREMENT_BUFFER_SIZE+len;_buffer.resize(new_size);}private:std::vector<char> _buffer;  // 缓冲区(<1>不使用string: 每一条日志消息是以\0结束的, string中\0代表消息结束 <2> vector可以存储任意类型)size_t _reader_idx;         // 当前可读数据的指针---本质是下标size_t _writer_idx;         // 当前可写数据的指针};
}

代码测试:

  • 思路:读取文件数据, 一点点写入缓冲区中, 最终把缓冲区数据写入文件中, 判断生成的文件与源文件是否一致
    • 判断是否与源文件大小一致 ls -lh
    • 判断是否与源文件内容一致 md5sum 文件名
int main()
{// 异步缓冲区类的测试(buffer.hpp)// 思路: 读取文件数据, 一点点写入缓冲区中, 最终把缓冲区数据写入文件中, 判断生成的文件与源文件是否一致(md5)std::ifstream ifs("./logfile/test.log", std::ios::binary);if(ifs.is_open()==false)return -1;ifs.seekg(0,std::ios::end);      // 读写位置跳转到文件末尾size_t fsize=ifs.tellg();        // tellg获取当前读写位置相对于起始位置的偏移量(这里是文件末尾相对于起始位置)ifs.seekg(0,std::ios::beg);      // 重新跳转到起始位置std::string body;body.resize(fsize);ifs.read(&body[0],fsize);        // 把数据读取到body中if(ifs.good()==false){std::cout<<"read error\n";return -1;}ifs.close();// 把body数据放入缓冲区中yjlog::Buffer buffer;for(int i=0;i<body.size();++i){buffer.push(&body[i],1);}// 把缓冲区数据写入文件中std::ofstream ofs("./logfile/tmp.log",std::ios::binary);int read_sz=buffer.readAbleSize();for(int i=0;i<read_sz;++i){ofs.write(buffer.begin(),1);if (ofs.good() == false){std::cout << "write error!\n";return -1;}buffer.moveReader(1);}ofs.close();
}

运行结果:新文件与源文件大小相等且哈希值相同(内容相同)

在这里插入图片描述

8.7.2 异步工作器类设计

思想:双缓冲区思想

  • 外界将任务数据添加到输入缓冲区中,异步线程对处理缓冲区中的数据进行处理,若处理缓冲区中没有数据则交换缓冲区

实现:

  • 管理成员:

    • 双缓冲区(生产,消费)
    • 互斥锁(保证线程安全)
    • 条件变量(生产 && 消费)[生产缓冲区没有数据,处理完消费缓冲区数据后就休眠]
    • 回调函数(针对缓冲区中数据的处理接口—外界传入一个函数,告诉异步工作器数据该如何处理
  • 提供操作:

    • 停止异步工作器
    • 添加数据到缓冲区
  • 私有操作:

    • 创建线程;线程入口函数;交换缓冲区;对消费缓冲区数据使用回调函数进行处理,处理完后再次交换

异步工作器类设计在lopper.hpp中:

#pragma once
#include<mutex>
#include"buffer.hpp"
#include<condition_variable>
#include<thread>
#include<functional>
#include<memory>namespace yjlog
{using Functor=std::function<void(Buffer&)>;enum class AsyncType{ASYNC_SAFE,     // 安全状态, 表示缓冲区满了则阻塞, 避免资源耗尽的风险ASYNC_UNSAFE    // 不考虑资源耗尽的问题, 无限扩容, 常用于测试};class AsyncLopper{public:using ptr=std::shared_ptr<AsyncLopper>;AsyncLopper(const Functor&cb, AsyncType lopper_type=AsyncType::ASYNC_SAFE):_stop(false),_thread(std::thread(&AsyncLopper::threadEntry,this)),_callback(cb),_lopper_type(lopper_type){}~AsyncLopper(){stop();}void stop(){_stop=true;                    // 将退出标志设置为true_cond_consumer.notify_all();   // 唤醒所有工作线程_thread.join();                // 等待工作线程退出}void push(const char*data,size_t len){// 两种形式:  1. 无限扩容-非安全   2. 固定大小-生产缓冲区中充满数据就阻塞std::unique_lock<std::mutex> lock(_mutex);// wait表示: 在特定条件变量下等待, 满足条件则唤醒// 条件变量来控制, 若缓冲区剩余空间大小大于数据长度, 则可以添加数据if(_lopper_type==AsyncType::ASYNC_SAFE)_cond_producer.wait(lock,[&](){return _producer_buf.writeAbleSize()>=len;});// 可以走下来代表满足条件, 可以向缓冲区中添加数据_producer_buf.push(data,len);// 唤醒消费者对缓冲区的数据进行处理_cond_consumer.notify_one();}private:// 线程入口函数---对消费缓冲区中的数据进行处理, 处理完毕后, 初始化缓冲区, 交换缓冲区void threadEntry(){while(1){// 1. 判断生产缓冲区中是否有数据, 有则交换, 无则阻塞// 为互斥锁设置一个生命周期, 当缓冲区数据交换完毕后就解锁, 防止串行化(并不对数据的处理过程加锁保护){std::unique_lock<std::mutex> lock(_mutex);// 退出标志被设置, 且生产缓冲区已无数据, 这时候在退出, 否则可能会造成生产缓冲区中有数据, 但是没有被完全处理if(_stop && _producer_buf.empty())break;// 若是当前退出前被唤醒,或者有数据被唤醒,则返回真,继续向下运行, 重新陷入休眠_cond_consumer.wait(lock, [&](){ return _stop || !_producer_buf.empty();});_producer_buf.swap(_consumer_buf);// 2. 唤醒生产者(是安全状态才可能阻塞)if(_lopper_type==AsyncType::ASYNC_SAFE)_cond_producer.notify_all();}// 3. 被唤醒后, 对消费缓冲区进行数据处理_callback(_consumer_buf);// 4. 初始化消费缓冲区_consumer_buf.reset();}} private:Functor _callback;      // 具体对缓冲区数据进行处理的回调函数, 由异步工作器使用者传入private:bool _stop;             // 工作器停止标志Buffer _producer_buf;   // 生产缓冲区Buffer _consumer_buf;   // 消费缓冲区std::mutex _mutex;std::condition_variable _cond_producer;std::condition_variable _cond_consumer;std::thread _thread;    // 异步工作器对应的工作线程AsyncType _lopper_type;};
}
8.8 异步日志器(AsyncLogger)设计

设计:

  • 继承于Logger日志器类
    • 对于写日志操作进行函数重写(不再将数据直接写入文件,而是通过异步消息处理器,放到缓冲区中)
  • 通过异步消息处理器,进行日志数据的实际落地

管理的成员:

  • 异步工作器(异步消息处理器)
    • 完成后,完善日志器建造者,进行异步日志器安全模式的选择,提供异步日志器的创建

logger.hpp中补充异步日志器类:

    class AsyncLogger:public Logger{public:AsyncLogger(const std::string &logger_name, LogLevel::Value level,Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks,AsyncType looper_type): Logger(logger_name, level, formatter, sinks),_looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::real_Log,this,std::placeholders::_1),looper_type)){}// 异步日志器, 将数据写入到缓冲区virtual void log(const char*data,size_t len){_looper->push(data,len);}// 设计一个实际落地函数(将缓冲区中的数据落地)void real_Log(Buffer&buf)  // 给Functor传入的回调函数{//由于异步日志器实际落地只有一个线程,所以在消费时只有一个线程在消费是串行化执行的if(_sinks.empty())return;for(auto&sink:_sinks){sink->log(buf.begin(),buf.readAbleSize());}}private:AsyncLooper::ptr _looper;};

建造者模式中补充异步日志器的建造:

在这里插入图片描述

代码测试:

int main()
{// 异步日志器类的测试(logger.hpp buffer.hpp looper.hpp)std::unique_ptr<yjlog::LoggerBuilder> builder(new yjlog::LocalLoggerBuilder());builder->buildLoggername("async_logger");builder->buildLoggerLevel(yjlog::LogLevel::Value::WARN);builder->buildFormatter("[%c]%m%n");builder->buildLoggerType(yjlog::LoggerType::LOGGER_ASYNC);builder->buildSink<yjlog::FileSink>("./logfile/async.log");builder->buildSink<yjlog::StdoutSink>();yjlog::Logger::ptr logger=builder->build();logger->debug(__FILE__,__LINE__,"%s", "测试日志");logger->info(__FILE__,__LINE__,"%s", "测试日志");logger->warn(__FILE__,__LINE__,"%s", "测试日志");logger->error(__FILE__,__LINE__,"%s", "测试日志");logger->fatal(__FILE__,__LINE__,"%s", "测试日志");size_t cursize=0,count=0;  while(cursize<1024*1024*10)    {logger->fatal(__FILE__,__LINE__,"测试日志-%d", count++);cursize+=50;}
}

运行结果:

在这里插入图片描述

在非安全状态下写入50万条消息

在这里插入图片描述

运行结果:

在这里插入图片描述

8.9 单例日志器管理类设计

单例日志器管理类

  • 前提:

    • 日志的输出,我们希望能够在任意位置都可以进行,但是当我们创建了一个日志器之后,就会受到日志器所在作用域的访问属性限制
  • 思想:

    • 创建一个日志器管理类,且这个类是一个单例类,便于在程序任意位置,获取相同的单例对象,通过日志器进行输出,方便用户使用
  • 设计:

    • 管理的成员:
      • 默认日志器
      • 所管理的日志器数组
      • 互斥锁
    • 提供的接口:
    • 添加日志器管理
      • 判断是否管理了指定名称的日志器
      • 获取指定名称的日志器
      • 获取默认日志器

全局日志器的建造者类

基于单例日志器管理器的设计思想,我们对于日志器建造者类进行继承,继承出一个全局日志器建造者类,实现一个日志器在创建完毕后,直接将其添加到单例的日志器管理器中,以便于能够在任何位置通过日志器名称能够获取到指定的日志器进行日志输出

logger.hpp中补充两个类:

    // 单例日志器管理类class LoggerManger{public:static LoggerManger&getinstance()   // 懒汉模式{// 在C++11之后, 针对静态局部变量, 编译器在编译层面实现了线程安全// 当静态局部变量在没有构造完成之前, 其他的线程进入就会阻塞static LoggerManger eton;return eton;}void AddLogger(Logger::ptr&logger){if(HasLogger(logger->name()))return;std::unique_lock<std::mutex> lock(_mutex);_loggers.insert(std::make_pair(logger->name(),logger));}bool HasLogger(const std::string&name){std::unique_lock<std::mutex> lock(_mutex);auto it=_loggers.find(name);if(it==_loggers.end())return false;}Logger::ptr GetLogger(const std::string&name){std::unique_lock<std::mutex> lock(_mutex);auto it=_loggers.find(name);if(it==_loggers.end())return Logger::ptr();return it->second;}Logger::ptr rootlogger(){return _root_logger;}private:LoggerManger(){std::unique_ptr<yjlog::LoggerBuilder> builder(new yjlog::LocalLoggerBuilder());  // 这里只能创建局部对象builder->buildLoggername("root");_root_logger=builder->build();_loggers.insert(std::make_pair("root",_root_logger));}private:std::mutex _mutex;Logger::ptr _root_logger;    // 默认日志器std::unordered_map<std::string,Logger::ptr> _loggers;};// 设计一个全局日志器的建造者---在局部的基础上增加了一个功能: 将日志器添加到单例对象中class GlobalLoggerBuilder : public LoggerBuilder{public:virtual Logger::ptr build(){assert(!_logger_name.empty()); // 必须要有日志器名称if (_formatter.get() == nullptr) // 空, 就构造默认的_formatter = std::make_shared<Formatter>();if (_sinks.empty())buildSink<StdoutSink>();Logger::ptr logger;if (_logger_type == yjlog::LoggerType::LOGGER_ASYNC){logger=std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);}else{logger= std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);}LoggerManger::getinstance().AddLogger(logger);return logger;}};

代码测试:

void test_log()
{yjlog::Logger::ptr logger = yjlog::LoggerManger::getinstance(). GetLogger("async_logger");logger->debug(__FILE__,__LINE__,"%s", "测试日志");logger->info(__FILE__,__LINE__,"%s", "测试日志");logger->warn(__FILE__,__LINE__,"%s", "测试日志");logger->error(__FILE__,__LINE__,"%s", "测试日志");logger->fatal(__FILE__,__LINE__,"%s", "测试日志");size_t count=0;  while(count<500000)    {logger->fatal(__FILE__,__LINE__,"测试日志-%d", count++);}
}int main()
{// 全局日志器 + 单例的测试(logger.hpp)std::unique_ptr<yjlog::LoggerBuilder> builder(new yjlog::GlobalLoggerBuilder());builder->buildLoggername("async_logger");builder->buildLoggerLevel(yjlog::LogLevel::Value::WARN);builder->buildFormatter("[%c]%m%n");builder->buildLoggerType(yjlog::LoggerType::LOGGER_ASYNC);builder->buildEnableUnSafeAsync();                          // 启动非安全接口builder->buildSink<yjlog::FileSink>("./logfile/async.log");builder->buildSink<yjlog::StdoutSink>();builder->build();test_log();
}

运行结果:

在这里插入图片描述

8.10 日志宏&全局接口设计(代理模式)

目的:提供全局接口 & 宏函数,对日志系统接口进行使用便捷性优化

思想:

  • 提供获取指定日志器的全局接口(避免用户自己操作单例对象)

  • 使用宏函数对日志器的接口进行代理(代理模式)

  • 提供宏函数, 直接通过默认日志器进行日志的标准输出打印(不用在获取日志器了)

全局接口 & 宏函数设计在yjlog.h

// 全局接口 & 宏函数设计#pragma once
#include"logger.hpp"namespace yjlog
{// 1. 提供获取指定日志器的全局接口(避免用户自己操作单例对象)Logger::ptr getLogger(const std::string&name){return yjlog::LoggerManger::getinstance().GetLogger(name);}Logger::ptr rootLogger()   // 获取默认日志器{return yjlog::LoggerManger::getinstance().rootlogger();}// 2. 使用宏函数对日志器的接口进行代理(代理模式)#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__)// 3. 提供宏函数, 直接通过默认日志器进行日志的标准输出打印(不用在获取日志器了)#define DEBUG(fmt,...) yjlog::rootLogger()->debug(fmt,##__VA_ARGS__)#define INFO(fmt,...)  yjlog::rootLogger()->info(fmt,##__VA_ARGS__)#define WARN(fmt,...)  yjlog::rootLogger()->warn(fmt,##__VA_ARGS__)#define ERROR(fmt,...)  yjlog::rootLogger()->error(fmt,##__VA_ARGS__)#define FATAL(fmt,...)  yjlog::rootLogger()->fatal(fmt,##__VA_ARGS__)
}

代码测试:

#include"yjlog.h"void test_log()
{yjlog::Logger::ptr logger=yjlog::LoggerManger::getinstance().GetLogger("async_logger");logger->debug("%s", "测试日志");logger->info("%s", "测试日志");logger->warn("%s", "测试日志");logger->error("%s", "测试日志");logger->fatal("%s", "测试日志");size_t count=0;  while(count<500000)    {logger->fatal("测试日志-%d", count++);}
}
int main()
{// 全局日志器 + 单例的测试(logger.hpp)std::unique_ptr<yjlog::LoggerBuilder> builder(new yjlog::GlobalLoggerBuilder());builder->buildLoggername("async_logger");builder->buildLoggerLevel(yjlog::LogLevel::Value::WARN);builder->buildFormatter("[%c][%f:%l]%m%n");builder->buildLoggerType(yjlog::LoggerType::LOGGER_ASYNC);builder->buildEnableUnSafeAsync();                          // 启动非安全接口builder->buildSink<yjlog::FileSink>("./logfile/async.log");builder->buildSink<yjlog::StdoutSink>();builder->build();test_log();
}

运行结果:
在这里插入图片描述

测试默认日志器进行日志的标准输出打印:

在这里插入图片描述

运行结果:

在这里插入图片描述

9. 测试

9.1 功能测试
#include"../logs/yjlog.h"// 功能样例void test_log(const std::string&name)
{INFO("%s", "测试开始");yjlog::Logger::ptr logger=yjlog::LoggerManger::getinstance().GetLogger(name);logger->debug(__FILE__,__LINE__,"%s", "测试日志");logger->info(__FILE__,__LINE__,"%s", "测试日志");logger->warn(__FILE__,__LINE__,"%s", "测试日志");logger->error(__FILE__,__LINE__,"%s", "测试日志");logger->fatal(__FILE__,__LINE__,"%s", "测试日志");INFO("%s", "测试完毕");
}int main()
{// 全局日志器 + 单例的测试(logger.hpp)std::unique_ptr<yjlog::LoggerBuilder> builder(new yjlog::GlobalLoggerBuilder());builder->buildLoggername("sync_logger");builder->buildLoggerLevel(yjlog::LogLevel::Value::DEBUG);builder->buildFormatter("[%c][%f:%l][%p]%m%n");builder->buildLoggerType(yjlog::LoggerType::LOGGER_SYNC); builder->buildSink<yjlog::FileSink>("./logfile/sync.log");builder->buildSink<yjlog::StdoutSink>();builder->buildSink<yjlog::RollBySizeSink>("./logfile/roll-sync-by-size", 1024*1024);builder->build();test_log("sync_logger");
}

测试结果:

在这里插入图片描述

9.2 扩展测试
#include"../logs/yjlog.h"// 扩展样例int main()
{// 全局日志器 + 单例的测试(logger.hpp)std::unique_ptr<yjlog::LoggerBuilder> builder(new yjlog::GlobalLoggerBuilder());builder->buildLoggername("async_logger");builder->buildLoggerLevel(yjlog::LogLevel::Value::WARN);builder->buildFormatter("[%c]%m%n");builder->buildLoggerType(yjlog::LoggerType::LOGGER_ASYNC); builder->buildSink<yjlog::RollByTimeSink>("./logfile/roll-async-by-time", yjlog::TimeGap::GAP_SECOND);yjlog::Logger::ptr logger= builder->build();time_t cur=yjlog::util::Date::now();while(yjlog::util::Date::now() < cur+5){logger->fatal("这是一条测试日志");usleep(1000);}
}

测试结果:

在这里插入图片描述

9.3 性能测试

测试三要素

  1. 测试环境

  2. 测试方法

  3. 测试结果

9.3.1 测试环境
  • CPU: Intel® Xeon® Gold 6133 CPU @ 2.50GHz

  • 内存:最大容量2GB,Handle 0x1000, DMI type 16, 23 bytes

  • OS: CentOS 7.6(2核,内存2GB,SSD云硬盘40GB)

可以使用lscpu来查看cpu的配置

[yj@VM-4-17-centos ~]$ lscpu
Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                2
On-line CPU(s) list:   0,1
Thread(s) per core:    1
Core(s) per socket:    2
Socket(s):             1
NUMA node(s):          1
Vendor ID:             GenuineIntel
CPU family:            6
Model:                 94
Model name:            Intel(R) Xeon(R) Gold 6133 CPU @ 2.50GHz
Stepping:              3
CPU MHz:               2494.138
BogoMIPS:              4988.27
Hypervisor vendor:     KVM
Virtualization type:   full
L1d cache:             32K
L1i cache:             32K
L2 cache:              4096K
L3 cache:              28160K
NUMA node0 CPU(s):     0,1

dmidecode -t memory 来查看内存信息

[root@VM-4-17-centos ~]# dmidecode -t memory
# dmidecode 3.2
Getting SMBIOS data from sysfs.
SMBIOS 2.8 present.Handle 0x1000, DMI type 16, 23 bytes
Physical Memory ArrayLocation: OtherUse: System MemoryError Correction Type: Multi-bit ECCMaximum Capacity: 2 GBError Information Handle: Not ProvidedNumber Of Devices: 1Handle 0x1100, DMI type 17, 40 bytes
Memory DeviceArray Handle: 0x1000Error Information Handle: Not ProvidedTotal Width: UnknownData Width: UnknownSize: 2048 MBForm Factor: DIMMSet: NoneLocator: DIMM 0Bank Locator: Not SpecifiedType: RAMType Detail: OtherSpeed: UnknownManufacturer: SmdbmdsSerial Number: Not SpecifiedAsset Tag: Not SpecifiedPart Number: Not SpecifiedRank: UnknownConfigured Memory Speed: UnknownMinimum Voltage: UnknownMaximum Voltage: UnknownConfigured Voltage: Unknown
9.3.2 测试方法

编写要求:

  1. 可以控制写日志的线程数量和写日志的总数量
  2. 分别对于同步日志器与异步日志器进行各自的性能测试
  3. 分别测试但写日志线程的性能和多写日志线程的性能

实现:

  • 封装一个接口,传入日志器名称线程数量日志数量单条日志大小

  • 在接口内创建指定数量的线程,负责一部分日志的输出

  • 在输出之前计时开始,输出完毕后计时结束。

    • 所耗时间 = 结束时间 - 开始时间
    • 每秒输出量 = 日志数量 / 总耗时
    • 每秒输出大小 = 日志数量 * 单条日志大小 / 总耗时
  • 异步日志输出时,我们启动非安全模式,纯内存写入(不考虑实际落地时间)

代码接口

#include"../logs/yjlog.h"
#include<chrono>
#include <algorithm>void bench(const std::string&logger_name,size_t thread_num, size_t msg_num,size_t msg_len)
{// 1. 获取日志器yjlog::Logger::ptr logger=yjlog::getLogger(logger_name);if(logger.get()==nullptr)return;std::cout<<"测试日志: "<<msg_num<<" 条, 总大小: "<<(msg_num*msg_len)/1024<<"KB\n";// 2. 组织指定长度的日志消息std::string msg(msg_len-1,'A');   // 少一个字节, 是为了给末尾到时候添加换行// 3. 创建指定数量的线程std::vector<std::thread> threads;std::vector<double> cost_array(thread_num);  // 用于辅助计算总耗时的数组size_t msg_pre_thread=msg_num/thread_num;    // 每个线程要输出的日志数量 = 总日志线程数量 / 线程数量for(int i=0;i<thread_num;++i){threads.emplace_back([&,i](){// 4. 线程函数内部开始计时auto start=std::chrono::high_resolution_clock::now();// 5. 开始循环写日志for(int j=0;j<msg_pre_thread;++j){logger->fatal("%s",msg.c_str());}// 6. 线程函数内部结束计时auto end=std::chrono::high_resolution_clock::now();std::chrono::duration<double> cost=end-start;cost_array[i]=cost.count();std::cout<<"\t线程"<<i<<": "<<"\t输出日志数量:"<<msg_pre_thread<<", 耗时:"<<cost.count()<<"s"<<std::endl;});}for(int i=0;i<thread_num;++i)threads[i].join();// 7. 计算总耗时: 在多线程中, 每个线程会消耗时间, 但是线程是并发处理的, 因此耗时最高的那个就是总时间double cost_max=cost_array[0];for(int i=0;i<thread_num;++i)cost_max=std::max(cost_max,cost_array[i]);size_t msg_pre_sec=msg_num/cost_max;                      // 每秒输出日志数量size_t size_pre_sec=(msg_num*msg_len) / (cost_max*1024);  // 每秒输出日志大小// 8. 进行输出打印std::cout<<"\t总耗时"<<"s\n";std::cout<<"每秒输出日志数量: "<<msg_pre_sec<<"条\n";std::cout<<"每秒输出日志大小: "<<size_pre_sec<<"kb\n";
}

同步,异步测试代码

void sync_bench()
{std::unique_ptr<yjlog::LoggerBuilder> builder(new yjlog::GlobalLoggerBuilder());builder->buildLoggername("sync_logger");builder->buildFormatter("%m%n");builder->buildLoggerType(yjlog::LoggerType::LOGGER_SYNC); builder->buildSink<yjlog::FileSink>("./logfile/sync.log");builder->build();// bench("sync_logger",1, 1000000,100);   // 单线程同步(串行, 无锁冲突)bench("sync_logger",3, 1000000,100);   // 多线程同步(锁冲突, 并不提高效率, 因为磁盘的性能已经达到极限了)
}
void async_bench()
{std::unique_ptr<yjlog::LoggerBuilder> builder(new yjlog::GlobalLoggerBuilder());builder->buildLoggername("async_logger");builder->buildFormatter("%m%n");builder->buildLoggerType(yjlog::LoggerType::LOGGER_ASYNC); builder->buildEnableUnSafeAsync();                   // 开启非安全模式---主要是为了将实际落地时间排除在外builder->buildSink<yjlog::FileSink>("./logfile/async.log");builder->build();// bench("async_logger",1, 1000000,100);      // 单线程异步bench("async_logger",3, 1000000,100);    // 多线程异步(考虑cpu与内存的性能,延展性比磁盘高)}
int main()
{// sync_bench();async_bench();return 0;
}
9.3.3 测试结果
(1) 单线程同步日志
[yj@VM-4-17-centos bench]$ ./bench 
测试日志: 1000000, 总大小: 97656KB线程0: 	输出日志数量:1000000, 耗时:2.02894s总耗时: 2.02894s每秒输出日志数量: 492867条每秒输出日志大小: 48131kb
(2) 多线程同步日志
[yj@VM-4-17-centos bench]$ ./bench 
测试日志: 1000000, 总大小: 97656KB线程1: 	输出日志数量:333333, 耗时:2.06445s线程0: 	输出日志数量:333333, 耗时:2.06615s线程2: 	输出日志数量:333333, 耗时:2.07384s总耗时: 2.07384s每秒输出日志数量: 482197条每秒输出日志大小: 47089kb
(3) 单线程异步日志
[yj@VM-4-17-centos bench]$ ./bench 
测试日志: 1000000, 总大小: 97656KB线程0: 	输出日志数量:1000000, 耗时:1.97751s总耗时: 1.97751s每秒输出日志数量: 505686条每秒输出日志大小: 49383kb
(4) 多线程异步日志
[yj@VM-4-17-centos bench]$ ./bench 
测试日志: 1000000, 总大小: 97656KB线程1: 	输出日志数量:333333, 耗时:1.46186s线程0: 	输出日志数量:333333, 耗时:1.46382s线程2: 	输出日志数量:333333, 耗时:1.55684s总耗时: 1.55684s每秒输出日志数量: 642326条每秒输出日志大小: 62727kb
9.3.4 结论

在我的测试环境下:

  • 在单线程情况下,异步效率看起来还没有同步高,这个我们得了解,现在的IO操作在用户态都会有缓冲区进行缓冲区,因此我们当前测试用例看起来的同步其实大多时候也是在操作内存,只有在缓冲区满了才会涉及到阻塞写磁盘操作,而异步单线程效率看起来低,也有一个很重要的原因就是单线程同步操作中不存在锁冲突,而单线程异步日志操作存在大量的锁冲突,因此性能也会有一定的降低。
  • 但是,我们也要看到限制同步日志效率的最大原因是磁盘性能,打日志的线程多少并无明显区别,线程多了反而会降低,因为增加了磁盘的读写争抢,而对于异步日志的限制,并非磁盘的性能,而是cpu的处理性能,打日志并不会因为落地而阻塞,因此在多线程打日志的情况下性能有了显著的提高。

10. 扩展

  • 丰富sink类型:
    • 支持按小时按天滚动文件
    • 支持将log通过网络传输落地到日志服务器(tcp/udp)
    • 支持在控制台通过日志等级渲染不同颜色输出方便定位
    • 支持落地日志到数据库
    • 支持配置服务器地址,将日志落地到远程服务器

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/498791.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【前端素材】推荐优质在线手表商城电商网页 WristArc平台模板(附源码)

一、需求分析 1、系统定义 在线服饰购物商城是指一个通过互联网提供服装和配饰购买服务的电子商务平台。这类商城通常提供一个网站或移动应用程序&#xff0c;让顾客可以浏览、选择和购买各种类型的服装、鞋帽、包包、配饰等时尚商品。 2、功能需求 在线手表商城是指一个通…

【前端素材】推荐优质在线高端蜂蜜商城电商网页Beejar平台模板(附源码)

一、需求分析 1、系统定义 在线礼品商城平台是一种通过互联网提供礼品购买服务的电子商务平台。这类平台通常为消费者和商家提供一个交易和互动的虚拟空间&#xff0c;使用户可以浏览、选择和购买各种礼品产品。 2、功能需求 在线礼品商城平台是一种通过互联网提供礼品购买…

精酿啤酒:原料的采购策略与市场动态

对于啤酒制造商而言&#xff0c;原料的采购策略与市场动态紧密相连&#xff0c;直接影响到产品的成本、品质和市场竞争力。Fendi Club啤酒在这方面有着与众不同的见解和实践&#xff0c;确保了原料的稳定供应和品质&#xff0c;同时也灵活应对市场变化。 首先&#xff0c;Fendi…

SQLSERVER 2014 删除数据库定时备份任务提示失败DELETE 语句与 REFERENCE 约束“FK_subplan_job_id“冲突

SQLSERVER 2014 删除数据库定时备份任务提示失败DELETE 语句与 REFERENCE 约束“FK_subplan_job_id“冲突 &#xff0c;错误如图&#xff1a; 问题原因&#xff1a;不能直接删除作业 任务&#xff0c;需要先删除计划里面的日志、删除代理作业、删除子计划以后才能删除作业。 解…

Vue3 学习笔记(Day5)

「写在前面」 本文为尚硅谷禹神 Vue3 教程的学习笔记。本着自己学习、分享他人的态度&#xff0c;分享学习笔记&#xff0c;希望能对大家有所帮助。推荐先按顺序阅读往期内容&#xff1a; 1. Vue3 学习笔记&#xff08;Day1&#xff09; 2. Vue3 学习笔记&#xff08;Day2&…

BasicForm使用v-if条件判断时报错的解决办法

当我们使用vben Admin的BasicForm表单组件时&#xff0c;用了v-if条件判断&#xff0c;如下所示&#xff1a; <BasicForm register"registerForm" submit"XXXXHandler" v-if"XXXX" /> 我们在浏览器控制台看到如下报错&#xff1a; 这个时…

【C++】数组、函数、指针

文章目录 1.数组1.1一维数组1.2二维数组 2.函数3.指针&#xff1a;可以通过指针间接访问内存(指针记录地址&#xff09;3.1 指针的定义和使用3.2 指针所占用空间3.3 空指针和野指针3.4 const修饰指针3.5指针和数组3.6指针和函数3.7练习&#xff08;指针、数组、函数&#xff09…

C++观察者模式代码实例

文章目录 C观察者模式代码实例一C观察者模式代码实例二 C观察者模式代码实例一 下面是一个简单的C观察者模式的实现示例&#xff0c;这里仅给出核心代码框架&#xff0c;完整的工程应包含对应的头文件声明及必要的#include指令等。 // 观察者接口&#xff08;Observer&#x…

【STM32】STM32学习笔记-WDG看门狗(46)

00. 目录 文章目录 00. 目录01. WDG简介02. IWDG概述03. IWDG框图04. IWDG键寄存器05. WWDG简介06. WWDG框图07. WWDG工作特性08. IWDG和WWDG对比09. 预留10. 附录 01. WDG简介 WDG&#xff08;Watchdog&#xff09;看门狗 看门狗可以监控程序的运行状态&#xff0c;当程序因为…

CentOS7如何使用Docker部署Wiki.Js知识库并实现公网远程访问?

文章目录 1. 安装Docker2. 获取Wiki.js镜像3. 本地服务器打开Wiki.js并添加知识库内容4. 实现公网访问Wiki.js5. 固定Wiki.js公网地址 不管是在企业中还是在自己的个人知识整理上&#xff0c;我们都需要通过某种方式来有条理的组织相应的知识架构&#xff0c;那么一个好的知识整…

tcpdump 常用用法

简要记录下tcpdump用法 监控某个ip上的某个端口的流量 tcpdump -i enp0s25 tcp port 5432 -nn -S 各个参数作用 -i enp0s25 指定抓包的网卡是enp0s3 -nn 显示ip地址和数字端口 &#xff0c;如果只 -n 则显示ip&#xff0c;但是端口为services文件中的服务名 如果一个n…

HTTP状态码合集

目录 1、HTTP状态码 2、状态码的分类 3、常用的状态码 4、总结 1、HTTP状态码 当服务端返回 HTTP 响应时&#xff0c;会带有一个状态码&#xff0c;用于表示特定的请求结果。比如 HTTP/1.1 200 OK&#xff0c;里面的 HTTP/1.1 表示协议版本&#xff0c;200 则是状态码&…