【同步异步可并发日志系统】设计及实现

news2024/12/26 16:25:03

  • 1. 项⽬介绍
  • 2. 开发环境
  • 3. 项目核⼼技术
  • 4. 环境搭建
  • 5. ⽇志系统介绍
    • 5.1 为什么需要⽇志系统
    • 5.2⽇志系统技术实现
      • 5.2.1 同步写⽇志
      • 5.2.2 异步写⽇志
  • 6. ⽇志系统框架设计
  • 7. 代码设计
    • 7.1 实⽤类设计
    • 7.2 ⽇志等级类设计
    • 7.3 ⽇志消息类设计
    • 7.4 ⽇志格式化输出设计思想
      • 7.4.1FormatItem类设计及实现
      • 7.4.2 Formatter_str类设计及实现
    • 7.5 ⽇志落地(Sink)类设计思想
      • 7.5.1日志落地(Sink)类实现
      • 7.5.1简单工厂模式构造日志落地类 —— SinkFactroy类
    • 7.6 ⽇志器类(Logger)设计思想
      • 7.6.1 ⽇志器类(Logger)及同步日志器实现
    • 7.7⽇志器类建造者抽象类设计思路及实现
      • 7.7.1局部⽇志器类建造者 设计思路及实现
    • 7.8 缓冲区类的设计思想集实现
    • 7.8.1 基于缓冲区类的 缓冲器 设计思想集实现 ——— 双缓冲区缓冲器
    • 7.9 异步⽇志器(AsyncLogger)设计
    • 7.10 全局单例⽇志器管理类设计(单例模式)
    • 7.11 ⽇志宏&全局接⼝设计(代理模式)
  • 8.0 性能测试

1. 项⽬介绍

本项⽬主要实现⼀个⽇志系统,其主要⽀持以下功能:

1:可进行日志消息格式的指定
2:可划分出多级别的⽇志消息,并可设置那个级别及以上的日志可以输出
3:可将⽇志写到控制台、⽂件以及按大小切换的滚动⽂件中(一个日志可输出0到多个目的文件)
4:⽀持扩展⽇志写入⽬标地及写入要求 ——— 可拓展性
5:⽀持同步写⽇志和异步写⽇志
6:同步或异步写日志均⽀持多线程程序并发写⽇志
7:拥有全局单列,且设有全局宏函数简化使用难度

2. 开发环境

• CentOS7
• vscode/vim
• g++/gdb
• Makefile

3. 项目核⼼技术

• 类的层次设计 —— 继承和多态的应⽤
• C++11(多线程、lambda、智能指针、包装器、右值引⽤等)
• 设计模式的应用(单例、⼯⼚、代理、建造、模板等)
• 异步日志采用双缓冲区构成缓冲器 ———— ⽣产消费模型
• 多线程

4. 环境搭建

本项⽬不依赖其他任何第三⽅库,只需要安装好CentOS/Ubuntu+vscode/vim环境即可开发。

5. ⽇志系统介绍

5.1 为什么需要⽇志系统

1: 已上线的客⼾端的产品出现bug后难以复现并解决,可以借助⽇志系统打印⽇志并上传到服务端帮助开发⼈员进⾏分析
2: 多线程/多进程代码中,出现bug⽐较难以定位,可以借助⽇志系统打印log帮助定位bug
3: 帮助⾸次接触项⽬代码的新开发⼈员理解代码的运⾏流程

5.2⽇志系统技术实现

日志系统主要分为同步⽇志和异步⽇志⽅式

5.2.1 同步写⽇志

同步⽇志是指当输出⽇志时,必须等待⽇志输出语句执⾏完毕后,才能执⾏后⾯的业务逻辑语句,⽇志输出语句与程序的业务逻辑语句在同⼀个线程运⾏。每次调⽤⼀次写⽇志API就对应⼀次系统调⽤write写⽇志⽂件。
在这里插入图片描述
在⾼并发场景下,随着⽇志数量不断增加,同步⽇志系统容易产⽣系统瓶颈:
1:⼀⽅⾯,⼤量的⽇志打印陷⼊等量的write系统调⽤,有⼀定系统开销
2: 另⼀⽅⾯,使得打印⽇志的进程附带了⼤量同步的磁盘IO,影响程序性能

5.2.2 异步写⽇志

⽇志输出语句与业务逻辑语句并不是在同⼀个线程中运⾏,⽽是有专⻔的线程⽤于进⾏⽇志输出操作
业务线程只需要将⽇志放到⼀个内存缓冲区中不⽤等待即可继续执⾏后续业务逻辑(作为⽇志的⽣产者),⽽⽇志的落地操作交给单独的⽇志线程去完成(作为⽇志的消费者),这是⼀个典型的⽣产-消费模型。
在这里插入图片描述

这样做的好处是即使⽇志没有真的地完成输出也不会影响程序的主业务,可以提⾼程序的性能:
1:使主线程调⽤⽇志打印接⼝成为⾮阻塞操作
2: 同步的磁盘IO从主线程中剥离出来交给单独的线程完成

6. ⽇志系统框架设计

项⽬的框架设计将项⽬分为以下⼏个模块来实现

1、⽇志等级模块:对输出⽇志的等级进⾏划分,以便于控制⽇志的输出,并提供等级枚举转字符串功能
2、⽇志消息模块:中间存储⽇志输出所需的各项要素信息
3、⽇志消息格式化模块:设置⽇志输出格式,并提供对⽇志消息进⾏格式化功能。
4、⽇志消息落地模块:决定了⽇志的落地⽅向,可以是标准输出,也可以是⽇志⽂件,也可以滚动⽂件输出…
5、⽇志器模块:包含有⽇志消息落地模块对象,⽇志消息格式化模块对象,⽇志输出等级
6、⽇志器管理模块:对创建的所有⽇志器进⾏统⼀管理。
7、异步线程模块:负责⽇志的落地输出功能

7. 代码设计

7.1 实⽤类设计

做项目前先完成⼀些零碎的功能接⼝,以便于项⽬中编写过程中使用。
1:获取系统时间
2:判断⽂件是否存在
3:获取⽂件的所在⽬录路径
4:创建⽬录

代码如下,注意下面命名空间的划分以及函数均使用了static修饰

#ifndef __Uitl__
#define __Uitl__

#include<iostream>
#include<time.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>

namespace zgbLog //定义一个日志器命名空间,避免与库中命名出现冲突
{
    
    namespace Uitl
    {
        class Data
        {
        public:
            //获取当前时间戳
            static time_t getTime()
            {
                return time(nullptr);
            }
        };
        
        class File
        {
        public:
        
            //判断文件是否存在
            static bool exists(std::string pathname)
            {
                struct stat buf;
                return (stat(pathname.c_str() , &buf) == 0);
                /*stat用于获取文件属性并将信息写入到struct stat 结构体中
                如文件不存在则会获取失败并返回-1 , 存在且获取成功返回0
                一般只要存在都会获取成功*/
            }

            //获取文件路径
            static std::string path(std::string pathname)
            {
                //从字符串结尾查找"/\\"中任意一个字符 ———— '\'和'/'
                //保证linux路径分隔符为'/',windows路径分隔符为'\'均可用
                int pos = pathname.find_last_of("/\\");
                if(pos == std::string::npos)
                    return ".";
                
                return pathname.substr(0 , pos);
            }
            
            //创建目录
            static bool createDriectory(const std::string& pathname)
            {
                if(pathname.empty())//为空 
                    return false;
                if(exists(pathname))//如果目录存在直接返回
                    return true;

                
                int pos = 0;
                while(pos != std::string::npos)
                {
                    pos = pathname.find_first_of("/\\" , pos);
                    if(pos == std::string::npos)//目录均存在了,只剩最后一个目录
                    {
                        umask(0);//在此作用域将umask设为0
                        mkdir(pathname.c_str(), 0755);//创建目录
                        return true;
                    }
                    //不为npos说明已分割出第 x个 目录分割符

                    if(exists(pathname.substr(0 , pos)) || pos == 0)// 过滤 /abs/ss 这类情况(同时能过滤掉 . 和 ..)
                    {
                        pos += 1;
                        continue;//如果目录存在则检查下一个
                    }
                    
                    //目录不存在
                    mkdir(pathname.substr(0 , pos).c_str(), 0777);//创建目录
                    pos += 1;
                }
            }
        };
    }
}
#endif

7.2 ⽇志等级类设计

日志等级类中包含有:

1、 ⽇志等级,其总共分为6个等级,分别为:

OFF :关闭所有⽇志输出
DRBUG:进⾏debug时候打印⽇志的等级
INFO:打印⼀些⽤⼾提⽰信息
WARN:打印警告信息
ERROR:打印错误信息
FATAL :打印致命信息导致程序崩溃的信息

2、 将日志等级转换为对应字符串的函数

///                         日志等级模块

#ifndef __LEVEL__
#define __LEVEL__
#include <iostream>


