[C++]——同步异步日志系统(5)

一、日志消息格式化设计

  1. 日志格式化模块的作用:对日志消息进行格式化,并且组织成指定格式的字符串。

%d ⽇期
%T 缩进
%t 线程id
%p ⽇志级别
%c ⽇志器名称
%f ⽂件名
%l ⾏号
%m ⽇志消息
%n 换⾏
如:[2024-07-09 17:04][root][1234567][main.c:99][FATAL]:\t创建套接字失败…\n

格式化字符串控制了日志的输出格式
定义格式化字符,是为了让日志系统进行日志格式化更加的灵活方便。

成员:
1.格式化字符串(用户定义的输出格式格式)
2.格式化子项数组(对格式化字符串进行解析,保存了日志消息要素的排序)
不同的格式化子项,会从日志消息中取出指定的元素,转化为字符串。
[%d{%H:%M:%S}][%f:%l]%m%n

格式化子项:

其他信息(非格式化字符)子项:[
日期子项:%H%M%S
其他信息子项:]
其他信息子项:[
文件名子项:main.c
其他信息子项::
行号信息子项:99
其他信息子项:]
消息主体子项:吃饭睡觉打豆豆
换行子项:\n

[12:40;50][main.c:99]吃饭睡觉打豆豆\n

1.1 格式化子项类的定义和实现

  1. 格式化子项的实现思想:从日志消息中取出指定的元素,追加到一块内存空间中。
    设计思想:
    1.抽象出一个格式化子项的基类
    2.基于基类,派生出不同的格式化子项子类:
    主体消息、日志等级、时间子项、文件名、行号、日志器名称、线程ID、制表符、换行、非格式化的原始字符串。
    这样就可以在父类中定义父类指针的数组,指向不同的格式化子项子类的对象。

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

  • MsgFormatItem :表示要从LogMsg中取出有效⽇志数据
  • LevelFormatItem:表示要从LogMsg中取出⽇志等级
  • NameFormatItem :表示要从LogMsg中取出⽇志器名称
  • ThreadFormatItem :表示要从LogMsg中取出线程ID
  • TimeFormatItem:表示要从LogMsg中取出时间戳并按照指定格式进行格式化
  • CFileFormatItem :表示要从LogMsg中取出源码所在⽂件名
  • CLineFormatItem :表示要从LogMsg中取出源码所在⾏号
  • TabFormatItem :表示⼀个制表符缩进
  • NLineFormatItem :表示⼀个换行
  • OtherFormatItem :表示⾮格式化的原始字符串
  1. 首先搭架子,定义抽象的格式化子项的基类
  //抽象格式化子类基类
    class FormatItem{
        //c++17语法与typedef作用一样
        using ptr=std::shared_ptr<FormatItem>;
        //纯虚函数
        virtual void format(std::ostream &out,const LogMsg &msg)=0;
    }
  1. 在基类的基础上,派生出格式化子项的子类
#ifndef __M_FORMAT_H__
#define __M_FORMAT_H__
//日志消息格式化模块
#include <ctime>
#include "level.hpp"
#include "message.hpp"

namespace logslearn{
    //抽象格式化子类基类
    class FormatItem{
        //c++17语法与typedef作用一样
        using ptr=std::shared_ptr<FormatItem>;
        //纯虚函数
        virtual void format(std::ostream &out,const LogMsg &msg)=0;
    };

    //派生出格式化子项的子类:主体消息、日志等级、时间子项、文件名、行号、日志器名称、线程ID、制表符、换行、非格式化的原始字符串
    //主体消息
    class MsgFormatItem:public FormatItem{
        public:
        //虚函数进行重写
        void format(std::ostream &out,const LogMsg &msg) override{
            out<<msg._payload;
        }
    };
    //日志等级
    class LevelFormatItem:public FormatItem{
        public:
        //虚函数进行重写
        void format(std::ostream &out,const LogMsg &msg) override{
            out<<loglevel::tostring(msg._level);
        }
    };

