【实战项目】同步&异步日志系统

项目介绍

本项目主要实现一个日志系统,其主要支持以下功能:

  • 支持多级别日志消息

将日志分为不同等级的日志,有调试级别的日志、提示级别的日志、警告级别的日志、错误级别的日志、致命级别的日志……不同级别的日志应对着不同的场景。可以通过日志级别来实现限制输出的级别:当调试时候可以规定打印调试级别以上的日志信息,方便调试;当发布之后可以设置成输出错误级别以上的日志信息,这样能够使定位更加明确,分析更快速准确。

  • 支持同步日志和异步日志

同步日志就是将日志写入到文件/数据库这个操作由业务线程自己来完成;异步日志就是指业务线程将日志放到内存中,并不由业务线程将日志写入到文件/数据库(避免因为文件/数据库问题使业务线程卡在这里,不能执行下面的业务),而是由其他的工作线程将日志写入到文件/数据库,进行实际的输出。

  • 支持可靠写入日志到控制台、文件以及滚动文件中

支持日志消息的不同落地方向,可以将这条消息进行打印、或写入到指定的文件中、或写入到滚动文件中(滚动文件是指当日志信息写入到一个文件中,当这个文件达到指定的大小(也可以是指定的日期)之后切换到下一个文件写入)。

  • 支持多线程程序并发写日志

是一个线程安全的日志系统,当两个线程同时通过日志器来同时写入到同一个文件中时,交叉写入会引发线程安全的问题。要实现的日志系统会解决上面的问题,系统是线程安全的,就算是多个线程同时写也不会出现数据二义的情况。

  • 支持扩展不同的日志落地目标地

可以将日志写入到数据库、或者云服务器中的日志分析服务器中等,支持自定义落地方向。

性能测试:分别对于同步日志器和异步日志器进行各自的性能测试(测试单线程写日志的性能&测试多线程写日志的性能):

实现:封装一个接口,传入日志器名称,线程个数,日志总数量,单条日志的大小,在接口内,创建指定数量的线程,各自负责一部分日志的输出,在输出之前计时开始,在输出完毕后计时结束,所耗时间=结束时间-起始时间,每秒输出量=日志数量/总耗时

日志器的性能测试是在Centos7环境下测试,测试日志总条数为1百万条,每条日志大小是100个字节,总大小为97M左右,在同步日志器下单线程测试:日志输出大概耗时2秒,每秒输出日志数量为50万条,每秒输出日志大小为50M左右;同步日志器下多线程测试:线程数为3个,每条线程输出数量为33万左右,每秒输出日志数量和每秒输出日志大小都和单线程下差不多;异步日志器下单线程测试日志输出大概耗时1.7秒,每秒输出日志数量为60万条,每秒输出日志大小为56M左右;异步日志器多线程下测试日志输出:线程数为3个,每条线程输出数量为33万左右,每秒输出日志数量为70万条,每秒输出日志大小为70M左右。

实用工具类

判断文件是否存在

可以使用头文件unistd.h中的access函数,可以通过access函数检测用户对于这个文件是否有操作权限,还可以判断文件是否存在;

image-20230823105508516

第一个参数是字符串类型,第二个参数是文件权限使用F_OK进行操作,如果文件存在返回0,如果产生错误了,文件不存在就会返回一个-1;

但是这个接口是一个系统调用接口,也就意味着跨平台移植性不太好,如果拿到Windows下的话无法实现功能,因为Windows下没有这个系统调用接口,所以如果要在其他系统下使用,可以使用一些系统通用的接口如stat来判断(获取文件属性,如果获取成功了就代表文件存在,获取失败了就代表文件不存在)这个接口在Windows下也有,Linux下也有,遵循_POSIX_C(可移植)标准:

image-20230823105839224

// 都定义成静态成员函数,不用去实例化对象直接调用
// 判断文件是否存在,参数是传入的路径名
static bool exist(const std::string& pathname)
{
    // access函数检测用户对于这个文件是否有操作权限,还可以判断文件是否存在,在头文件unistd.h
    // 文件存在返回0,不存在返回-1
    // return (access(pathname.c_str(),F_OK) == 0);
    // 为了更好的移植性,我们可以使用stat获取文件属性,成功了代表文件存在,不成功表示文件不存在
    // 在头文件<sys/types.h>\<sys/stat.h>\<unistd.h>中
    struct stat st;
    // 如果小于0,返回false
    if(stat(pathname.c_str(),&st) < 0)
    {
        return false;
    }
    return true;
}

获取文件所在路径

获取文件路径函数传入的参数就是文件路径,通过使用find_last_of函数找到最后一个\和/的位置pos,然后使用substr函数截取从0开始到pos的字符就找到了,+1是将/也截取出来,如果没找到\的位置说明就是在当前目录文件下

// 获取当前文件所在路径,参数是传入的路径名,返回的是文件路径
static std::string path(const std::string& pathname)
{
    // 找到最后一个\\/的位置
    size_t pos = pathname.find_last_of("\\/");
    if(pos == std::string::npos)
    {
        return ".";
    }
    //返回要截取的长度,坐标是从0开始计算的,所以如果想截取/就要+1
    return pathname.substr(0,pos+1);
}

创建文件所在目录

创建文件目录时需要一层一层的创建,使用mkdir函数创建目录,文件权限默认给777,谁都能够访问:

image-20230823111756843

// 创建目录
static void createDirectory(const std::string& pathname)
{
    // 创建目录时需要一层一层的创建
    // pos用来查找\\/的位置,idx用来标定查找的起始位置
    size_t pos = 0,idx = 0;
    while(idx < pathname.size())
    {
        // 从开始位置去找
        pos = pathname.find_first_of("/\\",idx);
        if(pos == std::string::npos)
        {
            // 此时路径中没有\\/,如a.txt,此时就可以直接创建目录
            // 创建目录时需要使用到mkdir系统接口,在头文件<sys/stat.h>\<sys/types.h>中
            // mkdir(const char* pathname,mode_t mode);第二个参数是创建出来的文件目录的权限
            mkdir(pathname.c_str(),0777);
            break;
        }
        // 如果找到了\,那么就要先创建父级目录,pos是\的位置,+1是将\也截取出来了
        std::string parent_dir = pathname.substr(0,pos+1);
        // 判断父级目录是否存在,这里的exist也会判断.和..的情况
        if(exist(parent_dir) == true)
        {
            // 如果存在更新idx的位置,跳出本次循环
            idx = pos + 1;
            continue;
        }
        // 如果父级目录不存在就创建
        mkdir(parent_dir.c_str(),0777);
        idx = pos + 1;//创建之后更新idx的位置
    }
}

实用工具类测试

// 实用工具类的测试
int main()
{
    // 获取当前系统时间,获取的是时间戳
    std::cout << nmzlog::util::Date::now() << std::endl;
    // 先判断文件是否存在,然后创建目录,再获取路径名
    std::string pathname = "./abc/def/txt.log";
    // 如果文件不存在,创建目录
   if(nmzlog::util::File::exist(pathname) == false)
   {
        nmzlog::util::File::createDirectory(pathname);
        std::cout << "创建成功\n";
   }
    // 获取文件路径名中的路径 
    nmzlog::util::File::path(pathname);
}

