轻量级高性能多模式日志系统的设计

  • 概念简介

  • 什么是日志系统&为什么需要日志系统

什么是日志系统

即程序运行过程中所记录的程序运行的状态信息 记录该消息的系统称为日志系统

为什么需要日志系统

记录程序运行的状态信息 以便于程序员对系统的运行状态进行分析

  • 日志所支持的功能

在使用方面,我们需要设计多种级别 来标识不同的日志信息 不同的级别应对不同的场景 以便于用户分析;内置三种日志消息落地方式即写入控制台,本地文件,以及滚动文件,为了更好的用户体验,还可以用户自定义扩展不同的日志落地目标地

在性能方面,我们需要支持同步模式和异步模式,还需要支持多线程并发来写入日志 ,以保证高性能

  • 开发环境

在开发环境我采用的是CentOS7;vscode/vim;g++/gdb;makefile

  • 需要的核心技术

类层次技术(继承多态);C++11(多线程 auto 智能指针 右值引用等);双缓冲区;生产者消费者模型;设计模式(单例 工厂 代理 建造者等);多线程

  • 项目设计

  • 日志系统的框架模块设计
  • 日志等级模块

主要功能:枚举出日志分为多少个等级

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

1.每个项目中都会设置一个默认的日志输出等级 只有输出的日志等级大于等于默认限制等级的时候才可以进行输出
2. 提供一个接口 将对应等级的枚举 转换成一个对应的字符串
DEBUG ---》 “DEBUG”

  • 日志消息模块

主要功能:封装一条日志所需要的各种要素 即:时间 线程ID 文件名 行号 日志等级 消息主体

1.日志的输出时间 用于过滤日志输出时间
2.日志的等级 用于进行日志过滤分析
3.源文件名称
4.源代码行号 用于定位出现错误的代码位置
5.线程ID   用于过滤出错的线程
6.实际的日志主体消息
7.日志器名称(当前支持多日志器的同时使用)
【2012-09-23 12:23:49】【root】【1234023】【main.c:88】【FATAL】创建套接字失败

我们需要一个pattern成员 来保存日志输出的格式字符串
【%d{%H:%M:%S}】【%t】【%c】【%p】【%f:%I】%T%m%n
用户可以自定义输出格式 格式化字符串控制了日志的输出格式
定义格式化字符 是为了让日志系统进行日志格式化更加灵活方便

操作步骤
1.格式化字符串
2.格式化子项数组 //格式化字符串进行解析 保存了日志消息要素的排序

    不同的格式化子项 会从日志消息中取出指定的元素,转换为字符串

【%d{%H:%M:%S}】【%f:%I】%m%n  ---->解析

格式化子项:
1.其他消息子项//非格式化字符【
2.日期子项//%H,%M,%S
3.其他信息子项:】
4.其他消息子项//非格式化字符【
5.文件名子项:
6.其他信息子项::
7.其他信息子项:】
8.消息主体子项:
9.换行子项

LogMsg{
        size_t _ctime;//日志产生的时间 时间戳 11111
        Loglevel::value _level;//日志等级  DEGUB
        size_t _line;//行号    23
        std::string _file;//源文件名 main.c
        std::thread::id _tid;//线程id   1321313
        std::string _logger;//日志器名称  root
        std::string _payload;//有效载荷 有效消息数据 "创建套接字失败"
}

格式化子项的实现思想
作用:从日志消息中取出指定的元素 追加到一块内存空间中
class MsgFormatItem{
  public:
         void format(std::ostream &out,LogMsg &msg)
           {

              out << msg._payload;
           }
}
设计思想:
1.抽象一个格式化子项的基类
2.基于基类 派生出不同的格式化子项子类
   主体消息 日志等级 时间子项 文件名 行号 日志器名称 线程ID 制表符 换行 其他
这样就可以在父类中定义父类指针的数组 指向不同的格式化子项子类对象
  • 消息格式化模块

主要功能:按照指定格式 对于日志消息关键要素进行组织 得到一个指定格式的字符串

操作步骤1.对格式化规则字符串进行解析 