     //时间子项
    class TimeFormatItem:public FormatItem{
        public:
        //默认构造函数,设置时间的默认格式
        TimeFormatItem(const std::string &fmt="%H:%M:%S"):_time_fmt(fmt){}
        //虚函数进行重写
        void format(std::ostream &out,const LogMsg &msg) override{
            struct tm t;
            localtime_r(&msg._ctime,&t);
            char tmp[32]={0};
            strftime(tmp,31,_time_fmt.c_str(),&t);
            out<<tmp;
        }
        private:
        std::string _time_fmt;//默认的时间格式
    };

     //文件名
    class FileFormatItem:public FormatItem{
        public:
        //虚函数进行重写
        void format(std::ostream &out,const LogMsg &msg) override{
            out<<msg._file;
        }
    };

     //行号
    class LineFormatItem:public FormatItem{
        public:
        //虚函数进行重写
        void format(std::ostream &out,const LogMsg &msg) override{
            out<<msg._line;
        }
    };

     //日志器名称
    class LoggerFormatItem:public FormatItem{
        public:
        //虚函数进行重写
        void format(std::ostream &out,const LogMsg &msg) override{
            out<<msg._logger;
        }
    };

     //线程ID
    class ThreadFormatItem:public FormatItem{
        public:
        //虚函数进行重写
        void format(std::ostream &out,const LogMsg &msg) override{
            out<<msg._tid;
        }
    };

     //制表符
    class TabFormatItem:public FormatItem{
        public:
        //虚函数进行重写
        void format(std::ostream &out,const LogMsg &msg) override{
            out<<"\t";
        }
    };

     //换行
    class NLineFormatItem:public FormatItem{
        public:
        //虚函数进行重写
        void format(std::ostream &out,const LogMsg &msg) override{
            out<<"\n";
        }
    };

     //非格式化的原始字符串
    class OtherFormatItem:public FormatItem{
        public:
        //设置默认构造函数
        OtherFormatItem(std::string &str):_str(str){}
        //虚函数进行重写
        void format(std::ostream &out,const LogMsg &msg) override{
            out<<_str;
        }
        private:
        std::string _str;
    };
}
#endif

1.2 格式化类的定义和实现

  1. 确定框架,设计格式化类,设计需要的成员,需要完成的功能。
/*
 格式化类的定义和实现
 %d 表示日期 ,包含子格式{%H%M%S} 
 %t 表示线程id
 %c 表示⽇志器名称
 %f 表示源码⽂件名
 %l 表示源码⾏号
 %p 表示⽇志级别
 %T 表示制表符缩进
 %m 表示主体消息
 %n 表示换⾏
*/
class Formatter{
    public:
        //构造默认函数
        Formatter(const std::string &pattern="[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"):_pattern(pattern){}
        //对msg进行格式化
        void format(std::ostream &out,const LogMsg &msg);
        std::string format(); 
        //对格式化规则字符进行解析
        bool parsePatern();
    private:
        //根据不同的格式化字符创建不同的格式化子项对象
        FormatItem::ptr createItem(const std::string &key,const std::string &val);
    private:
        std::string _pattern;//格式化规则字符串
        std::vector<logslearn::FormatItem::ptr> _items;//格式化字符串解析出的格式化子项
};
  1. 对格式化的功能接口进行设计
#ifndef __M_FORMAT_H__
#define __M_FORMAT_H__
// 日志消息格式化模块

#include "message.hpp"
#include "level.hpp"
#include <memory>
#include <ctime>
#include <vector>
#include <assert.h>
#include <sstream>
namespace logslearn
{
    // 抽象格式化子类基类
    class FormatItem
    {
    public:
        // c++17语法与typedef作用一样
        using ptr = std::shared_ptr<FormatItem>;
        // 纯虚函数
        virtual void format(std::ostream &out,const LogMsg &msg) = 0;
    };