日志等级类

// 日志等级类
#ifndef __M_LEVEL_H__
#define __M_LEVEL_H__
namespace nmzlog{
    class LogLevel{
    public:
        // 日志等级用枚举类来表示,通过类来访问,避免枚举冲突
        // 定义出系统所包含的全部日志等级,每一个项目中都会设置一个默认的日志输出等级,大于等级默认时才输出
        enum class value{
            UNKNOW = 0,//未知级别
            DEBUG,//调试级别
            INFO,//提示级别
            WARN,//警告级别
            ERROR,//错误级别
            FATAL,//致命级别
            OFF//关闭日志等级输出
        };
        // 提供一个接口将枚举等级类转换为对应的字符串
        // 不需要传递this指针,可以定义成静态成员函数
        static const char* tostring(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";
        }
    };
}
#endif

日志等级类的测试

// 日志等级类的测试
int main()
{
    std::cout << nmzlog::LogLevel::tostring(nmzlog::LogLevel::value::DEBUG) <<std::endl;
}

日志消息类

1、日志的输出时间:一条错误是什么时候产生的

2、日志等级:描述了当前日志信息是一个什么样的错误,在排查的时候能够通过等级去进行过滤排查;

3、源文件名称

4、源代码行号

3和4用于定位出现错误的代码位置;2用于进行日志过滤分析,1用于过滤日志输出时间;

5、线程ID 用于过滤程序出错的线程

6、实际的日志主体消息,到底是什么样的错误;

7、因为要实现的是多日志器的系统,也就是在一个项目里面不同的项目组可能它们定义了不同的日志器进行不同的输出,不同的项目组要能够分辨出来到底是哪一个模块进行的输出,所以要有一个日志器名称(当前支持多日志器的同时使用),进行日志器的分辨

日志线程通过std::this_thread::get_id()获取当前线程:

// 日志消息类
// 定义日志消息类,进行日志中间信息的存储:
// 1、日志的输出时间:一条错误是什么时候产生的
// 2、日志等级:描述了当前日志信息是一个什么样的错误
// 3、源文件名称
// 4、源代码行号
// 3和4用于定位出现错误的代码位置;;2用于进行日志过滤分析,1用于过滤日志输出时间
// 5、线程ID:用于过滤程序出错的线程
// 6、实际的日志主体消息:是什么样的错误
// 7、日志器名称(支持多日志器的同时使用):进行日志器的分辨
#ifndef __M_MESSAGE_H__
#define __M_MESSAGE_H__
#include <iostream>
#include <thread>
#include "util.hpp"
#include "LogLevel.hpp"//用到日志等级
namespace nmzlog{
    // 使用struct默认所有访问都允许
    struct LogMsg{
        // 消息成员声明
        time_t _ctime;//日志产生的时间
        LogLevel::value _level;//日志等级
        std::string _file;//源文件名称
        size_t _line;//源文件行号
        std::thread::id _tid;//线程id
        std::string _payload;//日志器的主体消息
        std::string _logger;//日志器名称
        // 在构造函数中进行初始化,线程id和时间不需要在构造函数中传递
        LogMsg(LogLevel::value level,
        std::string file,
        size_t line,
        std::string payload,
        std::string logger)
        :_ctime(util::Date::now())//调用函数获取当前系统时间
        ,_level(level)
        ,_file(file)
        ,_line(line)
        ,_tid(std::this_thread::get_id())//获取当前线程id来进行初始化
        ,_payload(payload)
        ,_logger(logger)
        {}
    };
}
# endif

日志消息测试类

// 日志消息类的测试
int main()
{
    nmzlog::LogMsg msg(nmzlog::LogLevel::value::DEBUG,__FILE__,__LINE__,"日志测试","root");
    return 0;
}

日志消息格式化模块

派生格式化子项时间类时通过使用头文件time.h中的接口localtime_r:将一个时间戳转换成对应的时间结构:

image-20230823113328536

strct tm结构体中定义的成员如下:

image-20230823113459657

strftime接口:按照指定的格式将时间结构里面的数据组织成一个指定格式的字符串

image-20230823113546925