namespace zgbLog
{
	//日志等级类
    class LogLevel
    {
        public:
       		 // 1: 划分定义日志等级
            enum Varlue
            {
                UNKNOWN = 0, //未知等级错误
                DEBUG,       //调试等级日志
                INFO,        //消息等级日志
                WARN,        //警告等级日志
                ERROR,       //错误等级日志
                FATAL,       //致命等级错误
                OFF          //关闭日志
            };
            // 2: 返回日志等级对应的字符串 
            static std::string tostring(LogLevel::Varlue level)
            {
                switch(level)
                {
                    case LogLevel::Varlue::DEBUG: return "DEBUG";
                    case LogLevel::Varlue::INFO:  return "INFO";
                    case LogLevel::Varlue::WARN:  return "WARN";
                    case LogLevel::Varlue::ERROR:  return "ERROR";
                    case LogLevel::Varlue::FATAL:  return "FATAL";
                    case LogLevel::Varlue::OFF:  return "OFF";
                    default:
                            return "UNKNOWN";
                }
            }
    };

}
#endif

7.3 ⽇志消息类设计

⽇志消息类主要是封装⼀条完整的⽇志消息所需的内容,其中包括:⽇志等级、对应的loggername(日志器名)、调用打印⽇志消息的源⽂件的位置信息(包括⽂件名和⾏号)、线程ID、时间戳信息、具体的⽇志信息等内容。

                  日志消息类
#ifndef __Msg__
#define __Msg__

#include"Level.hpp"
#include"Uitl.hpp"
#include<thread>
#include<iostream>
#include<memory>
//存储日志的各项信息
namespace zgbLog
{
    struct LogMsg
    {
        using ptr = std::shared_ptr<LogMsg>;//类智能指针

        zgbLog::LogLevel::Varlue _level;//日志等级
        time_t _time;//日志触发时间
        std::thread::id _id;//触发日志的线程id
        int _line;//触发日志的行号
        std::string _file;//文件名
        std::string _name;//日志器名称
        std::string _payload;//⽇志消息(有效日志数据)

        LogMsg(){}//设置一个无参构造

        //构造函数
        LogMsg(zgbLog::LogLevel::Varlue level,int line,std::string file,
                std::string name,std::string payload)
        :_level(level)
        ,_line(line)
        ,_file(file)
        ,_name(name)
        ,_payload(payload)
        {
            _time = zgbLog::Uitl::Data::getTime();//获取时间戳
            _id = std::this_thread::get_id();//获取线程id
        }
    };
}
#endif

7.4 ⽇志格式化输出设计思想

将⽇志消息输出格式化需用到两个类:
1:FormatItem类:将LogMsg中内容一 一取出,并进行格式化
2:Formatter_str类:将用户指定的字符格式串,进行解析,并按照解析后的格式子项调用对应的FormatItem方法将LogMsg中的日志信息格式化成所指定格式的字符串

7.4.1FormatItem类设计及实现

FormatItem类设计思想:
1:先抽象一个格式化类,并基于抽象类派生出对应日志信息子项的格式化子类
2:子类中实现对应的纯虚函数方法 ——— 语法也规定必须重写

这样使用父类(抽象类)指针,就可获得子类实现的日志子项格式化方法

实现:
该类主要负责⽇志消息⼦项的获取及格式化。其包含以下⼦类
• FormatIter:为抽象基类
• FormatIter_Msg:表⽰要从LogMsg中取出有效⽇志数据
• FormatIter_Level:表⽰要从LogMsg中取出⽇志等级
• FormatIter_Name:表⽰要从LogMsg中取出⽇志器名称
• FormatIter_Threadid:表⽰要从LogMsg中取出线程ID
• FormatIter_Time:表⽰要从LogMsg中取出时间戳并按照指定格式进⾏格式化
• FormatIter_File:表⽰要从LogMsg中取出源码所在⽂件名
• FormatIter_Line:表⽰要从LogMsg中取出源码所在⾏号
• FormatIter_Tab:表⽰⼀个制表符缩进
• FormatIter_Nline:表⽰⼀个换⾏
• FormatIter_Other:表⽰⾮格式化的原始字符串,直接输出原始字符串即可

/   日志消息格式模块
#ifndef __FORMAT__
#define __FORMAT__

#include"Massage.hpp"
#include<time.h>
#include<vector>
#include<sstream>
#include<cassert>
namespace zgbLog
{
    //基类 ———— 要继承必须重写虚函数
    class FormatIter
    {
        public:
            using ptr = std::shared_ptr<FormatIter>;
            virtual ~FormatIter() {}
            virtual void Format(std::ostream &out , const LogMsg &msg) = 0;
    };

    //消息
    class FormatIter_Msg : public FormatIter
    {
        public:
            virtual void Format(std::ostream &out , const LogMsg &msg)override
            {
                out << msg._payload;
            }
    };

    //日志等级
    class FormatIter_Level : public FormatIter
    {
        public:
            virtual void Format(std::ostream &out , const LogMsg &msg)override
            {
                out << zgbLog::LogLevel::tostring(msg._level);
            }
    };

    //日志时间
    class FormatIter_Time : public FormatIter
    {
        public:
             // "%Y-%m-%d %H:%M:%S" 对应 年-月-日 时:分:秒
            //需传日期格式字符串
            FormatIter_Time(std::string str = "%H:%M:%S")
            :_str(str)
            {
                if(_str.empty())
                    _str = "%H:%M:%S";
            }
            virtual void Format(std::ostream &out , const LogMsg &msg)override
            {
                struct tm t;
                localtime_r(&msg._time , &t);//将时间戳转换成日期时间信息
                char buf[12] = {0};
                strftime(buf , 12 , _str.c_str(), &t);
                out << buf;
            }
        private:
            std::string _str;
    };
 
    //文件名
    class FormatIter_File : public FormatIter
    {
        public:
            virtual void Format(std::ostream &out , const LogMsg &msg)override
            {
                out << msg._file;
            }
    };

    //行号
    class FormatIter_Line : public FormatIter
    {
        public:
            virtual void Format(std::ostream &out , const LogMsg &msg)override
            {
                out << msg._line;
            }
    };

    //线程id
    class FormatIter_Threadid : public FormatIter
    {
        public:
            virtual void Format(std::ostream &out , const LogMsg &msg)override
            {
                out << msg._id;
            }
    };

    //日志器名称
    class FormatIter_Name : public FormatIter
    {
        public:
            virtual void Format(std::ostream &out , const LogMsg &msg)override
            {
                out << msg._name;
            }
    };
    //制表符
    class FormatIter_Tab : public FormatIter
    {
        public:
            virtual void Format(std::ostream &out , const LogMsg &msg)override
            {
                out << '\t';
            }
    };
    //换行
    class FormatIter_Nline : public FormatIter
    {
        public:
            virtual void Format(std::ostream &out , const LogMsg &msg)override
            {
                out << '\n';
            }
    };
    //其他
    class FormatIter_Other : public FormatIter
    {
        public:
        	//构造时保存字符串
            FormatIter_Other(const std::string str)
            :_str(str)
            {

            }
            virtual void Format(std::ostream &out , const LogMsg &msg)override
            {
                out << _str;
            }
        private:
        std::string _str;//保存字符串
    };
}

#endif

7.4.2 Formatter_str类设计及实现

首先先规定出每个符号对应的数据

/* 格式符对应的日志内容
	  %d ⽇期
	  %T 缩进
	  %t 线程id
	  %p ⽇志级别
	  %c ⽇志器名称
	  %f ⽂件名
	  %l ⾏号
	  %m ⽇志消息
	  %n 换⾏
	  
	  其他按字符返回,注意特列%% 为%号
*/

Formatter_str类中有两个成员变量:

1:_pattern成员:保存⽇志输出的格式字符串
2:std::vector<FormatIter::ptr> _items:⽤于按序保存格式化子项字符串对应的⼦格式化对象指针。

Formatter_str类中需自己写五个成员函数:

1:构造函数:构造时传入指定的格式串,用于指定日志的输出格式。并调用parsePattern()函数将格式符字符串分割 ,并调用createformatptr 将对应方法放入_items队列 ———— 但最终需保证_pattern不为空

2:format(const LogMsg& msg) :用 _items 的方法队列对msg进行格式化并返回日志字符串

3:format(std::ostream& out , const LogMsg& msg):用 _items 的方法队列对msg进行格式化并返回日志字符串的输出流

4:createformatptr(…):按照key存储的 格式符 返回"父类指针指向"的对应FormatIter派生类对象

5:parsePattern() :将格式符字符串分割 ,并调用createformatptr 将对应方法放入_items队列

//注意:该类应放在FormatIter模块下方

