【C++】基于多设计模式下的同步&异步日志系统

作者阿润021
📖专栏C++



一、项目介绍

简介

本项目主要实现一个日志系统,它可以根据不同的级别、配置和策略,以同步或异步的方式,将日志信息可靠地写入控制台文件或滚动文件中,同时支持多线程并发写日志和扩展不同的日志落地目标地模式。

开发环境
• CentOS 7
• vscode/vim
• g++/gdb
• Makefile

核心技术

1.类层次设计:如继承和多态的应用
2.C++11:如多线程、auto、智能指针、右值引⽤等
3. 双缓冲区设计:实现异步日志器
4. 生产消费模型:
5. 多线程:实现并发输出日志
6. 设计模式:单例日志管理器类设计、⼯⼚模式、代理模式、模板等

环境搭建

本项目不依赖其他任何第三⽅库, 只需要安装好CentOS/Ubuntu + vscode/vim环境即可开发。
测试结果展示:
在这里插入图片描述

为什么需要日志系统?

• 生产环境的产品为了保证其稳定性及安全性是不允许开发⼈员附加调试器去排查问题, 可以借助日志志系统来打印⼀些日志帮助开发⼈员解决问题

• 上线客户端的产品出现bug无法复现并解决, 可以借助⽇志系统打印⽇志并上传到服务端帮助开发⼈员进⾏分析

• 对于⼀些⾼频操作(如定时器、心跳包)在少量调试次数下可能⽆法触发我们想要的⾏为,通过断点的暂停⽅式,我们不得不重复操作⼏⼗次、上百次甚⾄更多,导致排查问题效率是⾮常低下, 可以借助打印⽇志的⽅式查问题

• 在分布式、多线程/多进程代码中, 出现bug⽐较难以定位, 可以借助⽇志系统打印log帮助定位bug

• 帮助首次接触项目代码的新开发人员理解代码的运⾏流程

二、项目实现准备工作

1.日志系统技术实现策略

日志系统的技术实现主要包括三种类型:

  1. 利用printf、std::cout等输出函数将⽇志信息打印到控制台

  2. 对于⼤型商业化项目, 为了放便排查问题,我们⼀般会将⽇志输出到⽂件或者是数据库系统⽅便查询和分析日志, 主要分为同步日志和异步日志方式:
    1> 同步写日志
    2> 异步写日志

同步写日志

同步⽇志是指当输出⽇志时,必须等待⽇志输出语句执⾏完毕后,才能执⾏后⾯的业务逻辑语句,⽇志输出语句与程序的业务逻辑语句将在同⼀个线程运⾏。每次调⽤⼀次打印⽇志API就对应⼀次系统调⽤write写⽇志⽂件。
在这里插入图片描述
但是在⾼并发场景下,随着⽇志数量不断增加,同步⽇志系统容易产⽣系统瓶颈:
• ⼀⽅⾯,⼤量的⽇志打印陷⼊等量的write系统调⽤,有⼀定系统开销.
• 另⼀⽅⾯,使得打印⽇志的进程附带了⼤量同步的磁盘IO,影响程序性能

异步写日志

异步日志是指在进行日志输出时,日志输出语句与业务逻辑语句并不是在同⼀个线程中运⾏,而是有专⻔的线程⽤于进行日志输出操作。业务线程只需要将⽇志放到⼀个内存缓冲区中不用等待即可继续执执后续业务逻辑(作为日志的生产者),而日志的落地操作交给单独的日志线程去完成(作为日志的消费者), 这是⼀个典型的生产-消费模型。
在这里插入图片描述
这样做的好处是即使⽇志没有真的地完成输出也不会影响程序的主业务,可以提⾼程序的性能:
• 主线程调⽤⽇志打印接⼝成为⾮阻塞操作
• 同步的磁盘IO从主线程中剥离出来交给单独的线程完成

2.相关技术知识补充

2.1 不定参函数设计

在初学C语⾔的时候,我们都⽤过printf函数进⾏打印。其中printf函数就是⼀个不定参函数,在函数内部可以根据格式化字符串中格式化字符分别获取不同的参数进⾏数据的格式化。
而这种不定参函数在实际的使⽤中也⾮常多⻅,在这⾥简单做⼀介绍:

C⻛格不定参函数

#include <iostream>
#include <cstdarg>
void printNum(int n, ...) {
 va_list al;
 va_start(al, n);//让al指向n参数之后的第⼀个可变参数
 for (int i = 0; i < n; i++) {
 int num = va_arg(al, int);//从可变参数中取出⼀个整形参数
 std::cout << num << std::endl;
 }
 va_end(al);//清空可变参数列表--其实是将al置空
}
int main()
{
 printNum(3, 11,22,33);
 printNum(5, 44,55,66,77,88);
 return 0;
}

C++⻛格不定参函数

#include <iostream>
#include <cstdarg>
#include <memory>
#include <functional>
void xprintf() {
 std::cout << std::endl;
}
template<typename T, typename ...Args>
void xprintf(const T &value, Args &&...args) {
 std::cout << value << " ";
 if ((sizeof ...(args)) > 0) {
 xprintf(std::forward<Args>(args)...);
 }else {
 xprintf();
 }
}
int main()
{
 xprintf("张三");
 xprintf("张三", 666);
 xprintf("张三", "法外狂徒", 666);
 return 0;
}

2.2 设计模式

设计模式是前辈们对代码开发经验的总结,是解决特定问题的⼀系列套路。它不是语法规定,⽽是⼀套⽤来提⾼代码可复⽤性、可维护性、可读性、稳健性以及安全性的解决⽅案。

六大原则:
从整体上来理解六⼤设计原则,可以简要的概括为⼀句话,⽤抽象构建框架,⽤实现扩展细节,具体到每⼀条设计原则,则对应⼀条注意事项:
• 单⼀职责原则告诉我们实现类要职责单⼀;
• ⾥⽒替换原则告诉我们不要破坏继承体系;
• 依赖倒置原则告诉我们要⾯向接⼝编程;
• 接⼝隔离原则告诉我们在设计接⼝的时候要精简单⼀;
• 迪⽶特法则告诉我们要降低耦合;
• 开闭原则是总纲,告诉我们要对扩展开放,对修改关闭

单例模式

⼀个类只能创建⼀个对象,即单例模式,该设计模式可以保证系统中该类只有⼀个实例,并提供⼀个访问它的全局访问点,该实例被所有程序模块共享。⽐如在某个服务器程序中,该服务器的配置信息存放在⼀个⽂件中,这些配置数据由⼀个单例对象统⼀读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种⽅式简化了在复杂环境下的配置管理。
单例模式有两种实现模式:饿汉模式和懒汉模式

• 饿汉模式: 程序启动时就会创建⼀个唯⼀的实例对象。 因为单例对象已经确定, 所以⽐较适⽤于多线程环境中, 多线程获取单例对象不需要加锁, 可以有效的避免资源竞争, 提⾼性能。

// 饿汉模式
template<typename T>
class Singleton {
private:
 static Singleton _eton;
private:
 Singleton(){}
 ~Singleton(){}
public:
 Singleton(const Singleton&) = delete;
 Singleton& operator=(const Singleton&) = delete;
 static T& getInstance() 
 {
 return _eton;
 }
};
Singleton Singleton::_eton;

懒汉模式:第⼀次使⽤要使⽤单例对象的时候创建实例对象。如果单例对象构造特别耗时或者耗费济源(加载插件、加载⽹络资源等), 可以选择懒汉模式, 在第⼀次使⽤的时候才创建对象。