    // 派生出格式化子项的子类:主体消息、日志等级、时间子项、文件名、行号、日志器名称、线程ID、制表符、换行、非格式化的原始字符串
    // 主体消息
    class MsgFormatItem : public FormatItem
    {
    public:
        // 虚函数进行重写
        void format(std::ostream &out,const LogMsg &msg) override
        {
            out << msg._payload;
        }
    };
    // 日志等级
    class LevelFormatItem : public FormatItem
    {
    public:
        // 虚函数进行重写
        void format(std::ostream &out,const LogMsg &msg) override
        {
            out << loglevel::tostring(msg._level);
        }
    };

    // 时间子项
    class TimeFormatItem : public FormatItem
    {
    public:
        // 默认构造函数,设置时间的默认格式
        TimeFormatItem(const std::string &fmt = "%H:%M:%S") : _time_fmt(fmt) {
            if (_time_fmt.empty()) _time_fmt = "%H:%M:%S";
        }
        // 虚函数进行重写
        void format(std::ostream &out,const LogMsg &msg) override
        {
            struct tm t;
            localtime_r(&msg._ctime, &t);
            char tmp[32] = {0};
            strftime(tmp, 31, _time_fmt.c_str(), &t);
            out<<tmp;
        }

    private:
        std::string _time_fmt; // 默认的时间格式
    };

    // 文件名
    class FileFormatItem : public FormatItem
    {
    public:
        // 虚函数进行重写
        void format(std::ostream &out,const LogMsg &msg) override
        {
            out << msg._file;
        }
    };

    // 行号
    class LineFormatItem : public FormatItem
    {
    public:
        // 虚函数进行重写
        void format(std::ostream &out,const LogMsg &msg) override
        {
            out << msg._line;
        }
    };

    // 日志器名称
    class LoggerFormatItem : public FormatItem
    {
    public:
        // 虚函数进行重写
        void format(std::ostream &out,const LogMsg &msg) override
        {
            out << msg._logger;
        }
    };

    // 线程ID
    class ThreadFormatItem : public FormatItem
    {
    public:
        // 虚函数进行重写
        void format(std::ostream &out,const LogMsg &msg) override
        {
            out << msg._tid;
        }
    };

    // 制表符
    class TabFormatItem : public FormatItem
    {
    public:
        // 虚函数进行重写
        void format(std::ostream &out,const LogMsg &msg) override
        {
            out << "\t";
        }
    };

    // 换行
    class NLineFormatItem : public FormatItem
    {
    public:
        // 虚函数进行重写
        void format(std::ostream &out,const LogMsg &msg) override
        {
            out << "\n";
        }
    };

    // 非格式化的原始字符串
    class OtherFormatItem : public FormatItem
    {
    public:
        // 设置默认构造函数
        OtherFormatItem(std::string str) : _str(str) {}
        // 虚函数进行重写
        void format(std::ostream &out,const LogMsg &msg) override
        {
            out << _str;
        }

    private:
        std::string _str;
    };
    /*
     格式化类的定义和实现
     %d 表示日期 ,包含子格式{%H%M%S}
     %t 表示线程id
     %c 表示⽇志器名称
     %f 表示源码⽂件名
     %l 表示源码⾏号
     %p 表示⽇志级别
     %T 表示制表符缩进
     %m 表示主体消息
     %n 表示换⾏
    */
    class Formatter
    {
    public:
    //基类指针,用来控制继承子类的对象
        using ptr=std::shared_ptr<Formatter>;
        // 构造默认函数
        Formatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n") : _pattern(pattern)
        {
            // 断言是否解析格式化规则字符
            assert(parsePatern());
        }
        // 对msg进行格式化
        void format(std::ostream &out, const LogMsg &msg)
        {
            for (auto &item : _items)
            {
                item->format(out, msg);
            }
        }
        std::string format(LogMsg &msg)
        {
            std::stringstream ss;
            format(ss, msg);
            return ss.str();
        }
        private:
        // 对格式化字符进行解析
        bool parsePatern()
        {
            // 1.对格式化规则字符串进行解析
            // 2.根据解析得到的数据初始化格式化子项数组成员
            // 规则字符串的处理过程是一个循环的过程,原始字符串结束后,遇到%,则处理一个格式化字符
            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;
                    continue;
                }
                // 能走下去,代表%后面是格式化字符,代表原始字符串处理完毕
                if (val.empty() == false)
                {
                    fmt_order.push_back(std::make_pair("", val));
                    val.clear(); // 清空
                }