// 格式解析加输出 ///

    //格式解析加输出
    class Formatter_str
    {
        //功能顺序:
        //1:构造时传入格式串
        //2:将格式串解析转换成对应方法 并将 并将对应方法放入_items队列
        //3:对msg进行格式化并返回字符串日志
    public:
            using ptr = std::shared_ptr<Formatter_str>;

            //构造 ———— 保证pattern不为空
            Formatter_str(std::string pattern = "[%d{%H:%M:%S}][%t][%p][%c][%f::%l] %m%n")
            :_pattern(pattern)
            {
                if(_pattern.empty())
                    _pattern =  "[%d{%H:%M:%S}][%t][%p][%c][%f::%l] %m%n";
                
                if(parsePattern() == false)//将格式串解析转换成对应方法 并将 并将对应方法放入_items队列
                {
                    //说明解析失败
                    _items.clear();//清空方法队列
                    _pattern = "[解析失败默认格式][%d{%H:%M:%S}][%t][%p][%c][%f::%l] %m%n";//使用默认格式保证正常输出
                    parsePattern();
                }
            }

            //用 _items 的方法队列对msg进行格式化并返回日志字符串
            std::string format(const LogMsg& msg)
            {
                std::stringstream ss;//定义一个输出流
                for(auto& item : _items)
                {
                    item->Format(ss , msg);//按顺序调用对应多态,并将内容输出到ss中
                }
                return ss.str();//将ss中的字符串返回
            }

            //用 _items 的方法队列对msg进行格式化并返回输出流
            std::ostream& format(std::ostream& out , const LogMsg& msg)
            {
                for(auto& item : _items)
                {
                    item->Format(out , msg);//调用对应多态
                }
                return out;
            }
    private:
                    /* 格式符对应的日志内容

                        %d ⽇期
                        %T 缩进
                        %t 线程id
                        %p ⽇志级别
                        %c ⽇志器名称
                        %f ⽂件名
                        %l ⾏号
                        %m ⽇志消息
                        %n 换⾏
                        
                        其他按字符返回,注意特列%% 为%号
                    */

            //按照key存储的 格式符 返回"父类指针指向"的对应派生类对象 
            //将格式串解析转换成对应方法
            FormatIter::ptr createformatptr(const std::string& key , const std::string& val)
            {
                //Val为参数或未知字符串
                if(key == "%d") return FormatIter::ptr(new FormatIter_Time(val));//%d ⽇期多态
                if(key == "%T") return FormatIter::ptr(new FormatIter_Tab());    //%T 制表符
                if(key == "%t") return FormatIter::ptr(new FormatIter_Threadid());//%t 线程多态
                if(key == "%p") return FormatIter::ptr(new FormatIter_Level());//%p 日志级别多态
                if(key == "%c") return FormatIter::ptr(new FormatIter_Name());//%c 日志器名称多态
                if(key == "%f") return FormatIter::ptr(new FormatIter_File());//%f 文件名多态
                if(key == "%l") return FormatIter::ptr(new FormatIter_Line());//%l 行号多态
                if(key == "%m") return FormatIter::ptr(new FormatIter_Msg());// %m 日志消息多态
                if(key == "%n") return FormatIter::ptr(new FormatIter_Nline());//%n 换行多态

                //kay为空表示 ———— 父类随无储存参数但父类被重写虚函数后,父类指针其代码可访问子类内容
                if(key.empty()) return FormatIter::ptr(new FormatIter_Other(val));//%n 其他多态

                //未知key
                std::cout << "未知的格式符:" << key <<std::endl;
                return nullptr;
            }

            //将格式符字符串分割 ,并调用createformatptr 将对应方法放入_items队列
            bool parsePattern()
            {
                //参考"aacc%%[%d{%H:%M:%S}][%t][%p][%c][%f:%l] %m%n"
                std::string key;//存储格式符
                std::string val;//存储非格式符
                std::vector<std::pair<std::string , std::string>> d1;//格式符及非格式符子项队列
                int pos = 0;//字符串下标
                int size = _pattern.size();
                while(pos < size)
                {
                    if(_pattern[pos] != '%')//为非格式字符
                    {
                        val.push_back(_pattern[pos]);//将字符存入val中
                        pos++;
                        continue;//重新循环
                    }
                    //当前为%号
                    if(pos +1 < size && _pattern[pos+1] == '%')
                    {
                        val.push_back(_pattern[pos]);//将%号存入val中
                        pos += 2;//跳过两个%号
                        continue;//重新循环
                    }
                    //pos位为%且pos+1不为%号,则可能为格式符 ———— 但注意可能非法
                    if( ! val.empty())//如果val不为空则需将非格式字符串(子项)存储进队列
                    {
                        d1.push_back(std::make_pair(key , val));
                        val.clear();//存储子项后清空
                    }

                    key.push_back(_pattern[pos]);//存储格式符
                    pos++;
                    if(pos < size)
                        key.push_back(_pattern[pos]);//存储格式符
                    
                    pos++;
                    if(pos < size && _pattern[pos] == '{')//格式符后跟{}则为该操作符具体格式应存入val
                    {
                        pos++;//跳过{
                        while(pos < size && _pattern[pos] != '}')
                        {
                            val.push_back(_pattern[pos++]);//存储具体格式内容
                        }
                        if(pos >= size && _pattern[size-1] != '}')//如果为非 } 结束 则参数非法
                        {
                            std::cout << "未匹配到 }" << std::endl;
                            return false;//直接返回失败
                        }

                        pos++;//跳过}
                    }
                    d1.push_back(std::make_pair(key , val));//存储子项格式符 及符具体格式(如果有)
                    val.clear();//清空
                    key.clear();//清空
                }
                for(auto& pa : d1)
                {
                    FormatIter::ptr p = createformatptr(pa.first , pa.second);
                    if(p != nullptr)
                        _items.push_back(p);//依次存储对应方法
                    else
                        return false;//未知的格式符会返回空
                }

                return true;
            }
        
        private:
            std::string _pattern;//输出格式 
            std::vector<FormatIter::ptr> _items;//日志各项的方法队列
    };

}

7.5 ⽇志落地(Sink)类设计思想

作用:负责将格式化好的日志字符串可靠写入到目的地 —— 如标准输出,指定文件等

注意:Sink类需支持可扩展,其功能性成员函数设为纯虚函数,当增加输出目标时增加一个派生类, 并在派生类中实现对应方法即可

7.5.1日志落地(Sink)类实现

在本次项目中主要实现三种日志落地方式

1:SinkStdout:标准输出

2:SinkFile:输出到指定文件

3:SinkRoolby:当文件到达文件指定大小时,关闭文件并创建新文件 ——— 滚动文件

其中滚动文件相当重要因为:

1:由于磁盘空间有限,不能一直无限向一个文件中怎加数据

2:如果文件太大,也不好打开且数据查找不便


//测试情况 :已测试
//                              日志日志落地类
#ifndef __SINK__
#define __SINK__

#include"Uitl.hpp"
#include<memory>
#include<fstream>
#include<assert.h>
namespace zgbLog
{
    class Sink
    {
    public:
        using ptr = std::shared_ptr<Sink>;

        Sink(){}
        virtual ~Sink(){}
        //将数据写入指定文件
        virtual void Log(const char* data , size_t len) = 0;//必须实现纯虚函数
    };

       写入到标准输出    


    //写入到标准输出
    class SinkStdout : public Sink
    {
    public:
        SinkStdout(){}
        virtual ~SinkStdout(){}
        //将数据写入标准输出
        virtual void Log(const char* data , size_t len)override
        {
                std::cout.write(data , len);//直接cout不一定支持data格式,以及数据长度的指定
        }
    };

    /        写入到指定文件       ///
    //创建时需传入文件名,并打开文件
    class SinkFile : public Sink
    {
    public:
        //创建时需传入文件名,并打开文件 ———— 默认为"./log_file/file1.log"
        SinkFile(std::string pathName = "./log_file/file1.log")
        :_pathName(pathName)
        {
            std::string path = Uitl::File::path(pathName);//获取文件路径
            Uitl::File::createDriectory(path);//查询文件路劲各目录如没有则创建
            _ofs.open(_pathName , std::ios::binary | std::ios::app);//以二进制打开(没有则创建)文件
            assert(_ofs.is_open());//打开失败则断言
        }
        virtual ~SinkFile(){}
        //将数据写入到指定文件
        virtual void Log(const char* data , size_t len)override
        {
                _ofs.write(data , len);
            assert(_ofs.good());//如果_ofs没有设置错误标志,则good返回为True 
        }
    private:
        std::string _pathName;//文件路径及文件名
        std::ofstream _ofs;//关联的文件(如果有)执行输入/输出操作。
    };


         滚动文件   / 
    //滚动文件
    class SinkRollby : public Sink
    {
    public:
        //默认文件名"./logRoll_file/file",默认大小1Gb
        //创建时需传入文件名及文件限制大小,并打开文件 ———— 
        SinkRollby(std::string baoseName = "./logRoll_file/file", const size_t max_fsize = 1024*1024*1024)
        :_baoseName(baoseName)
        ,_max_fsize(max_fsize)
        ,_cur_fsize(0)
        ,_count(1)
        {
            std::string pathName = createNewName();//获取新文件名
            std::string path = Uitl::File::path(pathName);//获取文件路径
            Uitl::File::createDriectory(path);//查询文件路劲各目录如没有则创建

            _ofs.open(pathName , std::ios::binary | std::ios::app);//以二进制打开(没有则创建)文件
            assert(_ofs.is_open());//打开失败则断言
        }
        virtual ~SinkRollby(){}