注:这里介绍的是《Effective C++》⼀书作者 Scott Meyers 提出的⼀种更加优雅简便的单例模式

// 懒汉模式
template <typename T> 
class Singleton { 
private:
 Singleton(){}
 ~Singleton(){}
public: 
 Singleton(const Singleton&) = delete;
 Singleton& operator=(const Singleton&) = delete;
 static T& getInstance() 
 { 
 static Singleton _eton;
 return _eton; 
 } 
};

⼯⼚模式
⼯⼚模式是⼀种创建型设计模式, 它提供了⼀种创建对象的最佳⽅式。在⼯⼚模式中,我们创建对象时不会对上层暴露创建逻辑,⽽是通过使⽤⼀个共同结构来指向新创建的对象,以此实现创建-使⽤的分离。

工厂模式可以分为: 简单工厂模式、 ⼯⼚⽅法模式、抽象⼯⼚模式

简单⼯⼚模式: 简单⼯⼚模式实现由⼀个⼯⼚对象通过类型决定创建出来指定产品类的实例。假设有个⼯⼚能⽣产出⽔果,当客⼾需要产品的时候明确告知⼯⼚⽣产哪类⽔果,⼯⼚需要接收⽤⼾提供的类别信息,当新增产品的时候,⼯⼚内部去添加新产品的⽣产⽅式。

//简单⼯⼚模式:通过参数控制可以⽣产任何产品
// 优点:简单粗暴,直观易懂。使⽤⼀个⼯⼚⽣产同⼀等级结构下的任意产品
// 缺点:
// 1. 所有东西⽣产在⼀起,产品太多会导致代码量庞⼤
// 2. 开闭原则遵循(开放拓展,关闭修改)的不是太好,要新增产品就必须修改⼯⼚⽅法。
#include <iostream>
#include <string>
#include <memory>
class Fruit {
public:
 Fruit(){}
 virtual void show() = 0;
};
class Apple : public Fruit {
 public:
 Apple() {}
 virtual void show() {
 std::cout << "我是⼀个苹果" << std::endl;
 }
};
class Banana : public Fruit {
 public:
 Banana() {}
 virtual void show() {
 std::cout << "我是⼀个⾹蕉" << std::endl;
 }
};
class FruitFactory {
 public:
 static std::shared_ptr<Fruit> create(const std::string &name) {
 if (name == "苹果") {
 return std::make_shared<Apple>();
 }else if(name == "⾹蕉") {
 return std::make_shared<Banana>();
 }
 return std::shared_ptr<Fruit>();
 }
};
int main()
{
 std::shared_ptr<Fruit> fruit = FruitFactory::create("苹果");
 fruit->show();
 fruit = FruitFactory::create("⾹蕉");
 fruit->show();
 return 0;
}

这个模式的结构和管理产品对象的⽅式⼗分简单, 但是它的扩展性⾮常差,当我们需要新增产品的时候,就需要去修改⼯⼚类新增⼀个类型的产品创建逻辑,违背了开闭原则。

⼯⼚⽅法模式: 在简单⼯⼚模式下新增多个⼯⼚,多个产品,每个产品对应⼀个⼯⼚。假设现在有A、B 两种产品,则开两个⼯⼚,⼯⼚ A 负责⽣产产品 A,⼯⼚ B 负责⽣产产品 B,⽤⼾只知道产品的⼯⼚名,⽽不知道具体的产品信息,⼯⼚不需要再接收客⼾的产品类别,⽽只负责⽣产产品。

#include <iostream>
#include <string>
#include <memory>
//⼯⼚⽅法:定义⼀个创建对象的接⼝,但是由⼦类来决定创建哪种对象,使⽤多个⼯⼚分别⽣产指定
的固定产品
// 优点: 
// 1. 减轻了⼯⼚类的负担,将某类产品的⽣产交给指定的⼯⼚来进⾏
// 2. 开闭原则遵循较好,添加新产品只需要新增产品的⼯⼚即可,不需要修改原先的⼯⼚类
// 缺点:对于某种可以形成⼀组产品族的情况处理较为复杂,需要创建⼤量的⼯⼚类.
class Fruit {
 public:
 Fruit(){}
 virtual void show() = 0;
};
class Apple : public Fruit {
 public:
 Apple() {}
 virtual void show() {
 std::cout << "我是⼀个苹果" << std::endl;
 }
 private:
 std::string _color;
};
class Banana : public Fruit {
 public:
 Banana() {}
 virtual void show() {
 std::cout << "我是⼀个⾹蕉" << std::endl;
 }
};
class FruitFactory {
 public:
 virtual std::shared_ptr<Fruit> create() = 0;
};
class AppleFactory : public FruitFactory {
 public:
 virtual std::shared_ptr<Fruit> create() {
 return std::make_shared<Apple>();
 }
};
class BananaFactory : public FruitFactory {
 public:
 virtual std::shared_ptr<Fruit> create() {
 return std::make_shared<Banana>();
 }
};
int main()
{
 std::shared_ptr<FruitFactory> factory(new AppleFactory());
 fruit = factory->create();
 fruit->show();
 factory.reset(new BananaFactory());
 fruit = factory->create();
 fruit->show();
 return 0;
}

⼯⼚⽅法模式每次增加⼀个产品时,都需要增加⼀个具体产品类和⼯⼚类,这会使得系统中类的个数成倍增加,在⼀定程度上增加了系统的耦合度。

抽象⼯⼚模式: ⼯⼚⽅法模式通过引⼊⼯⼚等级结构,解决了简单⼯⼚模式中⼯⼚类职责太重的问题,但由于⼯⼚⽅法模式中的每个⼯⼚只⽣产⼀类产品,可能会导致系统中存在⼤量的⼯⼚类,势必会增加系统的开销。此时,我们可以考虑将⼀些相关的产品组成⼀个产品族(位于不同产品等级结构中功能相关联的产品组成的家族),由同⼀个⼯⼚来统⼀⽣产,这就是抽象⼯⼚模式的基本思想。

#include <iostream>
#include <string>
#include <memory>
//抽象⼯⼚:围绕⼀个超级⼯⼚创建其他⼯⼚。每个⽣成的⼯⼚按照⼯⼚模式提供对象。
// 思想:将⼯⼚抽象成两层,抽象⼯⼚ & 具体⼯⼚⼦类, 在⼯⼚⼦类种⽣产不同类型的⼦产品
class Fruit {
 public:
 Fruit(){}
 virtual void show() = 0;
};
class Apple : public Fruit {
 public:
 Apple() {}
 virtual void show() {
 std::cout << "我是⼀个苹果" << std::endl;
 }
 private:
 std::string _color;
};
class Banana : public Fruit {
 public:
 Banana() {}
 virtual void show() {
 std::cout << "我是⼀个⾹蕉" << std::endl;
 }
};
class Animal {
 public:
 virtual void voice() = 0;
};
class Lamp: public Animal {
 public:
 void voice() { std::cout << "咩咩咩\n"; }
};
class Dog: public Animal {
 public:
 void voice() { std::cout << "汪汪汪\n"; }
};
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<Animal> getAnimal(const std::string &name) {
 return std::shared_ptr<Animal>();
 }
 virtual std::shared_ptr<Fruit> getFruit(const std::string &name) {
 if (name == "苹果") {
 return std::make_shared<Apple>();
 }else if(name == "⾹蕉") {
 return std::make_shared<Banana>();
 }
 return std::shared_ptr<Fruit>();
 }
};
class AnimalFactory : public Factory {
 