// 日志输出格式化类
// 日志格式化模块作用:对日志信息进行格式化, 组织成为指定格式的字符串
// %d-日期 %T-缩进 %t-线程id %p-日志等级 %c-日志器名称 %f-文件名 %l-行号 %m-日志消息 %n-换行
#ifndef __M_FMT_H__
#define __M_FMT_H__
#include <iostream>
#include <memory>
#include <vector>
#include <assert.h>
#include <sstream>
#include "LogMessage.hpp"
#include "util.hpp"
#include "time.h"
#include "LogLevel.hpp"
namespace nmzlog{
    //抽象格式化子项基类
    class FormatItem{
    public:
        // 定义智能指针管理对象,便于后面的访问
        using ptr = std::shared_ptr<FormatItem>;
        virtual void format(std::ostream& out,const nmzlog::LogMsg& msg) = 0;
    };
    //派生格式化子项子类--消息,等级,时间,文件名,行号,线程id,日志器名称,制表符,换行,其他
    class MsgFormatItem : public FormatItem
    {
    public:
        void format(std::ostream& out,const nmzlog::LogMsg& msg) override
        {
            out << msg._payload;
        } 
    };
    // 派生格式化子项等级类
    class LevelFormatItem : public FormatItem
    {
    public:
        void format(std::ostream& out ,const nmzlog::LogMsg& msg) override
        {
            out << nmzlog::LogLevel::tostring(msg._level);
        }
    };
    // 派生格式化子项时间类,时间是有子格式的,不同的时间子格式有不同的组织方式
    class TimeFormatItem : public FormatItem
    {
        // 接口localtime:将一个时间戳转换成对应的时间结构,头文件time.h
        // strftime接口:按照指定的格式将时间结构里面的数据组织成一个指定格式的字符串
        // 其中就有%H,就是一个24小时制的小时,%M是分,%S是秒
    public:
        // 将其默认的时间格式的字符串缺省值设置为%H:%M:%S
        TimeFormatItem(const std::string& fmt = "%H:%M:%S")
        :_time_fmt(fmt)
        {}
        void format(std::ostream& out ,const nmzlog::LogMsg& msg) override
        {
            struct tm t;
            localtime_r(&msg._ctime,&t);//t里面保存对应的时间结构
            char tmp[32] = {0};
            // 组织成为一定格式的字符串
            strftime(tmp,31,_time_fmt.c_str(),&t);
            out << tmp;
        }
    private:
        //声明一个时间格式,msg中的时间是时间戳,要将其转换为指定格式的字符串的时间
        std::string _time_fmt;
    };
    // 派生格式化子项文件类
    class FileFormatItem:public FormatItem
    {
    public:
        void format(std::ostream& out ,const nmzlog::LogMsg& msg) override
        {
            out << msg._file;
        }
    };
    // 派生格式化子项行号类
    class LineFormatItem : public FormatItem
    {
    public:
        void format(std::ostream& out, const nmzlog::LogMsg& msg) override
        {
            //数字后来会转换成一个字符串
            out << msg._line;
        }
    };
    // 派生格式化子项线程id类
    class ThreadFormatItem : public FormatItem
    {
    public:
        void format(std::ostream& out, const nmzlog::LogMsg& msg) override
        {
            out << msg._tid;
        }
    };
    // 派生格式化子项日志器名称类
    class LoggerFormatItem:public FormatItem
    {
    public:
        void format(std::ostream& out, const nmzlog::LogMsg& msg) override
        {
            out << msg._logger;
        }
    };
    // 派生格式化子项制表符
    class TabFormatItem:public FormatItem
    {
    public:
        void format(std::ostream& out, const nmzlog::LogMsg& msg) override
        {
            out << "\t";
        }
    };
    // 派生格式化子项换行
    class NLineFormatItem:public FormatItem
    {
        public:
        void format(std::ostream& out,const nmzlog::LogMsg& msg) override
        {
            out << "\n";
        }
    };
    // 派生格式化子项其他
    class OtherFormatItem:public FormatItem
    {
    public:
        //str是原始字符串
        OtherFormatItem(const std::string& str)
        :_str(str)
        {}
        void format(std::ostream& out,const nmzlog::LogMsg& msg) override
        {
            out << _str;
        }
    private:
        std::string _str;
    };
    class Formatter{
    public:
        // 给定一个指针方便后面进行管理
        using ptr = std::shared_ptr<Formatter>;
        // 构造函数必须要有pattern格式化字符串,给指定格式的字符串
        Formatter(const std::string pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n")
        :_pattern(pattern)
        {
            // 对格式化字符串必须解析成功,解析失败就不知道按照什么格式输出,此时要报错
            // 所以在这里就已经进行成员解析了
            assert(parsePattern());
        }
        // 实现一个重载函数,专门来处理格式化子项数组
        // 格式化的思想就是把格式化子项数组按顺序从消息里面取出对应的内容,放到io流中
        void format(std::ostream& out,const nmzlog::LogMsg& msg)
        {
             //格式化就是按照解析顺序逐个从msg里面取出对应的信息
             for(auto& item : _items)
             {
                item->format(out,msg);//通过父类的指针指向子类,调用子类函数实现多态
             }
        }
        // 对msg进行格式化返回格式化之后的字符串
        std::string format(const nmzlog::LogMsg& msg)
        {
            // 自己定义一个stringstream
            std::stringstream ss;
            format(ss,msg);//调用上面的format函数
            return ss.str();
        }
        // 对格式化字符串进行解析,放到格式化子项数组中
        bool parsePattern()
        {
            //1、对格式化规则字符进行解析
            std::vector<std::pair<std::string,std::string>> fmt_order;
            size_t pos = 0;
            // 逐字符向后处理
            std::string key,val;
            while(pos < _pattern.size())
            {
                // 1、处理原始字符串,查看是否是%,不是就是原生字符
                if(_pattern[pos] != '%')
                {
                    val.push_back(_pattern[pos++]);
                    continue;
                }
                // 双%处理
                // 此时说明pos位置是%,判断后面跟着的是字符还是%
                if(pos + 1 < _pattern.size() && _pattern[pos+1] == '%')
                {
                    val.push_back('%');
                    pos += 2;//此时pos要向后走两步
                    continue;
                }
                // 到这里说明不是原始字符串,所以此时要判断val是否为空,不为空要清空
                if(val.empty() == false)
                {
                    fmt_order.push_back(std::make_pair("",val));
                    val.clear();
                }
                // 此时pos指向的是%位置,+1之后指向格式化字符位置
                // 此时还要判断+1之后是不是走到了末尾
                pos += 1;
                if(pos == _pattern.size())
                {
                    std::cout << "%之后,没有对应的格式化字符\n";
                    return false;
                }
                // 把格式化字符取出来
                key = _pattern[pos];
                // +1判断后面是不是时间格式的字符
                pos += 1;//格式化字符之后的位置
                if(pos < _pattern.size() && _pattern[pos] == '{')
                {
                    // 说明此时后面是时间字符串
                    // +1指向{之后,子规则起始地方
                    pos += 1;
                    while(pos < _pattern.size() && _pattern[pos] != '}')
                    {
                        val.push_back(_pattern[pos++]);
                    }
                    // 跳出循环时没有遇到},说明格式是错误的
                    if(_pattern.size() == pos)
                    {
                        std::cout << "子规则{}匹配出错\n";
                        return false; 
                    }
                    pos += 1;
                }
                fmt_order.push_back(std::make_pair(key,val));
                key.clear();
                val.clear();
            }
            // 处理完后fmt得到了一系列的结果
            // 2、根据解析得到的数据初始化格式化子项数组成员
            for(auto& it : fmt_order)
            {
                _items.push_back(createItem(it.first,it.second));
            }
            return true;
        }
    private:
        //根据不同的格式化字符创建不同的格式化子项对象
        FormatItem::ptr  createItem(const std::string& key,std::string& val)
        {
            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 == "f") return std::make_shared<FileFormatItem>();
            if(key == "l") return std::make_shared<LineFormatItem>();
            if(key == "p") return std::make_shared<LevelFormatItem>();
            if(key == "T") return std::make_shared<TabFormatItem>();
            if(key == "m") return std::make_shared<MsgFormatItem>();
            if(key == "n") return std::make_shared<NLineFormatItem>();
            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;//格式化子项数组(指针数组)
    };
}
#endif

日志消息格式化模块测试

// 日志输出格式化类测试
int main()
{
    // 先定义一个日志消息
    nmzlog::LogMsg msg(nmzlog::LogLevel::value::ERROR,__FILE__,__LINE__,"日志测试","root");
    nmzlog::Formatter fmt("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m");
    std::string str = fmt.format(msg);
    std::cout << str << std::endl;
}

日志落地类

日志落地类的目的:就是把一条日志信息输出到指定的一个位置

日志落地功能:将格式化完成后的日志消息字符串,输出到指定的位置

扩展:支持同时将日志落地到不同的位置

实现思想:

1、抽象出落地模块类;

2、不同落地方向从基类进行派生。系统里面使用基类指针来进行访问,所有的关系发生都是通过抽象来进行的,所以好处就是只要添加一个模块只要通过父类指针指向子类对象就可以调用接口来进行功能扩展,这就是扩展的思想。

3、使用工厂模式进行创建与表示的分离,以便于后面哪一块需要调整的话可以更好的进行调整不用在整个项目里面去找。只要在工厂这里找到进行修改就可以了。

// 日志落地类
#ifndef __M_SINK_H__
#define __M_SINK_H__
#include <iostream>
#include <memory>
#include <assert.h>
#include <sstream>
#include <fstream>
#include "util.hpp"
namespace nmzlog{
    // 抽象出落地模块类,抽象出基类
    class LogSink{
    public:
        // 后来通过指针进行访问,所以定义一个智能指针
        using ptr = std::shared_ptr<LogSink>;
        // 构造函数可有可无
        LogSink(){}
        // 用户进行扩展的时候可能会在子类析构中释放一些资源,所以要将析构函数定义成虚函数
        virtual ~LogSink(){}
        // 日志落地功能,参数为数据落地的起始位置、落地数据的长度
        virtual void log(const char* data,size_t len) = 0;
    };
    // 三个落地方向:1、标准输出 2、指定文件 3、滚动文件(以大小进行滚动)
    // 派生出标准输出的派生类
    class StdoutSink : public LogSink
    {
    public:
        // 在类中对虚函数进行重写
        void log(const char* data,size_t len) override
        {
            //这里使用write进行写入,<<没办法指定大小
            std::cout.write(data,len);
        }
    };
    // 2、派生出向指定文件输出的派生类
    class FileSink : public LogSink
    {
    public:
        // 构造的时候要知道要写入的文件名,通过操作句柄打开文件
        FileSink(const std::string& pathname)
        :_pathname(pathname)//初始化文件名
        {
            // 在构造函数直接打开文件,可能文件所在的路径不存在
            // 1、创建日志文件所在的目录,使用path获取文件目录
            nmzlog::util::File::createDirectory(nmzlog::util::File::path(pathname));
            // 2、创建并打开日志文件,ofstream当文件不存在的时候会自动创建文件
            // std::ios::binary以二进制方式打开文件
            // 我们每次写的时候都应该是追加字符串,所以以std::ios::app方式
            // 打开文件
            _ofs.open(_pathname,std::ios::binary | std::ios::app);
            assert(_ofs.is_open());//判断文件是否打开
        }
        // 在类中对虚函数进行重写
        void log(const char* data,size_t len) override
        {
            // 进行文件的写入
            _ofs.write(data,len);
            // 判断下句柄操作句柄是否正常
            assert(_ofs.good());
        }
    private:
        // 成员变量要有其具体的文件路径名和对文件进行操作的操作句柄
        std::string _pathname;//文件路径名
        std::ofstream _ofs;//对输出文件进行管理的一个句柄,头文件fstream,把内容写到文件中使用ofstream
    };
    // 3、向滚动文件中进行输出(以大小进行滚动,当文件达到指定大小创建新文件进行输出)
    class RollBySizeSink:public LogSink
    {
    public:
        // 构造的时候传入文件名并打开文件,通过操作句柄管理文件
        RollBySizeSink(const std::string& basename,size_t max_size)
        :_basename(basename)
        ,_max_size(max_size)
        ,_cur_size(0)//当前文件大小也要初始化
        ,_name_num(0)
        {
            // 在构造函数中去打开这个文件,在createNewFile进行文件的创建
            std::string pathname = createNewFile();
            _name_num++;
            //1、创建日志文件所在的目录,使用path获取文件目录
            util::File::createDirectory(util::File::path(pathname));
            // 打开文件
            _ofs.open(pathname,std::ios::binary | std::ios::app);
            // 判断文件是否打开
            assert(_ofs.is_open());
        }
        // 将日志消息写入到滚动文件中
        void log(const char* data,size_t len) override
        {
            // 1、首先就要判断当前文件大小是否大于文件最大大小
            if(_max_size <= _cur_size)
            {
                // 关闭当前文件
                _ofs.close();
                // 创建并打开新文件
                std::string pathname = createNewFile();
                _ofs.open(pathname,std::ios::binary | std::ios::app);
                assert(_ofs.is_open());
                _cur_size = 0;
                _name_num++;
            }
            // 数据的写入
            _ofs.write(data,len);
            // 判断当前文件是否操作正常,写入是否失败
            assert(_ofs.good());
            _cur_size += len;
        }
    private:
        // 创建新文件,通过基础文件名+扩展名生成新文件名
        std::string createNewFile()
        {
            // 1、首先要获取系统时间
            time_t t = util::Date::now();
            struct tm lt; //lt用来接收转换后的时间结构
            localtime_r(&t,&lt);//把结构化的时间信息放到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_num;
            filename << ".log";//文件后缀名
            // 通过str接口获取成员对象
            return filename.str();
        }
    private:
        std::string _basename;//文件基础名
        std::ofstream _ofs;//文件操作句柄
        size_t _max_size;//文件最大大小
        size_t _cur_size;//文件当前大小
        size_t _name_num;//避免文件写入过快导致的文件名相同
    };
    // 日志工厂类
    class SinkFactory
    {
    public:
        // 通过模板参数传递过来一个日志落地类型,但是因为落地类中的构造函数参数不同所以使用函数模板
        template<class SinkType,class ...Args>
        // 通过右值引用
        static LogSink::ptr create(Args&& ...args)
        {
            // 使用完美转发
            return std::make_shared<SinkType>(std::forward<Args>(args)...);
        }
    };
}
#endif

日志落地类的测试

// 日志落地类的测试
int main()
{
    nmzlog::LogMsg msg(nmzlog::LogLevel::value::ERROR,__FILE__,__LINE__,"日志落地类测试","root");
    // 使用默认的格式
    nmzlog::Formatter fmt;
    std::string str = fmt.format(msg);
    // 将日志进行不同方向的落地
    // 日志直接输出
    nmzlog::LogSink::ptr stdout_lsp = nmzlog::SinkFactory::create<nmzlog::StdoutSink>();
    // 向指定文件输出
    nmzlog::LogSink::ptr file_lsp = nmzlog::SinkFactory::create<nmzlog::FileSink>("./logfile/test.log");
    // 向滚动文件输出
    nmzlog::LogSink::ptr roll_lsp = 
    nmzlog::SinkFactory::create<nmzlog::RollBySizeSink>("./logfile/roll-",1024*1024);//最大大小1MB
    stdout_lsp->log(str.c_str(),str.size());
    file_lsp->log(str.c_str(),str.size());
    size_t cur_size = 0;//记录当前大小
    size_t count = 0; //文件计数器
    while(cur_size < 1024 * 1024 * 10)//10M
    {
        // 通过加一个计数器判断写入的数据是否都是连贯的
        std::string tmp = str + std::to_string(count++);//相当于在每个文件的末尾都加上了一个计数再进行写入
        roll_lsp->log(tmp.c_str(),tmp.size());//文件以1mb进行切换,也就是要切换出10个文件
        cur_size += tmp.size();
    }
}

日志落地类扩展

// 日志落地模块的扩展-以时间进行切换
#ifndef __M_EXPAND_H__
#define __M_EXPAND_H__
#include <iostream>
#include <assert.h>
#include <fstream>
#include "LogSink.hpp"
// 扩展一个以时间作为日志文件滚动切换类型的日志落地模块
namespace nmzlog{
    // 我们定义一个枚举类
    enum class TimeGap{
        GAP_SECOND,
        GAP_MINUTE,
        GAP_HOUR,
        GAP_DAY,
    };
    class LogSinkExpand :public nmzlog::LogSink
    {
    public:
        LogSinkExpand(const std::string& basename,TimeGap gap_type)
        :_basename(basename)//初始化文件名
        {
            // 得到时间段大小
            switch(gap_type)
            {
                case TimeGap::GAP_DAY:
                    _gap_size = 3600*24;
                    break;
                case TimeGap::GAP_HOUR:
                    _gap_size = 3600;
                    break;
                case TimeGap::GAP_MINUTE:
                    _gap_size = 60;
                    break;
                case TimeGap::GAP_SECOND:
                    _gap_size = 1;
                    break;
            }
            _cur_gap = _gap_size == 1 ? 
            nmzlog::util::Date::now() : nmzlog::util::Date::now()%_gap_size;//获取当前是第几个时间段
            // 创建文件
            std::string pathname = createNewFile();
            // 可能当前文件路径不存在,所以还要去创建文件路径
            nmzlog::util::File::createDirectory(nmzlog::util::File::path(pathname));
            // 打开文件
            _ofs.open(pathname,std::ios::binary | std::ios::app);
            // 判断文件是否打开
            assert(_ofs.is_open());
        }
        void log(const char* data,size_t len) override
        {
            // 获取当前系统时间
            time_t cur = nmzlog::util::Date::now();
            // 判断当前系统时间和时间段是否一致,如果一致说明要切换文件了,如果不一致就继续写入
            if((_gap_size == 1) || (cur%_gap_size) == _cur_gap)
            {
                // 关闭原来的文件
                _ofs.close();
                // 创建新的文件
                std::string pathname = createNewFile();
                _ofs.open(pathname,std::ios::binary | std::ios::app);
                // 判断文件是否打开
                assert(_ofs.is_open());
            }
            _ofs.write(data,len);
            assert(_ofs.good());
        }
    private:
        std::string createNewFile()
        {
            time_t t = nmzlog::util::Date::now();
            struct tm lt;//lt是用来接收转换后的时间结构
            // localtime_r是将一个时间戳转换为一个时间结构
            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();//通过str接口获取成员对象
        }
    private:
        std::string _basename;//基础文件名
        std::ofstream _ofs;//操作句柄,管理文件
        size_t _cur_gap;//当前时间段的大小
        size_t  _gap_size;//时间段的大小
    };
}
#endif

日志落地类扩展测试

// 日志落地类扩展测试
int main()
{
    nmzlog::LogMsg msg(nmzlog::LogLevel::value::ERROR,__FILE__,__LINE__,"日志落地类测试","root");
    nmzlog::Formatter fmt;
    std::string str = fmt.format(msg);
    nmzlog::LogSink::ptr time_lsp =
    nmzlog::SinkFactory::create<nmzlog::LogSinkExpand>("./logfile/base-",nmzlog::TimeGap::GAP_SECOND);
    // 定义一个时间,获取当前系统时间
    time_t old = nmzlog::util::Date::now();
    while(nmzlog::util::Date::now() < old + 5)//以5s作为结束
    {
        time_lsp->log(str.c_str(),str.size());
        usleep(1000);
    }
}

日志器模块&同步日志器模块

// 日志器模块-对前面的模块进行整合
#ifndef __M_LOGGER_H__
#define __M_LOGGER_H__
#include <unistd.h>
#include <stdarg.h>
#include <iostream>
#include <unordered_map>
#include <atomic>
#include <mutex>
#include "AsyncLooper.hpp"
#include "util.hpp"
#include "LogMessage.hpp"
#include "LogFormatter.hpp"
#include "SinkExpand.hpp"
#include "LogSink.hpp"
#include  "LogLevel.hpp"
namespace nmzlog{
    // 抽象出一个日志器基类,派生出同步日志器和异步日志器,差别就是落地方式的不同,一个是放到内存,一个是磁盘
    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,std::string 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);
            // 如果ret等于-1代表格式化字符串过程出错
            if(ret == -1)
            {
                std::cout << "vasprintf failed\n";
                return ;
            }
            // 到这里res就包含了格式化组织之后的字符串
            va_end(ap);
            serialize(LogLevel::value::DEBUG,file,line,res);
            free(res);//res指向的空间需要手动释放
        }
        void info(const std::string& file,size_t line,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);
            // 如果ret等于-1代表格式化字符串过程出错
            if(ret == -1)
            {
                std::cout << "vasprintf failed\n";
                return ;
            }
            // 到这里res就包含了格式化组织之后的字符串
            va_end(ap);
            serialize(LogLevel::value::INFO,file,line,res);
            free(res);//res指向的空间需要手动释放
        }
        void warn(const std::string& file,size_t line,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);
            // 如果ret等于-1代表格式化字符串过程出错
            if(ret == -1)
            {
                std::cout << "vasprintf failed\n";
                return ;
            }
            // 到这里res就包含了格式化组织之后的字符串
            va_end(ap);
            serialize(LogLevel::value::WARN,file,line,res);
            free(res);//res指向的空间需要手动释放
        }
        void error(const std::string& file,size_t line,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);
            // 如果ret等于-1代表格式化字符串过程出错
            if(ret == -1)
            {
                std::cout << "vasprintf failed\n";
                return ;
            }
            // 到这里res就包含了格式化组织之后的字符串
            va_end(ap);
            serialize(LogLevel::value::ERROR,file,line,res);
            free(res);//res指向的空间需要手动释放
        }
        void fatal(const std::string& file,size_t line,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);
            // 如果ret等于-1代表格式化字符串过程出错
            if(ret == -1)
            {
                std::cout << "vasprintf failed\n";
                return ;
            }
            // 到这里res就包含了格式化组织之后的字符串
            va_end(ap);
            serialize(LogLevel::value::FATAL,file,line,res);
            free(res);//res指向的空间需要手动释放
        }
        // 返回日志器名称,防止外界对其修改,使用const引用
        const std::string& name()
        {
            return _logger_name;
        }
    protected:
        void serialize(LogLevel::value level,const std::string& file,size_t line,char* str)
        {
            // 3、构造LogMsg对象
            LogMsg msg(level,file,line,str,_logger_name);
            // 4、通过格式化工具对msg消息进行格式化处理,得到格式化之后的字符串
            std::stringstream ss;
            _formatter->format(ss,msg);
            // 5、进行日志落地,调用具体的log接口
            log(ss.str().c_str(),ss.str().size());
        }
    protected:
        // 抽象接口完成实际的落地输出-不同的日志器会有不同的实际落地方式
        virtual void log(const char* data,size_t len) = 0;
    protected:
        Formatter::ptr _formatter;//格式化模块对象
        // 指针数组,通过父类指针指向子类对象进行访问
        std::vector<LogSink::ptr> _sinks;// 落地模块对象数组,可能有多个日志落地方向
        std::atomic<LogLevel::value> _limit_level;//日志输出限制等级,头文件atomic
        std::mutex _mutex;//互斥锁保证日志输出是线程安全的
        std::string _logger_name;//日志器名称
    };
    // 派生出同步日志器
    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:
        // 同步日志器是将日志直接通过落地模块句柄进行日志落地
        void log(const char* data,size_t len) override
        {
            // 加锁,使用锁的管理器将锁管理起来,自动对_mutex加锁和解锁
            std::unique_lock<std::mutex> lock(_mutex);
            if(_sinks.empty())
            {   
                return ;
            }
            // 如果不为空,遍历落地方向的数组
            for(auto& sinks:_sinks)
            {
                // 使用log进行日志落地
                sinks->log(data,len);
            }
        }
    };