                // 这时候pos指向的是%的位置,是格式化字符的处理
                pos += 1; // 这一步之后,pos位置指向格式化字符的位置
                if (pos == _pattern.size())
                {
                    std::cout << "%之后没有对应的格式化字符!\n";
                    return false;
                }
                key = _pattern[pos];
                // 这时候pos指向格式化字符后的位置
                pos += 1;
                if (pos < _pattern.size() && _pattern[pos] == '{')
                {
                    // 这时候pos指向{之后,子规则的起始位置
                    pos += 1;
                    while (pos < _pattern.size() && _pattern[pos] != '}')
                    {
                        val.push_back(_pattern[pos++]);
                    }
                    // 走到末尾跳出循环,则代表没有遇到},代表格式是错误的
                    if (pos == _pattern.size())
                    {
                        std::cout << "子规则{}匹配出错!\n";
                        return false; // 没有找到}
                    }
                    pos += 1; // 因为这时候pos指向的是}位置,向后走一步,走到了下次处理的新位置
                }
                fmt_order.push_back(std::make_pair(key, val)); // 添加处理的结果
                // 两次都清空,开始下一次处理
                key.clear();
                val.clear();
            }
            // 2.根据解析得到的数据初始化格式化子项数组成员
            for (auto &it : fmt_order)
            {
                _items.push_back(createItem(it.first, it.second));
            }
            return true;
        }

        // 根据不同的格式化字符创建不同的格式化子项对象
        FormatItem::ptr createItem(const std::string &key, const 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 == "")
                return std::make_shared<OtherFormatItem>(val);
            std::cout << "没有对应的格式化字符串:%" << key << std::endl;
            abort();
            return FormatItem::ptr();
        }

    private:
        std::string _pattern;                           // 格式化规则字符串
        std::vector<logslearn::FormatItem::ptr> _items; // 格式化字符串解析出的格式化子项
    };
}
#endif
  1. 对日志格式化模块进行测试和完善
    1)日志格式化的默认格式
//测试代码
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
int main()
{
   
    //日志格式化模块测试
    logslearn::LogMsg msg(logslearn::loglevel::value::INFO,53,"main.c","root","格式化功能测试...");
    logslearn::Formatter fmt;//不给格式会生成默认格式
    std::string str=fmt.format(msg);
    std::cout<<str;
    //std::cout << "Main thread ID: " << std::this_thread::get_id() << std::endl;
    return 0;
}

在这里插入图片描述
2)对日志进行边缘测试
测试了三种情况

//测试代码
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
int main()
{
    //日志格式化模块测试
    //边缘测试
    logslearn::LogMsg msg(logslearn::loglevel::value::INFO,53,"main.c","root","格式化功能测试...");
    //logslearn::Formatter fmt("abc%%abc[%d{%H:%M:%S}]%m%n");//1.测试%
     //logslearn::Formatter fmt("abc%%abc[%d{%H:%M:%S}]%m%n{");//2.测试子项的{}
    logslearn::Formatter fmt("abc%%abc[%d{%H:%M:%S}]%m%");//3.%后的字符
    std::string str=fmt.format(msg);
    std::cout<<str;
    return 0;    
}

在这里插入图片描述

二、日志落地类设计

功能:将格式化完成后的日志消息字符串,输出到指定位置。(支持同时将日志落地到不同位置)
位置分类:
1.标准输出
2.指定文件(事后进行日志分析)
3.滚动文件(文件按时间/大小进行滚动切换)