 public:
 virtual std::shared_ptr<Fruit> getFruit(const std::string &name) {
 return std::shared_ptr<Fruit>();
 }
 virtual std::shared_ptr<Animal> getAnimal(const std::string &name) {
 if (name == "⼩⽺") {
 return std::make_shared<Lamp>();
 }else if(name == "⼩狗") {
 return std::make_shared<Dog>();
 }
 return std::shared_ptr<Animal>();
 }
};
class FactoryProducer {
 public:
 static std::shared_ptr<Factory> getFactory(const std::string &name) {
 if (name == "动物") {
 return std::make_shared<AnimalFactory>();
 }else {
 return std::make_shared<FruitFactory>();
 }
 }
};
int main()
{
 std::shared_ptr<Factory> fruit_factory = FactoryProducer::getFactory("⽔
果");
 std::shared_ptr<Fruit> fruit = fruit_factory->getFruit("苹果");
 fruit->show();
 fruit = fruit_factory->getFruit("⾹蕉");
 fruit->show();
 std::shared_ptr<Factory> animal_factory = FactoryProducer::getFactory("动
物");
 std::shared_ptr<Animal> animal = animal_factory->getAnimal("⼩⽺");
 animal->voice();
 animal = animal_factory->getAnimal("⼩狗");
 animal->voice();
 return 0;
}

抽象⼯⼚模式适⽤于⽣产多个⼯⼚系列产品衍⽣的设计模式,增加新的产品等级结构复杂,需要对原有系统进⾏较⼤的修改,甚⾄需要修改抽象层代码,违背了“开闭原则”。

建造者模式

建造者模式是⼀种创建型设计模式, 使⽤多个简单的对象⼀步⼀步构建成⼀个复杂的对象,能够将⼀个复杂的对象的构建与它的表⽰分离,提供⼀种创建对象的最佳⽅式。主要⽤于解决对象的构建过于复杂的问题。

建造者模式主要基于四个核⼼类实现:
• 抽象产品类:
• 具体产品类:⼀个具体的产品对象类
• 抽象Builder类:创建⼀个产品对象所需的各个部件的抽象接⼝
• 具体产品的Builder类:实现抽象接⼝,构建各个部件
• 指挥者Director类:统⼀组建过程,提供给调⽤者使⽤,通过指挥者来构造产品
在这个项目中我们用不到指挥者类

#include <iostream>
#include <memory>
/*抽象电脑类*/
class Computer {
 public:
 using ptr = std::shared_ptr<Computer>;
 Computer() {}
 void setBoard(const std::string &board) {_board = board;}
 void setDisplay(const std::string &display) {_display = display;}
 virtual void setOs() = 0;
 std::string toString() {
 std::string computer = "Computer:{\n";
 computer += "\tboard=" + _board + ",\n"; 
 computer += "\tdisplay=" + _display + ",\n"; 
 computer += "\tOs=" + _os + ",\n"; 
 computer += "}\n";
 return computer;
 }
 protected:
 std::string _board;
 std::string _display;
 std::string _os;
};
/*具体产品类*/
class MacBook : public Computer {
 public:
 using ptr = std::shared_ptr<MacBook>;
 MacBook() {}
 virtual void setOs() {
 _os = "Max Os X12";
 }
};
/*抽象建造者类:包含创建⼀个产品对象的各个部件的抽象接⼝*/
class Builder {
 public:
 using ptr = std::shared_ptr<Builder>;
 virtual void buildBoard(const std::string &board) = 0;
 virtual void buildDisplay(const std::string &display) = 0;
 virtual void buildOs() = 0;
 virtual Computer::ptr build() = 0;
};
/*具体产品的具体建造者类:实现抽象接⼝,构建和组装各个部件*/
class MacBookBuilder : public Builder {
 public:
 using ptr = std::shared_ptr<MacBookBuilder>;
 MacBookBuilder(): _computer(new MacBook()) {}
 virtual void buildBoard(const std::string &board) {
 _computer->setBoard(board);
 }
 virtual void buildDisplay(const std::string &display) {
 _computer->setDisplay(display);
 }
 virtual void buildOs() {
 _computer->setOs();
 }
 virtual Computer::ptr build() {
 return _computer;
 }
 private:
 Computer::ptr _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:
 Builder::ptr _builder;
};
int main()
{
 Builder *buidler = new MackBookBuilder();
 std::unique_ptr<Director> pd(new Director(buidler));
 pd->construct("英特尔主板", "VOC显⽰器");
 Computer::ptr computer = buidler->build();
 std::cout << computer->toString();
 return 0;
}

代理模式

代理模式指代理控制对其他对象的访问, 也就是代理对象控制对原对象的引⽤。在某些情况下,⼀个对象不适合或者不能直接被引⽤访问,⽽代理对象可以在客⼾端和⽬标对象之间起到中介的作⽤。
代理模式的结构包括⼀个是真正的你要访问的对象(⽬标类)、⼀个是代理对象。⽬标对象与代理对象实现同⼀个接口,先访问代理类再通过代理类访问⽬标对象。代理模式分为静态代理、动态代理:

• 静态代理指的是,在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪个被代理类。
• 动态代理指的是,在运⾏时才动态⽣成代理类,并将其与被代理类绑定。这意味着,在运⾏时才能确定代理类要代理的是哪个被代理类。

以租房为例,房东将房⼦租出去,但是要租房⼦出去,需要发布招租启⽰, 带⼈看房,负责维修,这些⼯作中有些操作并⾮房东能完成,因此房东为了图省事,将房⼦委托给中介进⾏租赁。 代理模式实现:

/*房东要把⼀个房⼦通过中介租出去理解代理模式*/
#include <iostream>
#include <string>
class RentHouse {
 public:
 virtual void rentHouse() = 0;
};
/*房东类:将房⼦租出去*/
class Landlord : public RentHouse {
 public: 
 void rentHouse() {
 std::cout << "将房⼦租出去\n";
 }
};
/*中介代理类:对租房⼦进⾏功能加强,实现租房以外的其他功能*/
class Intermediary : public RentHouse {
public:
 void rentHouse() {
 std::cout << "发布招租启⽰\n";
 std::cout << "带⼈看房\n";
 _landlord.rentHouse();
 std::cout << "负责租后维修\n";
 }
 private:
 Landlord _landlord;
};
int main()
{
 Intermediary intermediary;
 intermediary.rentHouse();
 return 0;
}

三、日志项目框架设计

本项⽬实现的是⼀个多⽇志器⽇志系统,主要实现的功能是让程序员能够轻松的将程序运⾏⽇志信息落地到指定的位置,且⽀持同步与异步两种⽅式的⽇志落地方式。
项⽬的框架设计将项⽬分为以下⼏个模块来实现。

1.模块划分

⽇志等级模块:对输出⽇志的等级进⾏划分,以便于控制⽇志的输出,并提供等级枚举转字符串功能。
◦ OFF:关闭
◦ DEBUG:调试,调试时的关键信息输出。
◦ INFO:提⽰,普通的提⽰型⽇志信息。
◦ WARN:警告,不影响运⾏,但是需要注意⼀下的⽇志。
◦ ERROR:错误,程序运⾏出现错误的⽇志
◦ FATAL:致命,⼀般是代码异常导致程序⽆法继续推进运⾏的⽇志

⽇志消息模块:中间存储⽇志输出所需的各项要素信息
◦ 时间:描述本条⽇志的输出时间。
◦ 线程ID:描述本条⽇志是哪个线程输出的。
◦ ⽇志等级:描述本条⽇志的等级。
◦ ⽇志数据:本条⽇志的有效载荷数据。
◦ ⽇志⽂件名:描述本条⽇志在哪个源码⽂件中输出的。
◦ ⽇志⾏号:描述本条⽇志在源码⽂件的哪⼀⾏输出的。

⽇志消息格式化模块:设置⽇志输出格式,并提供对⽇志消息进⾏格式化功能。
◦ 系统的默认⽇志输出格式:%d{%H:%M:%S}%T[%t]%T[%p]%T[%c]%T%f:%l%T%m%n
◦ -> 13:26:32 [2343223321] [FATAL] [root] main.c:76 套接字创建失败\n
◦ %d{%H:%M:%S}:表⽰⽇期时间,花括号中的内容表⽰⽇期时间的格式。
◦ %T:表⽰制表符缩进。
◦ %t:表⽰线程ID
◦ %p:表⽰⽇志级别
◦ %c:表⽰⽇志器名称,不同的开发组可以创建⾃⼰的⽇志器进⾏⽇志输出,⼩组之间互不影响。
◦ %f:表⽰⽇志输出时的源代码⽂件名。
◦ %l:表⽰⽇志输出时的源代码⾏号。
◦ %m:表⽰给与的⽇志有效载荷数据
◦ %n:表⽰换⾏
◦ 设计思想:设计不同的⼦类,不同的⼦类从⽇志消息中取出不同的数据进⾏处理。

⽇志消息落地模块:决定了⽇志的落地⽅向,可以是标准输出,也可以是⽇志⽂件,也可以滚动⽂件输出…
◦ 标准输出:表⽰将⽇志进⾏标准输出的打印。
◦ ⽇志⽂件输出:表⽰将⽇志写⼊指定的⽂件末尾。
◦ 滚动⽂件输出:当前以⽂件⼤⼩进⾏控制,当⼀个⽇志⽂件⼤⼩达到指定⼤⼩,则切换下⼀个⽂件进⾏输出
◦ 后期,也可以扩展远程⽇志输出,创建客⼾端,将⽇志消息发送给远程的⽇志分析服务器。
◦ 设计思想:设计不同的⼦类,不同的⼦类控制不同的⽇志落地⽅向。

⽇志器模块:
此模块是对以上⼏个模块的整合模块,⽤⼾通过⽇志器进⾏⽇志的输出,有效降低⽤⼾的使⽤难度。
◦ 包含有:⽇志消息落地模块对象,⽇志消息格式化模块对象,⽇志输出等级

⽇志器管理模块:
◦ 为了降低项⽬开发的⽇志耦合,不同的项⽬组可以有⾃⼰的⽇志器来控制输出格式以及落地⽅向,因此本项⽬是⼀个多⽇志器的⽇志系统。
◦ 管理模块就是对创建的所有⽇志器进⾏统⼀管理。并提供⼀个默认⽇志器提供标准输出的⽇志输出。

• 异步线程模块:
◦ 实现对⽇志的异步输出功能,⽤⼾只需要将输出⽇志任务放⼊任务池,异步线程负责⽇志的落地输出功能,以此提供更加⾼效的⾮阻塞⽇志输出。

2.各模块关系图

日志系统 1.写入指定位置 2.不同写入方式 3.多输出策略
在这里插入图片描述

四、详细代码实现

注意:由于此日志项目是多次迭代后完成的,从0开始描述迭代过程太冗长,故只介绍最终版本的实现思路

1.实用工具类设计

有一些我们会经常在项目用的零碎的功能接口,我们提前在一个类中实现。
• 获取系统时间
• 判断文件是否存在
• 获取文件的所在目录路径
• 创建一个目录

#ifndef _M_UTIL_H_
#define _M_UTIL_H_

//实用工具类的实现
// 1.获取系统时间
// 2.判断文件是否存在
// 3.获取文件所在路径
// 4.创建目录

#include<iostream>
#include<ctime>
#include<sys/stat.h>

namespace HUE 
{
    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.txt
                size_t pos =0, idx =0;//标定查找的起始位置

                while(idx < pathname.size())
                {
                    pos = pathname.find_first_of("/\\",idx);
                if(pos == std::string::npos)
                {
                    mkdir(pathname.c_str(),0777);
                }
                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

2.日志等级类设计

我们将日志等级封装成一个类,定义出日志系统所包含的所以日志等级:
• UNKONW=0
• OFF 关闭所有⽇志输出
• DRBUG 进⾏debug时候打印⽇志的等级
• INFO 打印⼀些⽤⼾提⽰信息
• WARN 打印警告信息
• ERROR 打印错误信息
• FATAL 打印致命信息- 导致程序崩溃的信息

这里的思想是:每一个项目中都会设置一个默认的日志输出等级,只有输出的日志等级大于等于默认限制等级的时候才可以进行输出
在这里插入图片描述

//1.定义枚举类,枚举出日志等级
//2.提供转换接口:将枚举准换为对应字符串

#ifndef _M_LEVEL_H
#define _M_LEVEL_H

namespace HUE 
{
    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";   
            }
            return "UNKNOW";
        }
    };
}

#endif

3.日志消息类设计

我们⽇志消息类主要是封装⼀条完整的⽇志消息所需的内容,其中包括⽇志等级、对应的logger name、打印⽇志源⽂件的位置信息(包括⽂件名和⾏号)、线程ID、时间戳信息、具体的⽇志信息等内容。
在这里插入图片描述

#ifndef _M_MSG_H_
#define _M_MSG_H_
//⽇志消息类主要是封装⼀条完整的⽇志消息所需的内容,其中包括
//⽇志等级、对应的logger name、打印⽇志源⽂件的位置信息(包括⽂件名和⾏号)、线程ID、时间戳信息、具体的⽇志信息等内容

#include"level.hpp"
#include"util.hpp"
#include<thread>


namespace HUE
{
    struct LogMsg
    {
        time_t _ctime; //日志产生的时间戳
        LogLevel::value _level; //日志等级
        size_t _line;//行号
        std::thread::id _tid;//线程ID
        std::string _file;//源文件名
        std::string _logger;//日志器名称
        std::string _payload; //有效消息数据


        LogMsg(LogLevel::value level,
            size_t line,
            const std::string file,
            const std::string logger,
            const std::string msg)
            :_ctime(util::Date::now())
            ,_level(level)
            ,_line(line)
            ,_tid(std::this_thread::get_id())
            ,_file(file)
            ,_logger(logger)
            ,_payload(msg){}

    };

}

#endif

4.日志输出格式化模块

我们可以让用户自定义输出内容,通过对日志消息进行格式化,组织称为指定格式的字符串。

我们的设计思想就是:
1.抽象一个格式化子项基类
2.基于基类,派生出不同的格式化子项子类
比如说:主体消息、日志等级、时间子项、文件名、行号等等。
这样我们就可以在父类中定义父类指针的数组,用来指向不同的格式化子项子类对象嘛!
在这里插入图片描述

字符串解析设计 — 循环处理

我们规定字符串处理是一个循环的过程
while(){
1.处理原始字符串
2.原始字符串处理结束后,遇到%,则处理一个格式化字符串嘛
}
在这里插入图片描述
在处理过程中,我们需要将处理得到的信息保存下来,创建对应的格式化子项对象,添加到item成员数组中。

#ifndef _M_FMT_H_
#define _M_FMT_H_

#include "level.hpp"
#include "logmsg.hpp"
#include <ctime>
#include <vector>
#include <cassert>
#include <sstream>

namespace HUE
{
    // 抽象格式化子项基类
    class FromatIem
    {
    public:
        using ptr = std::shared_ptr<FromatIem>;
        virtual void format(std::ostream &out,const LogMsg &msg) = 0;
    };

    // 派生格式化子项子类 -- 消息 等级 时间 文件名 行号 线程ID 日志器名 制表符 换行 其他

    class LevelFormatItem : public FromatIem
    {
    public:
        void format(std::ostream &out,const LogMsg &msg) override
        {
            out << LogLevel::toString(msg._level);
        }
    };

    class TimeFormatItem : public FromatIem
    {
    public:
        TimeFormatItem(const std::string &fmt = "%H:%M:%S") : _time_fmt(fmt) {}
        void format(std::ostream &out,const LogMsg &msg) override
        {
            struct tm t;
            localtime_r(&msg._ctime, &t);
            char tmp[32] = {0};
            strftime(tmp, 31, _time_fmt.c_str(), &t);

            out << tmp;
        }

    private:
        std::string _time_fmt; // 格式 %H:%M:%S
    };

    class FileFormatItem : public FromatIem
    {
    public:
        void format(std::ostream &out,const LogMsg &msg) override
        {
            out << msg._file;
        }
    };

    class LineFormatItem : public FromatIem
    {
    public:
        void format(std::ostream &out,const LogMsg &msg) override
        {
            out << msg._line;
        }
    };

    class ThreadFormatItem : public FromatIem
    {
    public:
        void format(std::ostream &out,const LogMsg &msg) override
        {
            out << msg._tid;
        }
    };

    class LoggerFormatItem : public FromatIem
    {
    public:
        void format(std::ostream &out,const LogMsg &msg) override
        {
            out << msg._logger;
        }
    };

    class MsgFormatItem : public FromatIem
    {
    public:
        void format(std::ostream &out,const LogMsg &msg) override
        {
            out << msg._payload;
        }
    };

    class TabFormatItem : public FromatIem
    {
    public:
        void format(std::ostream &out,const LogMsg &msg) override
        {
            out << "\t";
        }
    };

    class NLineFormatItem : public FromatIem
    {
    public:
        void format(std::ostream &out, const LogMsg &msg) override
        {
            out << "\n";
        }
    };

    // 其他字符:
    class OtherFormatItem : public FromatIem
    {
    public:
        OtherFormatItem(const std::string &str) : _str(str) {}
        void format(std::ostream &out,const LogMsg &msg) override
        {
            out << _str;
        }

    private:
        std::string _str;
    };

    /*
         %d 时间
         %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进行格式化
        void format(std::ostream &out, const LogMsg &msg)
        {
            for (auto &item : _items)
            {
                item->format(out, msg); // 按照顺序把信息从msg取出来
            }
        }
        std::string format(const LogMsg &msg)
        {
            std::stringstream ss;
            format(ss, msg);
            return ss.str();
        }

        private:
        // 对格式化规则字符串进行解析
        bool parsePattern()
        {
           //1.对格式化规则字符串进行解析
            //ab%%cd[%d{%H:%M:%S}][%p][%f:%l]%T%m%n

            std::vector<std::pair<std::string,std::string>>fmt_order;
            size_t pos =0;

            std::string key,val;
            while (pos <_pattern.size())
            {
                //1. 判断处理原始字符串,是否为%,否为原始字符
                if(_pattern[pos] !='%')
                {
                    val.push_back(_pattern[pos++]); continue;
                }
                //能走下来代表pos位置为%,%%处理称为一个原始%字符
                if(_pattern[pos+1] =='%' && pos+1 < _pattern.size())
                {
                    val.push_back('%'); pos+=2; continue;
                }
                //能走下来代表%后面是个格式化字符,代表原始字符串处理完毕
                if(val.empty() == false){
                fmt_order.push_back(std::make_pair("",val));
                val.clear();
                }

                //格式化字符处理 
                pos+=1; //这一步之后,pos指向格式化字符位置
                if(pos == _pattern.size())
                {
                    std::cout << "%之后,没有对应的格式化字符!\n";
                    return false;
                }
                key = _pattern[pos];
                //+1此时pos指向格式化字符后的位置
                pos+=1;
                if(pos<_pattern.size() && _pattern[pos] =='{')
                {
                    pos+=1;//此时pos指向子规则的起始位置
                    while(pos<_pattern.size() && _pattern[pos] !='}')
                    {
                        val.push_back(_pattern[pos++]);
                    }
                    //如果是走到了末尾跳出循环,则代表没有遇到 },格式错误
                    if(pos == _pattern.size())
                    {
                        std::cout << "子规则{}匹配出错!\n";
                        return false;
                    }
                    pos +=1; //因为此时pos指向的 }位置,需要向后走一步,更新位置
                }   
                
                fmt_order.push_back(std::make_pair(key,val));
                key.clear();
                val.clear();
            }
            
            //2.根据解析得到的数据初始化格式化子项数组成员

            for(auto &it:fmt_order)
            {
                _items.push_back(createItem(it.first,it.second));
            }
            return true;
        }

        // 跟进不同的格式化字符创建不同的格式化子项对象
        FromatIem::ptr createItem(const std::string &key, const std::string &val)
        {
        if(key == "d") return std::make_shared<TimeFormatItem>(val);
        if(key == "t") return std::make_shared<ThreadFormatItem>();
        if(key == "c") return std::make_shared<LoggerFormatItem>();
        if(key == "f") return std::make_shared<FileFormatItem>();
        if(key == "l") return std::make_shared<LineFormatItem>();
        if(key == "p") return std::make_shared<LevelFormatItem>();
        if(key == "T") return std::make_shared<TabFormatItem>();
        if(key == "m") return std::make_shared<MsgFormatItem>();
        if(key == "n") return std::make_shared<NLineFormatItem>();
        if(key == "")  return std::make_shared<OtherFormatItem> (val);
        std::cout << "没有对应的格式化字符:%"<<key<<std::endl;
        abort();
        }

    private:
        std::string _pattern; // 格式化规则字符串
        std::vector<FromatIem::ptr> _items;
    };

}

#endif

5.日志落地模块(简单工厂模式)

日志落地类主要负责将格式化后的日志消息字符串,输出到指定位置。
在这里插入图片描述

它主要包括以下内容:
• Formatter⽇志格式化器:主要是负责格式化⽇志消息
• mutex互斥锁:保证多线程⽇志落地过程中的线程安全,避免出现交叉输出的情况。
这个类⽀持可扩展,其成员函数log设置为纯虚函数,当我们需要增加⼀个log输出⽬标, 可以增加⼀个类继承⾃该类并重写log⽅法实现具体的落地⽇志逻辑。

⽬前实现了三个不同⽅向上的⽇志落地:
• 标准输出:StdoutSink
• 固定⽂件:FileSink
• 滚动⽂件:RollSink

滚动⽇志⽂件输出的必要性:
▪ 由于机器磁盘空间有限, 我们不可能⼀直⽆限地向⼀个⽂件中增加数据
▪ 如果⼀个⽇志⽂件体积太⼤,⼀⽅⾯是不好打开,另⼀⽅⾯是即时打开了由于包含数据巨⼤,也不利于查找我们需要的信息
▪ 所以实际开发中会对单个⽇志⽂件的⼤⼩也会做⼀些控制,即当⼤⼩超过某个⼤⼩时(如1GB),我们就重新创建⼀个新的⽇志⽂件来滚动写⽇志。 对于那些过期的⽇志, ⼤部分企业内部都有专⻔的运维⼈员去定时清理过期的⽇志,或者设置系统定时任务,定时清理过期⽇志。

⽇志⽂件的滚动思想

⽇志⽂件滚动的条件有两个:⽂件⼤⼩ 和 时间。我们可以选择:
▪ ⽇志⽂件在⼤于 1GB 的时候会更换新的⽂件
▪ 每天定点滚动⼀个⽇志⽂件

本项⽬基于⽂件⼤⼩的判断滚动⽣成新的⽂件:

#ifndef __M_SINK_H__
#define __M_SINK_H__
#include "util.hpp"
#include "message.hpp"
#include "formatter.hpp"
#include <memory>
#include <mutex>
namespace bitlog{
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:
 using ptr = std::shared_ptr<StdoutSink>;
 StdoutSink() = default;
 void log(const char *data, size_t len) {
 std::cout.write(data, len);
 }
};
class FileSink : public LogSink {
 public:
 using ptr = std::shared_ptr<FileSink>;
 FileSink(const std::string &filename):_filename(filename) {
 util::file::create_directory(util::file::path(filename));
 _ofs.open(_filename, std::ios::binary | std::ios::app);
 assert(_ofs.is_open());
 }
 const std::string &file() {return _filename; }
 void log(const char *data, size_t len) {
 _ofs.write((const char*)data, len);
 if (_ofs.good() == false) {
 std::cout << "⽇志输出⽂件失败!\n";
 }
 }
 private:
 std::string _filename;
 std::ofstream _ofs;
};
class RollSink : public LogSink {
 public:
 using ptr = std::shared_ptr<RollSink>;
 RollSink(const std::string &basename, size_t max_fsize):
 _basename(basename), _max_fsize(max_fsize), _cur_fsize(0){
 util::file::create_directory(util::file::path(basename));
 }
 void log(const char *data, size_t len) {
 initLogFile();
 _ofs.write(data, len);
 if (_ofs.good() == false) {
 std::cout << "⽇志输出⽂件失败!\n";
 }
 _cur_fsize += len;
 }
 private:
 void initLogFile() {
 if (_ofs.is_open() == false || _cur_fsize >= _max_fsize) {
 _ofs.close();
 std::string name = createFilename();
 _ofs.open(name, std::ios::binary | std::ios::app);
 assert(_ofs.is_open());
 _cur_fsize = 0;
 return;
 }
 return;
 } 
 std::string createFilename() {
 time_t t = time(NULL);
 struct tm lt;
 localtime_r(&t, &lt);
 std::stringstream ss;
 ss << _basename;
 ss << lt.tm_year + 1900;
ss << lt.tm_mon + 1;
 ss << lt.tm_mday;
 ss << lt.tm_hour;
 ss << lt.tm_min;
 ss << lt.tm_sec;
 ss << ".log";
 return ss.str();
 }
 private:
 std::string _basename;
 std::ofstream _ofs;
 size_t _max_fsize;
 size_t _cur_fsize;
};
class SinkFactory {
 public:
 template<typename SinkType, typename ...Args>
 static LogSink::ptr create(Args &&...args) {
 return std::make_shared<SinkType>(std::forward<Args>(args)...);
 }
};
}
#endif

6.日志器类(Logger)设计(建造者模式)

在日志器模块里,我们需要对前面所有模块进行整合,向外提供接口完成不同等级的日志的输出.

⽇志器主要是⽤来和前端交互, 当我们需要使⽤⽇志系统打印log的时候, 只需要创建Logger对象,调⽤该对象debug、info、warn、error、fatal等⽅法输出⾃⼰想打印的⽇志即可,⽀持解析可变参数列表和输出格式, 即可以做到像使⽤printf函数⼀样打印⽇志。

当前⽇志系统⽀持同步⽇志 & 异步⽇志两种模式,两个不同的⽇志器唯⼀不同的地⽅在于他们在⽇志的落地⽅式上有所不同:
1.同步⽇志器:直接对⽇志消息进⾏输出。
2.异步⽇志器:将⽇志消息放⼊缓冲区,由异步线程进⾏输出。

因此⽇志器类在设计的时候先设计出⼀个Logger基类,在Logger基类的基础上,继承出SyncLogger同步⽇志器和AsyncLogger异步⽇志器。
且因为⽇志器模块是对前边多个模块的整合,想要创建⼀个⽇志器,需要设置⽇志器名称,设置⽇志输出等级,设置⽇志器类型,设置⽇志输出格式,设置落地⽅向,且落地⽅向有可能存在多个,整个⽇志器的创建过程较为复杂,为了保持良好的代码⻛格,编写出优雅的代码,因此⽇志器的创建这⾥采⽤了建造者模式来进⾏创建

详见代码(show the code):

/*日志器模块
    1.抽象日志器基类
    2.派生出不同的子类(同步日志器类&异步日志器类)
*/

#ifndef _M_LOGGER_H_
#define _M_LOGGER_H_

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "util.hpp"
#include "level.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "looper.hpp"
#include <atomic>
#include <mutex>
#include <cstdarg>
#include<unordered_map>

namespace HUE
{
    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);
            if (ret == -1)
            {
                std::cout << "vasprintf failed!\n";
                return;
            }
            va_end(ap); // 将ap指针置空

            serialize(LogLevel::value::DEBUG, file, line, res);
            free(res); // 防止内存 泄漏,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); // 防止内存 泄漏,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); // 防止内存 泄漏,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); // 防止内存 泄漏,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 == -1)
            {
                std::cout << "vasprintf failed!\n";
                return;
            }
            va_end(ap); // 将ap指针置空

            serialize(LogLevel::value::FATAL, file, line, res);
            free(res); // 防止内存 泄漏,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) {}

    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)
        {
            if (_sinks.empty()) return;
            for (auto &sink : _sinks)   {
                sink->log(buf.begin(),buf.readAbleSize());
            }
        }

    private:
        AsyncLooper::ptr _looper;
    };

    enum LoggerType
    {
        LOGGER_SYNC,
        LOGGER_ASYNC
    };

    /*使用建造者模式去建造日志器,而不是让用户直接去构造日志器,简化用户操作*/
    /*
        1.抽象一个日志器建造者类(完成日志器对象所需零部件的构建 & 日志器的构建)
            1.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() == false); // 必须有日志器名称
            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<HUE::LoggerBuilder> builder(new HUE::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){
                    //这里要是return了就没有把句柄添加到管理器中了
                    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

7. 双缓冲区异步任务处理器(AsyncLooper)设计

设计思想:异步处理线程 + 双缓冲区数据池

因为我们前面完成的是同步日志器的功能,就是直接将日志消息进行格式化写入文件。所有接下来我们要完成的就是异步日志器的实现。
实现思想:为了避免因为写日志的过程阻塞,导致业务线程在写日志的时候影响效率,我们异步的思想就是不让业务线程去进行日志的实际落地操作,而是将日志消息放入缓冲区(一块我们指定的内存)中,接下来有一个专门的异步线程,去针对缓冲区中的数据进行处理(实际的落地操作)。
在这里插入图片描述
我们采用环形队列来减少内存开辟消耗,同时因为多线程并发,所以缓冲区的操作必须保证线程安全 — 读写加锁

问题1

因为这个缓冲区的操作会涉及到多线程,因此缓冲区的操作必须保证线程安全
线程安全实现:对缓冲区的读写加锁
又因为写日志操作中,在实际开发中,并不会分配太多的资源,所以工作线程只需要有一个日志器就可以。
这里面涉及的锁冲突:生产者与生产者的互斥&生产者与消费者的互斥

问题2: 锁冲突较为严重,因为所有线程之间都存在互斥关系

我们采用双缓冲区的设计:1.减少空间频繁申请释放 2.减少生产者消费者锁冲突次数
在这里插入图片描述
对单个缓冲区设计思想:
在这里插入图片描述
优势:避免了空间的频繁申请释放,且尽可能的减少了⽣产者与消费者之间锁冲突的概率,提⾼了任务处理效率。

在任务池的设计中,有很多备选⽅案,⽐如循环队列等等,但是不管是哪⼀种都会涉及到锁冲突的情况,因为在⽣产者与消费者模型中,任何两个⻆⾊之间都具有互斥关系,因此每⼀次的任务添加与取出都有可能涉及锁的冲突,⽽双缓冲区不同,双缓冲区是处理器将⼀个缓冲区中的任务全部处理完毕后,然后交换两个缓冲区,重新对新的缓冲区中的任务进⾏处理,虽然同时多线程写⼊也会冲突,但是冲突并不会像每次只处理⼀条的时候频繁(减少了⽣产者与消费者之间的锁冲突),且不涉及到空间的频繁申请释放所带来的消耗。
在这里插入图片描述
buffer.hpp

#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <mutex>
#include <atomic>
#include <condition_variable>
#include <functional>
#include <cassert>
namespace bitlog{
#define BUFFER_DEFAULT_SIZE (1*1024*1024)
#define BUFFER_INCREMENT_SIZE (1*1024*1024)
#define BUFFER_THRESHOLD_SIZE (10*1024*1024)
class Buffer {
 public:
 Buffer(): _reader_idx(0), _writer_idx(0), _v(BUFFER_DEFAULT_SIZE){}
 bool empty() { return _reader_idx == _writer_idx; }
 size_t readAbleSize() { return _writer_idx - _reader_idx; }
 size_t writeAbleSize() { return _v.size() - _writer_idx; }
 void reset() { _reader_idx = _writer_idx = 0; }
 void swap(Buffer &buf) {
 _v.swap(buf._v);
 std::swap(_reader_idx, buf._reader_idx);
 std::swap(_writer_idx, buf._writer_idx);
 }
 void push(const char *data, size_t len) { 
 assert(len <= writeAbleSize());
 ensureEnoughSpace(len);
 std::copy(data, data+len, &_v[_writer_idx]);
 _writer_idx += len;
 }
 const char*begin() { return &_v[_reader_idx]; }
 void pop(size_t len) { 
 _reader_idx += len; 
 assert(_reader_idx <= _writer_idx);
 }
 protected:
 void ensureEnoughSpace(size_t len) {
 if (len <= writeAbleSize()) return;
 /*每次增⼤1M⼤⼩*/
 size_t new_capacity;
 if (_v.size() < BUFFER_THRESHOLD_SIZE)
 new_capacity = _v.size() * 2 + len;
 else
 new_capacity = _v.size() + BUFFER_INCREMENT_SIZE + len;
 _v.resize(new_capacity);
 }
 private:
 size_t _reader_idx;
 size_t _writer_idx;
 std::vector<char> _v;
};
}

