日志项目2

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

项目开发

项目源码链接

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.txt
                size_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.txt
                size_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 once

namespace 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;      // 线程ID
        std::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%m
                if (!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%m
        if (!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.txt
        std::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.log
        std::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.txt
        std::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 Array
	Location: Other
	Use: System Memory
	Error Correction Type: Multi-bit ECC
	Maximum Capacity: 2 GB
	Error Information Handle: Not Provided
	Number Of Devices: 1

Handle 0x1100, DMI type 17, 40 bytes
Memory Device
	Array Handle: 0x1000
	Error Information Handle: Not Provided
	Total Width: Unknown
	Data Width: Unknown
	Size: 2048 MB
	Form Factor: DIMM
	Set: None
	Locator: DIMM 0
	Bank Locator: Not Specified
	Type: RAM
	Type Detail: Other
	Speed: Unknown
	Manufacturer: Smdbmds
	Serial Number: Not Specified
	Asset Tag: Not Specified
	Part Number: Not Specified
	Rank: Unknown
	Configured Memory Speed: Unknown
	Minimum Voltage: Unknown
	Maximum Voltage: Unknown
	Configured 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)
    • 支持在控制台通过日志等级渲染不同颜色输出方便定位
    • 支持落地日志到数据库
    • 支持配置服务器地址,将日志落地到远程服务器
  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值