同步日志器模块测试

// 日志器,同步日志器测试
int main()
{
    std::string logger_name = "sync_logger";
    nmzlog::LogLevel::value limit = nmzlog::LogLevel::value::WARN;
    nmzlog::Formatter::ptr fmt(new nmzlog::Formatter("[%d{%H:%M:%S}][%c][%f:%l][%p]%T%m%n"));
    nmzlog::StdoutSink::ptr stdout_lsp = nmzlog::SinkFactory::create<nmzlog::StdoutSink>();
    nmzlog::FileSink::ptr file_lsp = nmzlog::SinkFactory::create<nmzlog::FileSink>("./logfile/test.log");
    nmzlog::RollBySizeSink::ptr roll_lsp = 
    nmzlog::SinkFactory::create<nmzlog::RollBySizeSink>("./logfile/base-",1024*1024);
    // 不同的日志落地方向
    std::vector<nmzlog::LogSink::ptr> sinks = {stdout_lsp,file_lsp,roll_lsp};
    nmzlog::Logger::ptr logger(new nmzlog::SyncLogger(logger_name,limit,fmt,sinks));
    logger->debug(__FILE__,__LINE__,"%s","测试日志");
    logger->warn(__FILE__,__LINE__,"%s","测试日志");
    logger->info(__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 += 24;
    }
}

