[项目] 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、核心技术
- 类层次化设计(继承、多态的实际应用)
- C++11新特性(多线程库、智能指针、右值引用等)
- 双缓冲区设计思想
- 生产者消费者模型
- 设计模式(单例、工厂、代理、建造者等)
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++风格的不定参数*/
template<typename T, typename ...Args>
void xprintf(const T &v, Args&&... args) {std::cout << v;if ((sizeof...(args)) > 0) {xprintf(std::forward<Args>(args)...);} else {xprintf();}
}int main() {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++
// C++11后是线程安全的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 << "I'm a apple" << std::endl;}
};class Banana : public Fruit{
public:void name() override {std::cout << "I'm a banana" << std::endl;}
};class FruitFactory {
public:static std::shared_ptr<Fruit> create(const std::string &name) {if (name == "苹果") {return std::make_shared<Apple>();} else {return std::make_shared<Banana>();}}
};int main() {std::shared_ptr<Fruit> 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 << "I'm a apple" << std::endl;}
};class Banana : public Fruit{
public:void name() override {std::cout << "I'm a banana" << std::endl;}
};
class FruitFactory {
public:virtual std::shared_ptr<Fruit> createFruit() = 0;
};class AppleFactory : public FruitFactory {
public:virtual std::shared_ptr<Fruit> createFruit() override {return std::make_shared<Apple>();}
};class BananaFactory : public FruitFactory {
public:virtual std::shared_ptr<Fruit> createFruit() override {return std::make_shared<Banana>();}
};int main() {std::shared_ptr<FruitFactory> ff(new AppleFactory());std::shared_ptr<Fruit> fruit1 = ff->createFruit();fruit1->name();ff.reset(new BananaFactory());std::shared_ptr<Fruit> 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 << "I'm a apple" << std::endl;}
};class Banana : public Fruit{
public:void name() override {std::cout << "I'm a banana" << std::endl;}
};class Animal {public:virtual void name() = 0;
};class Lamp : public Animal {public:virtual void name() override { std::cout << "I'm a Lamp" << std::endl;}
};class Dog : public Animal {public:virtual void name() override {std::cout << "I'm a dog" << std::endl;}
};class Factory {public: virtual std::shared_ptr<Fruit> getFruit(const std::string& name) = 0;virtual std::shared_ptr<Animal> getAnimal(const std::string& name) = 0;
};class FruitFactory : public Factory {public:virtual std::shared_ptr<Fruit> getFruit(const std::string& name) override{if (name == "苹果") {return std::make_shared<Apple>();} else {return std::make_shared<Banana>();}}virtual std::shared_ptr<Animal> getAnimal(const std::string& name) override{return std::shared_ptr<Animal>();}
};class AnimalFactory : public Factory {public:virtual std::shared_ptr<Fruit> getFruit(const std::string& name) override {return std::shared_ptr<Fruit>();}virtual std::shared_ptr<Animal> getAnimal(const std::string& name) override {if (name == "山羊") {return std::make_shared<Lamp>();} else {return std::make_shared<Dog>();}}
};class FactoryProducer {public: static std::shared_ptr<Factory> create(const std::string &name) {if (name == "水果") {return std::make_shared<FruitFactory>();} else {return std::make_shared<AnimalFactory>();}}
};int main() {std::shared_ptr<Factory> ff = FactoryProducer::create("水果");std::shared_ptr<Fruit> fruit = ff->getFruit("苹果");fruit->name();fruit = ff->getFruit("香蕉");fruit->name();ff = FactoryProducer::create("动物");std::shared_ptr<Animal> 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_ptr<Computer> 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_ptr<Computer> build() {return _computer;}private:std::shared_ptr<Computer> _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_ptr<Builder> _builder;
};int main() {Builder * builder = new MacBookBuilder();std::unique_ptr<Director> director(new Director(builder));director->construct("华硕主板", "三星显示器");std::shared_ptr<Computer> computer = builder->build();computer->showParamaters();return 0;
}
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.h>namespace 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, pos+1);if (exists(parent_dir) == true){idx = pos + 1;continue;}mkdir(parent_dir.c_str(), 0777);idx = pos + 1;}}};}
}#endif
Date类实现:
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{UNKNOW=0,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";}};
}#endif
7.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__
#include"util.hpp"
#include"level.hpp"
#include<iostream>
#include<string>
#include<thread>namespace 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){}};
}#endif
7.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_ptr<Formatter>;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::vector<FormatItem::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_ptr<FormatItem>; //使用智能指针管理类对象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, <);char tmp[128];strftime(tmp, 127, _format.c_str(), <);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_ptr<Formatter>;/*%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%n"std::vector<std::tuple<std::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 (pos+1 < _pattern.size() && _pattern[pos+1] == '%') {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::get<2>(it) == 0) {FormatItem::ptr fi(new OtherFormatItem(std::get<0>(it)));_items.push_back(fi);}else {FormatItem::ptr fi = createItem(std::get<0>(it), std::get<1>(it));if (fi.get() == nullptr) {std::cout << "没有对应的格式化字符: %" << std::get<0>(it) << std::endl;return false;}_items.push_back(fi);}}return true;}private:std::string _pattern;std::vector<FormatItem::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 <cassert>namespace logsystem
{class LogSink{public:using ptr = std::shared_ptr<LogSink>;LogSink() {}virtual ~LogSink() {}virtual void log(const char *data, size_t len) = 0;};// 落地方向:标准输出class StdoutSink : public LogSink{public:// 将日志消息写入到标准输出void log(const char *data, size_t len){std::cout.write(data, len);}};// 落地方向:指定文件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::cout<<data;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, <);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_shared<SinkType>(std::forward<Args>(args)...);}};
}#endif
ofstream 使用 : 代码中的ofs代表ofstream类型的对象
ofs.open(file_name, mode) // 打开文件.
ofs.is_open() // 判断文件是否打开成功
ofs.write(data, len) // 写文件
ofs.good() // 若文件读或写失败,某些字段会被设置,调用good()返回false
7.6 日志器类(建造者模式)
日志器是我们日志系统的核心,其负责和前端交互,当我们需要打印日志的时候,只需要获取对应的日志器对象(Logger),调用该对象的debug,info, 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_traits>namespace logsystem
{class Logger{public:using ptr = std::shared_ptr<Logger>;Logger(const std::string &logger_name,LogLevel::value level,Formatter::ptr &formatter,std::vector<LogSink::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::atomic<LogLevel::value> _limit_level;Formatter::ptr _formatter;std::vector<LogSink::ptr> _sinks;}; //同步日志器class SyncLogger : public Logger{public:SyncLogger(const std::string &logger_name,LogLevel::value level,Formatter::ptr &formatter,std::vector<LogSink::ptr> &sinks) : Logger(logger_name, level, formatter, sinks) {}//这些参数被传递给基类 Logger 的构造函数protected:// 同步日志器,是将日志通过落地模块句柄进行日志落地void log(const char *data, size_t len){std::unique_lock<std::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::vector<LogSink::ptr> &sinks,AsyncType looper_type) : Logger(logger_name, level, formatter, sinks),_looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::realLog, this, std::placeholders::_1), looper_type)) {}// 将数据写入缓冲区void log(const char *data, size_t len){_looper->push(data, len);}// 设计一个实际落地函数(将缓冲区中的数据落地)void realLog(Buffer &buf){//std::cout<<buf._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_shared<Formatter>(pattern);}template <typename SinkType, typename... Args>void buildSink(Args &&...args){LogSink::ptr psink = SinkFactory::create<SinkType>(std::forward<Args>(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::vector<LogSink::ptr> _sinks;};// 2.派生出具体的建造者类class LocalLoggerBuilder : public LoggerBuilder{public:Logger::ptr build() override{assert(!_logger_name.empty()); // 必须有日志器名称if (_formatter.get() == nullptr){_formatter = std::make_shared<Formatter>();}if (_sinks.empty()){buildSink<StdoutSink>();}if (_logger_type == LoggerType::LOGGER_ASYNC){return std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);}return std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);}};// 日志器管理器class LoggerManager{public:static LoggerManager& getInstance(){//在C++11之后,针对静态局部对象,编译器在编译的层面实现了线程安全//当静态局部变量在没有构造完成之前,其他的线程进入就会阻塞static LoggerManager eton;return eton;}void addLogger(Logger::ptr &logger){if(hasLogger(logger->name())) return;std::unique_lock<std::mutex> lock(_mutex);_loggers.insert(std::make_pair(logger->name(), logger));}bool hasLogger(const std::string &name){std::unique_lock<std::mutex> lock(_mutex);auto it=_loggers.find(name);if(it==_loggers.end()){return false;}return true;}Logger::ptr getLogger(const std::string &name){std::unique_lock<std::mutex> lock(_mutex);auto it=_loggers.find(name);if(it==_loggers.end()){return Logger::ptr();}return it->second;}Logger::ptr rootLogger(){return _root_logger;}private:LoggerManager() {std::unique_ptr<logsystem::LoggerBuilder> builder(new logsystem::LocalLoggerBuilder());builder->buildLoggerName("root");_root_logger=builder->build();_loggers.insert(std::make_pair("root", _root_logger));}private:std::mutex _mutex;Logger::ptr _root_logger;//默认日志器std::unordered_map<std::string, Logger::ptr> _loggers;};//全局日志器的建造者---在局部的基础上增加了一个功能:将日志器添加到单例对象中class GlobalLoggerBuilder : public LoggerBuilder{public:Logger::ptr build() override{assert(_logger_name.empty() == false); // 必须有日志器名称if (_formatter.get() == nullptr){_formatter = std::make_shared<Formatter>();}if (_sinks.empty()){buildSink<StdoutSink>();}Logger::ptr logger;if (_logger_type == LoggerType::LOGGER_ASYNC){logger = std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);}else{logger = std::make_shared<SyncLogger>(_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_SIZE+len;// 大于阈值,线性增长}_buffer.resize(new_size);}// 对写指针进行向后偏移void moveWriter(size_t len){assert((len + _writer_idx) <= _buffer.size());_writer_idx += len;}private:std::vector<char> _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.hpp"namespace logsystem
{using Functor = std::function<void(Buffer &)>;enum class AsyncType{ASYNC_SAFE, // 安全状态,表示缓冲区满了则阻塞,避免资源耗尽的发现ASYNC_UNSAFE // 不考虑资源耗尽的问题,无限扩容,常用于测试};class AsyncLooper{public:using ptr = std::shared_ptr<AsyncLooper>;// 新线程将要执行的函数:&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_lock<std::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_lock<std::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_type==AsyncType::ASYNC_SAFE){// 2.唤醒所有生产者_cond_pro.notify_all(); }}// 3.被唤醒后对消费者缓冲区进行数据处理_callBack(_con_buf);// 4.初始化消费者缓冲区_con_buf.reset();}}private:Functor _callBack; // 具体对缓冲区数据进行处理的回调函数,由异步工作器使用者传入private:AsyncType _looper_type;std::atomic<bool> _stop; // 工作器停止的标志Buffer _pro_buf; // 生产缓冲区Buffer _con_buf; // 消费缓冲区std::mutex _mutex;std::condition_variable _cond_pro;std::condition_variable _cond_con;std::thread _thread; // 异步工作器对应的工作线程};}#endif
7.9 单例日志器管理类
日志的输出,我们希望在任意位置都可以进行,但是我们创建一个日志器后,就会受到日志器所在域的访问属性限制
因此为了突破访问区域的限制,我们创建一个日志器管理类,这个类是一个单例类,这样的话我们就可以在任意位置来通过管理器单例获取日志器来进行日志输出了
基于单例日志器管理器的设计思想,我们对于日志器建造者类进行继承,继承出一个全局日志器建造者类,实现一个日志器在创建完毕后,直接将其添加到单例的日志器管理器中,以便能够在任何位置通过日志器名称获取指定的日志器进行日志输出
// 日志器管理器class LoggerManager{public:static LoggerManager& getInstance(){//在C++11之后,针对静态局部对象,编译器在编译的层面实现了线程安全//当静态局部变量在没有构造完成之前,其他的线程进入就会阻塞static LoggerManager eton;return eton;}void addLogger(Logger::ptr &logger){if(hasLogger(logger->name())) return;std::unique_lock<std::mutex> lock(_mutex);_loggers.insert(std::make_pair(logger->name(), logger));}bool hasLogger(const std::string &name){std::unique_lock<std::mutex> lock(_mutex);auto it=_loggers.find(name);if(it==_loggers.end()){return false;}return true;}Logger::ptr getLogger(const std::string &name){std::unique_lock<std::mutex> lock(_mutex);auto it=_loggers.find(name);if(it==_loggers.end()){return Logger::ptr();}return it->second;}Logger::ptr rootLogger(){return _root_logger;}private:LoggerManager() {std::unique_ptr<logsystem::LoggerBuilder> builder(new logsystem::LocalLoggerBuilder());builder->buildLoggerName("root");_root_logger=builder->build();_loggers.insert(std::make_pair("root", _root_logger));}private:std::mutex _mutex;Logger::ptr _root_logger;//默认日志器std::unordered_map<std::string, Logger::ptr> _loggers;};
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.hpp"namespace 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__);}#endif
8.功能测试
该模块编写的是项目编写过程中每一模块的测试代码,保证每个模块可以正常使用。可以用于测试日志器中各个模块的工作是否正常,测试一个日志器中包含所有的落地方向,观察是否每个方向都正常落地,分别测试同步方式和异步方式落地后数据是否正常
#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_size==1 ? 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 cur=logsystem::util::Date::now();if(cur % _gap_size == _cur_gap){_ofs.close();std::string filename=createNewFile();_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, <);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 logger=logsystem::LoggerManager::getInstance().getLogger(name);logger->debug("%s", "测试日志");logger->info("%s", "测试日志");logger->warn("%s", "测试日志");logger->error("%s", "测试日志");logger->fatal("%s", "测试日志");size_t cursize=0, count=0;//std::string str="测试日志-";while(cursize < 1024*1024*5){logger->fatal("测试日志-%d", count++);cursize+=40;}INFO("%s", "测试结束");// DEBUG("%s", "测试日志");// INFO("%s", "测试日志");// WARN("%s", "测试日志");// ERROR("%s", "测试日志");// FATAL("%s", "测试日志");// size_t cursize=0, count=0;// //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 fsize=ifs.tellg();//获取当前读写位置想对于起始位置的偏移量// ifs.seekg(0, std::ios::beg);//重新跳转到起始位置// std::string body;// body.resize(fsize);// ifs.read(&body[0], fsize);// if(ifs.good()==false) {std::cout<<"read error\n"; return -1;}// ifs.close();// //std::cout<<fsize<<std::endl;// logsystem::Buffer buffer;// for(int i=0;i<body.size();i++)// {// buffer.push(&body[i], 1);// //std::cout<<buffer._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 rsize=buffer.readAbleSize();// std::cout<<rsize<<std::endl;// for(int i=0;i<rsize;i++)// {// ofs.write(buffer.begin(), 1);// buffer.moveReader(1);// }// if(ofs.good()==false) {std::cout<<"write error\n"; return -1;}// ofs.close();// std::unique_ptr<logsystem::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->buildSink<logsystem::StdoutSink>();// builder->buildSink<logsystem::FileSink>("./logfile/async.log");// builder->buildSink<logsystem::RollBySizeSink>("./logfile/roll-asyncbysize-", 1024*1024);// builder->build();// test_log("async_logger");// std::string logger_name="sync_logger";// logsystem::LogLevel::value limit=logsystem::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_lsp=logsystem::SinkFactory::create<logsystem::StdoutSink>();// logsystem::LogSink::ptr file_lsp=logsystem::SinkFactory::create<logsystem::FileSink>("./logfile/test.log");// logsystem::LogSink::ptr roll_lsp=logsystem::SinkFactory::create<logsystem::RollBySizeSink>("./logfile/roll-", 1024*1024);// std::vector<logsystem::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 cursize=0, count=0;// std::string str="测试日志-";// while(cursize< 1024*1024*10)// {// logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);// cursize+=60;// }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::create<logsystem::StdoutSink>();// logsystem::LogSink::ptr file_lsp = logsystem::SinkFactory::create<logsystem::FileSink>("./logfile/test.log");// logsystem::LogSink::ptr roll_lsp = logsystem::SinkFactory::create<logsystem::RollBySizeSink>("./logfile/roll-", 1024 * 1024);logsystem::LogSink::ptr time_lsp = logsystem::SinkFactory::create<RollByTimeSink>("./logfile/roll-", TimeGap::GAP_TEST);time_t old=logsystem::util::Date::now();while(logsystem::util::Date::now() < old+20){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::cout<<logsystem::LogLevel::toString(logsystem::LogLevel::value::UNKNOW)<<std::endl;// std::cout<<logsystem::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 <chrono>void 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)/1024<<"KB\n";// 2.组织指定长度的日志消息std::string msg(msg_len - 1, 'A'); // 少一个字节,是为了给末尾到时候添加换行// 3.创建指定数量的线程std::vector<std::thread> threads;std::vector<double> 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 start=std::chrono::high_resolution_clock::now();//5.开始循环写日志for(int j=0; j<msg_per_thr; j++){logger->fatal("%s", msg.c_str());}//6.线程函数内部结束计时auto end=std::chrono::high_resolution_clock::now();std::chrono::duration<double> 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_cost<<"s\n";std::cout<<"平均每秒输出日志数量:"<<msg_per_sec<<"条\n";std::cout<<"平均每秒输出日志大小:"<<size_per_sec<<"KB\n";std::cout<<std::endl;
}void sync_bench()
{std::unique_ptr<logsystem::LoggerBuilder> builder(new logsystem::GlobalLoggerBuilder());builder->buildLoggerName("sync_logger");builder->buildFormatter("%m%n");builder->buildLoggerType(logsystem::LoggerType::LOGGER_SYNC);builder->buildSink<logsystem::FileSink>("./logfile/sync.log");builder->build();bench("sync_logger", 3, 1000000, 100);
}void async_bench()
{std::unique_ptr<logsystem::LoggerBuilder> builder(new logsystem::GlobalLoggerBuilder());builder->buildLoggerName("async_logger");builder->buildFormatter("%m%n");builder->buildLoggerType(logsystem::LoggerType::LOGGER_ASYNC);builder->buildEnableUnSafeAsync();//开启非安全模式--主要是为了将实际落地时间排除在外builder->buildSink<logsystem::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.引⼊⼀些设计模式:⼯⼚、单例、建造者、代理来增加⼯程代码的可扩展性、可维护性。