滚动⽇志⽂件输出的必要性:
由于机器磁盘空间有限, 我们不可能⼀直⽆限地向⼀个⽂件中增加数据
如果⼀个⽇志⽂件体积太⼤,⼀⽅⾯是不好打开,另⼀⽅⾯是即时打开了由于包含数据巨 ⼤,也不利于查找我们需要的信息
所以实际开发中会对单个⽇志⽂件的⼤⼩也会做⼀些控制,即当⼤⼩超过某个⼤⼩时(如
1MB),我们就重新创建⼀个新的⽇志⽂件来滚动写⽇志。 对于那些过期的⽇志, ⼤部分企业内部都有专⻔的运维⼈员去定时清理过期的⽇志,或者设置系统定时任务,定时清理过期⽇志。
⽇志⽂件的滚动思想:
⽇志⽂件滚动的条件有两个:⽂件⼤⼩和时间.
我们可以选择:
▪ ⽇志⽂件在⼤于 1MB 的时候会更换新的⽂件
▪ 每天定点滚动⼀个⽇志⽂件 本项⽬基于⽂件⼤⼩的判断滚动⽣成新的⽂件

扩展:支持落地方向的扩展
用户可以自己编写一个新的落地模块,将日志进行其他方向的落地
实现思想:
1.抽象出落地模块类
2.不同落地方向从基类进行派生(使用基类指针,指向子类对象,就可以调用子类对象的接口进行扩展)
3.使用工厂模式进行创建与表示的分离

2.1 日志落地模块功能实现与测试

  1. 第一步先要设计日志落地的模块,把大致的框架建好。
/*日志落地模块的实现
1.抽象落地基类
2.派生子类(根据不同的落地方向进行派生)
3.使用工厂模式进行创建与表示分离
*/
#ifndef __M_SINK_H__
#define __M_SINK_H__
#include "util.hpp"
#include <fstream>
#include <memory>
namespace logslearn
{
    // 抽象落地基类
    class LogSink
    {
    public:
        using ptr = std::shared_ptr<LogSink>;
        LogSink() {}
        virtual ~LogSink() {}
        // 纯虚函数,日志落地功能
        virtual void log(const char *data, size_t len) = 0;
    };
    // 落地方向:标准输出
    class StdoutSink : public LogSink
    {
    public:
        // 将日志消息写入到标准输出
        void log(const char *data, size_t len);
    };
    // 落地方向:指定文件
    class FileSink : public LogSink
    {
    public:
        // 构造时存入文件名,并打开文件,将操作句柄管理起来
        FileSink(const std::string &pathname);
        // 将日志消息写入到指定文件
        void log(const char *data, size_t len);
    private:
        std::string _pathname;
        std::ofstream _ofs;
    };
    // 落地方向:滚动文件(以大小进行滚动)
    class RoolBySizeSink : public LogSink
    {
    public:
        // 构造时存入文件名,并打开文件,将操作句柄管理起来
        RoolBySizeSink(const std::string &basename,size_t max_fsize);//需要用户告知,基础的文件名和文件大小
        // 将日志消息写入到标准输出--写入前判断文件大小,超过了最大大小就要切换文件
        void log(const char *data, size_t len);
        private:
        //创建一个新文件,不需要用户去创建,所有我们把权限设置为私有
        void createNewFile();//进行大小判断,超过指定大小则需要创建新文件
    private:
            //通过基础文件名+扩展文件名(以时间生成)组成一个实际的当前输出文件名
            size_t _name_count;//名称计数器
            std::string _basename;//文件的基础名字如./logs/base-    ./logs/base-20240710.log
            std::ostream _ofs;
            size_t _max_fsize;//最大文件大小,当前文件超过了这个大小就要切换文件
            size_t _cur_fsize;//记录当前文件已经写入的数据大小
    };
    //简单工厂模式,进行生成管理
    class SinkFactory{
    };
}
#endif
  1. 把框架的功能以及具体实现编写完成。