//1.对格式化规则字符串进行解析
//abcde(没有以%起始的字符串都是原始字符串)[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"如果遇到% 后面就是格式化字符
//处理思想 不是% 则一直向后走 遇到% 则是原始字符串的结束
//遇到% 看看紧随其后的字符是不是% 如果是 就是%字符 如果不是 则代表紧随其后的字符是个格式化字符
//紧随格式化字符之后 有没有{  如果有 则{之后 }之前的数据就是格式化字符的子格式
                        /*
                    规则字符串的处理过程是一个循环的过程 
                        while(){
                            1.处理原始字符串
                            2.原始字符串结束后 遇到% 则处理一个格式化字符
                }
                在处理过程中 需要将处理得到的信息保持下来
                key == nullptr val = abcde【
                key == d val = %H:%M:%S
                key == nullptr val = 】【
                key == p,val = nullptr
                key == nullptr,val = ]
                key == T val =null
                key == m val =null
                key == n val =null
                得到了数组后 根据数组内容 创建对应的格式化子项对象 添加到items成员数组中

                */

  • 日志落地模块

主要功能:负责对日志消息进行指定方向的写入输出

日志落地的设计思想
主要负责落地日志消息到目的地
功能:将格式化完成后的日志消息字符串 输出到指定位置
扩展:支持同时将日志落地到不同的位置
位置分类:
1.标准输出 
2.指定文件(事后进行日志分析)
3.滚动文件(文件按照时间/大小进行滚动切换)
扩展:支持落地方向的扩展
用户可以自己编写一个新的落地模块 将日志进行其他方向的落地

日志落地扩展
以时间作为日志文件滚动切换的类型的日志落地模块
1.以时间进行文件滚动 实际上是以时间段进行滚动
实现思想:time(nullptr) % gap;
time(nullptr) % 60 == 10 当前就是第n个60s
以当前系统时间 取模时间段大小 可以得到当前时间段是几个时间段
每次以当前系统时间取模 判断与当前文件的时间段是否一致 不一致代表不是同一个时间段

  • 日志器模块

主要功能: 对上面几个模块的整合

功能:对前边所有模块进行整合 向外提供接口完成不同等级日志的输出
管理的成员
1.格式化模块对象
2.落地模块对象(一个日志器可能会向多个位置进行日志输出 )
3.默认的日志输出限制等级(大于等于限制等级的日志才能输出)
4.互斥锁(保证日志输出过程是线程安全的 不会出现交叉日志)
5.日志器名称(日志器的唯一标识 以便于查找)
提供的操作:
debug等级日志的输出操作(分别会封装日志消息logMsg-各个接口日志等级不同)
info等级日志的输出操作
warn等级日志的输出操作
error等级日志的输出操作
fatal等级日志的输出操作

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

扩展:用户需要构造太多零部件 建造者模式
/*使用建造者模式来建造日志器 而不是让用户直接去构造日志器 简化用户的使用复杂度
        1.抽象一个日志器建造者类
        1.1 设置日志器类型
        1.2 将不同类型日志器的创建放到同一个日志器建造者类中完成

        2.派生出具体的建造者类---局部日志器的建造者 ?& 全局的日志器建造者(后边添加了全局单例管理器之后 ,将日志器添加全局管理)
    */

异步日志器的实现
前边完成的是同步日志器 直接将日志消息进行格式化写入文件
接下来完成的是异步日志器:
         思想:为了避免因为写日志的过程阻塞,导致业务线程在写日志的时候影响效率,因此异步思想就是不让业务线程进行日志的实际落地操作,而是将日志消息放到缓冲区当中(一块指定的内存)
  接下来有一个专门的异步线程 去针对缓冲区中的数据进行处理(实际的落地操作)

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


缓冲区详细设计:
1.使用队列 缓存日志消息 逐条处理 要求:不能涉及到空间的频繁申请与释放 否则会降低效率
结果:设计一个环形队列(提前将空间申请好 然后对空间循环利用)
问题:这个缓冲区的操作会涉及到多线程 因此缓冲区的操作必须保证线程安全
线程安全实现:对于缓冲区的读写加锁
  1.因为写日志操作 在实际开发中 并不会分配太多资源 所以工作线程只需要一个日志器有一个就行
  2.涉及到的锁冲突:生产者与生产者的互斥&生产者与消费者的互斥