同步日志器建造者模式

// 日志器建造者类
    // 日志器枚举类
    enum class LoggerType{
    LOGGER_SYNC,
    LOGGER_ASYNC,
    };

    class LoggerBuilder
    {
    // 第一步:设置日志器类型;第二步:将不同类型日志器的创建放到同一个日志器建造者类中完成
    public:
        LoggerBuilder()
        :_logger_type(LoggerType::LOGGER_SYNC)//默认日志器类型
        ,_limit_level(LogLevel::value::DEBUG)//默认输出限制等级
        ,_looper_type(AsyncLooper::AsyncType::ASYNC_SAFE)//默认是安全模式
        {}
        // 启动一个非安全的异步操作
        void buildEnableUnSafeAsync()
        {
            _looper_type = AsyncLooper::AsyncType::ASYNC_UNSAFE;
        }
        // 建造日志器类型
        void buildLoggerType(LoggerType type)
        {
            _logger_type = type;
        }
        // 日志器名称
        void buildLoggerName(const std::string& name)
        {
            _logger_name = name;
        }
        // 建造日志器等级
        void buildLoggerLevel(LogLevel::value level)
        {
            _limit_level = level;
        }
        // 建造一个日志格式化器,设置日志输出规则
        void buildFormatter(const std::string& pattern)
        {
            // 构造一个Formatter对象,通过智能指针来对这个对象进行管理
            _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);
        }
        // 创建一个添加一个,创建完了来一个build完成日志器的创建
        virtual Logger::ptr build() = 0;
    protected:
        AsyncLooper::AsyncType _looper_type;//异步器工作模式,是否是安全状态
        LoggerType _logger_type;//日志器类型
        std::string _logger_name;//日志器名称
        LogLevel::value _limit_level;//日志等级,这里不对他进行操作,所以不需要设置原子类型
        Formatter::ptr _formatter;//日志格式化对象
        std::vector<LogSink::ptr> _sinks;//日志落地方向
    };
    class LocalLoggerBuilder : public LoggerBuilder{
    public: 
        Logger::ptr build() override
        {
            // 构造对象
            assert(_logger_name.empty() == false);//判断是否有日志器名称
            // 必须要有formatter,如果为空构造一个
            if(_formatter.get() == nullptr)
            {
                _formatter = std::make_shared<Formatter>();
            }
            // 如果落地方向也为空,默认添加一个标准输出的落地器
            if(_sinks.empty())
            {
                buildSink<StdoutSink>();
            }
            // 如果是异步日志器
            if(_logger_type == LoggerType::LOGGER_ASYNC)
            {
                return std::make_shared<AsyncLogger>
                (_logger_name,_limit_level,_formatter,_sinks,_looper_type);
            }
            // 如果是同步日志器就返回同步日志器
            return std::make_shared<SyncLogger>
            (_logger_name,_limit_level,_formatter,_sinks);
        }
    };

