住房和城乡建设部网站进不去,html可视化编辑器,阿里云服务器建设网站选择那个镜像,网页传奇怎么赚钱本项目涉及的到所有源码见以下链接#xff1a;
https://gitee.com/ace-zhe/wz_log
一、项目简介
1.日志的概念#xff08;白话版#xff09; 日志类似于日记#xff0c;通常是指对完成某件事情的过程中状态等的记录#xff0c;而计算机中的日志是指日志数据#xff0c…本项目涉及的到所有源码见以下链接
https://gitee.com/ace-zhe/wz_log
一、项目简介
1.日志的概念白话版 日志类似于日记通常是指对完成某件事情的过程中状态等的记录而计算机中的日志是指日志数据是有价值的信息宝库各种操作系统、应用程序、设备和安全产品的日志数据能够帮助你提前发现和避开灾难找到安全事件的根本原因。 本项目涉及到的日志具体是指程序运行过程中所记录的程序运行状态信息。 2.日志系统功能概述 记录程序运行状态信息以便程序员能随时根据状态信息对系统运行状态进行分析能够让用户在运行测试程序时非常简便的进行日志的输出及控制。 3.日志系统具体功能 支持多级别日志消息支持多线程并发写入支持同步日志和异步日志支持写入日志到控制台、文件及滚动文件中支持扩展不同的日志落地目标地 4.日志系统的必要性 生产环境的产品为了保证其稳定性及安全性不允许开发人员附加调试器去排查问题可以借助日志系统来打印一些日志帮助开发人员解决问题。上线客户端的产品出现bug无法复现并解决时可以借助日志系统打印日志并上传到服务器帮助开发人员进行分析。对于一些高频操作定时器、心跳包在少量调试次数下可能无法触发我们想要的行为通过断点的暂停方式我们不得不重复操作几十次、上百次甚至更多导致排查问题的效率低下可以借助打印日志的方式排查。在分布式、多线程、多进程的代码中出现bug比较难以定位可以借助日志系统打印日志帮助定位bug。帮助首次接触项目代码的新开发人员理解代码的运行过程。 5.开发环境 CentOS 7vscode/vimg/gdbMakefile 6.核心技术 基于封装、继承、多态的面向对象的类层次设计设计模式单例、工厂、代理、建造者等生产者消费者模型多线程应用双缓冲区主要针对异步日志C11相关多线程、auto、智能指针、右值引用等 二、日志系统的技术实现
日志系统的技术实现主要包括三种类型 利用printf/std::cout等输出函数将日志信息打印到控制台【实际项目中不用】对于大型商业化项目为方便排查问题我们一般会将日志输出到文件或者是数据库系统方便查询分析主要分为同步日志方式和异步日志方式。 1.同步写日志 同步写日志是指输出日志时必须等待日志输出语句执行完毕后才能执行后面的业务逻辑语句日志输出语句与程序业务逻辑语句是在同一线程运行这种情况在高并发场景下随着日志数量的不断增加日志系统容易产生瓶颈一方面大量的打印陷入等量write系统调用有一定开销另一方面使得打印日志的进程附带了大量同步的磁盘IO影响程序性能其结构图如下 2.异步写日志 基于同步写日志的缺陷出现了异步写日志异步写日志是指在进行 日志输出是日志输出语句与业务逻辑语句并不在同一线程中运行而是有专门的线程用于日志输出操作。业务线程只需要将日志放到一个内存缓冲器中不用等待即可继续执行后续的业务逻辑而日志的落地操作交给单独的日志线程去完成这也可以看做一个生产-消费模型这样做的好处是即使日志没有真的完成传输也不会影响程序的主业务可以提高程序的性能其结构图如下 三、项目前置知识补充
1.不同风格不定参函数的用法 不定参宏函数 #includestdio.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风格的不定参函数 #includestdio.h
#includestdarg.h
#includestdlib.h//按顺序打印输入的指定数量的整数
void PrintNum(int count,...)
{va_list ap;va_start(ap,count);int i0;for(i0;icount;i){int numva_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 retvasprintf(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风格的不定参函数的实现测试
#includeiostream
void xprintf()
{std::coutstd::endl;
}
//利用C风格设计一个不定参的打印函数
templatetypename T,typename ...Args
void xprintf(const T v,Args ... args)
{std::coutv;if((sizeof ...(args))0){xprintf(std::forwardArgs(args)...);}else{xprintf();}
}
int main()
{xprintf(wz,666,7.8);return 0;
} 测试结果: 2.设计模式
概念 设计模式是前辈们对代码开发经验的总结是解决特定问题的一系列套路它不是语法规定而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。 设计代码应该遵循的六大原则 单一职责原则告诉我们实现类要职责单一 里氏替换原则告诉我们不要破坏继承体系 依赖倒置原则告诉我们要面向接口编程 接口隔离原则告诉我们在设计接口的时候要精简单一 迪米特法则告诉我们要降低耦合 开闭原则是总纲告诉我们要扩展开放关闭修改 以下几种设计者模式一定程度上应用了这些原则因此我们在实际工程项目中应视情况选择合适的设计模式来编写代码下面就介绍几种常见的。
单例模式
概念 一个类只能创建一个对象即单例模式该设计模式可以保证系统中该类只有一个实例并提供 一个访问它的全局访问点该实例被所有程序模块共享比如在某个服务器程序中该服务器的配置信息存放在一个文件中这些配置数据由一个单例对象统一读取然后服务程序进程中的其它对象再通过这个单例对象获取这些配置信息这种方式简化了复杂环境下额配置管理。 单例模式分两种实现模式饿汉模式和懒汉模式 饿汉模式 程序启动时就会创建一个唯一的实例对象。应为单例对象已经确定所以比较适合用于多线程环境中多线程获取单例对象不需要加锁可以有效的避免资源竞争提高性能。 懒汉模式 第一次要使用单例对象的时候再创建实例对象。如果单例对象构造特别耗时或者浪费资源加载插件、加载网络资源等可以选择懒汉模式在第一次使用时才创建对象。 这里我们将会实现一种更加简便的单例模式采用的是静态局部变量的方式来创建对象但要注意的是C11之后静态变量才能在满足线程安全的前提下唯一的被构造和析构。 饿汉模式实现及测试 #includeiostream
//饿汉模式将对象声明为静态私有成员变量
//程序启动就会创建出单例对象该对象的构造函数、析构函数全部私有化删除拷贝构造函数
//保证了整个程序实现过程中只会存在一个单例对象
//可以通过公共的接口去获取该对象和该对象的数据
//是一种以空间换时间的举动开始就创建好在后续使用时就不用耗费时间去创建了
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::coutSingleton::GetInstence().GetData()std::endl;return 0;
} 测试结果 懒汉模式实现及测试 #includeiostream
//懒汉模式懒加载其实是延迟加载的思想
//一个类在用的时候再实例化在初始化构造较为复杂的情况下
//使用懒汉思想就可以避免在不使用该类的情况下浪费资源去构造对象
#includeiostream
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::coutSingleton::GetInstence().GetData()std::endl;return 0;
} 测试结果 工厂模式
概念 工厂模式是一种创建型设计模式它提供了一种创建对象的最佳方式。在工厂模式中我们创建对象时不会对上层暴露创建逻辑而是通过使用一个共同结构来指向新创建的对象以此实现创建-使用的分离。 工厂模式分三种实现模式简单工厂模式、工厂方法模式和抽象工厂模式 简单工厂模式 简单工厂模式实现由一个工厂对象通过类型决定创建出来指定产品类的实例。假设有个工厂能生产水果当客户需要产品时明确告知工厂生产哪类水果工厂需要接收用户提供的类别信息当新增产品的时候工厂内部去添加新产品的生产方式。 工厂方法模式 在简单工厂模式下新增多个工厂每个产品对应一个工厂假设现在有A、B两种产品则设置两个工厂工厂A负责生产产品AB负责生产产品B用户只知道产品的工厂名而不知道具体产品的信息工厂不需要再接收客户的产品类别而只负责生产产品。 抽象工厂模式 工厂方法模式通过引入工厂等级结构解决了简单工厂模式中工厂类职责太重的问题但由于工厂方法模式中的每一个工厂只生产一类产品可能会导致系统中存在大量的工厂类势必会增加系统的开销。此时我们可以考虑将一些先关的产品组成一个产品组位于不同产品等级结构中功能相关联的产品组成家族由同一个工厂来统一生产这就是抽象工厂模式的基本方式。 简单工厂模式实现及测试 //简单工厂模式实现测试
#includeiostream
#includememory
//简单工厂模式通过参数控制可以生产任意产品
//思想简单粗暴直观使用一个工厂生产同一等级结构下的任意产品
//存在的问题1.所有东西都在一个工厂生产产品太多导致代码量庞大
// 2.没有很好的遵循开闭原则新增产品就必须修改工厂方法
class Fruit
{
public:
virtual void name()0;
};
class Apple:public Fruit
{
public:
void name() override
{std::coutIm Applestd::endl;
}
};
class Banana:public Fruit
{
public:
void name() override
{std::coutIm Bananastd::endl;
}
};
class FruitFactory
{
public:static std::shared_ptrFruit produce(const std::string name){if(nameApple){return std::make_sharedApple();}else{return std::make_sharedBanana();}}};int main()
{std::shared_ptrFruit fruitFruitFactory::produce(Apple);fruit-name();fruitFruitFactory::produce(Banana);fruit-name();return 0;
} 测试结果 工厂方法模式实现及测试 //工厂方法模式实现测试
#includeiostream
#includememory
//工厂方法模式定义一个创建对象的窗口由子类决定创建哪种对象
//使用多个工厂分别生产指定的固定产品
//好处是减轻了工厂类的负担将指定产品交规指定工厂来进行生产
//同时很好地遵循了开闭原则增添新的产品只需要新增产品工厂即可不需要修改原来的工厂类
//存在的问题是对于某种可以形成一组产品组的情况处理比较复杂需要创建大量工厂类
class Fruit
{
public:
virtual void name()0;
};
class Apple:public Fruit
{
public:
void name() override
{std::coutIm Applestd::endl;
}
};
class Banana:public Fruit
{
public:
void name() override
{std::coutIm Bananastd::endl;
}
};
class FruitFactory
{
public:virtual std::shared_ptrFruit produce()0;
};
class AppleFactory:public FruitFactory
{
public:std::shared_ptrFruit produce() override{return std::make_sharedApple();}
};
class BananaFactory:public FruitFactory
{
public:std::shared_ptrFruit produce() override{return std::make_sharedBanana();}
};
int main()
{std::shared_ptrFruitFactory ff(new AppleFactory());std::shared_ptrFruit fruitff-produce();fruit-name();ff.reset(new BananaFactory());fruitff-produce();fruit-name();return 0;
} 测试结果 抽象工厂模式实现及测试 //抽象工厂模式实现测试
#includeiostream
#includememory
//抽象工厂是围绕一个超级工厂去创建其它工厂每个生成的工厂按照工厂模式提供对象
//将工厂分成了抽象的两层抽象工厂和具体子工厂类在工厂子类中生产不同类型的子产品
//抽象工厂模式适用于生产多个工厂系列产品衍生的设计模式增加新的产品等级结构复杂
//需要对原有系统进行较大的修改甚至需要修改抽象层代码因此也违背了“开闭原则”
class Fruit
{
public:
virtual void name()0;
};
class Apple:public Fruit
{
public:
void name() override
{std::coutIm Applestd::endl;
}
};
class Banana:public Fruit
{
public:
void name() override
{std::coutIm Bananastd::endl;
}
};class Ball
{
public:
virtual void name()0;
};
class BasketBall:public Ball
{
public:
void name() override
{std::coutIm BasketBallstd::endl;
}
};
class FootBall:public Ball
{
public:
void name() override
{std::coutIm FootBallstd::endl;
}
};class Factory
{
public:
virtual std::shared_ptrFruit GetFruit(const std::string name)0;
virtual std::shared_ptrBall GetBall(const std::string name)0;
};class FruitFactory :public Factory
{
public:std::shared_ptrBall GetBall(const std::string name){return std::shared_ptrBall();}std::shared_ptrFruit GetFruit(const std::string name){if(nameApple){return std::make_sharedApple();}else{return std::make_sharedBanana();}}
};class BallFactory :public Factory
{
public:std::shared_ptrFruit GetFruit(const std::string name){return std::shared_ptrFruit();}std::shared_ptrBall GetBall(const std::string name){if(nameBasketBall){return std::make_sharedBasketBall();}else{return std::make_sharedFootBall();}}
};class FactoryProducer
{public:static std::shared_ptrFactory produce(const std::string name){if(nameFruit){return std::make_sharedFruitFactory();}else{return std::make_sharedBallFactory();}}
};
int main()
{std::shared_ptrFactory factory FactoryProducer::produce(Fruit);std::shared_ptrFruit fruitfactory-GetFruit(Apple);fruit-name();fruitfactory-GetFruit(Banana);fruit-name();std::shared_ptrFactory ff FactoryProducer::produce(Ball);std::shared_ptrBall ballff-GetBall(BasketBall);ball-name();ballff-GetBall(FootBall);ball-name();return 0;
} 测试结果 建造者模式
概念 建造者模式是一种创建型设计模式使用多个简单的对象一步一步构建成一个复杂的对象能够将一个复杂的对象的构建与它的表示分离提供一种创建对象的最佳方式。主要用于解决对象过于复杂的问题。 建造者模式主要基于五个核心类的实现 抽象产品类 具体产品类一个具体的产品类 抽象Builder类:创建一个产品对象所需要各个部件的抽象接口 具体产品的Builder类实现抽象接口构建各个部件 指挥者Director类统一组建过程提供给调用者使用通过指挥者来获取产品 下面以生产建造一台苹果笔记本电脑为例来理解建造者模式 建造者模式实现及测试 //以建造模式构建一台苹果笔记本电脑
//实现测试
#includeiostream
#includememory
//产品抽象类电脑类
class Computer
{
public:Computer() {}void setBoard(const std::string board){_boardboard;}void setDisplay(const std::string display){_displaydisplay;}void showParamaters(){std::string paramComputer Paramaters:\n;param\tBoard: _board\n;param\tDisplay: _display\n;param\tOs: _os\n;std::coutparamstd::endl;}virtual void setOs()0;protected:std::string _board;//主板std::string _display;//显示器std::string _os;//操作系统
};//具体产品类苹果笔记本电脑类
class MacBook:public Computer
{public:void setOs() override {_osMac 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_ptrComputer 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_ptrComputer build(){return _computer;}private:std::shared_ptrComputer _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_ptrBuilder _builder;
};int main()
{Builder * builder new MacBookBuilder();std::unique_ptrDirector director(new Director(builder));director-construct(惠普主板,华硕显示器);std::shared_ptrComputer computerbuilder-build();computer-showParamaters();return 0;
} 测试结果 代理模式
概念 代理模式是指代理控制对其它对象的访问也就是代理对象控制对原对象的引用。在某种情况下一个对象不适合或者不能直接被引用访问而代理对象可以在客户端和目标对象之间起到中介的作用。 代理模式的结构包括一个真正的你要访问的对象目标类、一个是代理对象。目标对象与代理对象实现同一个接口先访问代理类再通过代理类访问目标对象。 代理模式分为静态代理、动态代理 静态代理 在编译时就已经确定好了代理类和被代理类的关系。也就是说在编译时就已经确定了代理类要代理的是哪个被代理类。 动态代理 在运行时才动态生成代理类并将其与被代理类绑定。这意味着在运行时才确定代理类要代理的是哪个被代理类。 下面只实现静态代理以租房为例租客租房中间经过房屋中介向房东租房。 静态代理模式实现及测试 //代理模式实现测试
//以租房子为例房东构建被代理类中介构建代理类
//租房子的时候直接找中介
#includeiostream
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__#includeiostream
#includectime
#includesys/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下还可以用accesspathname,F_OK,存在返回0不存在返回1struct stat st;if(stat(pathname.c_str(),st)0){return false;}return true;}//获取文件所在目录[获取当前指定路径文件的父目录]static std::string Path(const std::string pathname){size_t pospathname.find_last_of(/\\);if(posstd::string::npos)return .;return pathname.substr(0,pos1);}//创建目录有就跳过没有就创建static void CreatDirectory(const std::string pathname){//一定要记得初始化size_t pos0,idx0;while(idxpathname.size()){//从头开始找目录分割标识符pospathname.find_first_of(/\\,idx);//如果没找到就创建当前路径if(posstd::string::npos){mkdir(pathname.c_str(),0777);break;}//找到了就依次看父级目录是否存在如果存在就直接跳过本次不存在就顺便创建std::string parent_dirpathname.substr(0,pos1);if(Exist(parent_dir)true){idxpos1;continue;}mkdir(parent_dir.c_str(),0777);idxpos1;}}};}
}#endif 测试代码:test.cpp #include util.hppint main()
{std::coutwz_logs::util::Time::GetTime()std::endl;std::string pathname./abc/def/ksh/wz.txt;wz_logs::util::File::CreatDirectory(pathname);std::coutwz_logs::util::File::Exist(./abc/def)std::endl;std::coutwz_logs::util::File::Exist(./Abc/def)std::endl;wz_logs::util::File::CreatDirectory(./abc/def/llp);std::coutwz_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__#includeiostream
namespace wz_logs
{class LogLevel{public:enum class value//enum class 是C11之后{UNKNOW0,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__#includeiostream
#includestring
#includethread
#includelevel.hppnamespace wz_logs
{struct LogMsg{time_t _ctime;//日志产生的时间戳LogLevel::value _level;//日志等级size_t _line;//行号std::thread::id _tid;//线程idstd::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__
#includeiostream
#includecassert
#includevector
#includestring
#includememory
#include sstream
#includeutility
namespace wz_logs
{//首先实现一个格式化子项类//实际上用的是多态的思想ptrx相当于一个父类指针当调用重写过的format函数//就可以实现不同格式化子项对象调用相应的format函数实现对应内容的提取及格式化//1.抽象格式化子项基类class FormatItem{public://定义一个指向基类的智能指针using ptr std::shared_ptrFormatItem;virtual void format(std::ostream out,LogMsg msg)0;};//派生格式化子项子类--消息等级时间文件名。行号...class MsgFormatItem:public FormatItem{public:void format(std::ostream out,LogMsg msg) override{outmsg._payload;}};class LevelFormatItem:public FormatItem{public:void format(std::ostream out,LogMsg msg) override{outLogLevel::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);outtmp;}private:std::string _time_fmt;//%H:%M:%S};class FileFormatItem:public FormatItem{public:void format(std::ostream out,LogMsg msg) override{outmsg._file;}};class LineFormatItem:public FormatItem{public:void format(std::ostream out,LogMsg msg) override{outmsg._line;}};class ThreadFormatItem:public FormatItem{public:void format(std::ostream out,LogMsg msg) override{outmsg._tid;}};class LoggerFormatItem:public FormatItem{public:void format(std::ostream out,LogMsg msg) override{outmsg._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::vectorstd::pairstd::string,std::string fmt_order;size_t pos0;std::string key,val;while(pos_pattern.size()){//如果不是%表示是普通字符if(_pattern[pos]!%){val.push_back(_pattern[pos]);continue;}//走到这里说明就遇到%了下一个是不是格式化字符取决于下一个是不是%if(pos1_pattern.size()_pattern[pos1]%){val.push_back(%);pos2;continue;}//原始字符串处理完毕可以添加到fmt_order数组中了if(val.empty()false){fmt_order.push_back(std::make_pair(,val));val.clear();}//走到这里说明此时pos指向一个百分号且下一个是格式化字符pos1;if(pos_pattern.size()){std::cout%之后没有格式化字符...std::endl;return false;}key_pattern[pos];pos1;if(pos_pattern.size()_pattern[pos]{){pos1;while(pos_pattern.size()_pattern[pos]!}){val.push_back(_pattern[pos]);}//如果走到了末尾还没遇到}则说明出错了if(pos_pattern.size()){std::cout子规则{}匹配错误...std::endl;return false;}pos1;}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(keyd) return std::make_sharedTimeFormatItem(val);if(keyt) return std::make_sharedThreadFormatItem();if(keyc) return std::make_sharedLoggerFormatItem();if(keyf) return std::make_sharedFileFormatItem();if(keyl) return std::make_sharedLineFormatItem();if(keyp) return std::make_sharedLevelFormatItem();if(keyT) return std::make_sharedTabFormatItem();if(keyn) return std::make_sharedNlineFormatItem();if(keym) return std::make_sharedMsgFormatItem();return std::make_sharedOtherFormatItem(val);}std::string _pattern;std::vectorFormatItem::ptr _items;};
}
#endif 测试代码test.cpp //formatter.hpp功能测试
#include util.hpp
#includemessage.hpp
#includelevel.hpp
#includeformatter.hpp
int main()
{//注意分开测试每组情况一起测会出问题//普通测试wz_logs::LogMsg msg(wz_logs::LogLevel::value::INFO,66,main.c,root,测试...);wz_logs::Formatter fmt;std::coutfmt.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::coutfmt1.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::coutfmt2.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::coutfmt3.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::coutfmt4.format(msg4)std::endl;// return 0;
} 测试结果其中顺便完成了对日志等级和日志消息模块的测试 5.落地类模块的设计测试
概念 日志落地类主要负责落地日志消息到目的地即将格式化完成后的日志消息格式化字符串输出到指定的位置扩展支持同时将日志落地到不同的位置用简单工厂模式来实现具体的我们要实现的落地方向有三个标准输出指定文件滚动文件。 实现思想 1.抽象出落地模块类 2.不同落地方向从基类进行派生 3.使用工厂模式进行创建与表示分离 实现 实现代码sink.hpp #ifndef __M_SINK_H__
#define __M_SINK_H__
#includeiostream
#includememory
#includefstream
#includesstream
#includeutil.hpp
#includecassert
namespace wz_logs
{//抽象一个落地基类class LogSink{public:using ptrstd::shared_ptrLogSink;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 pathnameCreatNewFile();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 pathnameCreatNewFile();_ofs.open(pathname,std::ios::binary|std::ios::app);assert(_ofs.is_open());_cur_fsize0;//每次新建一个滚动文件当前大小都要清空}_ofs.write(data,len);assert(_ofs.good());_cur_fsizelen;}private://进行大小判断超过指定大小创建新文件std::string CreatNewFile(){//获取系统时间以时间来构造文件拓展名time_t tutil::Time::GetTime();struct tm lt;localtime_r(t,lt);std::stringstream filename;filenamelt.tm_year1900;filenamelt.tm_mon1;filenamelt.tm_mday;filenamelt.tm_hour;filenamelt.tm_min;filenamelt.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 ...Argsstatic LogSink::ptr creat(Args ...args){return std::make_sharedSinkType(std::forwardArgs(args)...);}};
}#endif 测试代码test.cpp //sink.hpp功能测试
#include util.hpp
#includemessage.hpp
#includelevel.hpp
#includeformatter.hpp
#includesink.hpp
int main()
{//普通测试wz_logs::LogMsg msg(wz_logs::LogLevel::value::INFO,66,main.c,root,测试...);wz_logs::Formatter fmt;std::string strfmt.format(msg);wz_logs::LogSink::ptr stdout_lspwz_logs::SinkFactory::creatwz_logs::StdoutSink();wz_logs::LogSink::ptr file_lspwz_logs::SinkFactory::creatwz_logs::FileSink(./logfile/test.log);wz_logs::LogSink::ptr roll_lspwz_logs::SinkFactory::creatwz_logs::RollBySizeSink(./logfile/roll-,1024*1024);stdout_lsp-sink(str.c_str(),str.size());file_lsp-sink(str.c_str(),str.size());size_t cursize0;size_t count0;while(cursize1024*1024*10){std::string tmpstrstd::to_string(count);roll_lsp-sink(tmp.c_str(),tmp.size());cursizetmp.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_size1; break;case TIMEGAP::GAP_MIN: _gap_size60; break;case TIMEGAP::GAP_HOUR: _gap_size3600; break;case TIMEGAP::GAP_DAY: _gap_size3600*24; break;}_cur_gap_gap_size1?wz_logs::util::Time::GetTime():wz_logs::util::Time::GetTime()%_gap_size;//1.创建文件及文件所在的目录std::string pathnameCreatNewFile(); 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 curwz_logs::util::Time::GetTime();if((cur%_gap_size)!_cur_gap){_ofs.close();//关闭原来打开的文件std::string pathnameCreatNewFile();_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 twz_logs::util::Time::GetTime();struct tm lt;localtime_r(t,lt);std::stringstream filename;filenamelt.tm_year1900;filenamelt.tm_mon1;filenamelt.tm_mday;filenamelt.tm_hour;filenamelt.tm_min;filenamelt.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 strfmt.format(msg);wz_logs::LogSink::ptr rollbytime_lspwz_logs::SinkFactory::creatRollByTimeSink(./logfile/roll-,TIMEGAP::GAP_SEC);time_t oldwz_logs::util::Time::GetTime();while(wz_logs::util::Time::GetTime()old5){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__
#includeutil.hpp
#includelevel.hpp
#includeformatter.hpp
#includesink.hpp
#includemessage.hpp
#includeatomic
#includemutex
#includestdio.h
#includestdarg.h
#includestdlib.hnamespace wz_logs
{class Logger{public:Logger( const std::string logger_name,Formatter::ptr formatter,std::vectorLogSink::ptr sinks,LogLevel::value level):_logger_name(logger_name),_formatter(formatter),_limit_level(level),_sinks(sinks.begin(),sinks.end()) {}//需要向外提供的是一系列对不同等级日志消息输出的方法using ptrstd::shared_ptrLogger;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 retvasprintf(res,fmt,ap);if(ret-1){std::coutvasprintf 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 retvasprintf(res,fmt,ap);if(ret-1){std::coutvasprintf 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 retvasprintf(res,fmt,ap);if(ret-1){std::coutvasprintf 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 retvasprintf(res,fmt,ap);if(ret-1){std::coutvasprintf 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 retvasprintf(res,fmt,ap);if(ret-1){std::coutvasprintf 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::vectorLogSink::ptr _sinks;std::atomicLogLevel::value _limit_level;};class SyncLogger:public Logger{public:SyncLogger( const std::string logger_name,Formatter::ptr formatter,std::vectorLogSink::ptr sinks,LogLevel::value level):Logger(logger_name,formatter,sinks,level) {}void log(const char* data,size_t len){//同步日志器是将日志直接通过落地模块句柄进行日志落地std::unique_lockstd::mutex lock(_mutex);if(_sinks.empty()) return;for(auto sink:_sinks){sink-sink(data,len); }}};
}#endif 测试代码test.cpp //logger.cpp同步日志器测试
#includelogger.hpp
int main()
{std::string logger_namesynclogger;wz_logs::LogLevel::value limitwz_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_lspwz_logs::SinkFactory::creatwz_logs::StdoutSink();wz_logs::LogSink::ptr file_lspwz_logs::SinkFactory::creatwz_logs::FileSink(./logfile/test.log);wz_logs::LogSink::ptr roll_lspwz_logs::SinkFactory::creatwz_logs::RollBySizeSink(./logfile/roll-,1024*1024);std::vectorwz_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 cursize0,count0;while(cursize1024*1024*10){logger-fatal(__FILE__,__LINE__,测试日志-%d,count);cursize20;}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_typetype;}void buildLoggerName(const std::string name){_logger_namename;}void buildLoggerLevel(wz_logs::LogLevel::value level){_limit_levellevel;}void buildFormatter(const std::string pattern){_formatterstd::make_sharedwz_logs::Formatter(pattern);}template typename SinkType,typename...Argsvoid buildSink(Args ...args){wz_logs::LogSink::ptr psinkSinkFactory::creatSinkType(std::forwardArgs(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::vectorwz_logs::LogSink::ptr _sinks;
};//2.派生出具体的建造者类--局部日志器的建造者、全局日志器的建造者
class LocalLoggerBuilder:public LoggerBuilder{public:Logger::ptr build() override{assert(!_logger_name.empty());//日志器名称是使用日志器的唯一标识即必须有if(_formatter.get()nullptr){_formatterstd::make_sharedFormatter();}if(_sinks.empty()){buildSinkStdoutSink();}if(_logger_typeLoggerType::LOGGER_ASYNC){}return std::make_sharedSyncLogger(_logger_name,_formatter,_sinks,_limit_level);}
};看得出来调用接口来实现零件构造明显比上面直接构造方便很多由于我们对各个模块构建的顺序不会影响到最后的落地情况因此这里不需要再生成指挥者类。 测试代码test.cpp //logger.cpp建造者模式下同步日志器测试
#includelogger.hpp
int main()
{std::unique_ptrwz_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-buildSinkwz_logs::FileSink(./logfile/test.log);builder-buildSinkwz_logs::StdoutSink();wz_logs::Logger::ptr loggerbuilder-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 cursize0,count0;while(cursize1024*1024*10){logger-fatal(__FILE__,__LINE__,测试日志-%d,count);cursize20;}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__
#includevector
#includecassert
#includeiostream
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,datalen,_buffer[_writer]);//3.可写入地址向后偏移writemov(len);}//读位置向后偏移void readmov(size_t len){assert(len_buffer.size());_readerlen;}//返回可读位置起始地址const char* begin(){return _buffer[_reader];}//重置缓冲区初始化缓冲区void reset(){_reader0;_writer0;}//返回缓冲区可读取数据长度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());_writerlen;}//扩容void containerReset(const size_t len){//为保证合理性我们采用阈值的方式来划分扩容方式//设定一个阈值没达到阈值之间2倍扩容达到阈值后线性扩容size_t newsize0;if(_buffer.size()THRESHOLD_BUFFER_SIZE){newsize_buffer.size()*2len;//len是因为扩容后可能容量依旧不够即newsizelen}else{newsize_buffer.size()INCREMENT_BUFFER_SIZElen;//日志一般不会存在扩容后空间仍然不够的问题但是也要考虑到异常的处理}_buffer.resize(newsize);}private:std::vectorchar _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 fsizeifs.tellg();//获取当前读写位置相对于起始位置的偏移量ifs.seekg(0,std::ios::beg);//重新跳转至起始位置std::string body;body.resize(fsize);ifs.read(body[0],fsize);if(ifs.good()false){std::coutread error\n;return -1;}ifs.close();std::coutfsizestd::endl;wz_logs::Buffer buffer;for(int i0; ibody.size();i){buffer.push(body[i],1);}std::ofstream ofs(./logfile/tmp.log,std::ios::binary);//测试时一定要保证这个文件是存在的否则运行时找不到会报错size_t bsizebuffer.readAblesize(); for(int i0;ibsize;i){ofs.write(buffer.begin(),1);if(ofs.good()false) {std::coutwrite 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__#includebuffer.hpp
#includethread
#includemutex
#includecondition_variable
#includeatomic
#includefunctional
#includememorynamespace wz_logs
{using Functorstd::functionvoid(Buffer );enum class Looper_Type{ASYNC_SAFE,//安全状态缓冲区满了则阻塞避免资源耗尽的风险用于实践ASYNC_UNSAFE//非安全不考虑资源耗尽问题无限扩容常用于测试};class AsyncLooper{public:using ptrstd::shared_ptrAsyncLooper;AsyncLooper(const Functor cb,Looper_Type loop_typeLooper_Type::ASYNC_SAFE):_stop(false),_thread(std::thread(AsyncLooper::threadEntry,this)),_callBack(cb),_looper_type(loop_type){}~AsyncLooper(){stop();}void stop(){_stoptrue;//将退出标志设置为true_cond_con.notify_all();//唤醒所有的工作线程}void push(const char* data,size_t len){//1.无限扩容是不安全的2.固定大小生产缓冲区满了就阻塞std::unique_lockstd::mutex lock(_mutex);//2.条件变量控制若缓冲区剩余空间大小大于数据长度则可以添加数据if(_looper_typeLooper_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_lockstd::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_typeLooper_Type::ASYNC_SAFE)_cond_pro.notify_all();}//3.被唤醒后对消费缓冲区进行数据处理_callBack(_con_buf);//4.初始化消费缓冲区_con_buf.reset();}}Functor _callBack;//具体对缓冲区数据进行处理的回调函数由异步工作器使用者传入private:Looper_Type _looper_type;std::atomicbool _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建造者模式下异步日志器测试
#includelogger.hpp
int main()
{std::unique_ptrwz_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-buildSinkwz_logs::FileSink(./logfile/async.log);builder-buildSinkwz_logs::StdoutSink();wz_logs::Logger::ptr loggerbuilder-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 cursize0,count0;while(cursize1024*1024*10){logger-fatal(__FILE__,__LINE__,测试日志-%d,count);cursize20;}return 0;
} 测试等将异步日志器写完即完整的日志器模块基本实现再来测试功能。 日志器模块完善--异步日志器的实现 logger.hpp class AsyncLogger:public Logger{public:AsyncLogger( const std::string logger_name,Formatter::ptr formatter,std::vectorLogSink::ptr sinks,LogLevel::value level,Looper_Type looper_type):Logger(logger_name,formatter,sinks,level) ,_looper(std::make_sharedAsyncLooper(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建造者模式下异步日志器测试
#includelogger.hpp
int main()
{std::unique_ptrwz_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-buildSinkwz_logs::FileSink(./logfile/async.log);builder-buildSinkwz_logs::StdoutSink();wz_logs::Logger::ptr loggerbuilder-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 cursize0,count0;while(cursize1024*1024*10){logger-fatal(__FILE__,__LINE__,测试日志-%d,count);cursize20;}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_typetype;}void buildLoggerName(const std::string name){_logger_namename;}void buildLoggerLevel(wz_logs::LogLevel::value level){_limit_levellevel;}//新增接口--设置异步日志器的工作模式一般极限测试性能时调用此接口即可设置日志器工作模式void buildEnableUnSafeAsync(){_looper_typeLooper_Type::ASYNC_UNSAFE;}void buildFormatter(const std::string pattern){_formatterstd::make_sharedwz_logs::Formatter(pattern);}template typename SinkType,typename...Argsvoid buildSink(Args ...args){wz_logs::LogSink::ptr psinkSinkFactory::creatSinkType(std::forwardArgs(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::vectorwz_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){_formatterstd::make_sharedFormatter();}if(_sinks.empty()){buildSinkStdoutSink();}//完善构建异步日志器智能指针对象if(_logger_typeLoggerType::LOGGER_ASYNC){//记得异步日志器还多一个参数【用来设置异步日志器的工作模式】return std::make_sharedAsyncLogger(_logger_name,_formatter,_sinks,_limit_level,_looper_type);}return std::make_sharedSyncLogger(_logger_name,_formatter,_sinks,_limit_level);}
}; 日志器管理器模块
概念 作用对所有创建的日志器统一进行管理。 特性以单例模式来实现日志器管理器这样我们就可以在程序的任意位置获取相同的单例对象获取其中的日志器进行日志输出。 扩展单例管理器创建时默认先创建一个日志器用于进行标准输出打印方便用户使用 综上我们要实现的单例模式管理器应包含以下内容 管理的成员 1.默认日志器 2.管理的日志器数据结构 3.互斥锁 提供的接口 1.添加日志器管理 2.判断是否管理了指定名称的日志器 3.获取指定名称的日志器 4。获取默认日志器 实现 logger.hpp //设置一个单例模式的日志器管理类
class LoggerManager{public:static LoggerManager getInstance(){//C11后的新特性针对静态局部变量编译器实现了线程安全//也就是说在静态局部变量没有构造完成之前其他线程就会进入阻塞static LoggerManager eton;return eton;}void addLogger(Logger::ptr logger){if(hasLogger(logger-name())) return;std::unique_lockstd::mutex lock(_mutex);_loggers.insert(std::make_pair(logger-name(),logger));}bool hasLogger(const std::string name){std::unique_lockstd::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_lockstd::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_ptrwz_logs::LoggerBuilder builder(new wz_logs::LocalLoggerBuilder());builder-buildLoggerName(root);_root_loggerbuilder-build();_loggers.insert(std::make_pair(root,_root_logger));}private:std::mutex _mutex;Logger::ptr _root_logger;std::unordered_mapstd::string,Logger::ptr _loggers;
}; 实现之后结合实际情况思考我们发现当我们用户在创建了一个日志器后想要进行全局的管理时就要把这个日志器添加到我们的单例对象中然而我们发现用户自己添加的过程是比较麻烦的需要动用LoggerManagergetInstence().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){_formatterstd::make_sharedFormatter();}if(_sinks.empty()){buildSinkStdoutSink();}Logger::ptr logger;if(_logger_typeLoggerType::LOGGER_ASYNC){loggerstd::make_sharedAsyncLogger(_logger_name,_formatter,_sinks,_limit_level,_looper_type);}else{loggerstd::make_sharedSyncLogger(_logger_name,_formatter,_sinks,_limit_level);}//构建好的日志器直接在内部添加到单例对象中LoggerManager::getInstance().addLogger(logger);return logger;}
}; 接下来我们就可以对管理器模块以及全局建造者模块来进行统一的测试 测试代码test.cpp //日志器管理类及全局建造者类测试
#includelogger.hppvoid test_log()
{wz_logs::Logger::ptr loggerwz_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 cursize0,count0;while(cursize1024*1024*10){logger-fatal(__FILE__,__LINE__,测试日志-%d,count);cursize20;}
}
int main()
{
//创建管理全局建造者对象的智能指针std::unique_ptrwz_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-buildSinkwz_logs::FileSink(./logfile/test.log);builder-buildSinkwz_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__
#includelogger.hppnamespace 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 //全局接口测试
//提供封装全局接口的头文件之后测试就可以只包含这个头文件符合用户使用习惯
#includewz_log.hppvoid test_log()
{//1.调用封装好的接口来获取日志器//wz_logs::Logger::ptr logger wz_logs::getLogger(sync_logger);wz_logs::Logger::ptr loggerwz_logs::LoggerManager::getInstance().getLogger(sync_logger);logger-debug(%s,测试日志);logger-info(%s,测试日志);logger-warn(%s,测试日志);logger-error(%s,测试日志);logger-fatal(%s,测试日志);size_t count0;while(count500000){logger-fatal(测试日志-%d,count);}//2.不用获取日志器直接用封装好了默认日志器的操作// DEBUG(%s,测试日志);// INFO(%s,测试日志);// WARN(%s,测试日志);// ERROR(%s,测试日志);// FATAL(%s,测试日志);// size_t count0;// while(count500000)// {// FATAL(测试日志-%d,count);// }
}
int main()
{std::unique_ptrwz_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-buildSinkwz_logs::FileSink(./logfile/test.log);builder-buildSinkwz_logs::StdoutSink();builder-build();test_log();return 0;
} 六、项目目录结构梳理 考虑到我们项目的用户层面实用性我们要对整个项目的目录结构进行梳理最终用户只需要拿到一个logs文件里面包含实现各个模块的头文件使用时只包含其中的一个提供全局接口的头文件wz_log.hpp即可使用同时还要有一个存放测试用例example文件里面存放一些重要的测试用例主要用于让用户快速熟悉这套系统的用法另外还要有一个包含细碎知识点测试的practice文件本项目中存放的是一些前置知识的测试代码最后再创建一个extend文件用来保存扩展部分的代码本项目中扩展的是落地方向落地到以时间为划分标准的滚动文件整理好的目录结构如下 1.测试用例模块功能检测 代码./example/test.cpp //全局接口测试
#include../logs/wz_log.hpp
#includeunistd.hvoid 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_ptrwz_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-buildSinkwz_logs::FileSink(./logfile/test.log);builder-buildSinkwz_logs::StdoutSink();builder-buildSinkwz_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
#includeunistd.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_size1; break;case TIMEGAP::GAP_MIN: _gap_size60; break;case TIMEGAP::GAP_HOUR: _gap_size3600; break;case TIMEGAP::GAP_DAY: _gap_size3600*24; break;}_cur_gap_gap_size1?wz_logs::util::Time::GetTime():wz_logs::util::Time::GetTime()%_gap_size;//1.创建文件及文件所在的目录std::string pathnameCreatNewFile(); 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 curwz_logs::util::Time::GetTime();if((cur%_gap_size)!_cur_gap){_ofs.close();//关闭原来打开的文件std::string pathnameCreatNewFile();_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 twz_logs::util::Time::GetTime();struct tm lt;localtime_r(t,lt);std::stringstream filename;filename_basename.c_str();filenamelt.tm_year1900;filenamelt.tm_mon1;filenamelt.tm_mday;filenamelt.tm_hour;filenamelt.tm_min;filenamelt.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_ptrwz_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-buildSinkRollByTimeSink(./logfile/rool-sync-by-size,TIMEGAP::GAP_SEC);wz_logs::Logger::ptr loggerbuilder-build();time_t oldwz_logs::util::Time::GetTime();while(wz_logs::util::Time::GetTime()old5){logger-fatal(这是一条测试日志);usleep(1000);}return 0;
} 测试结果 七、项目性能测试工具的设计 在我们完成一个项目之后还有一步重要的步骤是不能忽略的那就是进行性能测试项目作者需要设计出项目的性能测试工具同时还要说明当前测试环境下的测试结果也就是整个测试模块需要包含测试三要素测试环境、测试方法、测试结果接下来我会基于我本人的环境依次对本项目进行测试方法的提供以及测试结果的分析。 1.测试环境说明 CPUIntel(R) Xeon(R) Platinum 8269CY CPU 2.50GHz RAM2GB ROM50GB OSCentOS7.6 2.测试方法编写 我们要测试的内容是分别在同步日志器和异步日志器下单线程写日志的性能和多线程写日志的性能即我们的测试工具要求可以控制写日志的数量以及写日志所用线程的总数量实现思想如下 1.封装一个接口传入日志器名称日志数量、线程数量、单条日志消息的大小 在接口内 创建一个批线程分别负责一部分日志的输出 在输出之前开始计时输出完毕结束计时所耗时间结束时间-起始时间 每秒日志输出量日志数量/所耗时间 每秒输出大小日志数量*单条日志大小/所耗时间 需要注意在测试异步输入时我们启动非安全模式纯内存写入不考虑实际落地时间。 测试工具的实现bench.cc #include../logs/wz_log.hpp
#includevector
#includechrono
#includethread
void bench(const std::string loggername,size_t thr_num,size_t msg_num,size_t msg_len)
{//获取日志器wz_logs::Logger::ptr loggerwz_logs::getLogger(loggername);if(logger.get()nullptr){return;}std::cout测试日志msg_num条总大小(msg_num*msg_len)/1024KB\n;//2.组织指定长度的日志消息std::string msg(msg_len-1,w);//-1是想要在每条日志消息最后填一个换行//3.创建指定数量的线程std::vectorstd::thread threads;std::vectordouble cost_arry(thr_num);//用来记录每条线程处理日志消息的所用时间size_t msg_per_thrmsg_num/thr_num;//每个线程需要输出的日志数量日志总量/线程总量,这里不准确存在不能整除这里只为观察现象因此不作为重点处理for(int i0;ithr_num;i){//emplace_back是vector提供的操作功能是在vector已有的空间基础上直接构造并尾插threads.emplace_back([,i](){//线程函数内部开始计时auto start std::chrono::high_resolution_clock::now();//开始循环写日志for(int j0;jmsg_per_thr;j){logger-fatal(%s,msg.c_str());}//线程内部结束计时auto endstd::chrono::high_resolution_clock::now();std::chrono::durationdouble costend-start;cost_arry[i]cost.count();std::cout线程i:\t输出数量msg_per_thr耗时:cost.count()s\n;});}for(int i0;ithr_num;i){threads[i].join();}//4.计算总耗时在多线程中每个线程都会耗费时间但是线程是并发处理的因此耗时最高的那个就是总时间double max_costcost_arry[0];for(int i0;ithr_num;i){max_costmax_costcost_arry[i]?cost_arry[i]:max_cost;}size_t msg_per_secmsg_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_secKB\n;
} 测试代码 void sync_bench()
{std::unique_ptrwz_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-buildSinkwz_logs::FileSink(./logfile/test.log);builder-build();bench(sync_logger,1,1000000,100);//bench(sync_logger,3,1000000,100);
}
void async_bench()
{std::unique_ptrwz_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-buildSinkwz_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); 异步日志器的实现有些问题还在排查中后续会更新。。。 到此一个支持多落地方向且支持同步日志器和异步日志器的日志系统就完成了异步日志器还存在一些问题 后续会排查并更正。