/*日志落地模块的实现
1.抽象落地基类
2.派生子类(根据不同的落地方向进行派生)
3.使用工厂模式进行创建与表示分离
*/
#ifndef __M_SINK_H__
#define __M_SINK_H__
#include "util.hpp"
#include <fstream>
#include <sstream>
#include <memory>
#include <cassert>
#include <unistd.h>
namespace logslearn
{
    // 抽象落地基类
    class LogSink
    {
    public:
        using ptr = std::shared_ptr<LogSink>;
        LogSink() {}
        virtual ~LogSink() {}
        // 纯虚函数,日志落地功能
        virtual void log(const char *data, size_t len) = 0;
    };
    // 落地方向:标准输出
    class StdoutSink : public LogSink
    {
    public:
        // 将日志消息写入到标准输出
        void log(const char *data, size_t len)
        {
            std::cout.write(data, len); // 因为日志输出不一定是字符串,所以不能直接打印,因此需要调用write接口,从data位置开始写,写入len长度的数据
        }
    };
    // 落地方向:指定文件
    class FileSink : public LogSink
    {
    public:
        // 构造时存入文件名,并打开文件,将操作句柄管理起来
        FileSink(const std::string &pathname) : _pathname(pathname)
        {
            // 1.创建日志文件所在的目录,没有文件就创建文件
            logsLearn::util::File::createDirectory(logsLearn::util::File::path(pathname));
            // 2.按特殊方式打开文件
            _ofs.open(_pathname, std::ios::binary | std::ios::app); // 二进制可写可追加权限
            assert(_ofs.is_open());
        }
        // 将日志消息写入到标准输出
        void log(const char *data, size_t len)
        {
            _ofs.write(data, len);
            assert(_ofs.good()); // 打开失败就报错
        }

    private:
        std::string _pathname;
        std::ofstream _ofs; // 会默认以写的方式打开文件
    };
    // 落地方向:滚动文件(以大小进行滚动)
    class RoolBySizeSink : public LogSink
    {
    public:
        // 构造时存入文件名,并打开文件,将操作句柄管理起来// 需要用户告知,基础的文件名和文件大小
        RoolBySizeSink(const std::string &basename, size_t max_fsize) :_basename(basename), _max_fsize(max_fsize), _cur_fsize(0),_name_count(0){
            std::string pathname=createNewFile();
            // 1.创建日志文件所在的目录,没有文件就创建文件
            logsLearn::util::File::createDirectory(logsLearn::util::File::path(pathname));
            // 2.按特殊方式打开文件
            _ofs.open(pathname, std::ios::binary | std::ios::app); //打开文件 二进制可写可追加权限
            assert(_ofs.is_open());
        } 
        // 将日志消息写入到标准输出--写入前判断文件大小,超过了最大大小就要切换文件
        void log(const char *data, size_t len){
            if(_cur_fsize>=_max_fsize){
                _ofs.close();//打开文件,就必须关闭文件(这里关闭以前的文件)
                std::string pathname =createNewFile();//创建新文件
                 _ofs.open(pathname, std::ios::binary | std::ios::app); //打开文件 二进制可写可追加权限
                 assert(_ofs.is_open());//打开失败就报错
                 _cur_fsize=0;
            }
            _ofs.write(data,len);
            assert(_ofs.good());//检测文件流状态和文件读写过程是否正常    
             _cur_fsize+=len;   
        }

    private:
        // 创建一个新文件,不需要用户去创建,所有我们把权限设置为私有
        std::string createNewFile(){
            //获取系统时间,以时间来构造文件名的扩展名
            time_t t=logsLearn::util::Data::now();
            struct tm lt;
            localtime_r(&t,&lt);
            std::stringstream filename;
            filename<<_basename;
            filename<<lt.tm_year+1900;
            filename<<lt.tm_mon+1;
            filename<<lt.tm_mday;
            filename<<lt.tm_hour;
            filename<<lt.tm_min;
            filename<<lt.tm_sec;
            filename<<"-";
            filename<<_name_count++;
            filename<<".log";
            return filename.str();
        }; // 进行大小判断,超过指定大小则需要创建新文件,将一个时间戳,转化为时间结构
    private:
        // 通过基础文件名+扩展文件名(以时间生成)组成一个实际的当前输出文件名
        size_t _name_count;//名称计数器
        std::string _basename; // 文件的基础名字如./logs/base-    ./logs/base-20240710.log
        std::ofstream _ofs;
        size_t _max_fsize; // 最大文件大小,当前文件超过了这个大小就要切换文件
        size_t _cur_fsize; // 记录当前文件已经写入的数据大小
    };
    // 简单工厂模式,进行生成管理
    class SinkFactory
    {
    };
}
#endif
  1. 使用简单工厂模式
    // 简单工厂模式,进行生成管理
    //SinkType通过模板参数,可以生产我们需要的落地方式,因为落地方式需要传参的参数不一样,这里我们需要用到不定参的知识
   
    class SinkFactory
    {
        public:
         template<typename SinkType,typename ...Args>
        static LogSink::ptr create(Args && ...args){
            return std::make_shared<SinkType>(std::forward<Args>(args)...);
        }

    };
  1. 功能测试以及完善
