一、项目简介
1.日志的概念(白话版)
日志类似于日记,通常是指对完成某件事情的过程中状态等的记录,而计算机中的日志是指日志数据,是有价值的信息宝库,各种操作系统、应用程序、设备和安全产品的日志数据能够帮助你提前发现和避开灾难,找到安全事件的根本原因。
本项目涉及到的日志具体是指:程序运行过程中所记录的程序运行状态信息。
2.日志系统功能概述
记录程序运行状态信息,以便程序员能随时根据状态信息对系统运行状态进行分析,能够让用户在运行测试程序时非常简便的进行日志的输出及控制。
3.日志系统具体功能
- 支持多级别日志消息
- 支持多线程并发写入
- 支持同步日志和异步日志
- 支持写入日志到控制台、文件及滚动文件中
- 支持扩展不同的日志落地目标地
4.日志系统的必要性
- 生产环境的产品为了保证其稳定性及安全性不允许开发人员附加调试器去排查问题,可以借助日志系统来打印一些日志帮助开发人员解决问题。
- 上线客户端的产品出现bug无法复现并解决时,可以借助日志系统打印日志并上传到服务器帮助开发人员进行分析。
- 对于一些高频操作(定时器、心跳包)在少量调试次数下可能无法触发我们想要的行为,通过断点的暂停方式,我们不得不重复操作几十次、上百次甚至更多,导致排查问题的效率低下,可以借助打印日志的方式排查。
- 在分布式、多线程、多进程的代码中,出现bug比较难以定位,可以借助日志系统打印日志帮助定位bug。
- 帮助首次接触项目代码的新开发人员理解代码的运行过程。
5.开发环境
- CentOS 7
- vscode/vim
- g++/gdb
- Makefile
6.核心技术
- 基于封装、继承、多态的面向对象的类层次设计
- 设计模式(单例、工厂、代理、建造者等)
- 生产者消费者模型
- 多线程应用
- 双缓冲区(主要针对异步日志)
- C++11相关(多线程、auto、智能指针、右值引用等)
二、日志系统的技术实现
日志系统的技术实现主要包括三种类型:
- 利用printf/std::cout等输出函数将日志信息打印到控制台【实际项目中不用】
- 对于大型商业化项目,为方便排查问题,我们一般会将日志输出到文件或者是数据库系统方便查询分析,主要分为同步日志方式和异步日志方式。
1.同步写日志
同步写日志是指输出日志时,必须等待日志输出语句执行完毕后才能执行后面的业务逻辑语句,日志输出语句与程序业务逻辑语句是在同一线程运行,这种情况在高并发场景下,随着日志数量的不断增加,日志系统容易产生瓶颈:一方面,大量的打印陷入等量write系统调用,有一定开销;另一方面,使得打印日志的进程附带了大量同步的磁盘IO,影响程序性能,其结构图如下:
2.异步写日志
基于同步写日志的缺陷,出现了异步写日志,异步写日志是指在进行 日志输出是,日志输出语句与业务逻辑语句并不在同一线程中运行,而是有专门的线程用于日志输出操作。业务线程只需要将日志放到一个内存缓冲器中不用等待即可继续执行后续的业务逻辑,而日志的落地操作交给单独的日志线程去完成,这也可以看做一个生产-消费模型,这样做的好处是即使日志没有真的完成传输也不会影响程序的主业务,可以提高程序的性能,其结构图如下:
三、项目前置知识补充
1.不同风格不定参函数的用法
-
不定参宏函数
#include<stdio.h> #define Log(fmt,...) printf("[%s:%d]"fmt,__FILE__,__LINE__,##__VA_ARGS__) int main() { Log("hello wz!\n"); Log("%s-%d\n","hello world!",99); return 0; }
测试结果:
-
C风格的不定参函数
#include<stdio.h> #include<stdarg.h> #include<stdlib.h> //按顺序打印输入的指定数量的整数 void PrintNum(int count,...) { va_list ap; va_start(ap,count); int i=0; for(i=0;i<count;i++) { int num=va_arg(ap,int); printf("param:[%d]-%d\n",i,num); } va_end(ap); } //模拟实现printf void myprintf(const char* fmt,...) { va_list ap; va_start(ap,fmt); char* res; int ret=vasprintf(&res,fmt,ap); if(ret!=-1) { printf(res); free(res); } va_end(ap); } //测试代码 int main() { PrintNum(5,1,2,3,4,5); PrintNum(3,666,999,555); myprintf("%s-%d:%s\n","wz",666,"nice"); return 0; }
测试结果:
-
C++风格的不定参函数
//C++风格的不定参函数的实现+测试 #include<iostream> void xprintf() { std::cout<<std::endl; } //利用C++风格设计一个不定参的打印函数 template<typename T,typename ...Args> void xprintf(const T& v,Args &&... args) { std::cout<<v; if((sizeof ...(args))>0) { xprintf(std::forward<Args>(args)...); } else { xprintf(); } } int main() { xprintf("wz",666,7.8); return 0; }
测试结果:
2.设计模式
概念:
设计模式是前辈们对代码开发经验的总结,是解决特定问题的一系列套路,它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。
设计代码应该遵循的六大原则:
单一职责原则告诉我们实现类要职责单一;
里氏替换原则告诉我们不要破坏继承体系;
依赖倒置原则告诉我们要面向接口编程;
接口隔离原则告诉我们在设计接口的时候要精简单一;
迪米特法则告诉我们要降低耦合;
开闭原则是总纲告诉我们要扩展开放,关闭修改;
以下几种设计者模式一定程度上应用了这些原则,因此我们在实际工程项目中应视情况选择合适的设计模式来编写代码,下面就介绍几种常见的。
单例模式
概念:
一个类只能创建一个对象,即单例模式,该设计模式可以保证系统中该类只有一个实例,并提供 一个访问它的全局访问点,该实例被所有程序模块共享,比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务程序进程中的其它对象再通过这个单例对象获取这些配置信息,这种方式简化了复杂环境下额配置管理。
单例模式分两种实现模式:饿汉模式和懒汉模式
饿汉模式:
程序启动时就会创建一个唯一的实例对象。应为单例对象已经确定,所以比较适合用于多线程环境中,多线程获取单例对象不需要加锁,可以有效的避免资源竞争,提高性能。
懒汉模式:
第一次要使用单例对象的时候再创建实例对象。如果单例对象构造特别耗时或者浪费资源(加载插件、加载网络资源等),可以选择懒汉模式,在第一次使用时才创建对象。
这里我们将会实现一种更加简便的单例模式,采用的是静态局部变量的方式来创建对象,但要注意的是C++11之后,静态变量才能在满足线程安全的前提下唯一的被构造和析构。
饿汉模式实现及测试:
#include<iostream> //饿汉模式,将对象声明为静态私有成员变量 //程序启动就会创建出单例对象,该对象的构造函数、析构函数全部私有化,删除拷贝构造函数 //保证了整个程序实现过程中只会存在一个单例对象 //可以通过公共的接口去获取该对象和该对象的数据 //是一种以空间换时间的举动,开始就创建好在后续使用时就不用耗费时间去创建了 class Singleton { private: static Singleton _eton; Singleton():_data(1) { std::cout<<"单例对象构造成功"<<std::endl; } ~Singleton() {} private: int _data; public: static Singleton& GetInstence() { return _eton; } int GetData() { return _data; } }; //类内声明的静态成员变量要在类外定义,且要加上类域 Singleton Singleton::_eton; int main() { std::cout<<Singleton::GetInstence().GetData()<<std::endl; return 0; }
测试结果:
懒汉模式实现及测试:
#include<iostream> //懒汉模式:懒加载,其实是延迟加载的思想 //一个类在用的时候再实例化,在初始化构造较为复杂的情况下 //使用懒汉思想就可以避免在不使用该类的情况下浪费资源去构造对象 #include<iostream> class Singleton { private: Singleton():_data(1) { std::cout<<"单例对象构造成功"<<std::endl; } Singleton(const Singleton&)=delete; ~Singleton() {} private: int _data; public: static Singleton& GetInstence() { static Singleton _eton; return _eton; } int GetData() { return _data; } }; int main() { std::cout<<Singleton::GetInstence().GetData()<<std::endl; return 0; }
测试结果:
工厂模式
概念:
工厂模式是一种创建型设计模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们创建对象时不会对上层暴露创建逻辑,而是通过使用一个共同结构来指向新创建的对象,以此实现创建-使用的分离。
工厂模式分三种实现模式:简单工厂模式、工厂方法模式和抽象工厂模式
简单工厂模式:
简单工厂模式实现由一个工厂对象通过类型决定创建出来指定产品类的实例。假设有个工厂能生产水果,当客户需要产品时明确告知工厂生产哪类水果,工厂需要接收用户提供的类别信息,当新增产品的时候,工厂内部去添加新产品的生产方式。
工厂方法模式:
在简单工厂模式下新增多个工厂,每个产品,对应一个工厂,假设现在有A、B两种产品,则设置两个工厂,工厂A负责生产产品A,B负责生产产品B,用户只知道产品的工厂名,而不知道具体产品的信息,工厂不需要再接收客户的产品类别,而只负责生产产品。
抽象工厂模式:
工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每一个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,我们可以考虑将一些先关的产品组成一个产品组(位于不同产品等级结构中功能相关联的产品组成家族),由同一个工厂来统一生产,这就是抽象工厂模式的基本方式。
简单工厂模式实现及测试:
//简单工厂模式实现+测试 #include<iostream> #include<memory> //简单工厂模式通过参数控制可以生产任意产品 //思想简单粗暴,直观,使用一个工厂生产同一等级结构下的任意产品 //存在的问题:1.所有东西都在一个工厂生产,产品太多导致代码量庞大 // 2.没有很好的遵循开闭原则,新增产品就必须修改工厂方法 class Fruit { public: virtual void name()=0; }; class Apple:public Fruit { public: void name() override { std::cout<<"I'm Apple"<<std::endl; } }; class Banana:public Fruit { public: void name() override { std::cout<<"I'm Banana"<<std::endl; } }; class FruitFactory { public: static std::shared_ptr<Fruit> produce(const std::string &name) { if(name=="Apple") { return std::make_shared<Apple>(); } else { return std::make_shared<Banana>(); } } }; int main() { std::shared_ptr<Fruit> fruit=FruitFactory::produce("Apple"); fruit->name(); fruit=FruitFactory::produce("Banana"); fruit->name(); return 0; }
测试结果:
工厂方法模式实现及测试:
//工厂方法模式实现+测试 #include<iostream> #include<memory> //工厂方法模式定义一个创建对象的窗口,由子类决定创建哪种对象 //使用多个工厂分别生产指定的固定产品 //好处是减轻了工厂类的负担,将指定产品交规指定工厂来进行生产 //同时很好地遵循了开闭原则,增添新的产品只需要新增产品工厂即可,不需要修改原来的工厂类 //存在的问题是:对于某种可以形成一组产品组的情况处理比较复杂,需要创建大量工厂类 class Fruit { public: virtual void name()=0; }; class Apple:public Fruit { public: void name() override { std::cout<<"I'm Apple"<<std::endl; } }; class Banana:public Fruit { public: void name() override { std::cout<<"I'm Banana"<<std::endl; } }; class FruitFactory { public: virtual std::shared_ptr<Fruit> produce()=0; }; class AppleFactory:public FruitFactory { public: std::shared_ptr<Fruit> produce() override { return std::make_shared<Apple>(); } }; class BananaFactory:public FruitFactory { public: std::shared_ptr<Fruit> produce() override { return std::make_shared<Banana>(); } }; int main() { std::shared_ptr<FruitFactory> ff(new AppleFactory()); std::shared_ptr<Fruit> fruit=ff->produce(); fruit->name(); ff.reset(new BananaFactory()); fruit=ff->produce(); fruit->name(); return 0; }
测试结果:
抽象工厂模式实现及测试:
//抽象工厂模式实现+测试 #include<iostream> #include<memory> //抽象工厂是围绕一个超级工厂去创建其它工厂,每个生成的工厂按照工厂模式提供对象 //将工厂分成了抽象的两层,抽象工厂和具体子工厂类,在工厂子类中生产不同类型的子产品 //抽象工厂模式适用于生产多个工厂系列产品衍生的设计模式,增加新的产品等级结构复杂 //需要对原有系统进行较大的修改,甚至需要修改抽象层代码,因此也违背了“开闭原则” class Fruit { public: virtual void name()=0; }; class Apple:public Fruit { public: void name() override { std::cout<<"I'm Apple"<<std::endl; } }; class Banana:public Fruit { public: void name() override { std::cout<<"I'm Banana"<<std::endl; } }; class Ball { public: virtual void name()=0; }; class BasketBall:public Ball { public: void name() override { std::cout<<"I'm BasketBall"<<std::endl; } }; class FootBall:public Ball { public: void name() override { std::cout<<"I'm FootBall"<<std::endl; } }; class Factory { public: virtual std::shared_ptr<Fruit> GetFruit(const std::string &name)=0; virtual std::shared_ptr<Ball> GetBall(const std::string &name)=0; }; class FruitFactory :public Factory { public: std::shared_ptr<Ball> GetBall(const std::string &name) { return std::shared_ptr<Ball>(); } std::shared_ptr<Fruit> GetFruit(const std::string &name) { if(name=="Apple") { return std::make_shared<Apple>(); } else { return std::make_shared<Banana>(); } } }; class BallFactory :public Factory { public: std::shared_ptr<Fruit> GetFruit(const std::string &name) { return std::shared_ptr<Fruit>(); } std::shared_ptr<Ball> GetBall(const std::string &name) { if(name=="BasketBall") { return std::make_shared<BasketBall>(); } else { return std::make_shared<FootBall>(); } } }; class FactoryProducer { public: static std::shared_ptr<Factory> produce(const std::string &name) { if(name=="Fruit") { return std::make_shared<FruitFactory>(); } else { return std::make_shared<BallFactory>(); } } }; int main() { std::shared_ptr<Factory> factory =FactoryProducer::produce("Fruit"); std::shared_ptr<Fruit> fruit=factory->GetFruit("Apple"); fruit->name(); fruit=factory->GetFruit("Banana"); fruit->name(); std::shared_ptr<Factory> ff =FactoryProducer::produce("Ball"); std::shared_ptr<Ball> ball=ff->GetBall("BasketBall"); ball->name(); ball=ff->GetBall("FootBall"); ball->name(); return 0; }
测试结果:
建造者模式
概念:
建造者模式是一种创建型设计模式,使用多个简单的对象一步一步构建成一个复杂的对象,能够将一个复杂的对象的构建与它的表示分离,提供一种创建对象的最佳方式。主要用于解决对象过于复杂的问题。
建造者模式主要基于五个核心类的实现:
抽象产品类;
具体产品类:一个具体的产品类;
抽象Builder类:创建一个产品对象所需要各个部件的抽象接口;
具体产品的Builder类:实现抽象接口,构建各个部件;
指挥者Director类:统一组建过程,提供给调用者使用,通过指挥者来获取产品;
下面以生产建造一台苹果笔记本电脑为例来理解建造者模式:
建造者模式实现及测试:
//以建造模式构建一台苹果笔记本电脑 //实现+测试 #include<iostream> #include<memory> //产品抽象类:电脑类 class Computer { public: Computer() {} void setBoard(const std::string &board) { _board=board; } void setDisplay(const std::string &display) { _display=display; } void showParamaters() { std::string param="Computer Paramaters:\n"; param+="\tBoard: "+_board+"\n"; param+="\tDisplay: "+_display+"\n"; param+="\tOs: "+_os+"\n"; std::cout<<param<<std::endl; } virtual void setOs()=0; protected: std::string _board;//主板 std::string _display;//显示器 std::string _os;//操作系统 }; //具体产品类:苹果笔记本电脑类 class MacBook:public Computer { public: void setOs() override { _os="Mac OS x12"; } }; //抽象Builder类 class Builder { public: virtual void buildBoard(const std::string& board)=0; virtual void buildDisplay(const std::string& display)=0; virtual void buildOs()=0; virtual std::shared_ptr<Computer> build()=0; }; //MacBook Builder类 class MacBookBuilder:public Builder { public: MacBookBuilder():_computer(new MacBook()){} void buildBoard(const std::string& board) { _computer->setBoard(board); } void buildDisplay(const std::string& display) { _computer->setDisplay(display); } void buildOs() { _computer->setOs(); } std::shared_ptr<Computer> build() { return _computer; } private: std::shared_ptr<Computer> _computer; }; //Director类 class Director { public: Director(Builder *builder):_builder(builder) {} void construct (const std::string &board,const std::string &display) { _builder->buildBoard(board); _builder->buildDisplay(display); _builder->buildOs(); } private: std::shared_ptr<Builder> _builder; }; int main() { Builder * builder =new MacBookBuilder(); std::unique_ptr<Director> director(new Director(builder)); director->construct("惠普主板","华硕显示器"); std::shared_ptr<Computer> computer=builder->build(); computer->showParamaters(); return 0; }
测试结果:
代理模式
概念:
代理模式是指代理控制对其它对象的访问,也就是代理对象控制对原对象的引用。在某种情况下,一个对象不适合或者不能直接被引用访问,而代理对象可以在客户端和目标对象之间起到中介的作用。
代理模式的结构包括一个真正的你要访问的对象(目标类)、一个是代理对象。目标对象与代理对象实现同一个接口,先访问代理类再通过代理类访问目标对象。
代理模式分为:静态代理、动态代理
静态代理:
在编译时就已经确定好了代理类和被代理类的关系。也就是说在编译时就已经确定了代理类要代理的是哪个被代理类。
动态代理:
在运行时才动态生成代理类,并将其与被代理类绑定。这意味着,在运行时才确定代理类要代理的是哪个被代理类。
下面只实现静态代理:以租房为例,租客租房,中间经过房屋中介向房东租房。
静态代理模式实现及测试:
//代理模式实现+测试 //以租房子为例,房东构建被代理类,中介构建代理类 //租房子的时候直接找中介 #include<iostream> class RentHouse { public: virtual void renthouse()=0; }; class Landlord:public RentHouse { public: void renthouse() { std::cout<<"将房子租出去\n"; } }; class Intermediary:public RentHouse { public: void renthouse() { std::cout<<"发布租房告示\n"; std::cout<<"带人看房\n"; _lanlord.renthouse(); std::cout<<"租后维修\n"; } private: Landlord _lanlord; }; int main() { Intermediary intermediary; intermediary.renthouse(); return 0; }
测试结果:
四、项目框架构建
1.功能具象化
根据项目简介,我们简单总结日志系统的作用如下:
将一条消息,进行格式化后形成指定格式的字符串后,写入到指定位置
注意:
1.日志要写入到指定的位置,指定位置包括:标准输出、指定文件、滚动文件且支持将日志消息落地到不同位置,即支持多落地方向。
2.日志写入支持不同的写入方式:同步写入和异步写入
同步写入:业务线程自己负责日志的写入
异步写入:业务线程将日志放入缓冲区内存,让其它异步线程负责将日志写入指定位置
3.日志输出以日志器为单位,支持多日志器(使不同的项目组有不同的输出策略)
2.模块划分
1.日志等级模块:枚举出日志分为多少个等级,对不同的日志等级有不同的标记--以便控制输出。
2.日志消息模块:封装一条日志所需要的各种要素(时间,线程ID,文件名,行号,日志等级,消息主体等...)。
3.消息格式化模块:按照指定的格式,对日志消息关键要素进行组织,最终得到一个指定格式的字符串。
例:
[%d{%H%M%S}]%T[%t]%T[%p]%T[%c]%T%f:%l%T%m%n
[09:18:44] [98765] [FATAL] [root] main.c:166 段错误...\n
4.日志落地模块负责对日志消息进行指定方向的写入输出,用工厂模式实现。
5.日志器模块:对以上模块的整合,用建造者模式实现。
日志限制输出等级,消息格式化模块对象,日志落地模块对象,同步日志器模块,异步日志器模块
6.异步线程模块:实际运用代理模式。
7.单例的日志器管理模块:对日志进行全局管理,以便能够在项目的任何位置获取指定的日志器进行日志输出。
五、项目编写
1.实用工具类模块的设计+测试
概念:
实用工具类用于提前完成一些零碎功能接口,以便于项目中使用,本项目需要的功能如下:
- 获取系统时间
- 判断文件路径是否存在
- 获取文件的所在路径
- 创建目录
实现:
实现代码:util.hpp
//使用宏定义,防止头文件被重复包含 #ifndef __M_UTIL_H__ #define __M_UTIL_H__ #include<iostream> #include<ctime> #include<sys/stat.h> #include <sys/types.h> namespace wz_logs { namespace util { //有获取系统时间的需求,因此设置Time类 class Time { public: //定义为静态成员函数是为了方便访问 //静态成员函数属于整个类,后续在项目全局都可以直接通过类名访问而不用再定义对象访问 static size_t GetTime() { return (size_t)time(nullptr); } //本质就是对获取时间戳的一个函数的封装 }; //剩下无论是判断文件路径是否存在,判断文件所在路径,还是创建目录都是和文件有关的操作 //因此设置File类 class File { public: //判断文件是否存在 static bool Exist(const std::string &pathname) { //stat 函数的功能是获取文件的属性,成功返回0,失败返回-1 //这个系统调用函数在Linux和Windows都适用 //Linux下还可以用access(pathname,F_OK),存在返回0,不存在返回1 struct stat st; if(stat(pathname.c_str(),&st)<0) { return false; } return true; } //获取文件所在目录[获取当前指定路径文件的父目录] static std::string Path(const std::string &pathname) { size_t pos=pathname.find_last_of("/\\"); if(pos==std::string::npos) return "."; return pathname.substr(0,pos+1); } //创建目录,有就跳过,没有就创建 static void CreatDirectory(const std::string &pathname) { //一定要记得初始化 size_t pos=0,idx=0; while(idx<pathname.size()) { //从头开始找目录分割标识符 pos=pathname.find_first_of("/\\",idx); //如果没找到,就创建当前路径 if(pos==std::string::npos) { mkdir(pathname.c_str(),0777); break; } //找到了就依次看父级目录是否存在,如果存在,就直接跳过本次,不存在就顺便创建 std::string parent_dir=pathname.substr(0,pos+1); if(Exist(parent_dir)==true) { idx=pos+1; continue; } mkdir(parent_dir.c_str(),0777); idx=pos+1; } } }; } } #endif
测试代码:test.cpp
#include "util.hpp" int main() { std::cout<<wz_logs::util::Time::GetTime()<<std::endl; std::string pathname="./abc/def/ksh/wz.txt"; wz_logs::util::File::CreatDirectory(pathname); std::cout<<wz_logs::util::File::Exist("./abc/def")<<std::endl; std::cout<<wz_logs::util::File::Exist("./Abc/def")<<std::endl; wz_logs::util::File::CreatDirectory("./abc/def/llp"); std::cout<<wz_logs::util::File::Path(pathname)<<std::endl; return 0; }
测试结果:
2.日志等级类模块的设计+测试
概念:
日志等级一般包括7个等级,分别为:
- UNKNOW 未知
- OFF 关闭所有日志输出
- DEBUG进行调试时候打印日志
- INFO打印一些用户提示信息
- WARN打印警告信息
- ERROR打印错误信息
- FATAL打印致命信息-导致程序崩溃的信息
每一个项目中都会设置一个默认的日志输出等级,只有输出的日志等级大于或等于默认限制等级的时候才可以进行输出,由此我们的日志等级类模块需要包含两个部分,一部分为一个枚举变量,包含所有日志等级,另一个是将对应等级的枚举转换为一个对应的字符串。
实现:
#ifndef __M_LEVEL_H__ #define __M_LEVEL_H__ #include<iostream> namespace wz_logs { class LogLevel { public: enum class value//enum class 是C++11之后 { UNKNOW=0, DEBUG, INFO, WARN, ERROR, FATAL, OFF }; static 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
因代码很简单且需要配合其它模块使用因此,本模块的测试放到后面进行。
3.日志消息类模块的设计+测试
概念:
需要包括输出的一条实际的日志消息所需要的各项内容,分别为:
- 日志的输出时间
- 日志等级
- 源文件名称
- 源文件行号
- 线程ID
- 日主体消息
- 日志器名称
想要获取日志消息时先传入需要的参数定义消息对象,再对这个消息对象做输出处理
实现:
#ifndef __M_MSG_H__ #define __M_MSG_H__ #include<iostream> #include<string> #include<thread> #include"level.hpp" namespace wz_logs { struct LogMsg { time_t _ctime;//日志产生的时间戳 LogLevel::value _level;//日志等级 size_t _line;//行号 std::thread::id _tid;//线程id std::string _file;//源文件名称 std::string _logger;//日志器名称 std::string _payload;//有效信息载荷 //以上成员变量都在构造函数的初始化列表完成初始化 LogMsg(LogLevel::value _level, size_t line, const std::string file, const std::string logger, const std::string msg): _ctime(util::GetTime()), _level(level), _line(line), _tid(std::this_thread::get_id()), _file(file), _logger(logger) _payload(msg){} }; } #endif
同样本模块需要同其它模块结合测试。
4.日志格式化类模块的设计+测试
概念:
该模块是本项目较为复杂且重要的部分,我将从宏观到微观,从整体结构到细节依次总结:
日志格式化是指将日志消息组织成指定格式的字符串,因此日志格式化类中至少应该包括两个成员变量,一是用于描述组织形式的格式化字符串(可以看做是一个规则),另一个是用于保存根据格式化字符串取出的日志消息类中对应消息的一个数据结构,由于我们的消息中有日期信息这类包含子项[时、分、秒]的信息,因此这里用的是一个格式化子项数组,数组中应当存储依次存储的是解析格式化字符串后依次取出的子项内容转成的字符串。
不同格式的格式化子项:
以abc[%d{%H%M%S}][%f:%l]%m%n为例,依次列出格式化子项如下:
1.其他信息(指非格式化信息)子项->abc[
2.日期子项->%H%M%S
3.其它信息子项->]
4.其它信息子项->[
5.文件名子项->无
6.其它信息子项->:
7.行号子项->无
8.其它信息子项->]
9.消息主体子项->无
10.换行子项->无
有了上述分析,我们想实现一个日志格式化模块,需包括以下具体内容
1.首先要实现一个格式化子项类,用于把不同的格式化子项内容从日志信息模块中取出后输出;
2.其次在日志格式化类内部,因为我们的格式化字符串是自定义的,因此在对msg进行格式化之前,需要先检查合法性,即要定义一个检查合法性的接口;格式化字符串没问题了,接下来就是提供对msg进行格式化的接口;格式化的整个过程可以分成对不同格式化子项的格式化,因此还须提供创建格式化子类对象的接口。
实现:
实现代码:formatter.hpp
#ifndef __M_FMT_H__ #define __M_FMT_H__ #include<iostream> #include<cassert> #include<vector> #include<string> #include<memory> #include <sstream> #include<utility> namespace wz_logs { //首先实现一个格式化子项类 //实际上用的是多态的思想,ptrx相当于一个父类指针,当调用重写过的format函数 //就可以实现不同格式化子项对象调用相应的format函数,实现对应内容的提取及格式化 //1.抽象格式化子项基类 class FormatItem { public: //定义一个指向基类的智能指针 using ptr =std::shared_ptr<FormatItem>; virtual void format(std::ostream &out,LogMsg &msg)=0; }; //派生格式化子项子类--消息,等级,时间,文件名。行号... class MsgFormatItem:public FormatItem{ public: void format(std::ostream &out,LogMsg &msg) override { out<<msg._payload; } }; class LevelFormatItem:public FormatItem{ public: void format(std::ostream &out,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,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;//"%H:%M:%S" }; class FileFormatItem:public FormatItem{ public: void format(std::ostream &out,LogMsg &msg) override { out<<msg._file; } }; class LineFormatItem:public FormatItem{ public: void format(std::ostream &out,LogMsg &msg) override { out<<msg._line; } }; class ThreadFormatItem:public FormatItem{ public: void format(std::ostream &out,LogMsg &msg) override { out<<msg._tid; } }; class LoggerFormatItem:public FormatItem{ public: void format(std::ostream &out,LogMsg &msg) override { out<<msg._logger; } }; class TabFormatItem:public FormatItem{ public: void format(std::ostream &out,LogMsg &msg) override { out<<"\t"; } }; class NlineFormatItem:public FormatItem{ public: void format(std::ostream &out,LogMsg &msg) override { out<<"\n"; } }; class OtherFormatItem:public FormatItem{ public: OtherFormatItem(const std::string &str):_str(str) {} void format(std::ostream &out,LogMsg &msg) override { out<<_str; } private: std::string _str; }; class Formatter { public: Formatter(const std::string &pattern="[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n") :_pattern(pattern) { assert(ParsePattern()); } //对msg进行格式化 void format(std::ostream &out,LogMsg &msg) { for(auto &item:_items) { item->format(out,msg); } } std::string format(LogMsg &msg) { std::stringstream ss; format(ss,msg); return ss.str(); } private: //对格式化字符串的合法性进行解析 bool ParsePattern() { std::vector<std::pair<std::string,std::string>> fmt_order; size_t pos=0; std::string key,val; while(pos<_pattern.size()) { //如果不是%,表示是普通字符 if(_pattern[pos]!='%') { val.push_back(_pattern[pos++]); continue; } //走到这里说明就遇到%了,下一个是不是格式化字符,取决于下一个是不是% if(pos+1<_pattern.size()&&_pattern[pos+1]=='%') { val.push_back('%'); pos+=2; continue; } //原始字符串处理完毕,可以添加到fmt_order数组中了 if(val.empty()==false) { fmt_order.push_back(std::make_pair("",val)); val.clear(); } //走到这里说明此时pos指向一个百分号且下一个是格式化字符 pos+=1; if(pos==_pattern.size()) { std::cout<<"'%'之后没有格式化字符..."<<std::endl; return false; } key=_pattern[pos]; pos+=1; if(pos<_pattern.size()&&_pattern[pos]=='{') { pos+=1; while(pos<_pattern.size()&&_pattern[pos]!='}') { val.push_back(_pattern[pos++]); } //如果走到了末尾还没遇到'}',则说明出错了 if(pos==_pattern.size()) { std::cout<<"子规则{}匹配错误..."<<std::endl; return false; } pos+=1; } fmt_order.push_back(std::make_pair(key,val)); key.clear(); val.clear(); } //2.根据解析得到的数据初始化格式化子项数组成员 for(auto &it:fmt_order) { _items.push_back(creamItem(it.first,it.second)); } return true; } //根据不同的格式化字符创建不同的格式化子项对象 FormatItem::ptr creamItem(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=="n") return std::make_shared<NlineFormatItem>(); if(key=="m") return std::make_shared<MsgFormatItem>(); return std::make_shared<OtherFormatItem>(val); } std::string _pattern; std::vector<FormatItem::ptr> _items; }; } #endif
测试代码:test.cpp
//formatter.hpp功能测试 #include "util.hpp" #include"message.hpp" #include"level.hpp" #include"formatter.hpp" int main() { //注意分开测试每组情况,一起测会出问题 //普通测试 wz_logs::LogMsg msg(wz_logs::LogLevel::value::INFO,66,"main.c","root","测试..."); wz_logs::Formatter fmt; std::cout<<fmt.format(msg)<<std::endl; //边界情况测试1,%%问题 wz_logs::LogMsg msg1(wz_logs::LogLevel::value::INFO,66,"main.c","root","测试..."); wz_logs::Formatter fmt1("[%d{%%%H:%M}][%t][%c][%f:%l][%p]%T%m%n"); std::cout<<fmt1.format(msg1)<<std::endl; // //边界情况测试2,%后无格式化字符问题 // wz_logs::LogMsg msg2(wz_logs::LogLevel::value::INFO,66,"main.c","root","测试..."); // wz_logs::Formatter fmt2("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%"); // std::cout<<fmt2.format(msg2)<<std::endl; // //边界情况测试3,{}匹配问题 // wz_logs::LogMsg msg3(wz_logs::LogLevel::value::INFO,66,"main.c","root","测试..."); // wz_logs::Formatter fmt3("[%d{%H:%M:%S][%t][%c][%f:%l][%p]%T%m%n"); // std::cout<<fmt3.format(msg3)<<std::endl; // //边界情况测试4,没定义的格式化字符%g问题 // wz_logs::LogMsg msg4(wz_logs::LogLevel::value::INFO,66,"main.c","root","测试..."); // wz_logs::Formatter fmt4("[%d{%H:%M:%S}][%t][%c][%g%g%g:%l][%p]%T%m%n"); // std::cout<<fmt4.format(msg4)<<std::endl; // return 0; }
测试结果:其中顺便完成了对日志等级和日志消息模块的测试
5.落地类模块的设计+测试
概念:
日志落地类主要负责落地日志消息到目的地,即将格式化完成后的日志消息格式化字符串,输出到指定的位置,扩展支持同时将日志落地到不同的位置(用简单工厂模式来实现),具体的我们要实现的落地方向有三个:标准输出,指定文件,滚动文件。
实现思想:
1.抽象出落地模块类
2.不同落地方向从基类进行派生
3.使用工厂模式进行创建与表示分离
实现:
实现代码:sink.hpp
#ifndef __M_SINK_H__ #define __M_SINK_H__ #include<iostream> #include<memory> #include<fstream> #include<sstream> #include"util.hpp" #include<cassert> namespace wz_logs { //抽象一个落地基类 class LogSink { public: using ptr=std::shared_ptr<LogSink>; LogSink() {} virtual ~LogSink() {} virtual void sink(const char *data,size_t len)=0; }; //落地到标准输出 class StdoutSink:public LogSink { public: //将日志消息写入到标准输出 void sink(const char *data,size_t len) { //不能用cout<<直接输出,因为这种没办法指定大小,通常是遇到'\0'截止 std::cout.write(data,len); } }; //落地到指定文件 class FileSink:public LogSink { public: //为了提高操作效率,在构造落地项时就打开文件,此时需要传入文件名 FileSink(const std::string &pathname):_pathname(pathname) { //1.创建文件所在的目录 util::File::CreatDirectory(util::File::Path(pathname)); //2.创建并打开文件,以二进制可追加的形式打开,符合文件写入的条件 _ofs.open(_pathname,std::ios::binary|std::ios::app); //文件打开才能进行后续写入,因此加个断言 assert(_ofs.is_open()); } //将日志消息写入到指定文件 void sink(const char *data,size_t len) { _ofs.write(data,len); //加个断言,判断操作句柄是否正常,保证写入正常才能继续运行 assert(_ofs.good()); } private: std::string _pathname; std::ofstream _ofs; }; //落地到滚动文件 class RollBySizeSink:public LogSink { public: //为了提高操作效率,在构造落地项时就打开文件,此时需要传入文件名 //另外也要设置滚动文件中最大数据写入量 RollBySizeSink(const std::string &basename,size_t max_fsize) :_basename(basename), _max_fsize(max_fsize),_cur_fsize(0),_count(1) { //1.创建文件所在的目录 std::string pathname=CreatNewFile(); util::File::CreatDirectory(util::File::Path(pathname)); //2.创建并打开文件,以二进制可追加的形式打开,符合文件写入的条件 _ofs.open(pathname,std::ios::binary|std::ios::app); //文件打开才能进行后续写入,因此加个断言 assert(_ofs.is_open()); } //将日志消息写入到滚动 void sink(const char *data,size_t len) { if(_cur_fsize>=_max_fsize) { _ofs.close();//关闭原来打开的文件 std::string pathname=CreatNewFile(); _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 CreatNewFile() { //获取系统时间,以时间来构造文件拓展名 time_t t=util::Time::GetTime(); struct tm lt; localtime_r(&t,<); std::stringstream filename; 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<<_count++;//为了区分因写入过快导致的一秒内产生的多个滚动文件 filename<<".log"; return filename.str(); } private: //文件基础名,一个系列的滚动文件拥有共同的基础名+各自的扩展名 std::string _basename; std::ofstream _ofs; //用于记录滚动文件的最大大小,每次文件存放数据量达到最大,就要切换文件 size_t _max_fsize; //用于记录当前文件已经写入的大小,后续比较时就不用每次获取文件属性,提高了效率 size_t _cur_fsize; size_t _count; }; //不定参函数 class SinkFactory{ public: //因为不同的落地方向需要我们传入的参数类型和数量不确定,因此这里用不定参函数 template <typename SinkType,typename ...Args> static LogSink::ptr creat(Args &&...args) { return std::make_shared<SinkType>(std::forward<Args>(args)...); } }; } #endif
测试代码:test.cpp
//sink.hpp功能测试 #include "util.hpp" #include"message.hpp" #include"level.hpp" #include"formatter.hpp" #include"sink.hpp" int main() { //普通测试 wz_logs::LogMsg msg(wz_logs::LogLevel::value::INFO,66,"main.c","root","测试..."); wz_logs::Formatter fmt; std::string str=fmt.format(msg); wz_logs::LogSink::ptr stdout_lsp=wz_logs::SinkFactory::creat<wz_logs::StdoutSink>(); wz_logs::LogSink::ptr file_lsp=wz_logs::SinkFactory::creat<wz_logs::FileSink>("./logfile/test.log"); wz_logs::LogSink::ptr roll_lsp=wz_logs::SinkFactory::creat<wz_logs::RollBySizeSink>("./logfile/roll-",1024*1024); stdout_lsp->sink(str.c_str(),str.size()); file_lsp->sink(str.c_str(),str.size()); size_t cursize=0; size_t count=0; while(cursize<1024*1024*10) { std::string tmp=str+std::to_string(count++); roll_lsp->sink(tmp.c_str(),tmp.size()); cursize+=tmp.size(); } return 0; }
测试结果:
扩展模块:
用于检查是否支持使用用户自己定义的落地方向
我们自己定义以时间为切换标准的滚动文件 ,来测试扩展功能,如下:
//test.cpp,直接在测试文件中去模拟用户使用时自定义落地派生类 //实现 //定义一个枚举类,用户使用时只需要传入想要的枚举变量即可,方便用户使用 enum class TIMEGAP { GAP_SEC, GAP_MIN, GAP_HOUR, GAP_DAY }; //扩展一个落地方向为以时间为切换条件的滚动文件的派生类 class RollByTimeSink:public wz_logs::LogSink { public: RollByTimeSink(const std::string &basename,TIMEGAP gap_type) :_basename(basename),_cur_gap(0) { switch(gap_type) { case TIMEGAP::GAP_SEC: _gap_size=1; break; case TIMEGAP::GAP_MIN: _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?wz_logs::util::Time::GetTime():wz_logs::util::Time::GetTime()%_gap_size; //1.创建文件及文件所在的目录 std::string pathname=CreatNewFile(); wz_logs::util::File::CreatDirectory(wz_logs::util::File::Path(pathname)); //2.打开文件,以二进制可追加的形式打开,符合文件写入的条件 _ofs.open(pathname,std::ios::binary|std::ios::app); //文件打开才能进行后续写入,因此加个断言 assert(_ofs.is_open()); } //判断当前文件的_cur_gap是否是当前时间段,若不是,则要切换文件 void sink(const char *data,size_t len) { time_t cur=wz_logs::util::Time::GetTime(); if((cur%_gap_size)!=_cur_gap) { _ofs.close();//关闭原来打开的文件 std::string pathname=CreatNewFile(); _ofs.open(pathname,std::ios::binary|std::ios::app); assert(_ofs.is_open()); } _ofs.write(data,len); assert(_ofs.good()); } private: //进行大小判断,超过指定大小,创建新文件 std::string CreatNewFile() { //获取系统时间,以时间来构造文件拓展名 time_t t=wz_logs::util::Time::GetTime(); struct tm lt; localtime_r(&t,<); std::stringstream filename; 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; }; //测试 int main() { //RollByTimeSink测试 wz_logs::LogMsg msg(wz_logs::LogLevel::value::INFO,66,"main.c","root","测试..."); wz_logs::Formatter fmt; std::string str=fmt.format(msg); wz_logs::LogSink::ptr rollbytime_lsp=wz_logs::SinkFactory::creat<RollByTimeSink>("./logfile/roll-",TIMEGAP::GAP_SEC); time_t old=wz_logs::util::Time::GetTime(); while(wz_logs::util::Time::GetTime()<old+5) { rollbytime_lsp->sink(str.c_str(),str.size()); } return 0; }
测试结果:
6.日志器模块的设计+测试
概念:
日志器主要是用来和前端交互,当我们需要使用日志系统打印log时,只要创建logger对象,调用该对象不同等级的日志输出方法接口,就可以输出想输出的日志消息,支持解析可变参数列表和输出格式,即可以做到像使用prntf()函数一样打印日志。
当前日志系统支持同步日志和异步日志,两个不同的日志器唯一不同的地方在于他们在日志的落地方式上有所不同:
同步日志器:直接对日志消息进行输出
异步日志器:将日志消息放入到缓冲区中,有异步线程进行输出
因此日志器类在设计的时候应先设计出一个Lgger基类,在Logger基类的基础上,继承出同步日志器(Synclogger)和异步日志器(AsynLogger)。
另外,日志器模块可以看做是前面多个模块的整合,创建一个日志器,需要管理的对象及提供的方法分别如下:
管理的成员:
1.格式化模块对象
2.落地模块对象数组(一个日志器可能会向多个位置进行输出 )
3.默认的日志输出限制等级(大于等于限制等级的日志才可以输出)
4.互斥锁(保证日志输出过程是线程安全的,不会出现交叉日志)
5.日志器名称(日志器唯一标识,以便于查找)
提供的操作:
1.debug等级日志的输出操作
2.info等级日志的输出操作
3.warn等级日志的输出操作
4.error等级日志的输出操作
5.fatal等级日志的输出操作
其中每种输出操作中都分别会封装日志消息LogMsg,各个接口日志等级不同
由于整个日志器管理的模块较多且操作较为复杂,故而日志器模块的实现我们采用建造者模式来实现,模块关联中使用基类指针对子类日志器对象进行日志管理和操作。
日志器基类+同步日志器的实现:
先来实现日志器基类和同步日志器并进行功能测试
实现代码:logger.hpp
#ifndef __M_LOGGER_H__ #define __M_LOGGER_H__ #include"util.hpp" #include"level.hpp" #include"formatter.hpp" #include"sink.hpp" #include"message.hpp" #include<atomic> #include<mutex> #include<stdio.h> #include<stdarg.h> #include<stdlib.h> namespace wz_logs { class Logger { public: Logger( const std::string &logger_name, Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks, LogLevel::value &level): _logger_name(logger_name), _formatter(formatter), _limit_level(level), _sinks(sinks.begin(),sinks.end()) {} //需要向外提供的是一系列对不同等级日志消息输出的方法 using ptr=std::shared_ptr<Logger>; void debug(const std::string &file,size_t line,const char* fmt,...) { //通过传入的参数构造出一个日志消息对象,进行格式化,最终落地 //1.判断当前的日志输出等级是否达到输出等级 if(LogLevel::value::DEBUG<_limit_level) {return ;} //对fmt格式化字符串和不定参进行字符串组织,得到日志消息字符串 va_list ap; va_start(ap,fmt); char* res; int ret=vasprintf(&res,fmt,ap); if(ret==-1) { std::cout<<"vasprintf failed!\n"; return; } va_end(ap); serialize(LogLevel::value::DEBUG,file,line,res); free(res); } void info(const std::string &file,size_t line,const char*fmt,...) { //通过传入的参数构造出一个日志消息对象,进行格式化,最终落地 //1.判断当前的日志输出等级是否达到输出等级 if(LogLevel::value::INFO<_limit_level) {return ;} //对fmt格式化字符串和不定参进行字符串组织,得到日志消息字符串 va_list ap; va_start(ap,fmt); char* res; int ret=vasprintf(&res,fmt,ap); if(ret==-1) { std::cout<<"vasprintf failed!\n"; return; } va_end(ap); serialize(LogLevel::value::INFO,file,line,res); free(res); } void warn(const std::string &file,size_t line,const char*fmt,...) { //通过传入的参数构造出一个日志消息对象,进行格式化,最终落地 //1.判断当前的日志输出等级是否达到输出等级 if(LogLevel::value::WARN<_limit_level) {return ;} //对fmt格式化字符串和不定参进行字符串组织,得到日志消息字符串 va_list ap; va_start(ap,fmt); char* res; int ret=vasprintf(&res,fmt,ap); if(ret==-1) { std::cout<<"vasprintf failed!\n"; return; } va_end(ap); serialize(LogLevel::value::WARN,file,line,res); free(res); } void error(const std::string &file,size_t line,const char*fmt,...) { //通过传入的参数构造出一个日志消息对象,进行格式化,最终落地 //1.判断当前的日志输出等级是否达到输出等级 if(LogLevel::value::ERROR<_limit_level) {return ;} //对fmt格式化字符串和不定参进行字符串组织,得到日志消息字符串 va_list ap; va_start(ap,fmt); char* res; int ret=vasprintf(&res,fmt,ap); if(ret==-1) { std::cout<<"vasprintf failed!\n"; return; } va_end(ap); serialize(LogLevel::value::ERROR,file,line,res); free(res); } void fatal(const std::string &file,size_t line,const char*fmt,...) { //通过传入的参数构造出一个日志消息对象,进行格式化,最终落地 //1.判断当前的日志输出等级是否达到输出等级 if(LogLevel::value::FATAL<_limit_level) {return ;} //对fmt格式化字符串和不定参进行字符串组织,得到日志消息字符串 va_list ap; va_start(ap,fmt); char* res; int ret=vasprintf(&res,fmt,ap); if(ret==-1) { std::cout<<"vasprintf failed!\n"; return; } va_end(ap); serialize(LogLevel::value::FATAL,file,line,res); free(res); } protected: void serialize(LogLevel::value level,const std::string &file,size_t line,char*str) { //1.构造LogMsg对象 LogMsg msg(level,line,file,_logger_name,str); //2.通过格式化工具对LogMsg进行格式化,得到格式化后的日志字符串 std::string ss=_formatter->format(msg); //3.进行日志落地 log(ss.c_str(),ss.size()); } //抽象接口完成实际的落地输出--不同的日志器会有不同的落地方式 virtual void log(const char* data,size_t len)=0; protected: std::mutex _mutex; std::string _logger_name; Formatter::ptr _formatter; std::vector<LogSink::ptr> _sinks; std::atomic<LogLevel::value> _limit_level; }; class SyncLogger:public Logger{ public: SyncLogger( const std::string &logger_name, Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks, LogLevel::value &level): Logger(logger_name, formatter, sinks, level) {} void log(const char* data,size_t len) { //同步日志器,是将日志直接通过落地模块句柄进行日志落地 std::unique_lock<std::mutex> lock(_mutex); if(_sinks.empty()) return; for(auto &sink:_sinks) { sink->sink(data,len); } } }; } #endif
测试代码:test.cpp
//logger.cpp同步日志器测试 #include"logger.hpp" int main() { std::string logger_name="synclogger"; wz_logs::LogLevel::value limit=wz_logs::LogLevel::value::WARN; wz_logs::Formatter::ptr fmt(new wz_logs::Formatter("[%d{%H:%M:%S}][%c][%f:%l][%p]%T%m%n")); wz_logs::LogSink::ptr stdout_lsp=wz_logs::SinkFactory::creat<wz_logs::StdoutSink>(); wz_logs::LogSink::ptr file_lsp=wz_logs::SinkFactory::creat<wz_logs::FileSink>("./logfile/test.log"); wz_logs::LogSink::ptr roll_lsp=wz_logs::SinkFactory::creat<wz_logs::RollBySizeSink>("./logfile/roll-",1024*1024); std::vector<wz_logs::LogSink::ptr> sinks={stdout_lsp,file_lsp,roll_lsp}; wz_logs::Logger::ptr logger(new wz_logs::SyncLogger(logger_name,fmt,sinks,limit)); logger->debug(__FILE__,__LINE__,"%s","测试日志"); logger->info(__FILE__,__LINE__,"%s","测试日志"); logger->warn(__FILE__,__LINE__,"%s","测试日志"); logger->error(__FILE__,__LINE__,"%s","测试日志"); logger->fatal(__FILE__,__LINE__,"%s","测试日志"); size_t cursize=0,count=0; while(cursize<1024*1024*10) { logger->fatal(__FILE__,__LINE__,"测试日志-%d",count++); cursize+=20; } return 0; }
本测试用例实现同时向三个落地方向进行输出,测试结果如下:
建造者设计思想的引入:
我们在使用日志器模块进行日志打印之前,要进行日志消息模块、格式化器模块等的大量的零部件构造,如下图:
也就是说用户真正使用到我们的日志器模块之前还要自己承担构造各个零件的任务,显然,这对于用户来说,体验感并不好,因此我们使用建造者模式来建造日志器,而不要让用户直接建造,简化用户使用的复杂度,实现思想如下:
1.抽象一个日志器建造者基类:设置日志器类性、将所有的日志器创建放在同一个建造者类中完成;
2.派生出具体的建造者类--局部日志器的建造者、全局日志器的建造者
建造者模式初步实现:
这部分代码也在logger.hpp中:
enum LoggerType{ LOGGER_SYNC, LOGGER_ASYNC }; //1.抽象一个日志器建造者基类:设置日志器类性、将所有的日志器创建放在同一个建造者类中完成; class LoggerBuilder { public: LoggerBuilder():_logger_type(LoggerType::LOGGER_SYNC), _limit_level(wz_logs::LogLevel::value::DEBUG) {} void buildLoggerType(LoggerType type){_logger_type=type;} void buildLoggerName(const std::string &name){_logger_name=name;} void buildLoggerLevel(wz_logs::LogLevel::value level){_limit_level=level;} void buildFormatter(const std::string &pattern) { _formatter=std::make_shared<wz_logs::Formatter>(pattern); } template <typename SinkType,typename...Args> void buildSink(Args &&...args) { wz_logs::LogSink::ptr psink=SinkFactory::creat<SinkType>(std::forward<Args>(args)...); _sinks.push_back(psink); } virtual Logger::ptr build()=0; protected: LoggerType _logger_type; std::string _logger_name; wz_logs::Formatter::ptr _formatter; wz_logs::LogLevel::value _limit_level; std::vector<wz_logs::LogSink::ptr> _sinks; }; //2.派生出具体的建造者类--局部日志器的建造者、全局日志器的建造者 class LocalLoggerBuilder:public LoggerBuilder{ public: Logger::ptr build() override { assert(!_logger_name.empty());//日志器名称是使用日志器的唯一标识,即必须有 if(_formatter.get()==nullptr) { _formatter=std::make_shared<Formatter>(); } if(_sinks.empty()) { buildSink<StdoutSink>(); } if(_logger_type==LoggerType::LOGGER_ASYNC) { } return std::make_shared<SyncLogger>(_logger_name,_formatter,_sinks,_limit_level); } };
看得出来调用接口来实现零件构造明显比上面直接构造方便很多,由于我们对各个模块构建的顺序不会影响到最后的落地情况,因此这里不需要再生成指挥者类。
测试代码:test.cpp
//logger.cpp建造者模式下同步日志器测试 #include"logger.hpp" int main() { std::unique_ptr<wz_logs::LoggerBuilder> builder(new wz_logs::LocalLoggerBuilder()); builder->buildLoggerName("sync_logger"); builder->buildLoggerLevel(wz_logs::LogLevel::value::WARN); builder->buildFormatter("%m%n"); builder->buildLoggerType(wz_logs::LoggerType::LOGGER_SYNC); builder->buildSink<wz_logs::FileSink>("./logfile/test.log"); builder->buildSink<wz_logs::StdoutSink>(); wz_logs::Logger::ptr logger=builder->build(); logger->debug(__FILE__,__LINE__,"%s","测试日志"); logger->info(__FILE__,__LINE__,"%s","测试日志"); logger->warn(__FILE__,__LINE__,"%s","测试日志"); logger->error(__FILE__,__LINE__,"%s","测试日志"); logger->fatal(__FILE__,__LINE__,"%s","测试日志"); size_t cursize=0,count=0; while(cursize<1024*1024*10) { logger->fatal(__FILE__,__LINE__,"测试日志-%d",count++); cursize+=20; } return 0; }
测试结果:
异步日志器:
异步缓冲区类--概念:
之前完成的同步日志器是直接将日志消息进行格式化然后写入文件,接下来的异步日志器实现思想如下:
有了之前的铺垫,我们知道,异步日志器的实现是为了避免因为写日志的过程阻塞,导致业务线程在写日志的时候影响效率,我们的实现思想就是不让业务线程进行日志的实际落地操作,而是将日志消息放到缓冲区(一块指定的内存),接下来有一个专门的异步线程,去针对缓冲区中的数据进行处理(实际的落地操作),如下图:
那么我们要实现一个异步日志器,首先要有一个线程安全的缓冲区,其次要有一个处理消息实际落地的异步工作线程,线程安全是靠对缓冲区的读写加锁实现的。
缓冲区设计:
1.使用队列,缓存日志消息,逐条处理
考虑到效率问题,这个队列不能涉及到空间的频繁申请和释放,否则会降低效率,那就可以设计一个环形队列(提前将空间申请好,然后对空间进行循环利用),但是这种方式会遇到严重的锁冲突问题,生产者与生产者的互斥,生产者与消费者的互斥【因为写日志操作在实际开发过程中,并不会分配太多资源,所以工作线程只需要一个日志器就行,一般不考虑消费者与消费者之间的冲突】,为解决这一问题,我们采用双缓冲区的方式,思想如下:
双缓冲区是处理器将一个缓冲区中的任务全部处理完,然后交换两个缓冲区,重新对新的缓冲区中的任务进行处理,虽然同时多线程输入也会冲突,但是冲突并不会像每次只处理一条的时候频繁(主要是减少了生产者和消费者之间的锁冲突),且不涉及到空间的频繁申请和释放所带来的消耗,结构图如下:
接下来我们对单个缓冲区进行进一步设计:
生产者消费者缓冲区我们用共同的一个缓冲区类来同时实现,这个缓冲区类构造后可以直接存放格式化后的日志消息字符串(这样既可以减少LogsMsg对象的频繁构造,又可以针对缓冲区的日志消息,一次性进行IO操作,减少IO次数,提高效率),因此我们设计的缓冲区类应该管理的成员以及需要提供的接口如下:
成员变量:
1.一个存放字符串数据的缓冲区(使用管理 )
2.当前的写入数据位置的指针(指向可写区域的起始位置,避免数据写入覆盖)
3.当前的读取数据位置的指针(指向可读数据区域的起始位置,当读取指针与写入指针指向位置相同表示数据读取完了)
成员函数:
1.向缓冲区写入数据
2.获取可读、可写数据起始地址的接口
3.获取可读数据长度的接口
4.移动读写位置的接口
5.初始化缓冲区的操作(将一个缓冲区所有数据处理完毕之后,读写位置初始化)
6.交换缓冲区的操作
异步缓冲区类--实现:
buffer.hpp
#ifndef __M_BUF_H__ #define __M_BUF_H__ #include<vector> #include<cassert> #include<iostream> namespace wz_logs { #define DEFAULT_BUFFER_SIZE (1*1024*1024) //默认缓冲区大小1M #define THRESHOLD_BUFFER_SIZE (10*1024*1024)//缓冲区阈值 #define INCREMENT_BUFFER_SIZE (1*1024*1024)//线性增量大小 class Buffer { public: Buffer():_buffer(DEFAULT_BUFFER_SIZE),_writer(0),_reader(0) {} //将数据push进缓冲区 void push(const char* data,size_t len) { //1.判断容量,这里提供两种处理方式,应用于不同的场景 //1'数量达到设定好的大小,就阻塞/返回false---应用于实际场景,因为现实中资源不可能是无限使用的 // if(_buffer.writeAblesize()<len) return; //2'数量按照一定的规则来扩容,不设上限---应用于测试场景 if(writeAblesize()<len) { containerReset(len); } //2.将数据拷贝到缓冲区可写入起始地址处 std::copy(data,data+len,&_buffer[_writer]); //3.可写入地址向后偏移 writemov(len); } //读位置向后偏移 void readmov(size_t len) { assert(len<=_buffer.size()); _reader+=len; } //返回可读位置起始地址 const char* begin() { return &_buffer[_reader]; } //重置缓冲区,初始化缓冲区 void reset() { _reader=0; _writer=0; } //返回缓冲区可读取数据长度 size_t readAblesize() { return _writer-_reader; } //返回缓冲区可写入数据长度 size_t writeAblesize() { return _buffer.size()-_writer; } //交换读写缓冲区 void swap(Buffer &buf) { _buffer.swap(buf._buffer); std::swap(_writer,buf._writer); std::swap(_reader,buf._reader); } //判断缓冲区是否为空 bool empty() { return _writer==_reader; } private: //写指针向后偏移 void writemov(size_t len) { assert((len+_writer)<=_buffer.size()); _writer+=len; } //扩容 void containerReset(const size_t &len) { //为保证合理性,我们采用阈值的方式来划分扩容方式 //设定一个阈值,没达到阈值之间,2倍扩容,达到阈值后,线性扩容 size_t newsize=0; if(_buffer.size()<THRESHOLD_BUFFER_SIZE) { newsize=_buffer.size()*2+len;//+len是因为扩容后可能容量依旧不够,即newsize<len } else { newsize=_buffer.size()+INCREMENT_BUFFER_SIZE+len;//日志一般不会存在扩容后空间仍然不够的问题,但是也要考虑到异常的处理 } _buffer.resize(newsize); } private: std::vector<char> _buffer; size_t _writer;//本质是当前可写位置的下标 size_t _reader;//本质是当前可读位置的下标 }; } #endif
测试代码:test.cpp
//buffer.hpp的测试 //由于在此环境下,不好去查看内存空间值的情况 //因此为了方便,我们和文件读取写入配合的操作来测试,具体思想如下: //读取文件数据,逐字节写入缓冲区,最终将缓冲区数据写入文件,判断生成的新文件与源文件是否一致 #include "buffer.hpp" #include <fstream> int main() { //ifstream:该数据类型表示输入文件流,用于从文件读取信息。 std::ifstream ifs("./logfile/test.log",std::ios::binary); if(ifs.is_open()==false) return -1; ifs.seekg(0,std::ios::end);//读写位置跳转至文件末尾 size_t fsize=ifs.tellg();//获取当前读写位置相对于起始位置的偏移量 ifs.seekg(0,std::ios::beg);//重新跳转至起始位置 std::string body; body.resize(fsize); ifs.read(&body[0],fsize); if(ifs.good()==false) { std::cout<<"read error\n"; return -1; } ifs.close(); std::cout<<fsize<<std::endl; wz_logs::Buffer buffer; for(int i=0; i<body.size();i++) { buffer.push(&body[i],1); } std::ofstream ofs("./logfile/tmp.log",std::ios::binary);//测试时,一定要保证这个文件是存在的,否则运行时找不到会报错 size_t bsize=buffer.readAblesize(); for(int i=0;i<bsize;i++) { ofs.write(buffer.begin(),1); if(ofs.good()==false) {std::cout<<"write error\n";return -1;} buffer.readmov(1); } ofs.close(); return 0; }
测试结果:
补充知识:MD5算法常常被用来验证网络文件传输的完整性,防止文件被人篡改。MD5 全称是报文摘要算法(Message-Digest Algorithm 5),此算法对任意长度的信息逐位进行计算,产生一个二进制长度为128位(十六进制长度就是32位)的“指纹”(或“报文摘要”),不同的文件产生相同的报文摘要的可能性是非常非常之小的,常用来验证两文件内容是否完全一致。
异步工作器--概念:
设计思想:异步处理器+数据池(双缓冲区)
使用者将需要完成的任务添加到任务池中,由异步线程来完成任务的实际执行操作。
由此异步工作器需要管理的成员变量及提供的操作就很清楚了,如下:
私有成员变量:
1.双缓冲区(生产、消费)
2.互斥锁(保证线程安全)
3.条件变量--生产+ 消费(生产缓冲区没有数据,处理完消费缓冲区数据后就休眠)
4.回调函数(针对缓冲区中数据的处理接口-外部传入一个函数,告诉异步工作器数据该如何处理)
提供的操作接口:
1.停止异步工作器
2.添加数据到缓冲区
私有操作接口:
创建线程,线程入口函数中,交换缓冲区,对消费缓冲区数据使用回调函数进行处理,处理完后再次交换
异步工作器--实现:
looper.hpp
#ifndef __M_LOOPER_H__ #define __M_LOOPER_H__ #include"buffer.hpp" #include<thread> #include<mutex> #include<condition_variable> #include<atomic> #include<functional> #include<memory> namespace wz_logs { using Functor=std::function<void(Buffer &)>; enum class Looper_Type { ASYNC_SAFE,//安全状态,缓冲区满了则阻塞,避免资源耗尽的风险,用于实践 ASYNC_UNSAFE//非安全,不考虑资源耗尽问题,无限扩容,常用于测试 }; class AsyncLooper { public: using ptr=std::shared_ptr<AsyncLooper>; AsyncLooper(const Functor &cb,Looper_Type loop_type=Looper_Type::ASYNC_SAFE) :_stop(false) ,_thread(std::thread(&AsyncLooper::threadEntry,this)) ,_callBack(cb) ,_looper_type(loop_type) {} ~AsyncLooper() { stop(); } void stop() { _stop=true;//将退出标志设置为true _cond_con.notify_all();//唤醒所有的工作线程 } void push(const char* data,size_t len) { //1.无限扩容是不安全的;2.固定大小生产缓冲区满了就阻塞 std::unique_lock<std::mutex> lock(_mutex); //2.条件变量控制,若缓冲区剩余空间大小大于数据长度,则可以添加数据 if(_looper_type==Looper_Type::ASYNC_SAFE) _cond_pro.wait(lock,[&](){return _pro_buf.writeAblesize()>=len; }); //3.能够走下来说明条件满足,可以向缓冲区添加数据 _pro_buf.push(data,len); //4.唤醒消费者对缓冲区进行数据处理 _cond_con.notify_one(); } private: //线程入口函数 //对消费缓冲区中的数据进行处理,处理完成后,初始化缓冲区,交换缓冲区 void threadEntry() { while(1) { { //1.判断生产缓冲区有没有数据,有则交换,无则阻塞 //为互斥锁设置一个生命周期,当缓冲区交换完毕后就解锁 std::unique_lock<std::mutex> lock(_mutex); if(_stop&&_pro_buf.empty()) break; //若当前是退出前被唤醒,或者所有数据被唤醒,则返回真,继续向下运行,否则重新进入休眠 _cond_con.wait(lock,[&](){return _stop||!_pro_buf.empty();});//这里是为了需要等退出标志置为true且生产缓冲区数据已经全被处理完了,才进行阻塞 _con_buf.swap(_pro_buf); //2.交换完了,就唤醒生产者 if(_looper_type==Looper_Type::ASYNC_SAFE) _cond_pro.notify_all(); } //3.被唤醒后,对消费缓冲区进行数据处理 _callBack(_con_buf); //4.初始化消费缓冲区 _con_buf.reset(); } } Functor _callBack;//具体对缓冲区数据进行处理的回调函数,由异步工作器使用者传入 private: Looper_Type _looper_type; std::atomic<bool> _stop;//工作器停止标志 Buffer _pro_buf;//生产者缓冲区 Buffer _con_buf;//消费者缓冲区 std::mutex _mutex; std::condition_variable _cond_pro; std::condition_variable _cond_con; std::thread _thread;//异步工作器对应的工作线程 }; } #endif
测试代码:test.cpp
//!!!!!这个有问题,有待测试!!!!!// //总会有如下报错,应该是线程异常退出的问题// //terminate called without an active exception Aborted// //logger.cpp建造者模式下异步日志器测试 #include"logger.hpp" int main() { std::unique_ptr<wz_logs::LoggerBuilder> builder(new wz_logs::LocalLoggerBuilder()); builder->buildLoggerName("async_logger"); builder->buildLoggerLevel(wz_logs::LogLevel::value::WARN); builder->buildFormatter("[%c]%m%n"); builder->buildLoggerType(wz_logs::LoggerType::LOGGER_ASYNC); //builder->buildEnableUnSafeAsync(); builder->buildSink<wz_logs::FileSink>("./logfile/async.log"); builder->buildSink<wz_logs::StdoutSink>(); wz_logs::Logger::ptr logger=builder->build(); logger->debug(__FILE__,__LINE__,"%s","测试日志"); logger->info(__FILE__,__LINE__,"%s","测试日志"); logger->warn(__FILE__,__LINE__,"%s","测试日志"); logger->error(__FILE__,__LINE__,"%s","测试日志"); logger->fatal(__FILE__,__LINE__,"%s","测试日志"); size_t cursize=0,count=0; while(cursize<1024*1024*10) { logger->fatal(__FILE__,__LINE__,"测试日志-%d",count++); cursize+=20; } return 0; }
测试等将异步日志器写完,即完整的日志器模块基本实现再来测试功能。
日志器模块完善--异步日志器的实现 :
logger.hpp
class AsyncLogger:public Logger{ public: AsyncLogger( const std::string &logger_name, Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks, LogLevel::value &level, Looper_Type looper_type): Logger(logger_name, formatter, sinks, level) , _looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::realLog,this,std::placeholders::_1),looper_type)){} //将数据写入缓冲区 void log(const char* data,size_t len) { _looper->push(data,len); } //设计一个实际落地函数(将缓冲区中的数据落地) void realLog(Buffer &buf) { if(_sinks.empty()) return; for(auto &sink:_sinks) { sink->sink(buf.begin(),buf.readAblesize()); } } private: AsyncLooper::ptr _looper;//要管理一个异步工作器 };
此时,同步日志器和异步日志器我们就基本都实现了,接下来我们用之前测试同步日志器的代码来对异步日志器的功能进行测试。
测试代码:test.cpp
//!!!!!这个有问题,有待测试!!!!!// //总会有如下报错,应该是线程异常退出的问题// //terminate called without an active exception Aborted// //logger.cpp建造者模式下异步日志器测试 #include"logger.hpp" int main() { std::unique_ptr<wz_logs::LoggerBuilder> builder(new wz_logs::LocalLoggerBuilder()); builder->buildLoggerName("async_logger"); builder->buildLoggerLevel(wz_logs::LogLevel::value::WARN); builder->buildFormatter("[%c]%m%n"); builder->buildLoggerType(wz_logs::LoggerType::LOGGER_ASYNC); //builder->buildEnableUnSafeAsync(); builder->buildSink<wz_logs::FileSink>("./logfile/async.log"); builder->buildSink<wz_logs::StdoutSink>(); wz_logs::Logger::ptr logger=builder->build(); logger->debug(__FILE__,__LINE__,"%s","测试日志"); logger->info(__FILE__,__LINE__,"%s","测试日志"); logger->warn(__FILE__,__LINE__,"%s","测试日志"); logger->error(__FILE__,__LINE__,"%s","测试日志"); logger->fatal(__FILE__,__LINE__,"%s","测试日志"); size_t cursize=0,count=0; while(cursize<1024*1024*10) { logger->fatal(__FILE__,__LINE__,"测试日志-%d",count++); cursize+=20; } return 0; }
测试结果:
可见,我们内部的实现有一些问题,测试结果如下,具体原因尚未排查出来:
建造者模式进一步实现 :
logger.hpp
//注释处多为完善增加的内容 enum LoggerType{ LOGGER_SYNC, LOGGER_ASYNC }; class LoggerBuilder { public: LoggerBuilder():_logger_type(LoggerType::LOGGER_SYNC), _limit_level(wz_logs::LogLevel::value::DEBUG) {} void buildLoggerType(LoggerType type){_logger_type=type;} void buildLoggerName(const std::string &name){_logger_name=name;} void buildLoggerLevel(wz_logs::LogLevel::value level){_limit_level=level;} //新增接口--设置异步日志器的工作模式,一般极限测试性能时,调用此接口即可设置日志器工作模式 void buildEnableUnSafeAsync() { _looper_type=Looper_Type::ASYNC_UNSAFE; } void buildFormatter(const std::string &pattern) { _formatter=std::make_shared<wz_logs::Formatter>(pattern); } template <typename SinkType,typename...Args> void buildSink(Args &&...args) { wz_logs::LogSink::ptr psink=SinkFactory::creat<SinkType>(std::forward<Args>(args)...); _sinks.push_back(psink); } virtual Logger::ptr build()=0; protected: //新增成员变量--异步工作器工作模式 LoggerType _logger_type;//针对异步日志器提供的 std::string _logger_name; wz_logs::Formatter::ptr _formatter; wz_logs::LogLevel::value _limit_level; std::vector<wz_logs::LogSink::ptr> _sinks; Looper_Type _looper_type; }; class LocalLoggerBuilder:public LoggerBuilder{ public: Logger::ptr build() override { assert(!_logger_name.empty());//日志器名称是使用日志器的唯一标识,即必须有 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,_formatter,_sinks,_limit_level,_looper_type); } return std::make_shared<SyncLogger>(_logger_name,_formatter,_sinks,_limit_level); } };
日志器管理器模块
概念:
作用:对所有创建的日志器统一进行管理。
特性:以单例模式来实现日志器管理器,这样我们就可以在程序的任意位置,获取相同的单例对象,获取其中的日志器进行日志输出。
扩展:单例管理器创建时,默认先创建一个日志器(用于进行标准输出打印,方便用户使用)
综上,我们要实现的单例模式管理器应包含以下内容:
管理的成员:
1.默认日志器
2.管理的日志器数据结构
3.互斥锁
提供的接口:
1.添加日志器管理
2.判断是否管理了指定名称的日志器
3.获取指定名称的日志器
4。获取默认日志器
实现:
logger.hpp
//设置一个单例模式的日志器管理类 class LoggerManager{ public: static LoggerManager& getInstance() { //C++11后的新特性,针对静态局部变量,编译器实现了线程安全 //也就是说,在静态局部变量没有构造完成之前,其他线程就会进入阻塞 static LoggerManager eton; return eton; } void addLogger(Logger::ptr &logger) { if(hasLogger(logger->name())) return; std::unique_lock<std::mutex> lock(_mutex); _loggers.insert(std::make_pair(logger->name(),logger)); } bool hasLogger(const std::string &name) { std::unique_lock<std::mutex> lock(_mutex); auto it=_loggers.find(name); if(it==_loggers.end()) return false; 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() { //构造默认日志器 std::unique_ptr<wz_logs::LoggerBuilder> builder(new wz_logs::LocalLoggerBuilder()); builder->buildLoggerName("root"); _root_logger=builder->build(); _loggers.insert(std::make_pair("root",_root_logger)); } private: std::mutex _mutex; Logger::ptr _root_logger; std::unordered_map<std::string,Logger::ptr> _loggers; };
实现之后,结合实际情况思考,我们发现,当我们用户在创建了一个日志器后想要进行全局的管理时,就要把这个日志器添加到我们的单例对象中,然而我们发现,用户自己添加的过程是比较麻烦的,需要动用LoggerManager::getInstence().addLooger()才能完成添加,无疑增加了用户的使用复杂度,这个时候我们就要继续完善我们的建造者类,我们之前实现的LocalLoggerBuilder可以看做是本地建造者,哪里创建哪里使用,接下来我们就要再实现一个全局建造者,GlobalLoggerBuilder,这个类同样继承于LoggerBuilder,但其中在创建好日志器后,直接将日志器添加到管理器中,因此,以后用户想要创建一个全局的日志器就只需要使用GlobalLoggerBuilder进行构建即可,构建好的日志器天然就在管理器中。
全局建造者:GlobalLoggerBuilder的实现:
logger.hpp
class GlobalLoggerBuilder:public LoggerBuilder{ public: Logger::ptr build() override { assert(!_logger_name.empty());//日志器名称是使用日志器的唯一标识,即必须有 if(_formatter.get()==nullptr) { _formatter=std::make_shared<Formatter>(); } if(_sinks.empty()) { buildSink<StdoutSink>(); } Logger::ptr logger; if(_logger_type==LoggerType::LOGGER_ASYNC) { logger=std::make_shared<AsyncLogger>(_logger_name,_formatter,_sinks,_limit_level,_looper_type); } else{ logger=std::make_shared<SyncLogger>(_logger_name,_formatter,_sinks,_limit_level); } //构建好的日志器直接在内部添加到单例对象中 LoggerManager::getInstance().addLogger(logger); return logger; } };
接下来我们就可以对管理器模块以及全局建造者模块来进行统一的测试:
测试代码:test.cpp
//日志器管理类及全局建造者类测试 #include"logger.hpp" void test_log() { wz_logs::Logger::ptr logger=wz_logs::LoggerManager::getInstance().getLogger("sync_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 cursize=0,count=0; while(cursize<1024*1024*10) { logger->fatal(__FILE__,__LINE__,"测试日志-%d",count++); cursize+=20; } } int main() { //创建管理全局建造者对象的智能指针 std::unique_ptr<wz_logs::LoggerBuilder> builder(new wz_logs::GlobalLoggerBuilder()); builder->buildLoggerName("sync_logger"); builder->buildLoggerLevel(wz_logs::LogLevel::value::WARN); builder->buildFormatter("%m%n"); builder->buildLoggerType(wz_logs::LoggerType::LOGGER_SYNC); builder->buildSink<wz_logs::FileSink>("./logfile/test.log"); builder->buildSink<wz_logs::StdoutSink>(); builder->build(); test_log(); return 0; }
全局建造者创建的同步日志器落地方向为标准输出及指定文件,测试结果如下:
目录文件生成正常,输出等级判断正常:
7.全局接口的设计
概念:
上面在test.cpp中测试各个模块的功能时,大多都是从我们设计者的角度去测试的,对于用户来讲显然是不合理的,比如开始用全局建造者构建单例对象管理的日志器,用户怎么知道你有单例模式,还有在调用不同等级的日志输出方法时,又怎么知道要传__LINE__和__FILE__两个宏,还有用户在使用时,一般只需要有一个封装好的头文件即可,手动去包含一大堆头文件也是不合理的,因此为了对日志系统接口的使用便捷性进行优化,我们需要提供全局接口&宏函数。
思想:
1.提供获取指定日志器的全局接口(避免用户自己操作单例对象)
2.使用宏函数对日志器的接口进行代理(代理模式)
3.提供宏函数,直接通过默认日志器进行标准输出打印,不用获取日志器了
实现:
wz_log.hpp
#ifndef __M_WZLOG_H__ #define __M_WZLOG_H__ #include"logger.hpp" namespace wz_logs{ // 1.提供获取指定日志器的全局接口(避免用户自己操作单例对象) Logger::ptr getLogger(const std::string &name) { return wz_logs::LoggerManager::getInstance().getLogger(name); } Logger::ptr rootLogger() { return wz_logs::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,...) wz_logs::rootLogger()->debug(fmt,##__VA_ARGS__) #define INFO(fmt,...) wz_logs::rootLogger()->info(fmt,##__VA_ARGS__) #define WARN(fmt,...) wz_logs::rootLogger()->warn(fmt,##__VA_ARGS__) #define ERROR(fmt,...) wz_logs::rootLogger()->error(fmt,##__VA_ARGS__) #define FATAL(fmt,...) wz_logs::rootLogger()->fatal(fmt,##__VA_ARGS__) } #endif
测试代码:test.cpp
//全局接口测试 //提供封装全局接口的头文件之后,测试就可以只包含这个头文件,符合用户使用习惯 #include"wz_log.hpp" void test_log() { //1.调用封装好的接口来获取日志器 //wz_logs::Logger::ptr logger =wz_logs::getLogger("sync_logger"); wz_logs::Logger::ptr logger=wz_logs::LoggerManager::getInstance().getLogger("sync_logger"); logger->debug("%s","测试日志"); logger->info("%s","测试日志"); logger->warn("%s","测试日志"); logger->error("%s","测试日志"); logger->fatal("%s","测试日志"); size_t count=0; while(count<500000) { logger->fatal("测试日志-%d",count++); } //2.不用获取日志器,直接用封装好了默认日志器的操作 // DEBUG("%s","测试日志"); // INFO("%s","测试日志"); // WARN("%s","测试日志"); // ERROR("%s","测试日志"); // FATAL("%s","测试日志"); // size_t count=0; // while(count<500000) // { // FATAL("测试日志-%d",count++); // } } int main() { std::unique_ptr<wz_logs::LoggerBuilder> builder(new wz_logs::GlobalLoggerBuilder());//创建管理全局建造者对象的智能指针 builder->buildLoggerName("sync_logger"); builder->buildLoggerLevel(wz_logs::LogLevel::value::WARN); builder->buildFormatter("[%c][%f:%l]%m%n"); builder->buildLoggerType(wz_logs::LoggerType::LOGGER_SYNC); builder->buildSink<wz_logs::FileSink>("./logfile/test.log"); builder->buildSink<wz_logs::StdoutSink>(); builder->build(); test_log(); return 0; }
六、项目目录结构梳理
考虑到我们项目的用户层面实用性,我们要对整个项目的目录结构进行梳理,最终用户只需要拿到一个logs文件(里面包含实现各个模块的头文件),使用时只包含其中的一个提供全局接口的头文件wz_log.hpp即可使用;同时还要有一个存放测试用例example文件(里面存放一些重要的测试用例,主要用于让用户快速熟悉这套系统的用法),另外还要有一个包含细碎知识点测试的practice文件(本项目中存放的是一些前置知识的测试代码),最后再创建一个extend文件用来保存扩展部分的代码(本项目中扩展的是落地方向,落地到以时间为划分标准的滚动文件)整理好的目录结构如下:
1.测试用例模块功能检测:
代码:./example/test.cpp
//全局接口测试 #include"../logs/wz_log.hpp" #include<unistd.h> void test_log(const std::string &name) { INFO("%s","测试开始"); wz_logs::Logger::ptr logger =wz_logs::getLogger("sync_logger"); logger->debug("%s","测试日志"); logger->info("%s","测试日志"); logger->warn("%s","测试日志"); logger->error("%s","测试日志"); logger->fatal("%s","测试日志"); INFO("%s","测试结束"); } int main() { std::unique_ptr<wz_logs::LoggerBuilder> builder(new wz_logs::GlobalLoggerBuilder());//创建管理全局建造者对象的智能指针 builder->buildLoggerName("sync_logger"); builder->buildLoggerLevel(wz_logs::LogLevel::value::DEBUG); builder->buildFormatter("[%c][%f:%l]%m%n"); builder->buildLoggerType(wz_logs::LoggerType::LOGGER_SYNC); builder->buildSink<wz_logs::FileSink>("./logfile/test.log"); builder->buildSink<wz_logs::StdoutSink>(); builder->buildSink<wz_logs::RollBySizeSink>("./logfile/rool-sync-by-size",1024*1024); builder->build(); test_log("sync_logger"); return 0; }
测试结果:
2.拓展模块功能检测:
实现+测试 代码:./extend/test.cpp
#include"../logs/wz_log.hpp" #include<unistd.h> //扩展模块测试:模拟用户自定义落地派生类模块 //定义一个枚举类,用户使用时只需要传入想要的枚举变量即可,方便用户使用 enum class TIMEGAP { GAP_SEC, GAP_MIN, GAP_HOUR, GAP_DAY }; //扩展一个落地方向为以时间为切换条件的滚动文件的派生类 class RollByTimeSink:public wz_logs::LogSink { public: RollByTimeSink(const std::string &basename,TIMEGAP gap_type) :_basename(basename),_cur_gap(0) { switch(gap_type) { case TIMEGAP::GAP_SEC: _gap_size=1; break; case TIMEGAP::GAP_MIN: _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?wz_logs::util::Time::GetTime():wz_logs::util::Time::GetTime()%_gap_size; //1.创建文件及文件所在的目录 std::string pathname=CreatNewFile(); wz_logs::util::File::CreatDirectory(wz_logs::util::File::Path(pathname)); //2.打开文件,以二进制可追加的形式打开,符合文件写入的条件 _ofs.open(pathname,std::ios::binary|std::ios::app); //文件打开才能进行后续写入,因此加个断言 assert(_ofs.is_open()); } //判断当前文件的_cur_gap是否是当前时间段,若不是,则要切换文件 void sink(const char *data,size_t len) { time_t cur=wz_logs::util::Time::GetTime(); if((cur%_gap_size)!=_cur_gap) { _ofs.close();//关闭原来打开的文件 std::string pathname=CreatNewFile(); _ofs.open(pathname,std::ios::binary|std::ios::app); assert(_ofs.is_open()); } _ofs.write(data,len); assert(_ofs.good()); } private: //进行大小判断,超过指定大小,创建新文件 std::string CreatNewFile() { //获取系统时间,以时间来构造文件拓展名 time_t t=wz_logs::util::Time::GetTime(); struct tm lt; localtime_r(&t,<); std::stringstream filename; filename<<_basename.c_str(); 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; }; //RollByTimeSink测试 int main() { std::unique_ptr<wz_logs::LoggerBuilder> builder(new wz_logs::GlobalLoggerBuilder());//创建管理全局建造者对象的智能指针 builder->buildLoggerName("sync_logger"); builder->buildLoggerLevel(wz_logs::LogLevel::value::WARN); builder->buildFormatter("[%c][%f:%l]%m%n"); builder->buildLoggerType(wz_logs::LoggerType::LOGGER_SYNC); builder->buildSink<RollByTimeSink>("./logfile/rool-sync-by-size",TIMEGAP::GAP_SEC); wz_logs::Logger::ptr logger=builder->build(); time_t old=wz_logs::util::Time::GetTime(); while(wz_logs::util::Time::GetTime()<old+5) { logger->fatal("这是一条测试日志"); usleep(1000); } return 0; }
测试结果:
七、项目性能测试工具的设计
在我们完成一个项目之后,还有一步重要的步骤是不能忽略的,那就是进行性能测试,项目作者需要设计出项目的性能测试工具,同时还要说明当前测试环境下的测试结果,也就是整个测试模块需要包含测试三要素:测试环境、测试方法、测试结果,接下来我会基于我本人的环境依次对本项目进行测试方法的提供以及测试结果的分析。
1.测试环境说明
CPU:Intel(R) Xeon(R) Platinum 8269CY CPU @ 2.50GHz
RAM:2GB
ROM:50GB
OS:CentOS7.6
2.测试方法编写
我们要测试的内容是分别在同步日志器和异步日志器下,单线程写日志的性能和多线程写日志的性能,即我们的测试工具要求可以控制写日志的数量以及写日志所用线程的总数量,实现思想如下:
1.封装一个接口,传入日志器名称,日志数量、线程数量、单条日志消息的大小
在接口内:
创建一个(批)线程,分别负责一部分日志的输出;
在输出之前开始计时,输出完毕结束计时,所耗时间=结束时间-起始时间;
每秒日志输出量=日志数量/所耗时间;
每秒输出大小=日志数量*单条日志大小/所耗时间;
需要注意:在测试异步输入时,我们启动非安全模式,纯内存写入(不考虑实际落地时间)。
测试工具的实现:bench.cc
#include"../logs/wz_log.hpp" #include<vector> #include<chrono> #include<thread> void bench(const std::string &loggername,size_t thr_num,size_t msg_num,size_t msg_len) { //获取日志器 wz_logs::Logger::ptr logger=wz_logs::getLogger(loggername); if(logger.get()==nullptr) { return; } std::cout<<"测试日志:"<<msg_num<<"条,总大小:"<<(msg_num*msg_len)/1024<<"KB\n"; //2.组织指定长度的日志消息 std::string msg(msg_len-1,'w');//-1是想要在每条日志消息最后填一个换行 //3.创建指定数量的线程 std::vector<std::thread> threads; std::vector<double> cost_arry(thr_num);//用来记录每条线程处理日志消息的所用时间 size_t msg_per_thr=msg_num/thr_num;//每个线程需要输出的日志数量=日志总量/线程总量,这里不准确,存在不能整除,这里只为观察现象,因此不作为重点处理, for(int i=0;i<thr_num;i++) { //emplace_back是vector提供的操作,功能是在vector已有的空间基础上直接构造并尾插 threads.emplace_back([&,i](){ //线程函数内部开始计时 auto start =std::chrono::high_resolution_clock::now(); //开始循环写日志 for(int j=0;j<msg_per_thr;j++){ logger->fatal("%s",msg.c_str()); } //线程内部结束计时 auto end=std::chrono::high_resolution_clock::now(); std::chrono::duration<double> cost=end-start; cost_arry[i]=cost.count(); std::cout<<"线程"<<i<<":"<<"\t输出数量"<<msg_per_thr<<"耗时:"<<cost.count()<<"s\n"; }); } for(int i=0;i<thr_num;i++) { threads[i].join(); } //4.计算总耗时:在多线程中,每个线程都会耗费时间,但是线程是并发处理的,因此耗时最高的那个就是总时间 double max_cost=cost_arry[0]; for(int i=0;i<thr_num;i++) { max_cost=max_cost<cost_arry[i]?cost_arry[i]:max_cost; } size_t msg_per_sec=msg_num/max_cost;//每秒处理的日志消息数量 size_t size_per_sec=(msg_num*msg_len)/(max_cost*1024);//每秒处理的日志总大小 //打印测试结果 std::cout<<"每秒输出日志数量:"<<msg_per_sec<<"条\n"; std::cout<<"每秒输出日志大小:"<<size_per_sec<<"KB\n"; }
测试代码:
void sync_bench() { std::unique_ptr<wz_logs::LoggerBuilder> builder(new wz_logs::GlobalLoggerBuilder());//创建管理全局建造者对象的智能指针 builder->buildLoggerName("sync_logger"); builder->buildLoggerLevel(wz_logs::LogLevel::value::DEBUG); builder->buildFormatter("%m%n"); builder->buildLoggerType(wz_logs::LoggerType::LOGGER_SYNC); builder->buildSink<wz_logs::FileSink>("./logfile/test.log"); builder->build(); bench("sync_logger",1,1000000,100); //bench("sync_logger",3,1000000,100); } void async_bench() { std::unique_ptr<wz_logs::LoggerBuilder> builder(new wz_logs::GlobalLoggerBuilder());//创建管理全局建造者对象的智能指针 builder->buildLoggerName("sync_logger"); builder->buildLoggerLevel(wz_logs::LogLevel::value::DEBUG); builder->buildFormatter("%m%n"); builder->buildLoggerType(wz_logs::LoggerType::LOGGER_ASYNC); builder->buildSink<wz_logs::FileSink>("./logfile/test.log"); builder->build(); bench("async_logger",1,1000000,100); //bench("async_logger",3,1000000,100); } int main() { sync_bench(); //async_bench(); return 0; }
测试结果:
同步日志器下单线程测试结果:bench("sync_logger",1,1000000,100);
同步日志器下多线程测试结果:bench("sync_logger",3,1000000,100);
异步日志器下单线程测试结果:bench("async_logger",1,1000000,100);
异步日志器的实现有些问题,还在排查中,后续会更新。。。
异步日志器下多线程测试结果:bench("async_logger",3,1000000,100);
异步日志器的实现有些问题,还在排查中,后续会更新。。。
到此,一个支持多落地方向,且支持同步日志器和异步日志器的日志系统就完成了,异步日志器还存在一些问题 ,后续会排查并更正。