        //将数据写入到指定文件 ———— 注意写文件时需判断文件大小
        virtual void Log(const char* data , size_t len)override
        {
            _cur_fsize = _cur_fsize + len;
            if(_cur_fsize + len > _max_fsize) 
            {
                createFile();//关闭并创建新文件
                assert(_ofs.good());//如果_ofs没有设置错误标志,则good返回为True =
                _cur_fsize = 0;
            } 
            _ofs.write(data , len);
        }

    private:
        void createFile()//关闭并创建新文件
        {
            _ofs.close();//关闭文件

            std::string pathName = createNewName();//获取新文件名

            _ofs.open(pathName , std::ios::binary | std::ios::app);//以二进制打开(没有则创建)文件
            assert(_ofs.is_open());//打开失败则断言
        }

        std::string createNewName()//创建新文件名 ———— 一定不能生成重复
        {
            time_t ti = Uitl::Data::getTime();
            struct tm t;
            localtime_r(&ti , &t);//将时间戳转换成日期时间信息
            char buf[22] = {0};
            size_t n = strftime(buf , 22 , "%Y-%m-%dT%H-%M-%S", &t);//年_月_日_时_分_秒
            buf[n] = 0;//加入结束符
            std::string name(_baoseName);
            name += std::to_string((size_t)ti);
            name += buf; //将已有文件名 + 年月日...作为文件名
            name += '-';
            name += std::to_string(_count);//给文件添加序号
            name += ".log";
            _count++;//文件添加序号++
            return name;
        }

    private:
        std::string _baoseName;//目标文件路径及基础文件名
        std::ofstream _ofs;//关联的文件(如果有)执行输入/输出操作。

        size_t _max_fsize;//文件限制最大大小
        size_t _cur_fsize;//文件当前大小
        size_t _count;
    };
#endif

7.5.1简单工厂模式构造日志落地类 —— SinkFactroy类

SinkFactroy类作用:根据用户指定(用模板指定)的落地类进行构造,并用父类指针返回。由于父类中的功能型函数为纯虚函数,故落地类用父类指针返回即可达到多态的目的;

SinkFactroy类中有create()静态模板函数,可按模板构造对应函数,可变参数可根据构造对象参数要求传参

//请将该类放在Sink模块中
 class SinkFactory
    {
        public:
        SinkFactory(){}
        template<class SinkType ,class ...Args>//函数模板及参数
        static Sink::ptr create(Args&& ...args) //可变参数
        {
            return std::make_shared<SinkType>(std::forward<Args>(args)...);//对参数展开并完美转法
        }
    };

7.6 ⽇志器类(Logger)设计思想

思想:
1:先设计一个Logger基类

2:Logger基类中Log函数为纯虚函数,需派生类实现具体日志落地方法

3:在基类基础上派生出同步日志器(SyncLogger)和异步日志器(AsyncLogger)

7.6.1 ⽇志器类(Logger)及同步日志器实现

Logger中提供Debug,Info,Warn等函数并调用serialize()函数对日志字符串进行构造日志消息对象LogMsg并进行日志格式化,最后调用Log函数将日志落地

注意:构造日志消息对象前需判断当前日志等级是否达到最低输出等级,如未达到则直接返回

同步⽇志器:直接使用日志落地器对⽇志消息进⾏落地即可

 //日志器
    class Logger
    {
    public:
        //构造时需传日志器名称,该日志器输出的最低日志级别,日志消息格式字符串,日志落地方法
        Logger(const std::string LoggerName , LogLevel::Varlue level 
        , Formatter_str::ptr format , std::vector<zgbLog::Sink::ptr>& sinkPtrArr)
        :_loggerName(LoggerName)
        ,_level(level)
        ,_format(format)
        ,_sinkPtr_Arr(sinkPtrArr.begin() , sinkPtrArr.end())
        {

        }
    public:

        using ptr = std::shared_ptr<Logger>;
        //获取日志器名
        const std::string getName()
        {
            return _loggerName;
        }
        
        void Debug(const std::string file , int line,const std::string& fmt , ...)
        {
            if( LogLevel::Varlue::DEBUG < _level)
                return ;
            //目标:将可变参数展开
            va_list p;
            va_start(p , fmt);
            char* str;
            int state = vasprintf(&str , fmt.c_str() , p );//会写入‘\0’
            if(state < 0)//失败了
            {
                std::string s1("vasprintf defeated (Logger.hpp::Logger::Debug()) !");
                //将错误打进日志
                serialize(LogLevel::Varlue::FATAL, file , line , s1.c_str()); 
            }
            else//成功了
            {   //构造出一个日志消息对象,进行格式化最终调用log落地
                serialize(LogLevel::Varlue::DEBUG , file , line , str ); 
            }

            free(str);//释放内存
        }
        void Info(const std::string file , int line,const std::string& fmt , ...)
        {
            if( LogLevel::Varlue::INFO < _level)
                return ;
            //目标:将可变参数展开
            va_list p;
            va_start(p , fmt);
            char* str;
            int state = vasprintf(&str , fmt.c_str() , p );//会写入‘\0’
            if(state < 0)//失败了
            {
                std::string s1("vasprintf defeated (Logger.hpp::Logger::Info()) !");
                //将错误打进日志
                serialize(LogLevel::Varlue::FATAL, file , line , s1.c_str()); 
            }
            else//成功了
            {   //构造出一个日志消息对象,进行格式化最终调用log落地
                serialize(LogLevel::Varlue::INFO , file , line , str ); 
            }
            
            free(str);//释放内存
        }
        void Warn(const std::string file , int line,const std::string& fmt , ...)
        {
            if( LogLevel::Varlue::WARN < _level)
                return ;
            //目标:将可变参数展开
            va_list p;
            va_start(p , fmt);
            char* str;
            int state = vasprintf(&str , fmt.c_str() , p );//会写入‘\0’
            if(state < 0)//失败了
            {
                std::string s1("vasprintf defeated (Logger.hpp::Logger::Warn()) !");
                //将错误打进日志
                serialize(LogLevel::Varlue::FATAL, file , line , s1.c_str()); 
            }
            else//成功了
            {   //构造出一个日志消息对象,进行格式化最终调用log落地
                serialize(LogLevel::Varlue::WARN , file , line , str ); 
            }
            
            free(str);//释放内存
        }
        void Error(const std::string file , int line,const std::string& fmt , ...)
        {
            if( LogLevel::Varlue::ERROR < _level)
                return ;
            //目标:将可变参数展开
            va_list p;
            va_start(p , fmt);
            char* str;
            int state = vasprintf(&str , fmt.c_str() , p );//会写入‘\0’
            if(state < 0)//失败了
            {
                std::string s1("vasprintf defeated (Logger.hpp::Logger::Error() !");
                //将错误打进日志
                serialize(LogLevel::Varlue::FATAL, file , line , s1.c_str()); 
            }
            else//成功了
            {   //构造出一个日志消息对象,进行格式化最终调用log落地
                serialize(LogLevel::Varlue::ERROR , file , line , str ); 
            }
            
            free(str);//释放内存
        }
        void Fatal(const std::string file , int line,const std::string& fmt , ...)
        {
            if( LogLevel::Varlue::FATAL < _level)
                return ;
            //目标:将可变参数展开
            va_list p;
            va_start(p , fmt);
            char* str;
            int state = vasprintf(&str , fmt.c_str() , p );//会写入‘\0’
            if(state < 0)//失败了
            {
                std::string s1("vasprintf defeated (Logger.hpp::Logger::Fatal() !");
                //将错误打进日志
                serialize(LogLevel::Varlue::FATAL, file , line , s1.c_str()); 
            }
            else//成功了
            {   //构造出一个日志消息对象,进行格式化最终调用log落地
                serialize(LogLevel::Varlue::FATAL , file , line , str ); 
            }
            
            free(str);//释放内存
        }
    protected:
        //构造出一个日志消息对象,进行格式化最终调用log落地
        void serialize(LogLevel::Varlue level  ,const std::string file , int line,const char* data)
        {
            LogMsg msg(level , line , file , _loggerName , data);//创建日志消息
            std::string logStr = _format->format(msg);//将日志信息格式化为字符串
            log(logStr.c_str() , logStr.size());//将日志落地
        }

        //虚函数 ———— 决定是同步日志还是异步日志
        virtual void log(const char* data , const size_t len) = 0;
    protected:
        std::mutex _mutex;//互斥锁
        std::string _loggerName;//日志器名称
        std::atomic<LogLevel::Varlue> _level;//日志输出等级限制
        Formatter_str::ptr _format;//日志格式化器
        std::vector<zgbLog::Sink::ptr> _sinkPtr_Arr;//日志落地 ,支持一个日志落地到一个或多个
    };