//测试代码
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"
int main()
{
    //日志落地模块的测试
    logslearn::LogMsg msg(logslearn::loglevel::value::INFO,53,"main.c","root","格式化功能测试...");
    logslearn::Formatter fmt("abc%%abc[%d{%H:%M:%S}]%m%n");
    std::string str=fmt.format(msg);
    //设置落地方向
    logslearn::LogSink::ptr stdout_lsp=logslearn::SinkFactory::create<logslearn::StdoutSink>();//标准输出落地
    logslearn::LogSink::ptr file_lsp=logslearn::SinkFactory::create<logslearn::FileSink>("./logfile/test.log");//文件落地方式
    logslearn::LogSink::ptr roll_lsp=logslearn::SinkFactory::create<logslearn::RoolBySizeSink>("./logfile/test.log",1024*1024);//滚动文件落地方式
    //通过指针去控制打印的日志
    stdout_lsp->log(str.c_str(),str.size());//把str转化成常量字符
    file_lsp->log(str.c_str(),str.size());
    size_t cursize=0;
    size_t count=0;
    //用滚动文件的方法希望生产10个文件
    while(cursize<1024*1024*10)
    {
        std::string tmp=std::to_string(count++)+str;//每个生产的日志都有信号
        roll_lsp->log(tmp.c_str(),tmp.size());
        cursize+=tmp.size();
    }
    return 0;   
}

测试结果:
在这里插入图片描述
文件落地的日志消息
在这里插入图片描述
滚动文件的日志消息
在这里插入图片描述

2.2 日志落地模块功能功能扩展

  1. 扩展一个以时间作为日志文件滚动切换类型的日志落地模块
/*扩展一个以时间作为日志文件滚动切换类型的日志落地模块
   1.以时间进行文件滚动,实际上是以时间段进行滚动
       实现思想:以当前系统时间,取模获得时间段大小,可以得到当前时间段是第几个时间段
           time(nullptr)%gap;
           每次以当前系统时间取模,判断与当前文件的时间段是否一致,不一致代表不是同一个时间段
   */
// 使用枚举来确定时间段的大小
enum class TimeGap
{
    GAP_SECOND,
    GAP_MINUTE,
    GAP_HOUR,
    GAP_DAY,
};