同步日志器建造者模式测试

// 同步日志器建造者模式测试
int main()
{
    std::unique_ptr<nmzlog::LoggerBuilder> builder(new nmzlog::LocalLoggerBuilder());
    builder->buildLoggerName("sync_logger");
    builder->buildFormatter("%m%n");
    builder->buildLoggerLevel(nmzlog::LogLevel::value::WARN);
    builder->buildLoggerType(nmzlog::LoggerType::LOGGER_SYNC);
    builder->buildSink<nmzlog::RollBySizeSink>("./logfile/base-",1024*1024);
    builder->buildSink<nmzlog::FileSink>("./logfile/test.log");
    builder->buildSink<nmzlog::StdoutSink>();

    // 零部件构造完成之后构造一个对象出来
    nmzlog::Logger::ptr logger = builder->build();
    logger->debug(__FILE__,__LINE__,"%s","测试日志");
    logger->warn(__FILE__,__LINE__,"%s","测试日志");
    logger->info(__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 += 24;
    }
}

异步缓冲区模块

// 异步工作模式缓冲区
#ifndef __M_BUFFER_H__
#define __M_BUFFER_H__
#include <vector>
#include <iostream>
#include <algorithm>
#include <assert.h>
#include "util.hpp"
namespace nmzlog{
    // 先设置缓冲区大小
    #define DEFAULT_BUFFER_SIZE (1024*1024*100)//buffer大小为100M
    #define THRESHOLD_BUFFERSIZE (1024*1024*8)//阈值大小为8M
    #define INCREMENT_BUFFERSIZE (10*1024*1024)//超过阈值后每次增长大小10M
    class Buffer
    {
    public:
        // 构造函数初始化
        Buffer()
        :_buffer(DEFAULT_BUFFER_SIZE)
        ,_reader_idx(0)
        ,_writer_idx(0)
        {}
        // 向缓冲区中写数据 ,参数一个是起始地址,一个是数据长度
        void push(const char* data,size_t len)
        {
            // 如果缓冲区满了这里存在两种情况:1.扩容;2.阻塞/返回一个false
            // 在测试这里我们提供的是扩容的思想
            // 调用函数判断是否需要扩容
            ensureEnoughSize(len);
            // 1.将数据拷贝到缓冲区里面
            // 第一个参数是原数据起始地址,第二个参数是原数据的结束地址,第三个参数是目标位置
            std::copy(data,data+len,&_buffer[_writer_idx]);
            // 2.将当前写入位置向后移动
            moveWriter(len);
        }
        // 返回可读数据起始地址的接口
        const char* begin()
        {
            return &_buffer[_reader_idx];
        }
        // 返回可写数据的长度
        size_t writeAbleSize()
        {
            // 对于扩容思路来说,不存在可写空间大小,因为总是可写因此这个接口仅仅针对固定大小缓冲区的接口
            return (_buffer.size() - _writer_idx);
        }
        // 返回可读数据的长度
        size_t readAbleSize()
        {
            return (_writer_idx - _reader_idx);
        }
        // 移动读位置的接口,向后移动指定长度,保证读取数据之后向后偏移
        void moveReader(size_t len)
        {
            // 要保证len必须小于等于可读长度
            assert(len <= readAbleSize());
            _reader_idx += len;
        }
        // 重置读写位置,初始化缓冲区
        void reset()
        {
            _writer_idx = 0;
            _reader_idx = 0;
        }
        // 对缓冲区空间实现交换操作
        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 ;//不需要扩容
            }
            size_t new_size = 0;
            // 上面定义的有阈值,小于阈值的情况翻倍增长,大于阈值之后线性增长
            if(_buffer.size() < THRESHOLD_BUFFERSIZE)
            {
                new_size = _buffer.size() * 2 + len;
            }
            else{
                new_size = _buffer.size() + INCREMENT_BUFFERSIZE + len;
            }
            _buffer.resize(new_size);
        }
        // 移动写位置,写入数据之后向后偏移
        void moveWriter(size_t len)
        {
            assert((len + _writer_idx) <= _buffer.size());
            _writer_idx += len;
        }
    private:
        std::vector<char> _buffer;//缓冲区
        size_t _reader_idx;//当前可读数据的指针-本质是下标
        size_t _writer_idx;//当前可写位置的指针
    };
}
#endif