    //同步日志器
    class LoggerSync : public Logger
    {
    public:
        LoggerSync(const std::string LoggerName , LogLevel::Varlue level 
        , Formatter_str::ptr format , std::vector<zgbLog::Sink::ptr>& sinkPtrArr)
        :Logger(LoggerName , level , format , sinkPtrArr)//对父类进行初始化
        {

        }
        virtual void log(const char* data , size_t len)
        {
            assert(!_sinkPtr_Arr.empty());//落地方向不能没有
            _mutex.lock();//加锁
            for(auto &ch : _sinkPtr_Arr )//将数据落地
            {
                ch->Log(data , len);
            }
            _mutex.unlock();//解锁
        }
    };

7.7⽇志器类建造者抽象类设计思路及实现

注意:由于项目最终需要有全局日志器和局部日志器,所以日志器建造者仍需使用多态设计;

LoggerBuilderl类为抽象类:本项目最终会在该类基础上派生出局部日志器建造者类全局日志器建造者类

具体实现看下方代码

   ///  日志器建造者  ///
//设计思路:
//1:先抽象一个建造者类
//2:使用派生类对具体功能进行实现
//2_1:局部对象日志器

    //日志器类型
    enum LoggerType
    {
        LOGGER_SYNC,//同步日志器
        LOGGER_ASSYNC//异步日志器
    };

    //日志器建造者基类(抽象类)
    class LoggerBuilder
    {
        
    public:
    	//构造时需传最低日志输出等级,日志器类型,缓冲区类型(异步日志器需设置缓冲区类型)
    	//但不传则使用缺省参数
        using ptr = std::shared_ptr<LoggerBuilder>;
        LoggerBuilder()
        :_level(LogLevel::Varlue::DEBUG)//默认输出等级DEBUG
        ,_Logger_Type(LoggerType::LOGGER_SYNC)//默认为同步日志器
        ,_bufType(BufType::BUF_SAFE)//默认为安全的缓冲区
        {}
        //日志器类型 ———— 默认LOGGER_SYNC
        void LoggerTypefunc(LoggerType Logger_type){_Logger_Type = Logger_type;}
        //异步日志器缓冲区类型 ———— 默认BUF_SAFE
        void bufTypefunc(BufType buf_type){_bufType = buf_type;}
        //日志器名 ———— 必须设置
        void LoggerName(const std::string loggerName){_loggerName = loggerName ;}
        //日志器输出等级限制 ———— 默认为DEBUG
        void Loglevel( LogLevel::Varlue level){_level = level;}
        //日志输出格式 ———— 默认为"[%d{%H:%M:%S}][%t][%p][%c][%f:%l] %m%n"
        void Logformat(const std::string pattern)
        {
            _format = std::make_shared<Formatter_str>( pattern);//创建一个日志格式化器
        }

        //到落地集合 ———— 默认为标准输出
        template<class SinKType ,class ...Args >
        void BuilderSink(Args&& ... args)
        {
            Sink::ptr pp = SinkFactory::create<SinKType>(std::forward<Args>(args)...);//展开并完美转发
            _sinkPtr_Arr.push_back(pp);//保存到落地集合中
        }

        virtual Logger::ptr Builder()=0;//建造函数
        

    protected:
        BufType _bufType;
        LoggerType _Logger_Type;
        std::string _loggerName;//日志器名称
        LogLevel::Varlue _level;//日志输出等级限制
        Formatter_str::ptr _format;//日志格式化器
        std::vector<Sink::ptr> _sinkPtr_Arr;//日志落地 ,支持一个日志落地到一个或多个
    };

7.7.1局部⽇志器类建造者 设计思路及实现

日志器有局部日志器和异步日志器,局部日志器根据_Logger_Type 参数 构造对应的日志器,并返回日志器指针

注意:文章到这还未涉及到异步日志器所以可以先空着异步日志器

//局部日志器建造者类 
    class LoggerBuilderLocal : public LoggerBuilder
    {
    public:
        
        virtual Logger::ptr Builder()override
        {
            assert(!_loggerName.empty());//日志器名称不能为空(empty()为空返回真)
            if(_format.get() == nullptr)
            {
                //如果没有格式化器
                Logformat(std::string());//传空string,则Formatter_str会创建默认格式
            }
            if(_sinkPtr_Arr.empty())//落地器集为空则创建一个默认的标准输出
            {
               // _sinkPtr_Arr.push_back(SinkFactory::create<SinkStdout>());//默认的标准输出
               BuilderSink<SinkStdout>();
            }
            //如果为异步日志器类型
            if(_Logger_Type == LOGGER_ASSYNC)
            {
                //返回异步日志器
                //.........
                return std::make_shared<LoggerAsync>(_loggerName , _level , _format , _sinkPtr_Arr ,_bufType);
            }

            //返回同步日志器
            return std::make_shared<LoggerSync>(_loggerName , _level , _format , _sinkPtr_Arr);
        }
    };

7.8 缓冲区类的设计思想集实现

作用:当程序产生日志后只需将日志写入的缓冲区即可,由专门的写日志线程将日志落地到指定位置

缓冲区类中有三个成员

1、_buf :用于存储缓冲区中的数据
2、_read_sub:可读数据空间起始下标
3、_write_sub:可写数据空间起始下标

缓冲区类中除构造函数等默认成员函数外需自己实现10个函数:
1、push():向缓冲区中添加数据
2、readBegin() ://可读数据起始地址
3、empty():缓冲区是否为空
4、writeAblesize():可写数据长度
5、readAblesize():可读数据长度
6、reset():初始化缓冲区
7、swap( Buffer& buffer):缓冲区及其成员交换
8、moveRead(const size_t len):移动读下标
9、moveWrite(const size_t len):移动写下标
10、r_size(const size_t len):将缓冲区扩容

//缓冲区  ———— 已测试
#ifndef __BUFFER__
#define __BUFFER__
#include<iostream>
#include<vector>
#include<assert.h>
#include<algorithm>

namespace zgbLog
{
    const size_t DEFAULT_BUFFRE_SIZE = 20*1024*1024;//缓冲器初始大小为20M
    const size_t THRESHOLD_BUFFRE_SIZE = 50*1024*1024;//缓冲器容量阀值,到达阀值后线性增长
    const size_t SLOW_BUFFRE_SIZE =  1*1024*1024;//缓冲器容量大于阀值,线性缓慢增长
    const size_t MAX_BUFFRE_SIZE =  70*1024*1024;//缓冲器容量最大值
    //缓冲器
    class Buffer
    {

    public:
        Buffer()
        :_buf(DEFAULT_BUFFRE_SIZE)
        ,_read_sub(0)
        ,_write_sub(0)
        {
        }
        //向缓冲区中添加数据
        void push( const char* data , const size_t len)
        {
            //方式1:采用固定大小时,可写空间不够直接返回
            //方式2:采用扩容方式———— 比较激进会一直扩容(不安全但用于极限测试)
            //何种方式由异步缓冲器决定
            r_size(len);
            int old = _write_sub;
            moveWrite(len);
            //将数据写入缓冲区
            std::copy(data , data+len ,& _buf[old]);
            //将可写下标移动
        }
        //可读数据起始地址
        char* readBegin()
        {
            return &_buf[_read_sub];
        }
        //缓冲区是否为空
        bool empty()
        {
            return (_write_sub == _read_sub);
        }
        //可写数据长度
        size_t writeAblesize()
        {
            return _buf.size() - _write_sub;
        }
        //可读数据长度
        size_t readAblesize()
        {
            return _write_sub - _read_sub;//非循环缓冲区所以直接减即可
        }
        //初始化缓冲区
        void reset()
        {
            _read_sub = 0;
            _write_sub = 0;//将可读数据初始化为0
        }
        //缓冲器交换
        void swap( Buffer& buffer)
        {
            _buf.swap(buffer._buf);
            std::swap(_write_sub , buffer._write_sub);
            std::swap(_read_sub , buffer._read_sub);
        }
        //移动读下标
        void moveRead(const size_t len)
        {
            assert(readAblesize() >= len);//可读数据一定要大于len
            _read_sub += len;
        }
    protected:
        //移动写下标
        void moveWrite(const size_t len)
        {
            assert((_write_sub +len) <= _buf.size());//可写一等要大于len
            _write_sub += len;
        }
        //将缓冲区扩容
        void r_size(const size_t len)
        {
            if(writeAblesize() >= len)
            {
                return;//无需扩容
            }
            else if(_buf.size() < THRESHOLD_BUFFRE_SIZE)//小于阀值成倍增长
            {
                _buf.resize(_buf.size()*2);
            }
            else if(_buf.size() < MAX_BUFFRE_SIZE)//大于阀值但小于最大限制
            {
                _buf.resize(_buf.size() + SLOW_BUFFRE_SIZE);//大于阀值线性增长
            }
            else
            {
                //已到达最大限制
                assert(false);
                std::cout << " //已到达最大限制" << std::endl;
                return;
            }
        }
    