问题:锁冲突较为严重 因为所有线程之间都存在互斥关系
解决方案:双缓冲区
任务写入缓冲区    任务处理缓冲区

 

 单个缓冲区的进一步设计
设计一个缓冲区
1.直接存放格式化后的日志消息字符串
好处:
1.减少了logmsg对象频繁的构造的小号
2.可以针对缓冲区中的日志消息 一次性进行IO操作 减少IO次数 提高效率
缓冲区类的设计:
1.管理一个存放字符串数据的缓冲区(使用vector进行空间管理)
不能用string 因为string遇到\0就结束了 
2.当前的写入位置的指针 (指向可写区域的起始位置,避免数据的写入覆盖)
3.当前的读取数据位置的指针(指向可读数据区域的起始位置,当读取指针与写入指针指向相同位置表示数据取完了)
提供的操作
1.向缓冲区中写入操作
2.从缓冲区读取操作(读取指定长度数据,或者读取所有数据)(为了避免拷贝)(获取可读数据起始地址的接口)
3.获取可读数据长度的接口
4.移动读写位置的接口
5.初始化缓冲区的操作(将读写位置初始化-将一个缓冲区所有数据处理完毕之后)
6.提供交换缓冲区的操作(交换空间地址 并不交换空间数据)

 异步工作线程的设计:
1.异步工作使用双缓冲区思想
外界将任务数据 添加到输入缓冲区中
异步线程对处理缓冲区中的数据进行处理 若处理缓冲区中没有数据了则交换缓冲区
实现:管理的成员:
1.双缓冲区(生产 消费)
2.互斥锁//保证线程安全
3.条件变量-生产&消费 (生产缓冲区没有数据,处理完消费缓冲区数据后就休眠)
4.回调函数(针对缓冲区中数据的处理接口-外界传入一个函数,告诉异步工作器数据该如何处理)
提供的操作:
1.停止异步工作器
2.添加数据到缓冲区
私有操作:
创建线程 线程入口函数中 交换缓冲区 对消费缓冲区数据使用回调函数进行处理 处理完后再次交换

异步日志器设计:
1.继承于Logger日志器类
对于写日志操作进行函数重写(不再将数据直接写入文件,而是通过异步消息处理器,放到缓冲区中)
2.通过异步消息处理器 进行日志数据的实际落地
管理的成员
1.异步工作器(异步消息处理器)完成后,完善日志器建造者,进行异步日志器安全模式的选择,提供异步日志器的创建

  • 工具类的设计

主要功能:在实际开发中常会用到的功能 即集合成一个util类

 

  • 日志器管理器模块

主要功能:在实际开发中发现设计一个管理日志器的管理器将有会更好的用户体验

作用:对所有创建的日志器进行管理
特性:将管理器设计为单例
作用:可以在程序的任何位置 获取相同的单例对象 获取其中的日志器进行日志输出
扩展:单例管理器创建的时候 默认先创建一个日志器 (用于进行标准输出的打印)
目的:让用户在不创建任何日志器的情况下 也能进行标准输出的打印 方便用户使用
设计思想:
管理的成员:
1.默认日志器
2.所管理的日志器数组
3.互斥锁
提供的接口:
1.添加日志器管理
2.判断是否管理了指定名称的日志器
3.获取指定名称的日志器
4.获取默认日志器

  • 用户便利优化

主要功能:提供全局接口&宏函数 对日志系统接口进行使用 便利性优化

思想:
1.提供获取指定日志器的全局接口(避免用户自己操作单例对象)
2.使用宏函数对日志器的接口进行代理(代理模式)
3.提供宏函数 直接通过默认日志器进行日志的标准输出的打印(不用获取日志器了)

  • 项目性能测试

下面来看下实际结果

 以上就是本日志系统的全部内容了 感谢阅读!

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值