唐山网站建设价格,宁波网站设计哪家公司好,高质量外链代发,阜阳哪里有做网站的✍作者#xff1a;阿润021 #x1f4d6;专栏#xff1a;C 文章目录 一、项目介绍二、项目实现准备工作1.日志系统技术实现策略2.相关技术知识补充2.1 不定参函数设计2.2 设计模式 三、日志项目框架设计1.模块划分2.各模块关系图 四、详细代码实现1.实用工具类设计2.日志等级… ✍作者阿润021 专栏C 文章目录 一、项目介绍二、项目实现准备工作1.日志系统技术实现策略2.相关技术知识补充2.1 不定参函数设计2.2 设计模式 三、日志项目框架设计1.模块划分2.各模块关系图 四、详细代码实现1.实用工具类设计2.日志等级类设计3.日志消息类设计4.日志输出格式化模块5.日志落地模块(简单工厂模式)6.日志器类(Logger)设计建造者模式7. 双缓冲区异步任务处理器AsyncLooper设计8.异步日志器(AsyncLogger)设计9.单例日志器管理类设计单例模式10.日志宏全局接口设计代理模式 五、项目测试参考资料 一、项目介绍 简介 本项目主要实现一个日志系统它可以根据不同的级别、配置和策略以同步或异步的方式将日志信息可靠地写入控制台文件或滚动文件中同时支持多线程并发写日志和扩展不同的日志落地目标地模式。 开发环境 • CentOS 7 • vscode/vim • g/gdb • Makefile 核心技术 1.类层次设计如继承和多态的应用 2.C11如多线程、auto、智能指针、右值引⽤等 3. 双缓冲区设计实现异步日志器 4. 生产消费模型 5. 多线程实现并发输出日志 6. 设计模式单例日志管理器类设计、⼯⼚模式、代理模式、模板等 环境搭建 本项目不依赖其他任何第三⽅库 只需要安装好CentOS/Ubuntu vscode/vim环境即可开发。 测试结果展示 为什么需要日志系统 • 生产环境的产品为了保证其稳定性及安全性是不允许开发⼈员附加调试器去排查问题 可以借助日志志系统来打印⼀些日志帮助开发⼈员解决问题
• 上线客户端的产品出现bug无法复现并解决 可以借助⽇志系统打印⽇志并上传到服务端帮助开发⼈员进⾏分析
• 对于⼀些⾼频操作如定时器、心跳包在少量调试次数下可能⽆法触发我们想要的⾏为通过断点的暂停⽅式我们不得不重复操作⼏⼗次、上百次甚⾄更多导致排查问题效率是⾮常低下 可以借助打印⽇志的⽅式查问题
• 在分布式、多线程/多进程代码中 出现bug⽐较难以定位 可以借助⽇志系统打印log帮助定位bug
• 帮助首次接触项目代码的新开发人员理解代码的运⾏流程
二、项目实现准备工作
1.日志系统技术实现策略
日志系统的技术实现主要包括三种类型: 利用printf、std::cout等输出函数将⽇志信息打印到控制台 对于⼤型商业化项目 为了放便排查问题我们⼀般会将⽇志输出到⽂件或者是数据库系统⽅便查询和分析日志 主要分为同步日志和异步日志方式: 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;
}
templatetypename T, typename ...Args
void xprintf(const T value, Args ...args) {std::cout value ;if ((sizeof ...(args)) 0) {xprintf(std::forwardArgs(args)...);}else {xprintf();}
}
int main()
{xprintf(张三);xprintf(张三, 666);xprintf(张三, 法外狂徒, 666);return 0;
}2.2 设计模式
设计模式是前辈们对代码开发经验的总结是解决特定问题的⼀系列套路。它不是语法规定⽽是⼀套⽤来提⾼代码可复⽤性、可维护性、可读性、稳健性以及安全性的解决⽅案。 六大原则 从整体上来理解六⼤设计原则可以简要的概括为⼀句话⽤抽象构建框架⽤实现扩展细节具体到每⼀条设计原则则对应⼀条注意事项 • 单⼀职责原则告诉我们实现类要职责单⼀ • ⾥⽒替换原则告诉我们不要破坏继承体系 • 依赖倒置原则告诉我们要⾯向接⼝编程 • 接⼝隔离原则告诉我们在设计接⼝的时候要精简单⼀ • 迪⽶特法则告诉我们要降低耦合 • 开闭原则是总纲告诉我们要对扩展开放对修改关闭 单例模式 ⼀个类只能创建⼀个对象即单例模式该设计模式可以保证系统中该类只有⼀个实例并提供⼀个访问它的全局访问点该实例被所有程序模块共享。⽐如在某个服务器程序中该服务器的配置信息存放在⼀个⽂件中这些配置数据由⼀个单例对象统⼀读取然后服务进程中的其他对象再通过这个单例对象获取这些配置信息这种⽅式简化了在复杂环境下的配置管理。 单例模式有两种实现模式饿汉模式和懒汉模式
• 饿汉模式: 程序启动时就会创建⼀个唯⼀的实例对象。 因为单例对象已经确定 所以⽐较适⽤于多线程环境中 多线程获取单例对象不需要加锁 可以有效的避免资源竞争 提⾼性能。
// 饿汉模式
templatetypename 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_ptrFruit create(const std::string name) {if (name 苹果) {return std::make_sharedApple();}else if(name ⾹蕉) {return std::make_sharedBanana();}return std::shared_ptrFruit();}
};
int main()
{std::shared_ptrFruit 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_ptrFruit create() 0;
};
class AppleFactory : public FruitFactory {public:virtual std::shared_ptrFruit create() {return std::make_sharedApple();}
};
class BananaFactory : public FruitFactory {public:virtual std::shared_ptrFruit create() {return std::make_sharedBanana();}
};
int main()
{std::shared_ptrFruitFactory 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_ptrFruit getFruit(const std::string name) 0;virtual std::shared_ptrAnimal getAnimal(const std::string name) 0;
};
class FruitFactory : public Factory {public:virtual std::shared_ptrAnimal getAnimal(const std::string name) {return std::shared_ptrAnimal();}virtual std::shared_ptrFruit getFruit(const std::string name) {if (name 苹果) {return std::make_sharedApple();}else if(name ⾹蕉) {return std::make_sharedBanana();}return std::shared_ptrFruit();}
};
class AnimalFactory : public Factory {public:virtual std::shared_ptrFruit getFruit(const std::string name) {return std::shared_ptrFruit();}virtual std::shared_ptrAnimal getAnimal(const std::string name) {if (name ⼩⽺) {return std::make_sharedLamp();}else if(name ⼩狗) {return std::make_sharedDog();}return std::shared_ptrAnimal();}
};
class FactoryProducer {public:static std::shared_ptrFactory getFactory(const std::string name) {if (name 动物) {return std::make_sharedAnimalFactory();}else {return std::make_sharedFruitFactory();}}
};
int main()
{std::shared_ptrFactory fruit_factory FactoryProducer::getFactory(⽔
果);std::shared_ptrFruit fruit fruit_factory-getFruit(苹果);fruit-show();fruit fruit_factory-getFruit(⾹蕉);fruit-show();std::shared_ptrFactory animal_factory FactoryProducer::getFactory(动
物);std::shared_ptrAnimal 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_ptrComputer;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_ptrMacBook;MacBook() {}virtual void setOs() {_os Max Os X12;}
};
/*抽象建造者类包含创建⼀个产品对象的各个部件的抽象接⼝*/
class Builder {public:using ptr std::shared_ptrBuilder;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_ptrMacBookBuilder;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_ptrDirector 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.创建目录#includeiostream
#includectime
#includesys/stat.hnamespace 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,pos1);}static void createDirectory(const std::string pathname){// ./abc/bcd/a.txtsize_t pos 0, idx 0;//标定查找的起始位置while(idx pathname.size()){pos pathname.find_first_of(/\\,idx);if(pos std::string::npos){mkdir(pathname.c_str(),0777);}std::string parent_dir pathname.substr(0,pos1);//截取目录路径if(exists(parent_dir) true) {idx pos1;continue;}mkdir(parent_dir.c_str(),0777);idx pos1;}}};}
} #endif2.日志等级类设计
我们将日志等级封装成一个类定义出日志系统所包含的所以日志等级 • UNKONW • OFF 关闭所有⽇志输出 • DRBUG 进⾏debug时候打印⽇志的等级 • INFO 打印⼀些⽤⼾提⽰信息 • WARN 打印警告信息 • ERROR 打印错误信息 • FATAL 打印致命信息- 导致程序崩溃的信息
这里的思想是每一个项目中都会设置一个默认的日志输出等级只有输出的日志等级大于等于默认限制等级的时候才可以进行输出
//1.定义枚举类枚举出日志等级
//2.提供转换接口将枚举准换为对应字符串#ifndef _M_LEVEL_H
#define _M_LEVEL_Hnamespace 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;}};
}#endif3.日志消息类设计
我们⽇志消息类主要是封装⼀条完整的⽇志消息所需的内容其中包括⽇志等级、对应的logger name、打印⽇志源⽂件的位置信息包括⽂件名和⾏号、线程ID、时间戳信息、具体的⽇志信息等内容。
#ifndef _M_MSG_H_
#define _M_MSG_H_
//⽇志消息类主要是封装⼀条完整的⽇志消息所需的内容其中包括
//⽇志等级、对应的logger name、打印⽇志源⽂件的位置信息包括⽂件名和⾏号、线程ID、时间戳信息、具体的⽇志信息等内容#includelevel.hpp
#includeutil.hpp
#includethreadnamespace HUE
{struct LogMsg{time_t _ctime; //日志产生的时间戳LogLevel::value _level; //日志等级size_t _line;//行号std::thread::id _tid;//线程IDstd::string _file;//源文件名std::string _logger;//日志器名称std::string _payload; //有效消息数据LogMsg(LogLevel::value level,size_t line,const std::string file,const std::string logger,const std::string msg):_ctime(util::Date::now()),_level(level),_line(line),_tid(std::this_thread::get_id()),_file(file),_logger(logger),_payload(msg){}};}#endif4.日志输出格式化模块
我们可以让用户自定义输出内容,通过对日志消息进行格式化组织称为指定格式的字符串。
我们的设计思想就是 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 sstreamnamespace HUE
{// 抽象格式化子项基类class FromatIem{public:using ptr std::shared_ptrFromatIem;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%MS};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_ptrFormatter;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%nstd::vectorstd::pairstd::string,std::stringfmt_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[pos1] % pos1 _pattern.size()){val.push_back(%); pos2; continue;}//能走下来代表%后面是个格式化字符,代表原始字符串处理完毕if(val.empty() false){fmt_order.push_back(std::make_pair(,val));val.clear();}//格式化字符处理 pos1; //这一步之后pos指向格式化字符位置if(pos _pattern.size()){std::cout %之后没有对应的格式化字符\n;return false;}key _pattern[pos];//1此时pos指向格式化字符后的位置pos1;if(pos_pattern.size() _pattern[pos] {){pos1;//此时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_sharedTimeFormatItem(val);if(key t) return std::make_sharedThreadFormatItem();if(key c) return std::make_sharedLoggerFormatItem();if(key f) return std::make_sharedFileFormatItem();if(key l) return std::make_sharedLineFormatItem();if(key p) return std::make_sharedLevelFormatItem();if(key T) return std::make_sharedTabFormatItem();if(key m) return std::make_sharedMsgFormatItem();if(key n) return std::make_sharedNLineFormatItem();if(key ) return std::make_sharedOtherFormatItem (val);std::cout 没有对应的格式化字符%keystd::endl;abort();}private:std::string _pattern; // 格式化规则字符串std::vectorFromatIem::ptr _items;};}#endif5.日志落地模块(简单工厂模式)
日志落地类主要负责将格式化后的日志消息字符串输出到指定位置。
它主要包括以下内容 • 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_ptrLogSink;LogSink() {}virtual ~LogSink() {}virtual void log(const char *data, size_t len) 0;
};
class StdoutSink : public LogSink {public:using ptr std::shared_ptrStdoutSink;StdoutSink() default;void log(const char *data, size_t len) {std::cout.write(data, len);}
};
class FileSink : public LogSink {public:using ptr std::shared_ptrFileSink;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_ptrRollSink;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:templatetypename SinkType, typename ...Argsstatic LogSink::ptr create(Args ...args) {return std::make_sharedSinkType(std::forwardArgs(args)...);}
};
}
#endif6.日志器类(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
#includeunordered_mapnamespace HUE
{class Logger{public:using ptr std::shared_ptrLogger;Logger(const std::string logger_name, LogLevel::value level, Formatter::ptr formatter,std::vectorLogSink::ptr sinks) : _logger_name(logger_name), _limit_level(level), _formatter(formatter), _sinks(sinks.begin(), sinks.end()) {}const std::string name(){return _logger_name;}// 完成构造日志消息对象过程并进行初始化得到格式化后的日志消息字符串--进行落地输出void debug(const std::string file, size_t line, const std::string fmt, ...){// 通过传入的参数构造出一个日志消息对象进行日志的格式化最终落地// 1.判断当前的日志是否达到了输出等级if (LogLevel::value::DEBUG _limit_level)return;// 2.对fmt格式化字符串和不定参进行字符串组织得到一个新的日志消息字符串va_list ap;va_start(ap, fmt); // 获取地址char *res;int ret vasprintf(res, fmt.c_str(), ap);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::atomicLogLevel::value _limit_level; // 原子性 --- 线程安全Formatter::ptr _formatter;std::vectorLogSink::ptr _sinks;};class SyncLogger : public Logger{public:SyncLogger(const std::string logger_name, LogLevel::value level, Formatter::ptr formatter, std::vectorLogSink::ptr sinks) : Logger(logger_name, level, formatter, sinks) {}protected:// 同步日志器是将日志直接通过落地模块句柄进行日志落地void log(const char *data, size_t len){std::unique_lockstd::mutex lock(_mutex);if (_sinks.empty())return;for (auto sink : _sinks){sink-log(data, len);}}};// 异步日志器class AsyncLogger : public Logger{public:AsyncLogger(const std::string logger_name,LogLevel::value level,Formatter::ptr formatter, std::vectorLogSink::ptr sinks,AsyncType looper_type) : Logger(logger_name, level, formatter, sinks),_looper(std::make_sharedAsyncLooper(std::bind(AsyncLogger::reallog, this, std::placeholders::_1), looper_type)) {}// 将数据写入缓冲区void log(const char *data, size_t len){_looper-push(data, len);}// 设计一个实际落地函数将缓冲区的数据落地void reallog(Buffer buf){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_sharedFormatter(pattern);}template typename SinkType, typename... Argsvoid buildSink(Args ...args){LogSink::ptr psink SinkFactory::createSinkType(std::forwardArgs(args)...); // 进行完美转发_sinks.push_back(psink); // 添加到日志器数组中}virtual Logger::ptr build() 0;protected:AsyncType _looper_type;LoggerType _logger_type;std::string _logger_name;LogLevel::value _limit_level;Formatter::ptr _formatter;std::vectorLogSink::ptr _sinks;};/*2.派生出具体的建造者类 --- 局部日志器的建造者 全局日志器的建造者后面添加全局单例管理器之后将日志器添加全局管理*/class LocalLoggerBuilder : public LoggerBuilder{public:Logger::ptr build() override{assert(_logger_name.empty() false); // 必须有日志器名称if (_formatter.get() nullptr){_formatter std::make_sharedFormatter();}if (_sinks.empty()){buildSinkStdoutSink();}if (_logger_type LoggerType::LOGGER_ASYNC){return std::make_sharedAsyncLogger(_logger_name,_limit_level,_formatter,_sinks,_looper_type);}return std::make_sharedSyncLogger(_logger_name, _limit_level, _formatter, _sinks);}};class LoggerManager{public:static LoggerManager getInstance(){//C11针对静态局部变量在编译层面实现线程安全//当静态局部变量在没有构造完成之前其他的线程进入就会阻塞static LoggerManager eton;return eton;}void addLogger(Logger::ptr logger){if(hasLogger(logger-name())) return ;std::unique_lockstd::mutex lock(_mutex);_loggers.insert(std::make_pair(logger-name(),logger));}bool hasLogger(const std::string name){std::unique_lockstd::mutex lock(_mutex);auto it _loggers.find(name);if(it _loggers.end()){return false;}return true;}Logger::ptr getLogger(const std::string name){std::unique_lockstd::mutex lock(_mutex);auto it _loggers.find(name);if(it _loggers.end()){return Logger::ptr();}return it-second;}Logger::ptr rootLogger(){return _root_logger; }private:LoggerManager(){std::unique_ptrHUE::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_mapstd::string,Logger::ptr _loggers;};//设计一个全局日志器的建造者--在局部的基础上增加一个功能将日志器添加到单例对象里class GlobalLoggerBuilder:public LoggerBuilder{public:Logger::ptr build() override{assert(_logger_name.empty() false); // 必须有日志器名称if (_formatter.get() nullptr){_formatter std::make_sharedFormatter();}if (_sinks.empty()){buildSinkStdoutSink();}Logger::ptr logger;if (_logger_type LoggerType::LOGGER_ASYNC){//这里要是return了就没有把句柄添加到管理器中了logger std::make_sharedAsyncLogger(_logger_name,_limit_level,_formatter,_sinks,_looper_type);}else{logger std::make_sharedSyncLogger(_logger_name, _limit_level, _formatter, _sinks);}LoggerManager::getInstance().addLogger(logger);return logger;}};
}#endif7. 双缓冲区异步任务处理器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, datalen, _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;elsenew_capacity _v.size() BUFFER_INCREMENT_SIZE len;_v.resize(new_capacity);}private:size_t _reader_idx;size_t _writer_idx;std::vectorchar _v;
};
}looper.hpp
/*实现异步工作器*/
#ifndef _M_LOOPER_H_
#define _M_LOOPER_H_#includebuffer.hpp
#includemutex
#includecondition_variable
#includeatomic
#includefunctional
#includememory
#includethreadnamespace HUE
{using Fucntor std::functionvoid(Buffer );enum class AsyncType{ASYNC_SAFE,//安全状态表示缓冲区满了则阻塞避免资源耗尽的风险ASYNC_UNSAFE //不考虑资源耗尽的问题无限扩容常用于测试};class AsyncLooper{public:using ptr std::shared_ptrAsyncLooper;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_lockstd::mutex lock(_mutex);//条件变量空值若缓冲区剩余空间大小大于数据长度则可以添加数据if(_looper_type AsyncType::ASYNC_SAFE)_cond_pro.wait(lock,[](){return _pro_buf.writeAbleSize() len;});//能走下来说明可以向缓冲区中添加数据_pro_buf.push(data,len);//唤醒消费者对缓冲区中的数据进行处理_cond_con.notify_all();}private://线程入口函数--对消费缓冲区中的数据进行处理处理完毕后初始化缓冲区交换缓冲区void threadEntry(){while(1){//为互斥锁设置一个生命周期当缓冲区交换完毕后解锁并不对数据的处理过程加锁保护{//1.判断生产缓冲区有没有数据有则交换无则阻塞std::unique_lockstd::mutex lock(_mutex);//退出标志被设置且生产缓冲区已无数据此时退出否则有可能造成生产缓冲区中有数据但没被完全处理if(_stop _pro_buf.empty()) break;//若退出前被唤醒或者有数据被唤醒则返回真继续向下运行否则重新休眠 _cond_con.wait(lock,[](){return _stop ||! _pro_buf.empty();});_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;//异步工作器对应的工作线程};}#endif8.异步日志器(AsyncLogger)设计
这里异步工作器使用双缓冲区思想外界将任务数据添加到输入缓冲区中异步线程对处理缓冲区中的数据进行处理若处理缓冲区中没有了数据则交换缓冲区。
异步⽇志器类继承⾃⽇志器类 并在同步⽇志器类上拓展了异步消息处理器。当我们需要异步输出⽇志的时候 需要创建异步⽇志器和消息处理器 调⽤异步⽇志器的log、error、info、fatal等函数输出不同级别⽇志。 • log函数为重写Logger类的函数 主要实现将⽇志数据加⼊异步队列缓冲区中 • realLog函数主要由异步线程进⾏调⽤(是为异步消息处理器设置的回调函数)完成⽇志的实际落地⼯作。
实现脑图
class AsyncLogger : public Logger {public:using ptr std::shared_ptrAsyncLogger;AsyncLogger(const std::string name, Formatter::ptr formatter, std::vectorLogSink::ptr sinks, LogLevel::value level LogLevel::value::DEBUG): Logger(name, formatter, sinks, level),_looper(std::make_sharedAsyncLooper(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_mapstd::string, Logger::ptr _loggers;private:loggerManager(){ std::unique_ptrLocalLoggerBuilder 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_lockstd::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_lockstd::mutex lock(_mutex);_loggers.insert(std::make_pair(name, logger));}Logger::ptr getLogger(const std::string name) {std::unique_lockstd::mutex lock(_mutex);auto it _loggers.find(name);if (it _loggers.end()) {return Logger::ptr();}return it-second;}Logger::ptr rootLogger() {std::unique_lockstd::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_sharedFormatter();}if (_sinks.empty()) {std::cout 当前⽇志器 _logger_name ;std::cout 未检测到落地⽅向默认设置为标准输出!\n;_sinks.push_back(std::make_sharedStdoutSink());}Logger::ptr lp;if (_logger_type Logger::Type::LOGGER_ASYNC) {lp std::make_sharedAsyncLogger(_logger_name,_formatter,
_sinks, _level);}else {lp std::make_sharedSyncLogger(_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_
#includelogger.hppnamespace 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.我使用的腾讯云轻量级服务器它的配置是 CentOS72G RAM CPU 2核心 40G ROM 这里会测试同步下的单线程/多线程异步下的单线程/多线程情况下对100万条日志进行滚动文件输出对比二者在不同情况下的性能差异
主要的测试方法是每秒能打印日志数 打印日志条数 / 总的打印日志消耗时间 主要测试要素同步/异步 单线程/多线程 • 100w条指定长度的日志输出所耗时间 • 每秒可以输出多少条日志 • 每秒可以输出多少MB日志
注意异步测试输出我们启动非安全模式纯内存写入不考虑实际落地时间 下面是我编写的测试工具类 1.同步日志单线程输出测试
2.同步日志多线程输出测试 3.异步日志单线程输出测试 4.异步日志多线程输出测试 我们能够通过上边的测试看出来⼀些情况 在单线程情况下异步效率看起来还没有同步⾼这个我们得了解现在的IO操作在⽤⼾态都会有缓冲区进行缓冲区因此我们当前测试⽤例看起来的同步其实⼤多时候也是在操作内存只有在缓冲区满了才会涉及到阻塞写磁盘操作⽽异步单线程效率看起来低也有⼀个很重要的原因就是单线程同步操作中不存在锁冲突⽽单线程异步⽇志操作存在⼤量的锁冲突因此性能也会有⼀定的降低。
但是我们也要看到限制同步⽇志效率的最⼤原因是磁盘性能打⽇志的线程多少并⽆明显区别线程多了反⽽会降低因为增加了磁盘的读写争抢⽽对于异步⽇志的限制并⾮磁盘的性能⽽是cpu的处理性能打⽇志并不会因为落地⽽阻塞因此在多线程打⽇志的情况下性能有了显著的提高。 最后说一下这个日志器系统在简单整理之后我们只需要将项目实现放到logs文件夹里在使用时我们只需要包含一个全局接口的mylog.h头文件即可。
参考资料
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