    private:
        std::vector<char> _buf;//缓冲区
        size_t _read_sub;//可读数据空间起始下标
        size_t _write_sub;//可写数据空间起始下标
        //缓冲区为空时:_read_sub == _write_sub
        //缓冲区满时:_write_sub + 1 = _buf.size()
    };
}
#endif

7.8.1 基于缓冲区类的 缓冲器 设计思想集实现 ——— 双缓冲区缓冲器

注意:缓冲区和缓冲器是专供异步线程使用的

设计思想:利用两个缓冲区对象,一个提供给业务线写入日志,另一个缓冲区对象供写日志线程读取数据并将数据写到指定文件中

⽇志输出语句与业务逻辑语句并不是在同⼀个线程中运⾏,⽽是有专⻔的线程⽤于进⾏⽇志输出操作。

业务线程只需要将⽇志放到⼀个内存缓冲区中不⽤等待即可继续执⾏后续业务逻辑(作为⽇志的⽣产者),⽽⽇志的落地操作交给单独的⽇志线程去完成(作为⽇志的消费者),这是⼀个典型的⽣产-消费模型。

注意:写入日志和交换缓冲区时需进行加锁,否则会出现并发问题

//已测试
    
// 异步缓冲器(双缓冲区)
#ifndef __LOOPER__
#define __LOOPER__
#include"Buffer.hpp"
#include<condition_variable>
#include<memory>
#include<mutex>
#include<thread>
#include<functional>
namespace zgbLog
{

    enum BufType
    {
        BUF_SAFE,//安全的,缓冲区采用固定大小
        BUF_UNSA//不安全的,缓冲区采用无限扩容
    };

    using func_t = std::function<void (Buffer&)>;
    class AsyncLooper{
    public:
        using ptr = std::shared_ptr<AsyncLooper>;
        //构造
        AsyncLooper(func_t func , BufType bufType = BUF_SAFE)//默认缓冲区为安全类型
        :_stop(false)//默认创建时启动
        ,_thread(std::thread(&AsyncLooper::threadEntry, this))//创建线程
        ,_func(func)
        ,_buf_Type(bufType)
        {

        }
        ~AsyncLooper()
        {
            stop();
        }
        //停止运行
        void stop()
        {
            _stop = true;
            _cond_con.notify_all();//唤醒所又日志处理线程(消费者)
            _thread.join();//回收线程
        }
        //生产者写入数据到其冲器中
        void push(const char* data , const size_t len)
        {
            std::unique_lock<std::mutex> lock(_mutex);//申请锁
            //首次为true直接向后运行,fasle阻塞线程
            //被唤醒后执行第二个参数,为true则申请锁成功后跳出阻塞,为false阻塞等待,并释放锁,
            if(_buf_Type == BUF_SAFE )//为安全的才会阻塞
                _cond_pro.wait(lock , [&](){ return ( _pro_buf.writeAblesize() >= len ); });
            
            //到这说明容量充足
            //将数据写入缓冲器
            _pro_buf.push(data , len);//pus被处理过会提前扩容,该方案可改为固定容量
            
            //唤醒所又日志处理线程(消费者)
            _cond_con.notify_all();   
        }
    private:
        void threadEntry()//处理线程入口
        {
            while(!_stop || !_pro_buf.empty())//不停止则一直循环
            {
                 {                    
                    std::unique_lock<std::mutex> lock(_mutex);//申请锁
                    //首次为true直接向后运行,fasle阻塞线程
                    //被唤醒后执行第二个参数,为true则申请锁成功后跳出阻塞,为false阻塞等待,并释放锁,
                    _cond_con.wait(lock , [&](){ return ( !_pro_buf.empty() || _stop) ;} );//生产者有数据
                    //说明有数据
                    _pro_buf.swap(_con_buf);//交换缓冲器
                    if(_buf_Type == BUF_SAFE )//为安全的生产者才会阻塞
                        _cond_pro.notify_all();//唤醒所有生产者
                }
                _func(_con_buf);//对缓冲器内数据做处理
               _con_buf.reset();//初始化缓冲区
            }
        }
    private:
        BufType _buf_Type;
        bool _stop;//为真则异步缓冲器停止
        Buffer _pro_buf;//生产者缓冲器
        Buffer _con_buf;//消费者缓冲器 
        std::mutex _mutex;
        std::condition_variable _cond_pro;//生产者条件变量
        std::condition_variable _cond_con;//消费者条件变量
        std::thread _thread;//处理线程(消费者)
        func_t _func;//缓冲器处理方法
    };
}
#endif

7.9 异步⽇志器(AsyncLogger)设计

设计思想:
1、继承日志器类(Logger),并重写纯虚函数Log() , Log()函数直接将数据写入到缓冲区即可
2、需有自己的数据处理函数,并将数据处理函数提供给日志缓冲器供日志处理线程使用

 //异步日志器
    class LoggerAsync : public Logger
    {
    public:
        LoggerAsync(const std::string LoggerName , LogLevel::Varlue level 
        , Formatter_str::ptr format , std::vector<zgbLog::Sink::ptr>& sinkPtrArr
        ,BufType buftype)
        :Logger(LoggerName , level , format , sinkPtrArr)//对父类进行初始化
        ,_AsyncLooper(std::make_shared<AsyncLooper>(std::bind(&LoggerAsync::readlog , this , std::placeholders::_1),  buftype))
        {
            // _AsyncLooper构造需传入处理方法和buf类型
        }
        virtual void log(const char* data , size_t len)
        {
            _AsyncLooper->push(data , len);//push函数为线程安全的故在这无需加锁
        }
        //数据处理函数
        void readlog(Buffer& buffer)
        {
            for(auto& sink : _sinkPtr_Arr)
            {
                sink->Log(buffer.readBegin() , buffer.readAblesize());
            }
        }
   
    private:
        AsyncLooper::ptr _AsyncLooper;//异步缓冲器

    };

7.10 全局单例⽇志器管理类设计(单例模式)

⽇志的输出,我们希望能够在任意位置都可以进⾏,但是当我们创建了⼀个⽇志器之后,就会受到⽇
志器所在作⽤域的访问属性限制。

因此,为了突破访问区域的限制,我们创建⼀个⽇志器管理类,且这个类是⼀个单例类,这样的话,
我们就可以在任意位置来通过管理器单例获取到指定的⽇志器来进⾏⽇志输出了。

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

具体实现:
类中有三个成员变量
1、_rootLogger :为默认日志器,并默认将日志输出到标准输出
2、_LoggerMap:储存被管理全局日志器
3、_mutex :用于保证操作日志器并发安全

具体功能看以下代码:

//全局日志器管理器 ———— 懒汉单列
    class LoggerManager
    {
    public:
        //获取日志管理器单列
        static LoggerManager& getInstance()
        {
            static LoggerManager LM;//需保证编译环境c++11及以后的才为线程安全的
            return LM;
        }
        //添加日志器
        void addLogger(Logger::ptr& Logger_val)
        {
            if(hasLogger(Logger_val->getName()))//如果该日志器已存在直接返回
                return ;
            //添加日志器

            std::unique_lock<std::mutex>(_mutex);//加锁
            _LoggerMap[Logger_val->getName()] = Logger_val;

        }
        //判断日志器是否在日志器数组中
        bool hasLogger(const std::string& name)
        {
            std::unique_lock<std::mutex>(_mutex);//加锁
            auto it = _LoggerMap.find(name);
            if(it == _LoggerMap.end())
            {
                return false;
            }
            return true;//表示该日志器已存在
        }
        //获取日志器
        Logger::ptr getLogger(const std::string& name)
        {
            std::unique_lock<std::mutex>(_mutex);//加锁
            auto it = _LoggerMap.find(name);
            if(it == _LoggerMap.end())
            {
                return nullptr;//没找到该日志器
            }
            return it->second;//返回日志器
        }
        //获取默认日志器(标准输出)
        Logger::ptr getRootLogger()
        {
            return _rootLogger;
        }

        ~LoggerManager()
        {
            //map无需释放,其存储的时智能指针
        }
    private:
        LoggerManager()
        {
             
            //这里不可使用全局日志器否则会循环构造
            LoggerBuilder::ptr pp (new LoggerBuilderLocal());
            pp->LoggerName("斌斌默认同步标注输出日志器");
            _rootLogger = pp->Builder();
            _LoggerMap[_rootLogger->getName()] = _rootLogger;
        }
    
    private:
        Logger::ptr _rootLogger;//默认日志器
        std::unordered_map<std::string ,Logger::ptr> _LoggerMap;//日志器数组
        std::mutex _mutex;//互斥锁
    };

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

提供全局的⽇志器获取接⼝,方便日志器的获取

使⽤代理模式通过全局函数或宏函数来代理Logger类的log、debug、info、warn、error、fatal等接⼝,以便于控制源码⽂件名称和⾏号的输出控制,简化⽤⼾操作。

当仅需标准输出⽇志的时候可以通过主⽇志器来打印⽇志。且操作时只需要通过宏函数直接进⾏输出即可

getLegger():先获取全局日志管理器单列,再用单列获取指定名称的日志器
getRootLegger():先获取全局日志管理器单列,再用单列获取默认日志器

以及全局宏函数等

//已测试

#ifndef __ZGBLOG__
#define __ZGBLOG__
//全局宏函数 ———— 对日志使用便捷性进行优化
#include<stdio.h>
#include"Logger.hpp"

namespace zgbLog
{
    //获取指定的全局日志器
    zgbLog::Logger::ptr getLegger(const std::string name)
    {
    //注意:无对应名字日志器则返回空ju
    zgbLog::Logger::ptr logPtr = zgbLog::LoggerManager::getInstance().getLogger(name);//先获取全局日志管理器单列,再用单列获取指定名称的日志器
    assert(logPtr);//一定不为空
    return logPtr;
    }