异步缓冲区模块测试

异步日志器缓冲区测试
读取文件数据,一点一点写入缓冲区,最终将缓冲区数据写入文件,判断生成的新文件与源文件是否一致
int main()
{
    // 二进制形式读取文件,保证和文件中的数据完全一致
    std::ifstream ifs("./logfile/test.log",std::ios::binary);
    if(ifs.is_open() == false)
    {
        std::cout << "open failed\n";
        return -1;
    }
    ifs.seekg(0,std::ios::end);//读写位置跳转到文件末尾
    // 获取末尾的偏移量
    // 获取当前读写位置相对于起始位置的偏移量,这里就相当于文件长度
    size_t fsize = ifs.tellg();
    std::cout << fsize << std::endl;
    // 重新跳转到起始位置
    ifs.seekg(0,std::ios::beg);
    std::string body;
    body.resize(fsize);
    ifs.read(&body[0],fsize);
    if(ifs.good() == false)
    {
        std::cout << "read failed\n";
        return -1;
    }
    // 此时数据已经读到body字符串中了,接着就是逐个向buffer里面写入数据了
    nmzlog::Buffer buffer;
    for(int i = 0; i < body.size();i++)
    {
        buffer.push(&body[i],1);
    }
    std::cout << buffer.readAbleSize() << std::endl;
    // 添加之后将数据写入到文件中
    std::ofstream ofs("./logfile/tmp.log",  std::ios::binary);
    // 将数据一次性写入
    // ofs.write(buffer.begin(),buffer.readAbleSize());

    // 进行数据的写入,一次写入一个数据
    size_t buffersize = buffer.readAbleSize();
    for(int i = 0; i < buffersize;i++)
    {
        ofs.write(buffer.begin(),1);
        if(ofs.good() == false)
        {
            std::cout << "write failed\n";
            return -1;
        }
        buffer.moveReader(1);
    }
    ofs.close();
}

异步日志器及异步工作器

// 异步日志器-异步工作器
#ifndef __M_LOOPER_H__
#define __M_LOOPER_H__
#include <iostream>
#include <thread>
#include "AsyncBuffer.hpp"
#include <atomic>
#include <condition_variable>
#include <mutex>
#include <functional>
#include <memory>
namespace nmzlog{
    class AsyncLooper{
    public:
        // 定义一个枚举类型用来判断是否是安全状态
        enum class AsyncType{
            ASYNC_SAFE,//安全状态,缓冲区满了就阻塞,避免资源耗尽的风险
            ASYNC_UNSAFE,//不安全状态,无限扩容,常用于性能测试
        };  
        // 定义一个函数指针类型的function
        using Functor = std::function<void(Buffer&)>;
    public:
        // 定义一个智能指针类型
        using ptr = std::shared_ptr<AsyncLooper>;
        // 构造函数
        AsyncLooper(const Functor& cb,
        AsyncLooper::AsyncType looper_type = AsyncLooper::AsyncType::ASYNC_SAFE)
        :_stop(false)//false现在是不停止的
        ,_callBack(cb)
        ,_thread(std::thread(&AsyncLooper::threadEntry,this))//thread里的第一个参数就是入口函数
        ,_looper_type(looper_type)
        {}
        // 析构函数,当异步工作器要退出的时候停止工作线程
        ~AsyncLooper()
        {
            stop();
        }
        // 停止异步工作器
        void stop()
        {   
            // 将退出标志设置为true
            _stop = true;
            // 唤醒所有的工作线程
            _cond_con.notify_all();
            //等待工作线程的退出
            _thread.join();
        }
        // 提供数据,不断新增,扩容的时候就不断添加到内存中,如果是固定大小就会阻塞
        void push(const char* data,size_t len)
        {
            // 调用生产缓冲区压入数据
            // 两种思想:   
            // 1、动态扩容(无限扩容),并不存在存储数据的上限(非安全状态,因为在极限情况下靠近系统崩溃的风险)
            // 2、固定大小,达到生产缓冲区中满了就不断阻塞--安全状态
            // 实现的时候先加个锁,对互斥锁进行管理
            std::unique_lock<std::mutex> lock(_mutex); 
            // 如果是安全状态进行唤醒   
            if(_looper_type == AsyncType::ASYNC_SAFE)
            {
                // 等待被唤醒的条件
                _cond_pro.wait(lock,[&]()
                    {return _pro_buf.writeAbleSize() >= len;});
            }

            // 到这里说明能够向缓冲区中添加数据
            _pro_buf.push(data,len);
            // 唤醒消费者对缓冲区中的数据进行处理
            _cond_con.notify_one();
        }
    private:
        // 线程的入口函数-工作线程,对消费缓冲区中的数据进行处理
        void threadEntry()
        {
            // 使用stop判断当前线程是否停止运行
            // while(!_stop)
            // 测试时候将其死循环
             while(1)
            {   
                {
                    // 加锁,对互斥锁进行管理
                    std::unique_lock<std::mutex> lock(_mutex);
                    // 测试,当为trueu或者生产缓冲区为空才退出
                    if(_stop && _pro_buf.empty()) break;
                    // 消费者判断等待,
                    _cond_con.wait(lock,[&]()
                    {return _stop || !_pro_buf.empty();});
                    // 和生产缓冲区交换
                    _con_buf.swap(_pro_buf);
                    // 如果是安全状态进行唤醒
                    if(_looper_type == AsyncType::ASYNC_SAFE)
                    {
                        // 唤醒所有的生产者
                        _cond_pro.notify_all();
                    }
                }
                    // 唤醒后对消费者缓冲区进行数据处理
                    _callBack(_con_buf);
                    // 初始化消费缓冲区
                    _con_buf.reset();
            }
        }
    private:
        // 回调函数
        // 具体对缓冲区数据进行处理的回调函数,由异步工作器使用者来传入
        Functor _callBack;
        // 设置为原子类型
        std::atomic<bool> _stop; //工作器停止标志,是否停止异步工作器,是true,否false
        Buffer _pro_buf;//生产缓冲区
        Buffer _con_buf;//消费缓冲区
        std::mutex _mutex;//互斥锁保证安全
        // 两个条件变量,会提供两个pcb的等待队列
        std::condition_variable _cond_pro;//生产者的等待队列的条件变量
        std::condition_variable _cond_con;//消费者的等待队列的条件变量
        std::thread _thread;//异步工作器对应的工作线程
        AsyncLooper::AsyncType _looper_type;//判断是否是安全状态
    };
}
#endif