looper.hpp

/*实现异步工作器*/
#ifndef _M_LOOPER_H_
#define _M_LOOPER_H_

#include"buffer.hpp"
#include<mutex>
#include<condition_variable>
#include<atomic>
#include<functional>
#include<memory>
#include<thread>

namespace HUE
{
    using Fucntor = std::function<void(Buffer &)>;
    enum class AsyncType{
        ASYNC_SAFE,//安全状态,表示缓冲区满了则阻塞,避免资源耗尽的风险
        ASYNC_UNSAFE //不考虑资源耗尽的问题,无限扩容,常用于测试
    };
    class AsyncLooper{
        public:
            using ptr = std::shared_ptr<AsyncLooper>;
            AsyncLooper(const Fucntor &cb,AsyncType looper_type =AsyncType::ASYNC_SAFE):_stop(false),
            _thread(std::thread(&AsyncLooper::threadEntry,this)),
            _callBack(cb){}

            ~AsyncLooper() { stop();}
            void stop(){
                 _stop = 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_all();
            }
        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();});
                    _con_buf.swap(_pro_buf); 
                     //2.唤醒生产者
                    if(_looper_type == AsyncType::ASYNC_SAFE)
                        _cond_pro.notify_all();
                    }
                     //3.被唤醒后,对消费缓冲区进行数据处理 
                     _callBack(_con_buf);
                     //4.初始化消费缓冲区
                     _con_buf.reset();
                }
            }
        private:
            Fucntor _callBack;//具体对缓冲区数据进行处理的回调函数,由异步工作器使用者进行传入
        private:
            AsyncType _looper_type;
            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