    //获取默认的全局日志器 —————— 标准输出
    zgbLog::Logger::ptr getRootLegger()
    {
        //默认日志器在创建单列时自动创建
        zgbLog::Logger::ptr logPtr = zgbLog::LoggerManager::getInstance().getRootLogger();
        assert(logPtr);//一定不为空
        return logPtr;
    }

    //使用宏函数对日志接口进行代理

    //使用方法:
    //logger->Debug("日志消息 %d" , i);//
    #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__)//使用宏函数对日志接口进行代理
    
    使用方法:
    //Log_Debug(LoggerName,"日志消息 %d" , i);
    #define Log_Debug(LoggerName ,fmt , ...) zgbLog::getLegger(LoggerName)->Debug( fmt , ##__VA_ARGS__)//通过LogName取logger并输出日志
    #define Log_Info(LoggerName ,fmt , ...)  zgbLog::getLegger(LoggerName)->Info( fmt , ##__VA_ARGS__)//通过LogName取logger并输出日志
    #define Log_Warn(LoggerName ,fmt , ...)  zgbLog::getLegger(LoggerName)->Warn( fmt , ##__VA_ARGS__)//通过LogName取logger并输出日志
    #define Log_Error(LoggerName ,fmt , ...) zgbLog::getLegger(LoggerName)->Error( fmt , ##__VA_ARGS__)//通过LogName取logger并输出日志
    #define Log_Fatal(LoggerName ,fmt , ...) zgbLog::getLegger(LoggerName)->Fatal(fmt , ##__VA_ARGS__)//通过LogName取logger并输出日志

