怎么组建企业网站,沉默是金粤语谐音歌词,长沙免费模板建站,网商之窗麻将开挂[项目] C基于多设计模式下的同步异步日志系统 文章目录 [项目] C基于多设计模式下的同步异步日志系统日志系统1、项目介绍2、开发环境3、核心技术4、日志系统介绍4.1 日志系统的价值4.2 日志系统技术实现4.2.1 同步写日志4.2.2 异步写日志 5、相关技术知识5.1 不定参…[项目] C基于多设计模式下的同步异步日志系统 文章目录 [项目] C基于多设计模式下的同步异步日志系统日志系统1、项目介绍2、开发环境3、核心技术4、日志系统介绍4.1 日志系统的价值4.2 日志系统技术实现4.2.1 同步写日志4.2.2 异步写日志 5、相关技术知识5.1 不定参函数5.1.1 不定参宏函数 5.2 设计模式5.2.1 六大原则5.2.2 单例模式5.2.3 工厂模式5.2.4 建造者模式5.2.5 代理模式 日志系统框架设计6.1 模块划分6.2 模块关系图 7.代码实现7.1 实用工具类7.2 日志等级类7.3 日志消息类7.4 日志输出格式化类7.5 日志落地类工厂模式7.6 日志器类建造者模式7.7 异步日志双缓冲区类7.8 异步工作器类7.9 单例日志器管理类7.10 日志宏全局接口设计 8.功能测试9.性能测试10.扩展 日志系统
日志程序运行过程中记录的程序运行状态信息
作用记录了程序运行状态信息便于程序员能够随时根据状态信息对系统程序的运行状态进行分析。能够非常简便的进行详细的日志输出以及控制
1、项目介绍
本项目主要实现的是一个日志系统其支持以下功能
支持多级别日志信息支持同步日志信息和异步输出日志支持可靠写入日志到控制台、文件、滚动文件中支持多线程程序并发写日志支持扩展不同的日志落地
2、开发环境
操作系统Ubuntu 20.04编辑器vscode vim编译器/调试器g/ gdb项目自动化构建工具Makefile
3、核心技术
类层次化设计继承、多态的实际应用C11新特性多线程库、智能指针、右值引用等双缓冲区设计思想生产者消费者模型设计模式单例、工厂、代理、建造者等
4、日志系统介绍
4.1 日志系统的价值
在生产环境中的产品为了保证其稳定性以及安全性是不允许开发人员附加调试器去排查问题的可以借助日志系统来打印一些日志来帮助开发人员解决问题上线客户端的产品出现的Bug无法复现并解决可以借助日志系统打印日志并且上传到服务端帮助开发人员进行分析对于一些高频操作如定时器、心跳包等在少量调试下可能无法触发我们想要的行为通过断点暂停的方式我们需要重复几十次甚至上百次导致排查问题的效率非常低下可以借助打印日志的方式排查问题在分布式、多线程/多进程的代码中出现bug非常难定位可以借助日志系统打印日志帮助定位bug可以帮助刚接触项目不久的开发人员理解代码的运行流程
4.2 日志系统技术实现
日志系统的技术实现主要包括两种类型 利用printf、std::cout等输出函数将日志信息打印到控制台但是对于大型商业化项目为了方便排查问题我们一般会将日志输出到文件或者说数据库方便查询和分析日志主要分为同步日志和异步日志 4.2.1 同步写日志
同步写日志指的是当输出日志时必须等待日志输出语句执行完毕后才能执行后面的业务逻辑日志输出语句与程序的业务逻辑语句将在同一个线程种运行。每调用一次打印日志API就对应一次系统调用write写日志文件
在高并发场景下随着日志的数量越来越多同步日志系统容易产生瓶颈
一方面产生日志的速度大于文件写入操作I/O操作通常比较慢大量的日志同时等待写入就会造成线程阻塞降低系统的整体性能产生日志与I/O操作速度之间的矛盾另一方面大量的日志打印线程需要同时等待进行write系统调用来写入文件会产生资源竞争多个线程需要同时进行写入文件操作文件写入操作不是原子性的就需要对写入操作进行同步同步过程中会涉及到加锁和释放锁具有一定的性能开销
4.2.2 异步写日志
异步日志是指在进行日志输出时日志输出语句与业务逻辑语句并不是在同一个线程中运行而是有专门的线程用于进行日志输出操作业务线程只需要将日志放在放到一个内存缓冲区不需要等待即可继续执行后续业务逻辑作为日志的生产者而日志的落地操作交给单独的日志输出线程完成作为日志的消费者
这样的好处是即使日志没有真正的完成输出也不会影响业务线程以提高程序的性能
一方面业务线程产生日志后只需要将日志简单的放入一个缓冲区中这个过程是非常迅速的因为它只是非常简单的在内存中进行数据存储操作几乎不涉及复杂的I/O或资源竞争另一方面相比文件写入的并发控制缓冲区写入的并发控制要简单且容易得多对业务程序性能的消耗也要小得多
5、相关技术知识
5.1 不定参函数
在学C语言的时候我们就已经接触不定参函数了例如printf就是一个典型的可以根据格式化字符串解析对传上来的数据进行格式化的函数
这种不定参函数在实际的使用中非常多见这里简单的做一下介绍
5.1.1 不定参宏函数
__FILE__ 和 __LINE__ 是C语言的宏函数可以用于获取文件名和代码当前行数我们可以使用printf打印一条包含当前文件信息和行数信息的日志
#include cstdio
int main() {printf([%s : %d] %s - %d\n, __FILE__, __LINE__, zdp, 666); //输出 [test.cpp : 5] zdp - 666return 0;
}但是我们每次打印日志都要写printf,__FILE__,、__LINE__实在是太麻烦了我们可以使用不定参的宏函数对其进行替换
#define LOG(fmt, ...) printf([%s : %d] fmt, __FILE__, __LINE__, ##__VA_ARGS__);解释一下
fmt(format):就是我们的格式化字符串编译器就是以它为依据进行不定参解析的...: 就是我们的不定参数[%s:%d] fmt 因为这两个都是格式化字符串C语言是支持直接连接的__VA_ARGS__也是C语言给我们提供的宏函数用于给我们的fmt传参##__VA_ARGS__ 加了##是为了告诉编译器若我们只想传一个不定参数可以省略前面的fmt参数的传递 ##__VA_ARGS__的意思就是 本来我们只想传递一个不定参数需要这么写 LOG(“%s”, “zdp”); 现在可以省略fmt参数的传递 LOG(“zdp”);就可以了 5.1.2 C风格不定参使用
#include stdarg.h
void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);
void va_copy(va_list dest, va_list src);用一段代码来理解这一系列接口的使用
#include cstdio
#include cstdarg#define LOG(fmt, ...) printf([%s : %d] fmt, __FILE__, __LINE__, ##__VA_ARGS__);void printNum(int count, ...) { // count 不定参数的个数va_list ap; // va_list实际就是一个char*类型的指针va_start(ap, count); // 将char*类型指针指向不定参数的起始位置for (int i 0; i count; i) { int num va_arg(ap, int); // 从ap位置取一个整形大小空间数据拷贝给num并将ap向后移动一个整形大小空间printf(param[%d], %d\n, i, num); }va_end(ap); // 将ap指针置空
}int main() {printNum(2, 666, 222);return 0;
}va_list ap: 就是定义一个char* 类型的指针va_start : 让指针指向不定参数的起始位置第二个参数传的是不定参数的前一个参数因为函数调用过程中是会将实参一个个压入函数栈帧中的所以参数之间都是紧挨着的。我们找到了前一个参数count的地址也就等于找到了不定参数的起始地址va_arg : 用于从不定参数中解析参数第一个参数数据的起始位置第二个参数指定参数类型根据类型我们可以推导出参数的大小从而将参数数据解析出来va_end : 将ap指针置空
这里我们解释传入类型只能是int类型我们如何使用上述接口将不定参数分离的原理那么printf这类函数是如何将不定参数分离的呢这是因为我们在使用printf函数开始传递了format参数其中包含了%s, %d这类的信息printf内部通过对format 参数进行解析就知道了后面的参数依次都是什么类型的然后将类型依次放入va_arg函数就可以将参数全部提取出来了
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);
}vasprintf 函数会帮助提取不定参数并且将其拼接到格式化字符串中并开辟空间将处理好的字符串数据放入空间并将我们传入的指针指向这块空间成功返回打印的字节数失败返回-1
5.1.3 C风格不定参数使用
void xprintf() {std::cout std::endl;
}
/*C风格的不定参数*/
templatetypename T, typename ...Args
void xprintf(const T v, Args... args) {std::cout v;if ((sizeof...(args)) 0) {xprintf(std::forwardArgs(args)...);} else {xprintf();}
}int main() {printNum(2, 666, 222);myprintf(%s - %d\n, clx, 666);xprintf(hello);xprintf(hello, world);xprintf(hello, I, am , clx);return 0;
}5.2 设计模式
项目中用到了很多种设计模式设计模式是前辈们对代码开发经验的总结是解决特定问题的一系列套路。它是一套提高代码复用性可维护性可读性稳健性以及安全性的解决方案
5.2.1 六大原则
单一责任原则Single Responsibility Principle)
类的职责应该单一一个方法只做一件事职责划分清晰每次改动到最小单位的方法或类使用建议两个完全不一样的功能不应该放在一个类中一个类中应该是一组相关性很高的函数、数据的封装用例网络聊天类❌应该分割成网络通信类 聊天类
开闭原则Open Closed Principle
对扩展开放对修改封闭只添加新功能不修改原有内容使用建议对软件实体的改动最好用扩展而非修改的方式用例超时卖货商品价格—不是修改商品原来的价格而是新增促销的价格
里氏替换原则Liskov Substitution Principle
凡事父类能够出现的地方子类就可以出现而且替换为子类也不会出现任何的错误或者异常在继承类时务必重写父类中的所有方法尤其注意父类的protected方法子类尽量不要暴露自己的public方法供外界调用使用建议子类无比完全实现父类的方法还子类可以有自己的个性覆盖或者实现父类的方法时输入的参数可以被放大输出也可以缩小用例跑步运动员类会跑步 子类长跑运动员-会跑步且擅长长跑子类短跑运动员会跑步且擅长短跑
依赖倒置原则Dependence Inversion Principle
高层模块不应该依赖底层模块两者都应该依赖其抽象不可分隔的原子逻辑就是低层的模式原子逻辑组装成的就是高层模块模块间依赖通过抽象接口发生具体类之间不能直接依赖使用建议每一个类都尽量有抽象类任何类都不应该从具体类派生。尽量不要重写基类的方法。结合里氏替换原则使用用例奔驰车司机 – 只能开奔驰司机类给什么车开什么车 开车的人 司机 – 依赖抽象
迪米特法则Law of Demeter 最少知道法则
尽量减少对象之间的交互从而减少类之间的耦合。一个对象应该对其他对象有最少的了解对类的低耦合提出了明确的要求 只喝直接的朋友交流朋友间也是有剧烈的。自己的就是自己的如果一个方法放在本类中既不增加类间关系也不对本类造成负面影响那就放置在本类中用例老师让班长点名老师给班长名单班长点名勾选返回结果。老师只和班长交互同学们只和班长交互
接口隔离原则
客户端不应该依赖它不需要的接口类间的依赖关系应该建立在最小的接口上使用建议接口设计尽量精简单一但是不要对外暴露没有啥意义的接口用例修改密码不应该提供用户信息接口而是单一使用修改密码接口
从整体上理解六大设计原则可以简要概括为一句话用抽象构建框架用实现扩展细节具体到每一条设计原则则对应一条注意事项
5.2.2 单例模式
/* 饿汉单例模式 以空间换时间 */
class Singleton{
public:static Singleton getInstance() { return _eton; }int getData() { return _data; }
private:Singleton(int data 99) : _data(data){}~Singleton(){};Singleton(const Singleton) delete;Singleton operator(const Singleton) delete;private:static Singleton _eton;int _data;
};
Singleton Singleton::_eton;int main() {std::cout Singleton::getInstance().getData() std::endl;return 0;
}/* 懒汉单例模式 懒加载 -- 延时加载思想 -- 一个对象用的时候再实例化 */
// 这里介绍Effective C 作者提出的一种更加优雅简便的单例模式 Meyers Singleton int C
// C11后是线程安全的class Singleton{
public:static Singleton getInstance() {static Singleton _eton;return _eton;}int getData() { return _data; }
private:Singleton(int data 99) : _data(data){}~Singleton() {};Singleton(const Singleton) delete;Singleton operator(const Singleton) delete;int _data;
};int main() {std::cout Singleton::getInstance().getData() std::endl;return 0;
}
5.2.3 工厂模式
工厂模式是一种创建型的设计模式它提供了一种创建对象的最佳方式。在工厂模式中我们创建对象不会对上层暴露创建逻辑而是通过使用一个共同结构来指向新创建的对象因此实现创建-使用的分离
工厂模式分为
简单工厂模式简单工厂模式实现需要由一个工厂对象通过类型决定创建出来的制定产品类的实例。假设有个工厂可以生产水果当客户需要产品时明确告知工厂生产哪种水果工厂需要接收用户提供的类别信息当新增产品的时候工厂内部取添加新产品的生产方式
class Fruit{
public:virtual void name() 0;
private:
};class Apple : public Fruit{
public:void name() override{std::cout Im a apple std::endl;}
};class Banana : public Fruit{
public:void name() override {std::cout Im a banana std::endl;}
};class FruitFactory {
public:static std::shared_ptrFruit create(const std::string name) {if (name 苹果) {return std::make_sharedApple();} else {return std::make_sharedBanana();}}
};int main() {std::shared_ptrFruit fruit FruitFactory::create(苹果);fruit-name();fruit FruitFactory::create(香蕉);fruit-name();return 0;
}这个模式的结构和管理产品对象的方式非常简单但是它的扩展性非常差当我们需要新增产品的时候就需要去修改工厂类新增一个类型的产品创造逻辑违背了开闭原则
工厂方法模式在简单的工厂模式下新增了多个工厂多个产品每个产品对应一个工厂。假设现在有A、B两种产品则开两个工厂工厂A主要负责生产产品A工厂B主要生产产品B用户只要知道产品的工厂名而不需要知道具体的产品信息工厂不需要接收客户的产品类别只负责生产产品
/* 工厂方法模式 */
class Fruit{
public:virtual void name() 0;
private:
};class Apple : public Fruit{
public:void name() override{std::cout Im a apple std::endl;}
};class Banana : public Fruit{
public:void name() override {std::cout Im a banana std::endl;}
};
class FruitFactory {
public:virtual std::shared_ptrFruit createFruit() 0;
};class AppleFactory : public FruitFactory {
public:virtual std::shared_ptrFruit createFruit() override {return std::make_sharedApple();}
};class BananaFactory : public FruitFactory {
public:virtual std::shared_ptrFruit createFruit() override {return std::make_sharedBanana();}
};int main() {std::shared_ptrFruitFactory ff(new AppleFactory());std::shared_ptrFruit fruit1 ff-createFruit();fruit1-name();ff.reset(new BananaFactory());std::shared_ptrFruit fruit2 ff-createFruit();fruit2-name();return 0;
}工厂方法模式每次增减一个产品时都需要增加一个具体的产品类和工厂类这使得系统中类的个数成倍的增加在一定程度上增加了系统的耦合度
抽象工厂模式工厂方法模式通过引入工厂等级结构解决了简单工厂模式中工厂类职责太重的问题但由于工厂方法模式中每个工厂只生产一类产品可能会导致系统中存在大量的工厂类势必增加系统的开销此时我们可以考虑将一些相关的产品组成一个产品族位于不同产品等级结构中功能相互关联的产品组成的家族由于一个工厂统一生产这就是抽象工厂模式的基本思想
#include memory/* 简单工厂模式 */
class Fruit{
public:virtual void name() 0;
private:
};class Apple : public Fruit{
public:void name() override{std::cout Im a apple std::endl;}
};class Banana : public Fruit{
public:void name() override {std::cout Im a banana std::endl;}
};class Animal {public:virtual void name() 0;
};class Lamp : public Animal {public:virtual void name() override { std::cout Im a Lamp std::endl;}
};class Dog : public Animal {public:virtual void name() override {std::cout Im a dog std::endl;}
};class Factory {public: virtual std::shared_ptrFruit getFruit(const std::string name) 0;virtual std::shared_ptrAnimal getAnimal(const std::string name) 0;
};class FruitFactory : public Factory {public:virtual std::shared_ptrFruit getFruit(const std::string name) override{if (name 苹果) {return std::make_sharedApple();} else {return std::make_sharedBanana();}}virtual std::shared_ptrAnimal getAnimal(const std::string name) override{return std::shared_ptrAnimal();}
};class AnimalFactory : public Factory {public:virtual std::shared_ptrFruit getFruit(const std::string name) override {return std::shared_ptrFruit();}virtual std::shared_ptrAnimal getAnimal(const std::string name) override {if (name 山羊) {return std::make_sharedLamp();} else {return std::make_sharedDog();}}
};class FactoryProducer {public: static std::shared_ptrFactory create(const std::string name) {if (name 水果) {return std::make_sharedFruitFactory();} else {return std::make_sharedAnimalFactory();}}
};int main() {std::shared_ptrFactory ff FactoryProducer::create(水果);std::shared_ptrFruit fruit ff-getFruit(苹果);fruit-name();fruit ff-getFruit(香蕉);fruit-name();ff FactoryProducer::create(动物);std::shared_ptrAnimal animal ff-getAnimal(山羊);animal-name();animal ff-getAnimal(小狗);animal-name();return 0;
}抽象工厂模式适用于生产多个工厂系列产品衍生的设计模式增加新的产品等级结构复杂需要对原有系统进行较大修改甚至需要修改抽象层代码违背了开闭原则
5.2.4 建造者模式
建造者模式是一种创建型的设计模式使用多个简单对象一步一步构建成一个复杂的对象能够将一个复杂的对象的构建与它的表示分离提供一种创建对象的最佳方式。主要用于解决对象的构建过于复杂的问题
建造者模式主要基于四个核心实现抽象产品类具体产品类一个具体的产品对象类抽象Builder类创建一个产品对象所需要的各个零部件的抽象接口具体产品的Builder类实现抽象接口构建各个部件指挥者Director类统一组建过程提供给调用者使用通过指挥者来获取产品
#include iostream
#include string
#include memory/* 通过MacBook的构造理解建造者模式*/class Computer{public:Computer(){};void setBoard(const std::string board) { _board board; }void setDisplay(const std::string display) { _display display; }virtual void setOs() 0;void showParamaters() {std::string param Computer Paramaters: \n;param \tBoard: _board \n;param \tDispaly: _display \n;param \tOs: _os \n;std::cout param std::endl;}protected:std::string _board;std::string _display;std::string _os;
};
class MacBook : public Computer{public:virtual void setOs() override {_os Mac OS x12;}
};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;
};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;
};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 computer builder-build();computer-showParamaters();return 0;
}5.2.5 代理模式
代理模式指的是代理控制对其他对象的访问也就是代理对象控制对原对象的引用。在某些情况下一个对象不适合或者不能直接被引用访问而代理对象可以在客户端和目标对象之间起到中介作用
代理模式的结构包括一个是真正的你要访问的目标对象目标类、一个是代理对象。目标对象与代理对象实现同一个接口先访问代理类再通过代理类访问目标对象。代理模式一般分为静态代理、动态代理
静态代理指的是在编译时就已经确定好了代理类和被代理类的关系。也就是说在编译时已经确定了代理要代理的是哪一个被代理类动态代理指的是在运行时才动态生成代理类并将其与被代理类绑定。这意味着在运行时才能确定代理类要代理的是哪个被代理类
以租房为例租客租房中间经过房屋中介向房东租房使用代理模式实现
#include iostream/* 代理模式 */
class RentHouse {public:virtual void rentHouse() 0;
};class Landlord : public RentHouse {public:void rentHouse() {std::cout 房子租出去了 std::endl;}
};class Intermediary : public RentHouse {public:void rentHouse() {std::cout 发布招租启事 std::endl;std::cout 带人看房 std::endl;_landload.rentHouse();std::cout 负责租后维修 std::endl;}private:Landlord _landload;
};int main() {Intermediary intermediary;intermediary.rentHouse();return 0;
}日志系统框架设计 日志系统的作用 本项目实现的是一个多日志器日志系统主要实现的功能是让程序员能够轻松的将程序运行的日志信息落地到指定的位置且支持同步与异步两种方式的日志落地
日志要写入指定位置标准输出指定文件滚动文件…
日志系统需要支持将日志消息落地到不同位置—多落地方向
日志写入指定位置支持不同的写入方式同步异步
同步业务线程自己负责日志写入流程简单但是可能因为阻塞导致效率降低异步业务线程将日志放入缓冲区内存让其他异步线程负责将日志写入到指定位置业务线程不会阻塞
日志输出以日志器为单位支持多日志器不同的项目组有不同的输出策略日志器的管理
6.1 模块划分
首先我们在来重复一下本日志系统的核心功能:将一条日志消息按照指定格式化和输出等级写入到指定文件 由此根据本项目的核心功能我们可以完成日志系统的各个模块的肢解: 日志消息模块 我们的日志消息不可能就是一个简单的字符串吧根据C面向对象的思想我们需要将用户传递给我们的日志消息包装成一个一个日志对象该日志对象中包含:该日志消息所在的文件、所产生的行号、所发生的时间、那个线程产生的这条日志消息、本条日志消息的等级、以及本条日志消息的主体(也就是用户告诉我们的日志消息)等这些信息基于C的OO思想我们需要将这西基本日志消息打包起来这样更符合我们C程序员对于面向对象的思想的理解、也是我们的程序代码更具有封装性、独立性、健壮性! 格式化模块 我们最终写入到文件中的日志消息都是一个一个的字符串而不是一个个的日志对象因此我们需要一个专门将日志对象格式化成指定字符串的模块;我们只需要给该格式化类在初始化的时候指定好我们想要的格式化方式那么在后面的操作中我们只需要给该格式化类传递一个日志对象就能帮我们转换出一个指定格式的日志字符串 日志等级模块 由于我们的日志系统是需要支持日志输出等级的限制的因此我们需要单独拎出来一个日志等级的模块该模块规定了我们的日志消息具有那些等级 落地模块 现在格式化好的日志消息字符串已经有了那么我们就应该将这些格式化字符串写入到指定文件中去那么指定文件有那些?
根据我们刚开始所说我们支持将日志消息落地到:标准输出、指定文件、滚动文件等这几个方向因此我们在落地模块中针对标准输出我们可以封装一个落地方向、针对指定文件我们又可以封装一个落地方向、针对滚动文件我们又可以封装一个落地方向但是这些落地方向都需要提供一个同样的接口也就是落地接口,用户只要通过该接口就能将格式化日志信息落地到指定的落地方向; 写入方式模块(日志器模块) 上面几个模块中格式化日志字符串准备好了、落地方向也准备好了最终我们要完成的就是将格式化信息“写入”到实际的落地方向中可是怎么写啊?
根据我们目前的情况我们写日志有两种情况:1、同步写2、异步写; 这两种写入方式,也就是两种不同的日志器:同步日志器、异步日志器; 该模块是对于:格式化模块、落地模块、日志消息模块的整合; 最终与用户直接交互的也是该模块!
用户可以通过改模块将日志消息通过指定接口输出到指定文件; 就比如:用户选择了同步日志器模块那么用户只需要通过该模块提供的接口就能将用户想要输出的日志信息通过该日志器以同步写入的方式落地到指定文件异步日志器也是一样的
6.2 模块关系图
几大模块关系图
7.代码实现
7.1 实用工具类
这个类里面编写了我们这个项目中需要经常用的几个函数 实用工具类实现代码 #ifndef __M_UTIL_H__
#define __M_UTIL_H__// 实用工具类的实现:
// 1.获取系统时间
// 2.判断文件是否存在
// 3.获取文件所在路径
// 4.创建目录
//#pragma once
#include iostream
#include ctime
#include sys/stat.hnamespace logsystem
{namespace util{class Date{public:static size_t now(){return (size_t)time(nullptr);}};class File{public:static bool exists(const std::string pathname){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 createDirectory(const std::string pathname){// ./abc/bcd/a.txtsize_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); // 全部给予创建目录的权限}std::string parent_dir pathname.substr(0, pos1);if (exists(parent_dir) true){idx pos 1;continue;}mkdir(parent_dir.c_str(), 0777);idx pos 1;}}};}
}#endifDate类实现 1.获取系统时间 File类实现 2.判断文件是否存在 3.获取文件所在路径 4.创建目录
7.2 日志等级类
设计思路
定义出日志系统所包含的所有日志系统使用枚举类实现
UNKNOW 最低等级日志DEBUG 调试等级日志INFO 提示等级日志WARN 警告等级日志ERROR 错误等级日志FATAL 致命错误等级日志OFF 最高等级可用于禁止所有日志输出
只有输出的日志等级大于日志器默认限制等级才可以进行日志输出规定日志等级可以起到日志过滤的作用
提供一个接口将枚举类型转换成一个对应的字符串,方便我们打印 日志等级类实现代码 /*1.定义枚举类枚举出日志等级2.提供转换接口将枚举转换为对应字符串
*/#ifndef __M_LEVEL_H__
#define __M_LEVEL_H__namespace logsystem
{class LogLevel{public:enum class value{UNKNOW0,DEBUG,INFO,WARN,ERROR,FATAL,OFF};static const char *toString(LogLevel::value level){switch (level){case LogLevel::value::DEBUG: return DEBUG;case LogLevel::value::INFO: return INFO;case LogLevel::value::WARN: return WARN;case LogLevel::value::ERROR: return ERROR;case LogLevel::value::FATAL: return FATAL;case LogLevel::value::OFF: return OFF;}return UNKNOW;}};
}#endif7.3 日志消息类
日志消息类主要是为了封装一条完整的日志内容其中各个字段用于存储日志的各个属性信息只需简单提供构造函数即可 1、 日志的输出时间 可用于过滤日志 2、 日志的等级 用于进行日志的过滤分析 3、 源文件名称 4、 源代码行号 用于定位出现错误的代码位置 5、 线程ID 用于过滤出错的线程 6、 日志主题消息 7、 日志器名称 项目允许多日志器同时使用 日志消息类实现代码 /*定义日志消息类, 进行日志中间信息的存储1.日志的输出时间 用于过滤日志输出时间2.日志等级 用于进行日志过滤分析3.源代码名称 4.源代码行号 用于定位出现错误的代码位置5.线程ID 用于过滤出现错误的线程6.日志主体消息 7.日志器名称 (当前支持多日志器的使用)*/
#ifndef __M_MSG_H__
#define __M_MSG_H__
#includeutil.hpp
#includelevel.hpp
#includeiostream
#includestring
#includethreadnamespace logsystem
{//结构体默认是公共的谁都可以访问struct LogMsg{time_t _ctime;//日志产生的时间戳LogLevel::value _level;//日志等级size_t _line;//源代码行号std::thread::id _tid;//线程IDstd::string _file;//源代码文件名std::string _name;//日志器名称//std::string _logger;std::string _payload;//有效信息数据LogMsg(LogLevel::value level,size_t line,const std::string file,const std::string name,//const std::string logger,const std::string msg):_ctime(util::Date::now()),_level(level),_line(line),_tid(std::this_thread::get_id()),_file(file),_name(name),//_logger(logger),_payload(msg){}};
}#endif7.4 日志输出格式化类
日志格式化Formatter类主要负责对日志消息对象内各个字段进行格式化组织成为指定格式的字符串。 日志输出格式化类实现代码框架 /* %d 表示日期 子格式 {%H:%M:%S} %d{%H:%M:%S}%t 表示鲜橙ID %c 表示日志器名称%f 表示源码文件名%l 表示源码行号%p 表示日志级别%T 表示制表符缩进%m 表示主体消息%n 表示换行*/class Formatter {public:using ptr std::shared_ptrFormatter;Formatter(const std::string pattern [%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n):_pattern(pattern) {assert(parsePattern());}/* 对msg进行格式化*/std::string format(const LogMsg msg;void format(std::ostream out, const LogMsg msg);/* 对格式化字符串进行解析 */bool parsePattern();private:/* 根据不同的格式化字符创建不同的格式化子项对象 */FormatItem::ptr createItem(const std::string key, const::std::string val);private:std::string _pattern;std::vectorFormatItem::ptr _items;};
}其主要包含以下两个成员变量
1、格式化字符串
2、格式化子项数组 用于按序保存格式化字符串对应的字格式化对象 格式化字符串 定义格式化字符成员是为了让日志系统进行日志格式化时更加灵活方便我们可以通过解析格式化字符串取出格式化字符调用格式化子项数组对Msg各个字段数据进行组织拼接成指定格式输出 格式化子项数组 实现思想从日志中取出指定元素追加到一块内存空间中 设计思想 抽象一个格式化子项基类基于基类派生出不同的格式化子项子类主体消息日志等级时间子项文件名行号日志器名称线程ID制表符换行其他这样就可以让父类中定义父类指针的数组指向不同的格式化子项子类对象
比如这是一串用户输入的格式化字符串[%d{%H:%M:%S}][%f:%l]%m%n我们将其解析可以获得以下顺序的格式化子项
1、[ 其他信息子项 调用 OtherFormatItem 进行处理 输出 [字符到指定位置
2、%d 日期子项 调用 TimeFormatItem 进行处理 输出 00:00:00字符到指定字符串
3、%f 文件子项 调用 FileFormatItem 进行处理 输出 文件名到指定位置字符串
4、%l 行号子项 调用 LineFormatItem 进行处理 输出 行号到指定位置字符串
5、%m 用户输入子项 调用 LoggerFormatItem 进行处理 输出 用户日志信息到指定字符串
6、%n 换行子项 调用 NewLineFormatItem 进行处理 输出 \n字符到指定字符串
注意%d日期子项是特殊的日期的输出格式可以有很多种比如带年份的和不带年份的。为了满足多种情况日期子项后带有{}字段其代表日期子项的输出格式在后续解析过程中我们会将该部分交给TimeFormatItem的子项处理器进行日期信息格式化
//抽象格式化子项基类
class FormatItem{public:using ptr std::shared_ptrFormatItem; //使用智能指针管理类对象virtual ~FormatItem() {}virtual void format(std::ostream os, const LogMsg msg) 0; //用于将LogMsg各个字段格式化到指定out流中
};以下即是LogMsg各个字段的处理子项每个子项器会将对应的LogMsg对象的对应字段放入指定out流中
//派生格式化子项子类 -- 消息 等级 时间 文件名 行号 线程ID 日志器名 制表符 换行 其他
class MsgFormatItem : public FormatItem {public:MsgFormatItem(const std::string str ){}virtual void format(std::ostream os, const LogMsg msg) {os msg._payload;}
};
class LevelFormatItem : public FormatItem {public:LevelFormatItem(const std::string str ){}virtual void format(std::ostream os, const LogMsg msg) {os LogLevel::toString(msg._level);}
};
class NameFormatItem : public FormatItem {public:NameFormatItem(const std::string str ){}virtual void format(std::ostream os, const LogMsg msg) {os msg._name;}
};
class ThreadFormatItem : public FormatItem {public:ThreadFormatItem(const std::string str ){}virtual void format(std::ostream os, const LogMsg msg) {os msg._tid;}
};
class TimeFormatItem : public FormatItem {private:std::string _format;public:TimeFormatItem(const std::string format %H:%M:%S):_format(format){if (format.empty()) _format %H:%M:%S;}virtual void format(std::ostream os, const LogMsg msg) {time_t t msg._ctime;struct tm lt;localtime_r(t, lt);char tmp[128];strftime(tmp, 127, _format.c_str(), lt);os tmp;}
};
class CFileFormatItem : public FormatItem {public:CFileFormatItem(const std::string str ){}virtual void format(std::ostream os, const LogMsg msg) {os msg._file;}
};
class CLineFormatItem : public FormatItem {public:CLineFormatItem(const std::string str ){}virtual void format(std::ostream os, const LogMsg msg) {os msg._line;}
};
class TabFormatItem : public FormatItem {public:TabFormatItem(const std::string str ){}virtual void format(std::ostream os, const LogMsg msg) {os \t;}
};
class NLineFormatItem : public FormatItem {public:NLineFormatItem(const std::string str ){}virtual void format(std::ostream os, const LogMsg msg) {os \n;}
};
class OtherFormatItem : public FormatItem {private:std::string _str;public:OtherFormatItem(const std::string str ):_str(str){}virtual void format(std::ostream os, const LogMsg msg) {os _str;}
};Formatter 类成员函数实现 class Formatter {public:using ptr std::shared_ptrFormatter;/*%d 日期%T 缩进%t 线程id%p 日志级别%c 日志器名称%f 文件名%l 行号%m 日志消息%n 换行*///[时间{年-月-日 时:分:秒}]缩进 [线程ID] 缩进 [日志级别] 缩进 [日志器名称] 缩进 [文件名:行号] 缩进 消息换行Formatter(const std::string pattern [%d{%H:%M:%S}][%t][%p][%c][%f:%l] %m%n):_pattern(pattern){assert(parsePattern());}const std::string pattern() { return _pattern; }std::string format(const LogMsg msg) {std::stringstream ss;for (auto it : _items) {it-format(ss, msg);}return ss.str();}std::ostream format(std::ostream os, const LogMsg msg) {for (auto it : _items) {it-format(os, msg);}return os;}FormatItem::ptr createItem(const std::string fc, const std::string subfmt) {if (fc m) return FormatItem::ptr(new MsgFormatItem(subfmt));if (fc p) return FormatItem::ptr(new LevelFormatItem(subfmt));if (fc c) return FormatItem::ptr(new NameFormatItem(subfmt));if (fc t) return FormatItem::ptr(new ThreadFormatItem(subfmt));if (fc n) return FormatItem::ptr(new NLineFormatItem(subfmt));if (fc d) return FormatItem::ptr(new TimeFormatItem(subfmt));if (fc f) return FormatItem::ptr(new CFileFormatItem(subfmt));if (fc l) return FormatItem::ptr(new CLineFormatItem(subfmt));if (fc T) return FormatItem::ptr(new TabFormatItem(subfmt));return FormatItem::ptr();}//pattern解析 bool parsePattern() {//std::string _pattern sg{}fsg%d{%H:%M:%S}%Tsdf%t%T[%p]%T[%c]%T%f:%l%T%m%n ;//std::cout _pattern std::endl;//每个要素分为三部分// 格式化字符 : %d %T %p...// 对应的输出子格式 {%H:%M:%S}// 对应数据的类型 0-表示原始字符串也就是非格式化字符1-表示格式化数据类型// 默认格式 %d{%H:%M:%S}%T%t%T[%p]%T[%c]%T%f:%l%T%m%nstd::vectorstd::tuplestd::string, std::string, int arry;std::string format_key;//存放%后的格式化字符std::string format_val;//存放格式化字符后边 {} 中的子格式字符串std::string string_row;//存放原始的非格式化字符bool sub_format_error false;int pos 0;while (pos _pattern.size()) {if (_pattern[pos] ! %) {string_row.append(1, _pattern[pos]);continue;}if (pos1 _pattern.size() _pattern[pos1] %) {string_row.append(1, %);pos 2;continue;}if (!string_row.empty()) {arry.push_back(std::make_tuple(string_row, , 0));string_row.clear();}//当前位置是%字符位置pos 1;//pos指向格式化字符位置 if (pos _pattern.size() isalpha(_pattern[pos])) {//isalpha(ch)检查ch是否是字母format_key _pattern[pos];//保存格式化字符}else {std::cout _pattern[pos-1] 位置附近格式错误\n;return false;}pos 1;//pos指向格式化字符的下一个位置判断是否包含有子格式 %d{%Y-%m-%d}if (pos _pattern.size() _pattern[pos] {) {sub_format_error true;pos 1;//pos指向花括号下一个字符处while(pos _pattern.size()) {if (_pattern[pos] }) {sub_format_error false;pos 1;//让pos指向}的下一个字符处break;}format_val.append(1, _pattern[pos]);}}arry.push_back(std::make_tuple(format_key, format_val, 1));format_key.clear();format_val.clear();}if (sub_format_error) {std::cout {}对应出错\n;return false;}if (string_row.empty() false) arry.push_back(std::make_tuple(string_row, , 0));if (format_key.empty() false) arry.push_back(std::make_tuple(format_key, format_val, 1));for (auto it : arry) {if (std::get2(it) 0) {FormatItem::ptr fi(new OtherFormatItem(std::get0(it)));_items.push_back(fi);}else {FormatItem::ptr fi createItem(std::get0(it), std::get1(it));if (fi.get() nullptr) {std::cout 没有对应的格式化字符: % std::get0(it) std::endl;return false;}_items.push_back(fi);}}return true;}private:std::string _pattern;std::vectorFormatItem::ptr _items;
};7.5 日志落地类工厂模式
功能将格式化完成后的日志消息字符串输出到指定位置
目前实现了三个不同方向的日志落地并且包含一个扩展示例用户可以根据示例添加自己的扩展实现更多的日志落地方式
标准输出StdoutSink固定文件FileSink滚动文件RollBySizeSink根据大小滚动滚动文件RollByTimeSink根据时间滚动 滚动日志文件的必要性 由于机器的磁盘空间是有限的我们不可能一直无限的向某一个文件中增加数据。如果一个文件的体积很大一方面是不好打开另一方面数据量过大也不利于我们查找需要的信息
实际开发中也会对单个日志文件的大小进行一些控制当某个文件的大小超过限制大小时比如1GB我们就会重新创建一个新的日志文件来滚动写日志对于那些过期的日志大部分企业内部都会有专门的韵味人员清理过期的日志或者在系统内部设置定时任务定时清理过期日志
日志滚动的方式这里实现了根据大小滚动比如超过1GB就更换新文件时间滚动的方式每一天写一个文件 日志落地类的实现思想 1、抽象出落地模块类
2、不同落地方向从基类进行派生
3、使用工厂模式进行创建和表示分离 日志落地类实现代码 /*日志落地模块的实现1.抽象落地基类2.派生子类(根据不同的落地方向进行派生)3.使用工厂模式进行创建与表示的分离
*/
#ifndef __M_SINK_H__
#define __M_SINK_H__#include util.hpp
#include fstream
#include sstream
#include memory
#include cassertnamespace logsystem
{class LogSink{public:using ptr std::shared_ptrLogSink;LogSink() {}virtual ~LogSink() {}virtual void log(const char *data, size_t len) 0;};// 落地方向标准输出class StdoutSink : public LogSink{public:// 将日志消息写入到标准输出void log(const char *data, size_t len){std::cout.write(data, len);}};// 落地方向指定文件class FileSink : public LogSink{public:// 构造时传入文件名并打开文件将操作句柄管理起来FileSink(const std::string pathname) : _pathname(pathname){// 1.创建日志文件所在的目录util::File::createDirectory(util::File::path(pathname));// 2.创建并打开日志文件_ofs.open(_pathname, std::ios::binary | std::ios::app);assert(_ofs.is_open());}// 将日志消息写入到标准输出void log(const char *data, size_t len){_ofs.write(data, len);//std::coutdata;assert(_ofs.good());}private:std::string _pathname;std::ofstream _ofs;};// 落地方向 滚动文件以大小进行滚动class RollBySizeSink : public LogSink{public:// 构造时传入文件名并打开文件操作句柄管理起来RollBySizeSink(const std::string basename, size_t max_size) : _basename(basename), _max_fsize(max_size), _name_count(0), _cur_fsize(0){std::string pathname createNewFile();// 1.创建日志文件所在的目录util::File::createDirectory(util::File::path(pathname));// 2.创建并打开日志文件_ofs.open(pathname, std::ios::binary | std::ios::app);assert(_ofs.is_open());}// 将日志消息写入到标准输出--写入前判断文件大小超过了最大大小就要切换文件void log(const char *data, size_t len){if (_cur_fsize _max_fsize){_ofs.close();std::string pathname createNewFile();// 1.创建日志文件所在的目录//util::File::createDirectory(util::File::path(pathname));// 2.创建并打开日志文件_ofs.open(pathname, std::ios::binary | std::ios::app);assert(_ofs.is_open());_cur_fsize 0;}_ofs.write(data, len);assert(_ofs.good());_cur_fsize len;}private:// 进行大小判断超过指定大小则创建新文件std::string createNewFile(){// 获取系统时间以时间来构造文件名扩展名time_t t util::Date::now();struct tm lt;localtime_r(t, lt);std::stringstream filename;filename _basename;filename lt.tm_year 1900;filename lt.tm_mon 1;filename lt.tm_mday;filename lt.tm_hour;filename lt.tm_min;filename lt.tm_sec;filename -;filename _name_count;filename .log;return filename.str();}private:// 通过基础文件名扩展文件名(以时间生成)组成一个实际的当前输出文件名std::string _basename; // ./logs/base- - ./logs/base-20020809132332.logstd::ofstream _ofs;size_t _max_fsize; // 记录最大大小当前文件超过了这个大小就要切换文件size_t _cur_fsize; // 记录当前文件已经写入的数据大小size_t _name_count;};// 日志落地工厂类的实现class SinkFactory{public:template typename SinkType, typename... Args // 函数模板的不定参static LogSink::ptr create(Args ...args){return std::make_sharedSinkType(std::forwardArgs(args)...);}};
}#endifofstream 使用 : 代码中的ofs代表ofstream类型的对象
ofs.open(file_name, mode) // 打开文件.
ofs.is_open() // 判断文件是否打开成功
ofs.write(data, len) // 写文件
ofs.good() // 若文件读或写失败某些字段会被设置调用good()返回false7.6 日志器类建造者模式
日志器是我们日志系统的核心其负责和前端交互当我们需要打印日志的时候只需要获取对应的日志器对象Logger调用该对象的debuginfo warm,error fatal 方法 就可以打印日志日志器支持解析可变参数列表和输出格式即可以像printf函数一样打印日志
当前日志系统支持 同步日志异步日志两种模式两种日志器唯一不同的地方在于日志的落地方式有所不同
同步日志器直接对日志消息进行输出 异步日志器将日志消息放入缓冲区中由异步线程进行日志落地 因为两种日志器在接口的设计功能的实现上都非常类似我们在设计时先设计出一个Logger基类在基类的基础上派生出SynchLogger 同步日志器 和 AynchLogger 异步日志器
又因为日志器模块是对前面多个模块的整合创建一个日志器需要设置日志器名称设置日志器的输出等级设置日志器的输出格式设置落地方向可能存在多个使用数组构建整个日志器的创建过程较为复杂为了保证良好的代码风格编写出优雅的代码我们选择使用建造者模式进行创建 日志器类实现代码 #ifndef __M_LOGGER_H__
#define __M_LOGGER_H__#ifndef _GNU_SOURCE
#define _GNU_SOURCE // 如果没用定义则定义
#endif#include util.hpp
#include level.hpp
#include message.hpp
#include format.hpp
#include sink.hpp
#include looper.hpp
#include vector
#include list
#include atomic
#include unordered_map
#include cstdarg
#include type_traitsnamespace logsystem
{class Logger{public:using ptr std::shared_ptrLogger;Logger(const std::string logger_name,LogLevel::value level,Formatter::ptr formatter,std::vectorLogSink::ptr sinks) : _logger_name(logger_name),_limit_level(level),_formatter(formatter),_sinks(sinks.begin(), sinks.end()) {}const std::string name(){return _logger_name;}/*完成构造日志消息对象过程并进行格式化得到格式化后的日志消息字符串--然后进行落地输出*/void debug(const std::string file, size_t line, const std::string fmt, ...){// 通过传入的参数构造出一个日志消息进行日志的格式化最终落地// 1.判断当前的日志是否达到了输出等级if (LogLevel::value::DEBUG _limit_level){return;}// 2.对fmt格式化字符串和不定参进行字符串组织得到的日志消息的字符串va_list ap;va_start(ap, fmt);//根据上一个参数末尾找到下一个参数的开头char *res;int ret vasprintf(res, fmt.c_str(), ap);//解析fmt格式化字符串取出不定参数组织成指定字符串输出到内存中if (ret 0){std::cout vasprintf failed!\n;return;}va_end(ap); // 将ap指针置空serialize(LogLevel::value::DEBUG, file, line, res);free(res); // 动态开辟的空间需要手动释放}void info(const std::string file, size_t line, const std::string fmt, ...){// 通过传入的参数构造出一个日志消息进行日志的格式化最终落地// 1.判断当前的日志是否达到了输出等级if (LogLevel::value::INFO _limit_level){return;}// 2.对fmt格式化字符串和不定参进行字符串组织得到的日志消息的字符串va_list ap;va_start(ap, fmt);char *res;int ret vasprintf(res, fmt.c_str(), ap);if (ret -1){std::cout vasprintf failed!\n;return;}va_end(ap); // 将ap指针置空serialize(LogLevel::value::INFO, file, line, res);free(res); // 动态开辟的空间需要手动释放}void warn(const std::string file, size_t line, const std::string fmt, ...){// 通过传入的参数构造出一个日志消息进行日志的格式化最终落地// 1.判断当前的日志是否达到了输出等级if (LogLevel::value::WARN _limit_level){return;}// 2.对fmt格式化字符串和不定参进行字符串组织得到的日志消息的字符串va_list ap;va_start(ap, fmt);char *res;int ret vasprintf(res, fmt.c_str(), ap);if (ret -1){std::cout vasprintf failed!\n;return;}va_end(ap); // 将ap指针置空serialize(LogLevel::value::WARN, file, line, res);free(res); // 动态开辟的空间需要手动释放}void error(const std::string file, size_t line, const std::string fmt, ...){// 通过传入的参数构造出一个日志消息进行日志的格式化最终落地// 1.判断当前的日志是否达到了输出等级if (LogLevel::value::ERROR _limit_level){return;}// 2.对fmt格式化字符串和不定参进行字符串组织得到的日志消息的字符串va_list ap;va_start(ap, fmt);char *res;int ret vasprintf(res, fmt.c_str(), ap);if (ret -1){std::cout vasprintf failed!\n;return;}va_end(ap); // 将ap指针置空serialize(LogLevel::value::ERROR, file, line, res);free(res); // 动态开辟的空间需要手动释放}void fatal(const std::string file, size_t line, const std::string fmt, ...){// 通过传入的参数构造出一个日志消息进行日志的格式化最终落地// 1.判断当前的日志是否达到了输出等级if (LogLevel::value::FATAL _limit_level){return;}// 2.对fmt格式化字符串和不定参进行字符串组织得到的日志消息的字符串va_list ap;va_start(ap, fmt);char *res;int ret vasprintf(res, fmt.c_str(), ap);if (ret 0){std::cout vasprintf failed!\n;return;}va_end(ap); // 将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){// 3.构造LogMsg对象LogMsg msg(level, line, file, _logger_name, str);//日志信息结构体// 4.通过格式化工具对LogMsg进行格式化得到格式化后的日志字符串std::stringstream ss;_formatter-format(ss, msg);//一条日志信息// 5.进行日志落地log(ss.str().c_str(), ss.str().size());}/*抽象接口完成实际的落地输出--不同的日志器会有不同的实际落地方式*/virtual void log(const char *data, size_t len) 0;protected:std::mutex _mutex;std::string _logger_name;std::atomicLogLevel::value _limit_level;Formatter::ptr _formatter;std::vectorLogSink::ptr _sinks;}; //同步日志器class SyncLogger : public Logger{public:SyncLogger(const std::string logger_name,LogLevel::value level,Formatter::ptr formatter,std::vectorLogSink::ptr sinks) : Logger(logger_name, level, formatter, sinks) {}//这些参数被传递给基类 Logger 的构造函数protected:// 同步日志器是将日志通过落地模块句柄进行日志落地void log(const char *data, size_t len){std::unique_lockstd::mutex lock(_mutex);if (_sinks.empty())return;for (auto sink : _sinks){sink-log(data, len);}}};//异步日志器class AsyncLogger : public Logger{public:AsyncLogger(const std::string logger_name,LogLevel::value level,Formatter::ptr formatter,std::vectorLogSink::ptr sinks,AsyncType looper_type) : Logger(logger_name, level, formatter, sinks),_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){//std::coutbuf._buffer[0]std::endl;if (_sinks.empty())return;for (auto sink : _sinks){sink-log(buf.begin(), buf.readAbleSize());}}private:AsyncLooper::ptr _looper;};enum class LoggerType{LOGGER_SYNC,LOGGER_ASYNC};/*使用建造者模式来建造日志器而不要人用户直接去构造日志器i简化用户的使用复杂度*/// 1.抽象一个日志器建造者类完成日志器对象所需零部件的构造 日志器的构建// 1.设置日志器类型// 2.将不同类型日志器的创建放到同一个日志器建造类中完成class LoggerBuilder{public:LoggerBuilder() : _logger_type(LoggerType::LOGGER_SYNC),_limit_level(LogLevel::value::DEBUG),_looper_type(AsyncType::ASYNC_SAFE) {}void buildLoggerType(LoggerType type) { _logger_type type; }void buildEnableUnSafeAsync() { _looper_type AsyncType::ASYNC_UNSAFE; }void buildLoggerName(const std::string name) { _logger_name name; }void buildLoggerLevel(LogLevel::value level) { _limit_level level; }void buildFormatter(const std::string pattern){_formatter std::make_sharedFormatter(pattern);}template typename SinkType, typename... Argsvoid buildSink(Args ...args){LogSink::ptr psink SinkFactory::createSinkType(std::forwardArgs(args)...);_sinks.push_back(psink);}virtual Logger::ptr build() 0;protected:AsyncType _looper_type;LoggerType _logger_type;std::string _logger_name;LogLevel::value _limit_level;Formatter::ptr _formatter;std::vectorLogSink::ptr _sinks;};// 2.派生出具体的建造者类class LocalLoggerBuilder : public LoggerBuilder{public:Logger::ptr build() override{assert(!_logger_name.empty()); // 必须有日志器名称if (_formatter.get() nullptr){_formatter std::make_sharedFormatter();}if (_sinks.empty()){buildSinkStdoutSink();}if (_logger_type LoggerType::LOGGER_ASYNC){return std::make_sharedAsyncLogger(_logger_name, _limit_level, _formatter, _sinks, _looper_type);}return std::make_sharedSyncLogger(_logger_name, _limit_level, _formatter, _sinks);}};// 日志器管理器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_ptrlogsystem::LoggerBuilder builder(new logsystem::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;};//全局日志器的建造者---在局部的基础上增加了一个功能将日志器添加到单例对象中class GlobalLoggerBuilder : public LoggerBuilder{public:Logger::ptr build() override{assert(_logger_name.empty() false); // 必须有日志器名称if (_formatter.get() nullptr){_formatter std::make_sharedFormatter();}if (_sinks.empty()){buildSinkStdoutSink();}Logger::ptr logger;if (_logger_type LoggerType::LOGGER_ASYNC){logger std::make_sharedAsyncLogger(_logger_name, _limit_level, _formatter, _sinks, _looper_type);}else{logger std::make_sharedSyncLogger(_logger_name, _limit_level, _formatter, _sinks);}LoggerManager::getInstance().addLogger(logger);return logger;}};}#endif同步日志器 同步日志器的日志落地直接使用落地器进行落地操作我们的线程串行等待日志写入外设等待全部写完后继续执行后续业务逻辑 异步日志器 异步日志器的日志落地并不由业务线程做业务线程只负责将日志数据拷贝到日志缓冲区中然后继续执行业务程序即可无需等待日志数据写到外设 日志数据的实际落地工作由异步任务处理器lopper进行处理我们在异步日志器启动时创建异步任务处理器并在异步日志器中写好日志的实际落地方案realLog将这个方案传递给我们的异步任务处理器。 异步任务处理器启动后会创建异步线程该线程的工作就是不断循环的从到我们的日志缓冲区中获取数据并根据异步日志器传入的实际落地方案对获取到的数据进行处理负责数据的实际落地
每一个异步日志器还需要搭载一个异步日志处理器负责日志的实际落地任务
7.7 异步日志双缓冲区类
/*实现异步日志缓冲区*/
#include util.hpp
#include vector
#include cassert
namespace logsystem
{
#define DEFAULT_BUFFER_SIZE (1 * 1024 * 1024)
#define THRESHOLD_BUFFER_SIZE (8 * 1024 * 1024) // 阈值
#define INCREMENT_BUFFER_SIZE (1 * 1024 * 1024)class Buffer{public:Buffer() : _buffer(DEFAULT_BUFFER_SIZE), _writer_idx(0), _reader_idx(0) {}// 向缓冲区写入数据void push(const char *data, size_t len){// 缓冲区剩余空间不足的情况:1.扩容 2.阻塞/返回false// //1.固定大小则直接返回// if(len writeAbleSize()) return;// 2.(检测空间不够就扩容)动态空间用于极限性能测试--扩容//*这里只需要考虑空间不够就扩容ensureEnoughSize(len);// 1.将数据拷贝到缓冲区std::copy(data, data len, _buffer[_writer_idx]);// 2.将当前写入位置先后偏移moveWriter(len);}// 返回剩余空间大小size_t writeAbleSize(){// 对于扩容思路来说不存在可写数据的空间大小的概念// 因为总是可写的因此此接口仅存在于固定大小思路缓冲区使用return (_buffer.size() - _writer_idx);}// 返回可读数据的起始位置const char *begin(){return _buffer[_reader_idx];}// 返回可读数据空间的大小size_t readAbleSize(){// 当前实现的缓冲区设计思想是双缓冲区处理完就交换不存在空间循环使用return (_writer_idx - _reader_idx);}// 对读指针进行向后偏移操作void moveReader(size_t len){assert(len readAbleSize());_reader_idx len;}// 重置读写位置初始化缓冲区void reset(){_reader_idx 0; // 缓冲区所有空间但是空闲的_writer_idx 0; // 与_writer_idx相等表示没有数据可读}// 对Buffer实现交换操作void swap(Buffer buffer){_buffer.swap(buffer._buffer); // vector内部的交换交换的是地址std::swap(_reader_idx, buffer._reader_idx);std::swap(_writer_idx, buffer._writer_idx);}// 判断缓冲区是否为空bool empty(){return (_reader_idx _writer_idx);}private:// 对空间进行扩容void ensureEnoughSize(size_t len){if (len writeAbleSize())return; // 不需要扩容size_t new_size 0;if (_buffer.size() THRESHOLD_BUFFER_SIZE) // 小于阈值翻倍{new_size 2 * _buffer.size()len;}else{new_size _buffer.size() INCREMENT_BUFFER_SIZElen;// 大于阈值线性增长}_buffer.resize(new_size);}// 对写指针进行向后偏移void moveWriter(size_t len){assert((len _writer_idx) _buffer.size());_writer_idx len;}private:std::vectorchar _buffer;size_t _reader_idx; // 当前可读数据的指针--本质是下标size_t _writer_idx; // 当前可写数据的指针};
}7.8 异步工作器类
外界将任务数据添加到缓冲区中异步线程对处理缓冲区中的数据进行处理若处理缓冲区中没有数据了就交换缓冲区
/*实现异步工作器*/
#ifndef __M_LOOP_H__
#define __M_LOOP_H__
#include util.hpp
#include vector
#include thread
#include mutex
#include atomic
#include condition_variable
#include functional
//#include unistd.h
#include buffer.hppnamespace logsystem
{using Functor std::functionvoid(Buffer );enum class AsyncType{ASYNC_SAFE, // 安全状态表示缓冲区满了则阻塞避免资源耗尽的发现ASYNC_UNSAFE // 不考虑资源耗尽的问题无限扩容常用于测试};class AsyncLooper{public:using ptr std::shared_ptrAsyncLooper;// 新线程将要执行的函数:AsyncLooper::threadEntry// 使用的是成员函数指针因此需要额外传递一个指向类实例的指针即this指针AsyncLooper(const Functor cb, AsyncType looper_type AsyncType::ASYNC_SAFE) :_stop(false),_looper_type(looper_type),_thread(std::thread(AsyncLooper::threadEntry, this)),_callBack(cb) {}~AsyncLooper() { stop(); }void stop(){_stop true; // 将退出标志设置为true_cond_con.notify_all(); // 唤醒所有的工作线程_thread.join(); // 等待工作线程的退出}void push(const char *data, size_t len){// 1.无限扩容-非安全 2.固定大小--生产缓冲区中数据满了就阻塞std::unique_lockstd::mutex lock(_mutex);// 条件变量空值若缓冲区剩余大小小于数据长度则可以添加数据if (_looper_type AsyncType::ASYNC_SAFE){_cond_pro.wait(lock, [](){ return _pro_buf.writeAbleSize() len; });}// 能够走下来代表满足了条件可以向缓冲区添加数据_pro_buf.push(data, len);// 唤醒消费者对缓冲区中的数据进行处理_cond_con.notify_one();}private:// 线程入口函数--对消费者缓冲区中的数据进行处理处理完毕后初始化缓冲区交换缓冲区void threadEntry(){while (1){// 为互斥锁设置一个生命周期当缓冲区交换完毕后就解锁并不对数据处理过程加锁保护{// 1.判断生产缓冲区中有没有数据有则交换无则阻塞std::unique_lockstd::mutex lock(_mutex);//退出标志被设置且生产缓冲区已经没有数据这时候退出否则有可能会造成缓冲区内有数据但是没有处理if(_stop _pro_buf.empty()) break; _cond_con.wait(lock, [](){ return _stop || !_pro_buf.empty(); });//_cond_con.wait(lock, 返回false });阻塞等待_con_buf.swap(_pro_buf);if(_looper_typeAsyncType::ASYNC_SAFE){// 2.唤醒所有生产者_cond_pro.notify_all(); }}// 3.被唤醒后对消费者缓冲区进行数据处理_callBack(_con_buf);// 4.初始化消费者缓冲区_con_buf.reset();}}private:Functor _callBack; // 具体对缓冲区数据进行处理的回调函数由异步工作器使用者传入private:AsyncType _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; // 异步工作器对应的工作线程};}#endif7.9 单例日志器管理类
日志的输出我们希望在任意位置都可以进行但是我们创建一个日志器后就会受到日志器所在域的访问属性限制
因此为了突破访问区域的限制我们创建一个日志器管理类这个类是一个单例类这样的话我们就可以在任意位置来通过管理器单例获取日志器来进行日志输出了
基于单例日志器管理器的设计思想我们对于日志器建造者类进行继承继承出一个全局日志器建造者类实现一个日志器在创建完毕后直接将其添加到单例的日志器管理器中以便能够在任何位置通过日志器名称获取指定的日志器进行日志输出
// 日志器管理器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_ptrlogsystem::LoggerBuilder builder(new logsystem::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;};7.10 日志宏全局接口设计
提供全局的日志器获取接口
使用代理模式通过全局函数或宏函数来代理Logger类的log, debug. info, warn, error, fatal 等接口以便控制源码文件名称和行号的输出控制简化用户操作
当仅需标准输出日志的时候可以通过主日志器默认日志器打印日志。且操作时只需要通过宏函数直接进行输出即可
#ifndef __M_LOGSYSTEM_H__
#define __M_LOGSYSTEM_H__#include util.hpp
#include level.hpp
#include message.hpp
#include format.hpp
#include sink.hpp
#include logger.hpp
#include looper.hpp
#include logger.hppnamespace logsystem
{//1.提供获取指定日志器的全局接口避免用户自己操作单例对象Logger::ptr getLogger(const std::string name){return logsystem::LoggerManager::getInstance().getLogger(name);}Logger::ptr rootLogger(){return logsystem::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(logger, fmt, ...) logger-debug(fmt, ##__VA_ARGS__)// #define DLOG(fmt, ...) DEBUG(rootLogger(), fmt, ##__VA_ARGS__) //- _root_logger-debug(fmt, ##__VA_ARGS__)#define DEBUG(fmt, ...) logsystem::rootLogger()-debug(fmt, ##__VA_ARGS__);#define INFO(fmt, ...) logsystem::rootLogger()-info(fmt, ##__VA_ARGS__);#define WARN(fmt, ...) logsystem::rootLogger()-warn(fmt, ##__VA_ARGS__);#define ERROR(fmt, ...) logsystem::rootLogger()-error(fmt, ##__VA_ARGS__);#define FATAL(fmt, ...) logsystem::rootLogger()-fatal(fmt, ##__VA_ARGS__);}#endif8.功能测试
该模块编写的是项目编写过程中每一模块的测试代码保证每个模块可以正常使用。可以用于测试日志器中各个模块的工作是否正常测试一个日志器中包含所有的落地方向观察是否每个方向都正常落地分别测试同步方式和异步方式落地后数据是否正常
#include ../logs/logsystem.h
#include unistd.h
// #include fstream
// #include sstream
// #include memory/*扩展一个以时间作为日志文件滚动切换类型的日志落地模块1.以时间进行文件滚动实际上是以时间段进行滚动实现思想以当前系统时间取模时间段大小可以得到当前时间段是第几个时间段每次以当前系统时间取模判断与当前文件的时间段是否一致不一致代表不是一个时间段
*/
enum class TimeGap
{GAP_SECOND,GAP_TEST,GAP_MINUTE,GAP_HOUR,GAP_DAY,
};class RollByTimeSink : public logsystem::LogSink
{
public:// 构造时传入文件名并打开文件将操作句柄管理起来RollByTimeSink(const std::string basename, TimeGap gap_type) : _basename(basename){switch (gap_type){case TimeGap::GAP_SECOND:_gap_size 1;break;case TimeGap::GAP_TEST:_gap_size 5;break;case TimeGap::GAP_MINUTE:_gap_size 60;break;case TimeGap::GAP_HOUR:_gap_size 3600;break;case TimeGap::GAP_DAY:_gap_size 3600 * 24;break;}_cur_gap_gap_size1 ? logsystem::util::Date::now() : logsystem::util::Date::now() % _gap_size;//获取当前是第几个时间段std::string filename createNewFile();logsystem::util::File::createDirectory(logsystem::util::File::path(filename));_ofs.open(filename, std::ios::binary | std::ios::app);assert(_ofs.is_open());}// 将日志消息写入到标准输出void log(const char *data, size_t len){time_t curlogsystem::util::Date::now();if(cur % _gap_size _cur_gap){_ofs.close();std::string filenamecreateNewFile();_ofs.open(filename,std::ios::binary | std::ios::app);assert(_ofs.is_open());}_ofs.write(data, len);assert(_ofs.good());}
private:std::string createNewFile(){// 获取系统时间以时间来构造文件名扩展名time_t t logsystem::util::Date::now();struct tm lt;localtime_r(t, lt);std::stringstream filename;filename _basename;filename lt.tm_year 1900;filename lt.tm_mon 1;filename lt.tm_mday;filename lt.tm_hour;filename lt.tm_min;filename lt.tm_sec;filename .log;return filename.str();}private:std::string _basename;std::ofstream _ofs;size_t _gap_size; // 当前是第几个时间段size_t _cur_gap; // 时间段的大小
};void test_log(const std::string name)
{INFO(%s, 测试开始);logsystem::Logger::ptr loggerlogsystem::LoggerManager::getInstance().getLogger(name);logger-debug(%s, 测试日志);logger-info(%s, 测试日志);logger-warn(%s, 测试日志);logger-error(%s, 测试日志);logger-fatal(%s, 测试日志);size_t cursize0, count0;//std::string str测试日志-;while(cursize 1024*1024*5){logger-fatal(测试日志-%d, count);cursize40;}INFO(%s, 测试结束);// DEBUG(%s, 测试日志);// INFO(%s, 测试日志);// WARN(%s, 测试日志);// ERROR(%s, 测试日志);// FATAL(%s, 测试日志);// size_t cursize0, count0;// //std::string str测试日志-;// while(count 10000)// {// FATAL(测试日志-%d, count);// }}int main()
{// //读取数据一点一点写入缓冲区最终将缓冲区数据写入文件判断生成的新文件与源文件是否一致// 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;// logsystem::Buffer buffer;// for(int i0;ibody.size();i)// {// buffer.push(body[i], 1);// //std::coutbuffer._buffer[i]std::endl;// }// std::ofstream ofs(./logfile/tmp.log, std::ios::binary);// if(ofs.is_open()false) {return -1;}// //ofs.write(buffer.begin(), buffer.readAbleSize());// size_t rsizebuffer.readAbleSize();// std::coutrsizestd::endl;// for(int i0;irsize;i)// {// ofs.write(buffer.begin(), 1);// buffer.moveReader(1);// }// if(ofs.good()false) {std::coutwrite error\n; return -1;}// ofs.close();// std::unique_ptrlogsystem::LoggerBuilder builder(new logsystem::GlobalLoggerBuilder());// builder-buildLoggerName(async_logger);// builder-buildLoggerLevel(logsystem::LogLevel::value::DEBUG);// builder-buildFormatter([%c][%f:%l][%p]%m%n);// builder-buildLoggerType(logsystem::LoggerType::LOGGER_SYNC);// //builder-buildEnableUnSafeAsync();// builder-buildSinklogsystem::StdoutSink();// builder-buildSinklogsystem::FileSink(./logfile/async.log);// builder-buildSinklogsystem::RollBySizeSink(./logfile/roll-asyncbysize-, 1024*1024);// builder-build();// test_log(async_logger);// std::string logger_namesync_logger;// logsystem::LogLevel::value limitlogsystem::LogLevel::value::WARN;// logsystem::Formatter::ptr fmt(new logsystem::Formatter([%d{%H:%M:%S}][%c][%f:%l][%p]%T%m%n));// logsystem::LogSink::ptr stdout_lsplogsystem::SinkFactory::createlogsystem::StdoutSink();// logsystem::LogSink::ptr file_lsplogsystem::SinkFactory::createlogsystem::FileSink(./logfile/test.log);// logsystem::LogSink::ptr roll_lsplogsystem::SinkFactory::createlogsystem::RollBySizeSink(./logfile/roll-, 1024*1024);// std::vectorlogsystem::LogSink::ptr sinks{stdout_lsp, file_lsp, roll_lsp};// logsystem::Logger::ptr logger(new logsystem::SyncLogger(logger_name, limit, fmt, sinks));// 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;// std::string str测试日志-;// while(cursize 1024*1024*10)// {// logger-fatal(__FILE__, __LINE__, 测试日志-%d, count);// cursize60;// }logsystem::LogMsg msg(logsystem::LogLevel::value::INFO, 53, main.c, root, 格式化功能测试...);logsystem::Formatter fmt;std::string str fmt.format(msg);// logsystem::LogSink::ptr stdout_lsp logsystem::SinkFactory::createlogsystem::StdoutSink();// logsystem::LogSink::ptr file_lsp logsystem::SinkFactory::createlogsystem::FileSink(./logfile/test.log);// logsystem::LogSink::ptr roll_lsp logsystem::SinkFactory::createlogsystem::RollBySizeSink(./logfile/roll-, 1024 * 1024);logsystem::LogSink::ptr time_lsp logsystem::SinkFactory::createRollByTimeSink(./logfile/roll-, TimeGap::GAP_TEST);time_t oldlogsystem::util::Date::now();while(logsystem::util::Date::now() old20){time_lsp-log(str.c_str(), str.size());sleep(1);}// stdout_lsp-log(str.c_str(), str.size());// file_lsp-log(str.c_str(), str.size());// file_lsp-log(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-log(tmp.c_str(), tmp.size());// cursize tmp.size();// }// std::cout logsystem::LogLevel::toString(logsystem::LogLevel::value::DEBUG) std::endl;// std::cout logsystem::LogLevel::toString(logsystem::LogLevel::value::INFO) std::endl;// std::cout logsystem::LogLevel::toString(logsystem::LogLevel::value::WARN) std::endl;// std::cout logsystem::LogLevel::toString(logsystem::LogLevel::value::ERROR) std::endl;// std::cout logsystem::LogLevel::toString(logsystem::LogLevel::value::FATAL) std::endl;// std::cout logsystem::LogLevel::toString(logsystem::LogLevel::value::OFF) std::endl;// std::coutlogsystem::LogLevel::toString(logsystem::LogLevel::value::UNKNOW)std::endl;// std::coutlogsystem::util::Date::now()std::endl;// std::string pathname./abc/bcd/a.txt;// logsystem::util::File::createDirectory(logsystem::util::File::path(pathname));return 0;
}9.性能测试
以下是对日志系统项目做的一个性能测试测试一下平均每秒能公打印多少日志消息到文件
主要的测试方法每秒能打印日志数 / 总的打印日志消耗时间
主要的测试要素 同步/异步 单线程/多线程
测试环境 :
CPU: Intel® Xeon® Gold 6133 CPU 2.50GHz RAM: 2G DIMM RAM ROM: 40G OS: Linux VM-4-14-ubuntu 5.15.0-106-generic.x86_64 (腾讯云服务器)
测试方法
#include ../logs/logsystem.h
#include vector
#include thread
#include chronovoid bench(const std::string logger_name, size_t thr_count, size_t msg_count, size_t msg_len)
{// 1.获取日志器logsystem::Logger::ptr logger logsystem::getLogger(logger_name);if (logger.get() nullptr){return;}std::cout测试日志msg_count条总大小(msg_count * msg_len)/1024KB\n;// 2.组织指定长度的日志消息std::string msg(msg_len - 1, A); // 少一个字节是为了给末尾到时候添加换行// 3.创建指定数量的线程std::vectorstd::thread threads;std::vectordouble cost_array(thr_count);size_t msg_per_thr msg_count / thr_count; // 每个线程要输出的日志数量for (int i 0; i thr_count; i){//C标准线程库thread库中创建线程的一种方式在threads向量尾部直接创建以线程对象该线程对象执行任务定义为传入的lambda函数threads.emplace_back([, i](){//4.线程函数内部开始计时auto startstd::chrono::high_resolution_clock::now();//5.开始循环写日志for(int j0; jmsg_per_thr; j){logger-fatal(%s, msg.c_str());}//6.线程函数内部结束计时auto endstd::chrono::high_resolution_clock::now();std::chrono::durationdouble cost end-start;cost_array[i]cost.count();std::cout线程i:\t输出日志数量:msg_per_thr耗时:cost.count()s\n; });}for (int i 0; i thr_count; i){threads[i].join();}// 7.计算总耗时:在多线程中每个线程都会耗费时间但是线程是并发处理的因此耗时最高的那个就是总时间double max_cost cost_array[0];for (int i 0; i cost_array.size(); i){max_cost max_cost cost_array[i] ? max_cost : cost_array[i];}size_t msg_per_sec msg_count/max_cost;size_t size_per_sec (msg_count * msg_len)/(max_cost * 1024);// 8.进行输出打印std::cout总耗时max_costs\n;std::cout平均每秒输出日志数量msg_per_sec条\n;std::cout平均每秒输出日志大小size_per_secKB\n;std::coutstd::endl;
}void sync_bench()
{std::unique_ptrlogsystem::LoggerBuilder builder(new logsystem::GlobalLoggerBuilder());builder-buildLoggerName(sync_logger);builder-buildFormatter(%m%n);builder-buildLoggerType(logsystem::LoggerType::LOGGER_SYNC);builder-buildSinklogsystem::FileSink(./logfile/sync.log);builder-build();bench(sync_logger, 3, 1000000, 100);
}void async_bench()
{std::unique_ptrlogsystem::LoggerBuilder builder(new logsystem::GlobalLoggerBuilder());builder-buildLoggerName(async_logger);builder-buildFormatter(%m%n);builder-buildLoggerType(logsystem::LoggerType::LOGGER_ASYNC);builder-buildEnableUnSafeAsync();//开启非安全模式--主要是为了将实际落地时间排除在外builder-buildSinklogsystem::FileSink(./logfile/async.log);builder-build();bench(async_logger, 5, 1000000, 100);
}int main()
{async_bench();//sync_bench();return 0;
}测试结果
同步日志器单线程
同步日志器多线程
异步日志器单线程
异步日志器多线程 10.扩展
1.丰富Sink类型落地方式 支持按照时间滚动文件 支持将log通过网络传输落地到日志服务器tcp/udp 支持在控制台通过日志等级渲染不同的颜色输出方便定位 支持落地日志到数据库 支持配置服务器地址将日志落地到远程服务器
2.实现日志服务器负责存储日志并提供检索、分析、展示等功能
3.引⼊⼀些设计模式⼯⼚、单例、建造者、代理来增加⼯程代码的可扩展性、可维护性。