8.异步日志器(AsyncLogger)设计

这里异步工作器使用双缓冲区思想:外界将任务数据,添加到输入缓冲区中,异步线程对处理缓冲区中的数据进行处理,若处理缓冲区中没有了数据则交换缓冲区。

异步⽇志器类继承⾃⽇志器类, 并在同步⽇志器类上拓展了异步消息处理器。当我们需要异步输出⽇志的时候, 需要创建异步⽇志器和消息处理器, 调⽤异步⽇志器的log、error、info、fatal等函数输出不同级别⽇志。
• log函数为重写Logger类的函数, 主要实现将⽇志数据加⼊异步队列缓冲区中
• realLog函数主要由异步线程进⾏调⽤(是为异步消息处理器设置的回调函数),完成⽇志的实际落地⼯作。

实现脑图:
在这里插入图片描述

class AsyncLogger : public Logger {
 public:
 using ptr = std::shared_ptr<AsyncLogger>;
 AsyncLogger(const std::string &name, 
 Formatter::ptr formatter, 
 std::vector<LogSink::ptr> &sinks, 
 LogLevel::value level = LogLevel::value::DEBUG): 
 Logger(name, formatter, sinks, level),
 _looper(std::make_shared<AsyncLooper>(
 std::bind(&AsyncLogger::backendLogIt, this, 
std::placeholders::_1))) {
 std::cout << LogLevel::toString(level)<<"异步⽇志器: "<<name<<"创建成
功...\n";
 }
 protected:
 virtual void log(const std::string &msg) {
 _looper->push(msg);
 }
 void realLog(Buffer &msg) {
 if (_sinks.empty()) { return; }
 for (auto &it : _sinks) {
 it->log(msg.begin(), msg.readAbleSize());
 }
 }
 protected:
 AsyncLooper::ptr _looper;
};