    //提供宏函数,获取默认的全局日志器直接进行标准输出
    //和printf一样使用
    #define zgbDf_DEBUG(fmt , ...) zgbLog::getRootLegger()->Debug( fmt , ##__VA_ARGS__)//获取默认的全局日志器直接进行标准输出
    #define zgbDf_INFO(fmt , ...) zgbLog::getRootLegger()->Info( fmt , ##__VA_ARGS__)//获取默认的全局日志器直接进行标准输出
    #define zgbDf_WARN(fmt , ...) zgbLog::getRootLegger()->Warn( fmt , ##__VA_ARGS__)//获取默认的全局日志器直接进行标准输出
    #define zgbDf_ERROR(fmt , ...) zgbLog::getRootLegger()->Error( fmt , ##__VA_ARGS__)//获取默认的全局日志器直接进行标准输出
    #define zgbDf_FATAL(fmt , ...) zgbLog::getRootLegger()->Fatal( fmt , ##__VA_ARGS__)//获取默认的全局日志器直接进行标准输出
}

#endif

8.0 性能测试

测试条件
• 每条数据100字节数据
• 50w条⽇志输出所耗时间
• 记录输出总耗时
• 每秒可以输出多少条⽇志
最终得出该日志器每秒可以输出多少MB⽇志

// bench("同步性能测试" , 1 , 500000 , 100);//单线程测试
//测试结果:
// 总消耗时间:2.685447s
// 平均每秒写入日志的数量:186188 

//bench("同步性能测试" , 2, 500000 , 100);//3线程并发测试 
//测试结果:
// 总消耗时间:1.975402s
// 平均每秒写入日志的数量:262411

//bench("同步性能测试" , 5 , 500000 , 100);
//测试结果
// 总消耗时间:1.477518s
// 平均每秒写入日志的数量:338405

//bench("同步性能测试" , 7 , 500000 , 100);
//测试结果
// 总消耗时间:1.605660s
// 平均每秒写入日志的数量:311398

最终同步日志器在5个线程并发下,爆发出了33.84 m/s 每秒的日志写入量
同步日志器测试结论:同步写日志状态下多线程对性能提升不大,主要受限于磁盘

//bench("异步性能测试" , 1 , 500000 , 100);//单线程测试
//测试结果:
// 总消耗时间:3.134974s
// 平均每秒写入日志的数量:170359

//bench("异步性能测试" , 3 , 500000 , 100);//3 线程测试
//测试结果:
// 总消耗时间:1.406261s
// 平均每秒写入日志的数量:355552

//bench("异步性能测试" , 5 , 500000 , 100);//5 线程测试
//测试结果:
// 总消耗时间:1.136412s
// 平均每秒写入日志的数量:439981

bench("异步性能测试" , 7 , 500000 , 100);//5 线程测试
//测试结果:
// 总消耗时间:1.036186s
// 平均每秒写入日志的数量:482538

最终异步日志器在5个线程并发下,爆发出了48.25 m/s 每秒的日志写入量
异步日志器测试结论:异步写日志状态下多线程时写日志的单位速度比同步日志高,但多线程也是有瓶颈的会受限于CPU和内存性能

测试代码:

#include"../Logs/zgbLog.hpp"
#include <chrono>//计时



//参数有日志器名称,写日志线程数量(并发),数据的个数,单个数据长度
void bench(const std::string LogName , size_t thre_count , size_t dataNum , size_t dataLen)
{
    //1:获取日志器
    zgbLog::Logger::ptr logger_ptr(zgbLog::getLegger(LogName));
    //2:组织指定长度的日志消息
    std::string msg_str(dataLen-1 ,'A' );
    //2.1:每个线程要写入的日志数量
    size_t thre_MasNum =  dataNum / thre_count;
    printf("\t线程数量:%d \n" , thre_count);
    printf("\t每个线程日志输出个数:%d \n" , thre_MasNum);
    printf("\t日志总输出个数:%d \n" , dataNum);
    printf("\t日志总输出大小:%d Kb\n" , dataNum*dataLen);
    //3:创建指定数量的线程
    std::vector<std::thread> threadArr;
    std::vector<double> threadArr_time(thre_count);//提前开辟好
    for(int i = 0 ; i < thre_count ; i++)
    {
        threadArr.push_back(std::thread([& , i](){
            //4:线程函数內部开始计时
            auto start = std::chrono::high_resolution_clock::now();
            //5:开始循环写日志
            for(int j = 0 ; j < thre_MasNum ; j++)
            {
                logger_ptr->Fatal("%s" , msg_str.c_str());
            }
            //6:线程函数內部计时结束
            auto end = std::chrono::high_resolution_clock::now();
            //7:输出单线程耗时时间
            std::chrono::duration<double> thre_time = (end-start);//计算单线程消耗时间  
            threadArr_time[i] = thre_time.count();//保存单线程总消耗时间

            size_t Lognum = thre_MasNum / threadArr_time[i];//平均每秒写入日志的数量
            size_t logsize = Lognum * dataLen;//平均每秒写入的日志大小 ———— 单位Kb;
            printf("\n\n\t%d 号线程总消耗时间:%lf s\n",i , threadArr_time[i]);
            printf("\t%d 号线程平均每秒写入日志的数量:%d 个\n",i , Lognum);
            printf("\t%d 号线程总消耗时间:%d kb/s\n", i ,logsize);
        }));
    }
    for(auto& thr : threadArr)
    {
        thr.join();//回收线程
    }
    //8:技术总耗时 ———— 为所以线程最长的那个
    double max_tim = 0;
    for(auto tim : threadArr_time)
    {
        max_tim = max_tim > tim ? max_tim : tim;
    }
    size_t average_logNum = dataNum / max_tim ;
    //9:进行输出打印
    printf("\n\n\t总消耗时间:%lfs\n",max_tim);
    printf("\t平均每秒写入日志的数量:%d\n 个",average_logNum);
}

//同步日志器性能测试
void sync_bench()
{
    zgbLog::LoggerBuilder::ptr GlobalLogger (new zgbLog::LoggerBuilderGlobal());
    GlobalLogger->BuilderSink<zgbLog::SinkFile>("./log_file/file.log");
    GlobalLogger->LoggerName("同步性能测试");
    GlobalLogger->Logformat("%m%n");
    GlobalLogger->LoggerTypefunc(zgbLog::LoggerType::LOGGER_SYNC);//同步日志器
    GlobalLogger->Loglevel(zgbLog::LogLevel::Varlue::ERROR);
    //建造全局日志器
    GlobalLogger->Builder();

   // bench("同步性能测试" , 1 , 500000 , 100);//单线程测试
    //测试结果:
    // 总消耗时间:2.685447s
    // 平均每秒写入日志的数量:186188

    //bench("同步性能测试" , 2, 500000 , 100);//3线程并发测试 
    //测试结果:
    // 总消耗时间:1.975402s
    // 平均每秒写入日志的数量:262411

    //bench("同步性能测试" , 5 , 500000 , 100);
    //测试结果
    // 总消耗时间:1.477518s
    // 平均每秒写入日志的数量:338405

    //bench("同步性能测试" , 7 , 500000 , 100);
    //测试结果
    // 总消耗时间:1.605660s
    // 平均每秒写入日志的数量:311398
}

//异步日志器性能测试
void async_bench()
{
    zgbLog::LoggerBuilder::ptr GlobalLogger (new zgbLog::LoggerBuilderGlobal());
    GlobalLogger->BuilderSink<zgbLog::SinkFile>("./log_file/file.log");
    GlobalLogger->LoggerName("异步性能测试");
    GlobalLogger->Logformat("%m%n");
    GlobalLogger->LoggerTypefunc(zgbLog::LoggerType::LOGGER_ASSYNC);//异步日志器
    GlobalLogger->Loglevel(zgbLog::LogLevel::Varlue::ERROR);
    //建造全局日志器
    GlobalLogger->Builder();

    //bench("异步性能测试" , 1 , 500000 , 100);//单线程测试
    //测试结果:
    // 总消耗时间:3.134974s
    // 平均每秒写入日志的数量:170359

    //bench("异步性能测试" , 3 , 500000 , 100);//3 线程测试
    //测试结果:
    // 总消耗时间:1.406261s
    // 平均每秒写入日志的数量:355552

    //bench("异步性能测试" , 5 , 500000 , 100);//5 线程测试
    //测试结果:
    // 总消耗时间:1.136412s
    // 平均每秒写入日志的数量:439981

    bench("异步性能测试" , 7 , 500000 , 100);//5 线程测试
    //测试结果:
    // 总消耗时间:1.036186s
    // 平均每秒写入日志的数量:482538
}
int main() 
{
    //sync_bench();//同步日志器性能测试 —————— 测试结论同步写日志状态下多线程对性能提升不大,主要受限于磁盘
    async_bench();//异步日志器性能测试 —————— 测试结论异步写日志状态下多线程时写日志的单位速度比同步日志高,但多线程也是有瓶颈的会受限于CPU和内存性能
    return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OpenLayers7官方文档翻译,OpenLayers7中文文档,OpenLayers快速入门

快速入门 这个入门文档向您展示如何放一张地图在web网页上。 开发设置使用 NodeJS&#xff08;至少需要Nodejs 14 或更高版本&#xff09;&#xff0c;并要求安装 git。 设置新项目 开始使用OpenLayers构建项目的最简单方法是运行&#xff1a;npm create ol-app npm create…

中大许少辉博士后畅销榜《乡村振兴战略下传统村落文化旅游设计》自由营 ​​​

中大许少辉博士后畅销榜《乡村振兴战略下传统村落文化旅游设计》自由营 ​​​

Visual Studio(2022)生成链接过程的.map映射文件以及.map映射文件的内容说明

微软的官方说明 /MAP&#xff08;生成映射文件&#xff09; | Microsoft Learn 设置步骤 1. 右键项目属性, 连接器 -> 常规 -> 启用增量链接&#xff0c;设置为否。如下图&#xff1a; 2. 连接器 -> 调试 生成调试信息 设置为 生成调试信息 (/DEBUG) 生成程序数据库…

这一天,中国企业一同吹响数字化集结号

买一双袜子平均只要3天就可以收到货。 点一份外卖最快20分钟就可以送达。 消费互联网十年轰轰烈烈的发展&#xff0c;带来了全国商品的大流通&#xff0c;极大丰富了我们的物质消费生活&#xff0c;也为传统线下商家带来成百上千倍的增长。 消费互联网的流量鼎盛期过后&#xf…

无入侵接口文档smart-doc

Smart-doc优点&#xff1a; 1.非侵入式生成接口文档 2.减少接口文档的手动更新麻烦&保证了接口文档和代码的一致 3.随时可生成最新的接口文档 4.保持团队代码风格一致:smart-doc支持javadoc&#xff0c;必须按照这个才能生成有注释的接口文档 最终效果 1.导入依赖 <pl…

ssm+vue人力资源管理系统源码和论文

ssmvue人力资源管理系统源码和论文098 开发工具&#xff1a;idea 数据库mysql5.7 数据库链接工具&#xff1a;navcat,小海豚等 技术&#xff1a;ssm 系统目标 本系统主要目标是对大中型公司所设计&#xff0c;是对人力资源的科学化的管理&#xff0c;使信息存储达到精确…

2024年java面试(四)--spring篇

文章目录 1.BeanFactory 和 FactoryBean 的区别2.BeanFactory和ApplicationContext有什么区别?3.RequestBody、RequestParam、ResponseBody4.cookie和session的区别5.Servlet的生命周期6.Jsp和Servlet的区别7.SpringMvc执行流程8.RequestMapping是怎么使用9.如果一个接口有多个…

Android 蓝牙开发( 二 )

前言 上一篇文章给大家分享了Android蓝牙的基础知识和基础用法&#xff0c;不过上一篇都是一些零散碎片化的程序&#xff0c;这一篇给大家分享Android蓝牙开发实战项目的初步使用 效果演示 : Android蓝牙搜索&#xff0c;配对&#xff0c;连接&#xff0c;通信 Android蓝牙实…

数据包的处理流程

一个数据包从发送到接收都经历了那些过程 1.启动应用程序新建邮件&#xff0c;将收件人邮箱和邮件内容填写好&#xff0c;应用程序进行编码处理。&#xff08;应用层&#xff09; 2.应用在发送邮件那一刻建立TCP连接&#xff08;三次握手&#xff09;&#xff0c;将数据交给传…

在Nodejs中使用JWT进行鉴权

什么是 JSON Web Token&#xff08;JWT&#xff09;&#xff1f; JSON Web Token&#xff08;JWT&#xff09;是一种用于在web上传递信息的标准&#xff0c;它以JSON格式表示信息&#xff0c;通常用于身份验证和授权。 JWT由三个部分组成&#xff1a;Header&#xff08;头部&…

五、MySQL(DML)如何连接到DataGrip?并显示所有数据库?

前提&#xff1a;已经配置好DataGrip&#xff0c;并创建好一个项目&#xff1a; 1、选择数据库&#xff1a; 点击左上角加号&#xff0c;再选择数据源&#xff0c;选择MySQL数据源&#xff1a; 2、填写信息&#xff1a; 用户栏填写&#xff1a;root 密码填写&#xff1a;你…

算法设计 || 第9题:0-1背包问题动态规划(手写例题+源代码)

&#xff08;一&#xff09;背包问题知识点&#xff1a; &#xff08;二&#xff09;经典测试题&#xff1a; 已知n8种&#xff0c;每种一件。背包最大负载M110。 重量w和价值v如下表&#xff0c;怎样装价值最大?贪心算法 求X[N]最优解&#xff0c;写出求解过程;强化为0/1背包…

基于clip驱动的器官分割和肿瘤检测通用模型

论文&#xff1a;https://arxiv.org/abs/2301.00785 我看这篇主要是看看MRI的多模态融合方法的&#xff0c;所以会略一些东西&#xff0c;感兴趣细节的就翻原文好嘞 摘要 越来越多的公共数据集在自动器官分割和肿瘤检测方面显示出显著的影响。然而&#xff0c;由于每个数据集…

冠达管理:股票减持是什么意思?2023减持新规?

在a股商场上&#xff0c;大股东一般会进行大宗买卖、减持来影响股价&#xff0c;那么&#xff0c;股票减持是什么意思&#xff1f;2023减持新规&#xff1f;下面冠达管理为我们准备了相关内容&#xff0c;以供参阅。 ​ 股票减持是指上市公司持股比例较高的股东出售所持股份以…

ARM-M0 + 24bit 高精度ADC,采样率4KSPS,国产新品,传感器首选

ARM-M0内核MCU 内置24bit ADC &#xff0c;采样率4KSPS flash 64KB&#xff0c;SRAM 32KB 适用于传感器&#xff0c;电子秤&#xff0c;体脂秤等等

【爬虫】5.6 Selenium等待HTML元素

任务目标 在浏览器加载网页的过程中&#xff0c;网页的有些元素时常会有延迟的现象&#xff0c;在HTML元素还没有准备好的情况下去操作这个HTML元素必然会出现错误&#xff0c;这个时候Selenium需要等待HTML元素。例如&#xff1a;上节实例中出现的select的下拉框元素&#xff…

htmx-使HTML更强大

‍本文作者是360奇舞团开发工程师 htmx 让我们先来看一段俳句: javascript fatigue: longing for a hypertext already in hand 这个俳句很有意思&#xff0c;是开源项目htmx文档中写的&#xff0c;意思是说&#xff0c;我们已经有了超文本&#xff0c;为什么还要去使用javascr…

1、Spring是什么?

Spring 是一款主流的 Java EE 轻量级开源框架 。 框架 你可以理解为是一个程序的半成品&#xff0c;它帮我们实现了一部分功能&#xff0c;用这个框架我们可以减少代码的实现和功能的开发。 开源 也就是说&#xff0c;它开放源代码。通过源代码&#xff0c;你可以看到它是如何…

【问题思考总结】为什么B树中的搜索可以在分支结点上结束,而B+树必须到叶节点上才能结束?

问题提出 在刷到B树的时候&#xff0c;发现王道书上写B树非叶子结点仅仅起到索引作用&#xff0c;没有关键字对应记录的存储地址。 然而&#xff0c;观察B树的存储结构&#xff0c;我们发现&#xff0c;其中对于每个结点&#xff0c;也仅有结点的关键字信息和指向子树的指针…