异步日志器及异步工作器测试

异步日志器及异步工作器测试
int main()
{
    std::unique_ptr<nmzlog::LoggerBuilder> builder(new nmzlog::LocalLoggerBuilder());
    builder->buildLoggerName("async_logger");
    builder->buildFormatter("[%c]%m%n");
    builder->buildEnableUnSafeAsync();//直接放内存,不断扩容-不安全的状态
    builder->buildLoggerLevel(nmzlog::LogLevel::value::WARN);
    builder->buildLoggerType(nmzlog::LoggerType::LOGGER_ASYNC);
    builder->buildSink<nmzlog::StdoutSink>();
    builder->buildSink<nmzlog::FileSink>("./logfile/test.log");
    // 构造一个局部对象出来
    nmzlog::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 count = 0;
    // size_t cur_size = 0;
    // while(cur_size < 10 *1024 *1024)//10MB
    // {
    //     logger->fatal(__FILE__,__LINE__,"测试日志-%d",count++);
    //     cur_size += 24;
    // }
    // 通过条数来计算
    size_t count = 0;
    while(count < 500000)//10MB
    {
        logger->fatal(__FILE__,__LINE__,"测试日志-%d",count++);
    }
}

全局建造者

// 日志器管理器类
    class LoggerManager{
    public:
        // 单例对象的获取,默认使用单例懒汉模式
        static LoggerManager& getInstance()
        {
            static LoggerManager eton;
            return eton;
        }
        // 添加一个日志器,把日志器对象传递进来
        void addLogger(Logger::ptr& logger)
        {
            // 添加之前判断logger是否存在
            if(hasLogger(logger->name()))
                return;
            // 加锁,添加日志器
            std::unique_lock<std::mutex> lock(_mutex);
            _loggers.insert(std::make_pair(logger->name(),logger));
        }
        // 判断有没有指定的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;
            }
            return true;
        }
        // 获取指定的日志器,通过日志器名称来获取日志器
        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:
        // 构造函数私有
        LoggerManager()
        {
            // 首先定义一个builder对象,本地日志器的建造者
            std::unique_ptr<nmzlog::LoggerBuilder> builder(new  nmzlog::LocalLoggerBuilder());
            // 创建之后,通过builder设置一个名称
            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:
        Logger::ptr build() override
        {
            // 必须要有日志器名称
            assert(!_logger_name.empty());
            // 必须要有formatter
            if (_formatter.get() == nullptr)
            {
                _formatter = std::make_shared<Formatter>();
            }
            if(_sinks.empty())
            {
                buildSink<StdoutSink>();
            }
            Logger::ptr logger;
            // 如果是异步日志器
            if(LoggerType::LOGGER_ASYNC == _logger_type)
            {
                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);
            }
            // 获取之后进行一个单例对象的添加
            LoggerManager::getInstance().addLogger(logger);
            return logger;
        }
    };
}
#endif

全局建造者测试

// 全局建造者测试
void test_log()
{
    nmzlog::Logger::ptr logger = nmzlog::LoggerManager::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)//10MB
    {
        logger->fatal(__FILE__,__LINE__,"测试日志-%d",count);
        count++;
    }
}
int main()
{
    std::unique_ptr<nmzlog::LoggerBuilder> builder(new nmzlog::GlobalLoggerBuilder());
    builder->buildLoggerName("async_logger");
    builder->buildFormatter("[%c]%m%n");
    builder->buildEnableUnSafeAsync();//直接放内存,不断扩容-不安全的状态
    builder->buildLoggerLevel(nmzlog::LogLevel::value::WARN);
    builder->buildLoggerType(nmzlog::LoggerType::LOGGER_ASYNC);
    builder->buildSink<nmzlog::StdoutSink>();
    builder->buildSink<nmzlog::FileSink>("./logfile/test.log");
    //构造对象
    builder->build();

    // 在普通函数里面测试
    test_log();
}

全局接口

// 全局接口类
#ifndef __M_NMZLOG_H__
#define __M_NMZLOG_H__
#include <iostream>
#include "LogLogger.hpp"
namespace nmzlog{
    // 1、提供获取指定日志器的全局接口(避免用户自己操作单例对象)
    Logger::ptr getLogger(const std::string& name)
    {
        // 直接通过单例对象获取
        return nmzlog::LoggerManager::getInstance().getLogger(name);
    }
    // 获取默认日志器
    Logger::ptr rootLogger()
    {
        // 直接通过单例对象获取
        return nmzlog::LoggerManager::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,...) nmzlog::rootLogger()->debug(fmt,##__VA_ARGS__)
    #define INFO(fmt,...) nmzlog::rootLogger()->info(fmt,##__VA_ARGS__)
    #define WARN(fmt,...) nmzlog::rootLogger()->warn(fmt,##__VA_ARGS__)
    #define ERROR(fmt,...) nmzlog::rootLogger()->error(fmt,##__VA_ARGS__)
    #define FATAL(fmt,...) nmzlog::rootLogger()->fatal(fmt,##__VA_ARGS__)
}
#endif

全局接口测试

// 全局接口测试
// 只需要包含下面这一个头文件就可以了
#include "nmzlog.hpp"
void test_log()
{
    nmzlog::Logger::ptr logger = nmzlog::LoggerManager::getInstance().getLogger("async_logger");

    // logger->debug("%s","测试日志");
    // logger->info("%s","测试日志");
    // logger->warn("%s","测试日志");
    // logger->error("%s","测试日志");
    // logger->fatal("%s","测试日志");

    DEBUG("%s","测试日志");
    INFO("%s","测试日志");
    WARN("%s","测试日志");
    ERROR("%s","测试日志");
    FATAL("%s","测试日志");

    // 通过条数来计算
    size_t count = 0;
    while(count < 500000)//10MB
    {
        logger->fatal("测试日志-%d",count);
        count++;
    }
}
int main()
{
    std::unique_ptr<nmzlog::LoggerBuilder> builder(new nmzlog::GlobalLoggerBuilder());
    builder->buildLoggerName("async_logger");
    builder->buildFormatter("[%c][%f:%l]%m%n");
    builder->buildEnableUnSafeAsync();//直接放内存,不断扩容-不安全的状态
    builder->buildLoggerLevel(nmzlog::LogLevel::value::WARN);
    builder->buildLoggerType(nmzlog::LoggerType::LOGGER_ASYNC);
    builder->buildSink<nmzlog::StdoutSink>();
    builder->buildSink<nmzlog::FileSink>("./logfile/test.log");
    //构造对象
    builder->build();

    // 在普通函数里面测试
    test_log();
}


  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不 良

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值