9.单例日志器管理类设计(单例模式)

⽇志的输出,我们希望能够在任意位置都可以进⾏,但是当我们创建了⼀个⽇志器之后,就会受到⽇志器所在作⽤域的访问属性限制。
因此,为了突破访问区域的限制,我们创建⼀个⽇志器管理类,且这个类是⼀个单例类,这样的话,我们就可以在任意位置来通过管理器单例获取到指定的⽇志器来进⾏⽇志输出了。
基于单例⽇志器管理器的设计思想,我们对于⽇志器建造者类进⾏继承,继承出⼀个全局⽇志器建造者类,实现⼀个⽇志器在创建完毕后,直接将其添加到单例的⽇志器管理器中,以便于能够在任何位置通过⽇志器名称能够获取到指定的⽇志器进⾏⽇志输出。

class loggerManager{
 private:
 std::mutex _mutex;
 Logger::ptr _root_logger;
 std::unordered_map<std::string, Logger::ptr> _loggers;
 private:
 loggerManager(){ 
 std::unique_ptr<LocalLoggerBuilder> slb(new LocalLoggerBuilder());
 slb->buildLoggerName("root");
 slb->buildLoggerType(Logger::Type::LOGGER_SYNC);
 _root_logger = slb->build();
 _loggers.insert(std::make_pair("root", _root_logger));
 }
 loggerManager(const loggerManager&) = delete;
 loggerManager &operator=(const loggerManager&) = delete;
 public:
 static loggerManager& getInstance() {
 static loggerManager lm;
 return lm;
 }
 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;
 }
 void addLogger(const std::string &name, const Logger::ptr logger) {
 std::unique_lock<std::mutex> lock(_mutex);
 _loggers.insert(std::make_pair(name, logger));
 }
 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() {
 std::unique_lock<std::mutex> lock(_mutex);
 return _root_logger;
 }
};
class GlobalLoggerBuilder: public Logger::Builder {
 public:
 virtual Logger::ptr build() {
 if (_logger_name.empty()) {
 std::cout << "⽇志器名称不能为空!!";
 abort();
 }
 assert(loggerManager::getInstance().hasLogger(_logger_name) == 
false);
 if (_formatter.get() == nullptr) {
 std::cout << "当前⽇志器:" << _logger_name <<;
 std::cout << " 未检测到⽇志格式,默认设置为";
 std::cout << "[ %d{%H:%M:%S}%T%t%T[%p]%T[%c]%T%f:%l%T%m%n 
]!\n";
 _formatter = std::make_shared<Formatter>();
 }
 if (_sinks.empty()) {
 std::cout << "当前⽇志器:" << _logger_name <<;
 std::cout << " 未检测到落地⽅向,默认设置为标准输出!\n";
 _sinks.push_back(std::make_shared<StdoutSink>());
 }
 Logger::ptr lp;
 if (_logger_type == Logger::Type::LOGGER_ASYNC) {
 lp = std::make_shared<AsyncLogger>(_logger_name,_formatter, 
_sinks, _level);
 }else {
 lp = std::make_shared<SyncLogger>(_logger_name, _formatter, 
_sinks, _level);
 }
 loggerManager::getInstance().addLogger(_logger_name, lp);
 return lp;
 }
};