class RollByTimeSink : public logslearn::LogSink
{
public:
    // 构造时存入文件名,并打开文件,将操作句柄管理起来
    RollByTimeSink(const std::string &basename, TimeGap gap_type) : _basename(basename)
    {
        switch (gap_type)
        {
        case TimeGap::GAP_SECOND:
            _gap_size = 1;
            break; // 以秒为时间段
        case TimeGap::GAP_MINUTE:
            _gap_size = 60;
            break; // 以分钟为时间段
        case TimeGap::GAP_HOUR:
            _gap_size = 3600;
            break; // 以小时为时间段
        case TimeGap::GAP_DAY:
            _gap_size = 3600 * 24;
            break; // 以天为时间段
        }
        _cur_gap = _gap_size==1?logsLearn::util::Data::now():logsLearn::util::Data::now() / _gap_size; // 获取当前是第几个时间段
        // 创建文件
        std::string filename = createNewFile();
        // 1.创建日志文件所在的目录,没有文件就创建文件
        logsLearn::util::File::createDirectory(logsLearn::util::File::path(filename));
        // 2.按特殊方式打开文件
        _ofs.open(filename, std::ios::binary | std::ios::app); // 打开文件 二进制可写可追加权限
        assert(_ofs.is_open());
    }
    // 将日志消息写入到标准输出,判断当前时间是否是当前文件的时间段,不是就要切换文件。
    void log(const char *data, size_t len)
    {
        time_t cur = logsLearn::util::Data::now(); // 获取当前系统时间,时间戳
        if ((cur / _gap_size) != _cur_gap)//(每次写日志时判断当前的时间段与上次的时间段是否是一致得,一致的话就写入,不一致就创建新文件)
        {
            _ofs.close();                                          // 打开文件,就必须关闭文件(这里关闭以前的文件)
            std::string pathname = createNewFile();                // 创建新文件
            _cur_gap = _gap_size==1?logsLearn::util::Data::now():logsLearn::util::Data::now() / _gap_size; // 获取当前是第几个时间段
            _ofs.open(pathname, std::ios::binary | std::ios::app); // 打开文件 二进制可写可追加权限
            assert(_ofs.is_open());                                // 打开失败就报错
        }
        _ofs.write(data, len);
        assert(_ofs.good()); // 检测文件流状态和文件读写过程是否正常
    }

protected:
    // 创建一个新文件,不需要用户去创建,所有我们把权限设置为私有
    std::string createNewFile()
    {
        // 获取系统时间,以时间来构造文件名的扩展名
        time_t t = logsLearn::util::Data::now();
        struct tm lt;
        localtime_r(&t, &lt);
        std::stringstream filename;
        filename << _basename;
        filename << lt.tm_year + 1900;
        filename << lt.tm_mon + 1;
        filename << lt.tm_mday;
        filename << lt.tm_hour;
        filename << lt.tm_min;
        filename << lt.tm_sec;
        filename << ".log";
        return filename.str();
    }
private:
    std::string _basename; // 基本文件名
    std::ofstream _ofs;    // 会默认以写的方式打开文件
    size_t _cur_gap;       // 当前是第几个时间段
    size_t _gap_size;      // 时间段的大小
};
  1. 对扩展的功能进行测试
int main()
{
     // 日志落地扩展模块的测试
    logslearn::LogMsg msg(logslearn::loglevel::value::INFO, 53, "main.c", "root", "格式化功能测试...");
    logslearn::Formatter fmt("abc%%abc[%d{%H:%M:%S}]%m%n");
    std::string str = fmt.format(msg);
    // 设置落地方向
    logslearn::LogSink::ptr time_lsp = logslearn::SinkFactory::create<RollByTimeSink>("./logfile/rool-", TimeGap::GAP_MINUTE); // 滚动文件落地方式
    time_t old=logsLearn::util::Data::now();//获取当前系统时间
    while (logsLearn::util::Data::now()< old+63)//写3秒的数据
    {
        time_lsp->log(str.c_str(), str.size());
        usleep(1000);//等待1毫秒
    }
    return 0;
}
  1. 显示测试的结果
    创建了5个文件,每个文件有900条左右的日志。
    在这里插入图片描述

相关推荐

  1. 运用多设计模式的同步&异步滚动日志系统

    2024-07-14 13:06:03       42 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-14 13:06:03       66 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-14 13:06:03       70 阅读
  3. 在Django里面运行非项目文件

    2024-07-14 13:06:03       57 阅读
  4. Python语言-面向对象

    2024-07-14 13:06:03       68 阅读

热门阅读

  1. 设计模式之观察者模式

    2024-07-14 13:06:03       22 阅读
  2. VUE export import

    2024-07-14 13:06:03       20 阅读
  3. 数据结构——复杂度

    2024-07-14 13:06:03       27 阅读
  4. 【Rust】——不安全Rust

    2024-07-14 13:06:03       22 阅读