10.日志宏&全局接口设计(代理模式)

我们最后提供全局接口和一些宏函数,对日志系统的接口进行使用便捷性优化。
这里使用代理模式通过全局函数或宏函数来代理Logger类的log、debug、info、warn、error、fatal等接口,以便于控制源码⽂件名称和⾏号的输出控制,简化⽤⼾操作。
当仅需标准输出⽇志的时候可以通过主⽇志器来打印⽇志。 且操作时只需要通过宏函数直接进⾏输出即可。

#ifndef _M_HUELOG_H_
#define _M_HUELOG_H_
#include"logger.hpp"

namespace HUE{
    //1.提供获取指定日志器的全局接口(避免用户自己操作单例对象)
    Logger::ptr getlogger(const std::string &name){
        return HUE::LoggerManager::getInstance().getLogger(name);
    }
    Logger::ptr rootLogger(){
        return HUE::LoggerManager::getInstance().rootLogger();
    }
    //2.使用宏函数对日志器的接口进行代理(代理模式)
    #define debug(fmt, ...) debug(__FILE__,__LINE__,fmt,##__VA_ARGS__)
    #define info(fmt, ...) info(__FILE__,__LINE__,fmt,##__VA_ARGS__)
    #define warn(fmt, ...) warn(__FILE__,__LINE__,fmt,##__VA_ARGS__)
    #define error(fmt, ...) error(__FILE__,__LINE__,fmt,##__VA_ARGS__)
    #define fatal(fmt, ...) fatal(__FILE__,__LINE__,fmt,##__VA_ARGS__)

    //3.提供宏函数,直接通过默认日志器进行日志的标准输出打印(不用获取日志器了)
    #define DEBUG(fmt, ...)HUE::rootLogger()->debug(fmt,##__VA_ARGS__)
    #define INFO(fmt,...)  HUE::rootLogger()->info(fmt,##__VA_ARGS__)
    #define WARN(fmt, ...)  HUE::rootLogger()->warn(fmt,##__VA_ARGS__)
    #define ERROR(fmt, ...) HUE::rootLogger()->error(fmt,##__VA_ARGS__)
    #define FATAL(fmt, ...)  HUE::rootLogger()->fatal(fmt,##__VA_ARGS__)
}


#endif

五、项目测试

在完成项目编写之后,我们需要测试⼀个日志器中包含有所有的落地方向,观察是否每个方向都正常落地,分别测试同步方式和异步方式落地后数据是否正常。

因为不同的测试环境所呈现的测试数据差异巨大,所以这里先介绍一下我的测试环境:
1.我使用的腾讯云轻量级服务器,它的配置是:
CentOS7,2G RAM ,CPU 2核心 ,40G ROM
在这里插入图片描述
这里会测试:同步下的单线程/多线程,异步下的单线程/多线程情况下,对100万条日志进行滚动文件输出,对比二者在不同情况下的性能差异

主要的测试方法是:每秒能打印日志数 = 打印日志条数 / 总的打印日志消耗时间
主要测试要素:同步/异步 & 单线程/多线程
• 100w+条指定长度的日志输出所耗时间
• 每秒可以输出多少条日志
• 每秒可以输出多少MB日志

注意:异步测试输出,我们启动非安全模式,纯内存写入(不考虑实际落地时间)
下面是我编写的测试工具类
在这里插入图片描述
1.同步日志单线程输出测试:
在这里插入图片描述

2.同步日志多线程输出测试:
在这里插入图片描述
3.异步日志单线程输出测试:
在这里插入图片描述
4.异步日志多线程输出测试:
在这里插入图片描述
我们能够通过上边的测试看出来,⼀些情况
在单线程情况下,异步效率看起来还没有同步⾼,这个我们得了解,现在的IO操作在⽤⼾态都会有缓冲区进行缓冲区,因此我们当前测试⽤例看起来的同步其实⼤多时候也是在操作内存,只有在缓冲区满了才会涉及到阻塞写磁盘操作,⽽异步单线程效率看起来低,也有⼀个很重要的原因就是单线程同步操作中不存在锁冲突,⽽单线程异步⽇志操作存在⼤量的锁冲突,因此性能也会有⼀定的降低。

但是,我们也要看到限制同步⽇志效率的最⼤原因是磁盘性能,打⽇志的线程多少并⽆明显区别,线程多了反⽽会降低,因为增加了磁盘的读写争抢,⽽对于异步⽇志的限制,并⾮磁盘的性能,⽽是cpu的处理性能,打⽇志并不会因为落地⽽阻塞,因此在多线程打⽇志的情况下性能有了显著的提高。


最后说一下,这个日志器系统在简单整理之后我们只需要将项目实现放到logs文件夹里,在使用时我们只需要包含一个全局接口的mylog.h头文件即可。
在这里插入图片描述
扩展方向:
sink类型扩展
◦ ⽀持按⼩时按天滚动⽂件
◦ ⽀持将log通过⽹络传输落地到⽇志服务器(tcp/udp)
◦ ⽀持在控制台通过⽇志等级渲染不同颜⾊输出⽅便定位
◦ ⽀持落地⽇志到数据库
◦ ⽀持配置服务器地址,将⽇志落地到远程服务器
实现⽇志服务器负责存储⽇志并提供检索、分析、展示等功能

参考资料

https://www.imangodoc.com/174918.html
https://blog.csdn.net/w1014074794/article/details/125074038
https://zhuanlan.zhihu.com/p/472569975
https://zhuanlan.zhihu.com/p/460476053
https://gitee.com/davidditao/DDlog
https://www.cnblogs.com/ailumiyana/p/9519614.html
https://gitee.com/lqk1949/plog/
https://www.cnblogs.com/horacle/p/15494358.html
https://blog.csdn.net/qq_29220369/article/details/127314390

  • 10
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吉始

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值