🌇个人主页:平凡的小苏
📚学习格言:命运给你一个低的起点,是想看你精彩的翻盘,而不是让你自甘堕落,脚下的路虽然难走,但我还能走,比起向阳而生,我更想尝试逆风翻盘。
🛸C++项目实战:C++项目实战
> 家人们更新不易,你们的👍点赞👍和⭐关注⭐真的对我真重要,各位路 过的友友麻烦多多点赞关注。 欢迎你们的私信提问,感谢你们的转发! 关注我,关注我,关注我,你们将会看到更多的优质内容!!
一、项目介绍
本项⽬主要实现⼀个⽇志系统, 其主要⽀持以下功能:
• ⽀持多级别⽇志消息
• ⽀持同步⽇志和异步⽇志
• ⽀持可靠写⼊⽇志到控制台、⽂件以及滚动⽂件中
• ⽀持多线程程序并发写⽇志
• ⽀持扩展不同的⽇志落地⽬标地
1、开发环境
• CentOS 7
• vscode/vim
• g++/gdb
• Makefile
2、核心技术
• 类层次设计(继承和多态的应⽤)
• C++11(多线程、auto、智能指针、右值引⽤等)
• 双缓冲区
• ⽣产消费模型
• 多线程
• 设计模式(单例、⼯⼚、代理、模板等)
3、日志系统介绍
3.1、为什么需要日志系统
• ⽣产环境的产品为了保证其稳定性及安全性是不允许开发⼈员附加调试器
去排查问题, 可以借助⽇志系统来打印⼀些⽇志帮助开发⼈员解决问题
• 上线客⼾端的产品出现bug⽆法复现并解决
, 可以借助⽇志系统打印⽇志并上传到服务端帮助开发⼈员进⾏分析
• 对于⼀些⾼频操作(如定时器、⼼跳包)在少量调试次数下可能⽆法触发我们想要的⾏为,通过断点的暂停⽅式,我们不得不重复操作⼏⼗次、上百次甚⾄更多,导致排查问题效率是⾮常低下, 可以借助打印⽇志的⽅式查问题
• 在分布式、多线程/多进程代码中, 出现bug⽐较难以定位
, 可以借助⽇志系统打印log帮助定位bug
• 帮助⾸次接触项⽬代码的新开发⼈员理解代码的运⾏流程
3.2、日志系统技术实现
3.2.1、同步写日志
同步⽇志是指当输出⽇志时,必须等待⽇志输出语句
执⾏完毕后,才能执⾏后⾯的业务逻辑语句,⽇志输出语句与程序的业务逻辑语句将在同⼀个线程
运⾏。每次调⽤⼀次打印⽇志API就对应⼀次系统调⽤write写⽇志⽂件。
同步写日志的缺点:
在⾼并发场景下,随着⽇志数量不断增加,同步⽇志系统容易产⽣系统瓶颈:
• ⼀⽅⾯,⼤量的⽇志打印陷⼊等量的write系统调⽤,有⼀定系统开销.
• 另⼀⽅⾯,使得打印⽇志的进程附带了⼤量同步的磁盘IO,影响程序性能
3.2.2、异步写日志
异步⽇志是指在进⾏⽇志输出时,⽇志输出语句与业务逻辑语句并不是在同⼀个线程中
运⾏,⽽是有专⻔的线程
⽤于进⾏⽇志输出操作
。业务线程只需要将⽇志放到⼀个内存缓冲区中不⽤等待即可继续执⾏后续业务逻辑(作为⽇志的⽣产者),⽽⽇志的落地操作交给单独的⽇志线程去完成(作为⽇志的消费者), 这是⼀个典型的⽣产-消费模型。
异步相比于同步的好处:
• 主线程调⽤⽇志打印接⼝成为⾮阻塞操作
• 同步的磁盘IO从主线程中剥离出来交给单独的线程完成
4、相关技术知识
在初学C语⾔的时候,我们都⽤过printf函数进⾏打印。其中printf函数就是⼀个不定参函数,在函数内部可以根据格式化字符串中格式化字符分别获取不同的参数进⾏数据的格式化。
⽽这种不定参函数在实际的使⽤中也⾮常多⻅,在这⾥简单做⼀介绍:
不定参函数的声明格式如下:
return_type func_name(format_string, ...)
return_type
为返回值类型func_name
为函数名format_string
是一个格式化字符串,用于指定参数的数量和类型...
表示使用不定参数
4.1、c风格的不定参函数
在函数内部我们可以用一些宏或函数对不定参数进行分解,常见的宏有
va_start
、va_arg
、 va_end
void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);
va_list
是一个类型,用于声明一个参数列表的对象,它的本质时void*;va_start
用于初始化va_list对象,使其指向不定参数列表的第一个参数;va_arg
用于获取不定列表的参数va_end
用于清空可变参数列表
代码演示:
#include <iostream>
#include <stdarg.h>
using std::endl;
using std::cout;void PrintNum(const char* fmt, ...)
{va_list ap;va_start(ap,count);//初始化ap指针,使其指向count后面的第一个参数for(int i = 0; i < count; i++){int num = va_arg(ap,int);//从不定参数列表中抽取int类型的参数printf("%d ",num);}cout << endl;va_end(ap);//将ap置空
}
int main()
{myPrint();return 0;
}
代码演示:
#include <iostream>
#include <stdarg.h>
using std::endl;
using std::cout;void myPrint(const char* fmt ,...)
{char* res;va_list ap;va_start(ap,fmt);//初始化ap指针,使其指向count后面的第一个参数int len = vasprintf(&res,fmt,ap);cout << res << endl;va_end(ap);//将ap置空free(res);//res指向动态开辟的空间,需要手动释放
}
int main()
{myPrint("%s-%d","天青色等烟雨",199);return 0;
}
注意
vasprintf
是一个C的库函数,它可以通过可变参数创建一个格式化的字符串,并将其存储在动态分配的内存
中。
它的使用方法与printf类似,但它不会将结果打印到标准输出流中,而是将存储在一个指向字符串数组的指针里
.
函数原型如下:
#define _GNU_SOURCE#include <stdio.h>
int vasprintf(char **strp, const char *fmt, va_list ap);
4.2、c++风格的不定参函数
#include <iostream>
#include <stdarg.h>
#include <memory>
#include <functional>using std::endl;
using std::cout;void xprintf()//无参特化
{cout << endl;
}
template<typename T, typename ...Args>
void xprintf(const T& v, Args &&...args)
{cout << v << " ";if(sizeof ...(args) > 0){xprintf(std::forward<Args>(args)...);//完美转发,递归分解参数}else{xprintf();}}
int main()
{xprintf("aaa","vvv","撒旦发射点",10000);return 0;
}
4.3、不定参宏函数
#include <stdio.h>#define LOG(format, ...) printf("[%s:%d]\n" format, __FILE__, __LINE__, ##__VA_ARGS__);int main()
{LOG("日志消息");return 0;
}
5、设计模式
设计模式是前辈们对代码开发经验的总结,是解决特定问题的⼀系列套路。它不是语法规定
,⽽是⼀套⽤来提⾼代码可复⽤性、可维护性、可读性、稳健性以及安全性
的解决⽅案。
六⼤原则:
• 单⼀职责原则(Single Responsibility Principle)
◦ 类的职责应该单⼀
,⼀个⽅法只做⼀件事。职责划分清晰了,每次改动到最⼩单位的⽅法或类。
◦ 使⽤建议:两个完全不⼀样的功能不应该放⼀个类中,⼀个类中应该是⼀组相关性很⾼的函数、数据的封装
◦ ⽤例:⽹络聊天:⽹络通信 & 聊天,应该分割成为⽹络通信类 & 聊天类
• 开闭原则(Open Closed Principle)
◦ 对扩展开放,对修改封闭
◦ 使⽤建议:对软件实体的改动,最好⽤扩展⽽⾮修改
的⽅式。
◦ ⽤例:超时卖货:商品价格—不是修改商品的原来价格,⽽是新增促销价格。
• ⾥⽒替换原则(Liskov Substitution Principle)
◦ 通俗点讲,就是只要⽗类能出现的地⽅,⼦类就可以出现,⽽且替换为⼦类也不会产⽣任何错误或异常。
◦ 在继承类时,务必重写⽗类中所有的⽅法,尤其需要注意⽗类的protected
⽅法,⼦类尽量不要
暴露⾃⼰的public
⽅法供外界调⽤。
◦ 使⽤建议:⼦类必须完全实现⽗类的⽅法,孩⼦类可以有⾃⼰的个性。覆盖或实现⽗类的⽅法时,输⼊参数可以被放⼤,输出可以缩⼩
◦ ⽤例:跑步运动员类-会跑步,⼦类⻓跑运动员-会跑步且擅⻓⻓跑, ⼦类短跑运动员-会跑步且擅⻓短跑
• 依赖倒置原则(Dependence Inversion Principle)
◦ ⾼层模块不应该依赖低层模块,两者都应该依赖其抽象. 不可分割的原⼦逻辑就是低层模式,原⼦逻辑组装成的就是⾼层模块。
◦ 模块间依赖通过抽象(接⼝)发⽣,具体类之间不直接依赖
◦ 使⽤建议:每个类都尽量有抽象类,任何类都不应该从具体类派⽣。尽量不要重写基类的⽅法。结合⾥⽒替换原则使⽤。
◦ ⽤例:奔驰⻋司机类–只能开奔驰; 司机类 – 给什么⻋,就开什么⻋; 开⻋的⼈:司机–依赖于抽象
• 迪⽶特法则(Law of Demeter),⼜叫“最少知道法则”
◦ 尽量减少对象之间的交互,从⽽减⼩类之间的耦合。⼀个对象应该对其他对象有最少的了解。对类的低耦合提出了明确的要求:
◦ 只和直接的朋友交流, 朋友之间也是有距离的。⾃⼰的就是⾃⼰的(如果⼀个⽅法放在本类中,既不增加类间关系,也对本类不产⽣负⾯影响,那就放置在本类中)。
◦ ⽤例:⽼师让班⻓点名–⽼师给班⻓⼀个名单,班⻓完成点名勾选,返回结果,⽽不是班⻓点名,⽼师勾选
• 接⼝隔离原则(Interface Segregation Principle)
◦ 客⼾端不应该依赖它不需要的接⼝,类间的依赖关系应该建⽴在最⼩的接⼝上
◦ 使⽤建议:接⼝设计尽量精简单⼀
,但是不要对外暴露没有实际意义的接⼝。
◦ ⽤例:修改密码,不应该提供修改⽤⼾信息接⼝,⽽就是单⼀的最⼩修改密码接⼝,更不要暴露数据库操作
总结:
• 单⼀职责原则告诉我们实现类要职责单⼀;
• ⾥⽒替换原则告诉我们不要破坏继承体系;
• 依赖倒置原则告诉我们要⾯向接⼝编程;
• 接⼝隔离原则告诉我们在设计接⼝的时候要精简单⼀;
• 迪⽶特法则告诉我们要降低耦合;
• 开闭原则是总纲,告诉我们要对扩展开放,对修改关闭。
5.1、单例模式
⼀个类只能创建⼀个对象,即单例模式
,该设计模式可以保证系统中该类只有⼀个实例
,并提供⼀个访问它的全局访问点
,该实例被所有程序模块共享
。⽐如在某个服务器程序中,该服务器的配置信息存放在⼀个⽂件中,这些配置数据由⼀个单例对象统⼀读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种⽅式简化了在复杂环境下的配置管理。
单例模式有两种实现模式:饿汉模式和懒汉模式
-
饿汉模式:程序启动时就会创建⼀个唯⼀的实例对象。 因为单例对象已经确定, 所以⽐较适⽤于多线程环境中,
多线程获取单例对象不需要加锁(c++11后线程安全)
, 可以有效的避免资源竞争, 提⾼性能。代码示例
#include <iostream> class SingeLeton { public:static SingeLeton& getInstance(){return _eton;}int getnum(){return _num;}SingeLeton operator=(const SingeLeton&) = delete;SingeLeton (const SingeLeton&) = delete;private:int _num;static SingeLeton _eton;//声明SingeLeton(int num = 10){std::cout << "单例初始化成功" << std::endl;_num = num; }~SingeLeton(){} }; SingeLeton SingeLeton::_eton;int main() {std::cout << SingeLeton::getInstance().getnum() << std::endl;return 0; }
-
懒汉模式:
第⼀次使⽤
要使⽤单例对象的时候创建实例对象
。如果单例对象构造特别耗时
或者耗费济源(加载插件、加载⽹络资源等), 可以选择懒汉模式, 在第⼀次使⽤的时候才创建对象。#include <iostream> class SingeLeton { public:static SingeLeton& getInstance(){static SingeLeton _eton;//声明return _eton;}int getnum(){return _num;}SingeLeton operator=(const SingeLeton&) = delete;SingeLeton (const SingeLeton&) = delete; private:int _num;SingeLeton(int num = 10){std::cout << "单例初始化成功" << std::endl;_num = num; }~SingeLeton(){} }; int main() {std::cout << SingeLeton::getInstance().getnum() << std::endl;return 0; }
5.2、工厂模式
⼯⼚模式是⼀种创建型设计模式, 它提供了⼀种创建对象的最佳⽅式。在⼯⼚模式中,我们创建对象时不会对上层暴露创建逻辑,⽽是通过使⽤⼀个共同结构来指向新创建的对象,以此实现创建-使⽤的分离。
简单工厂模式:简单⼯⼚模式实现由⼀个⼯⼚对象通过类型决定创建出来指定产品类的实例。假设有个⼯⼚能⽣产出⽔果,当客⼾需要产品的时候明确告知⼯⼚⽣产哪类⽔果,⼯⼚需要接收⽤⼾提
供的类别信息,当新增产品的时候,⼯⼚内部去添加新产品的⽣产⽅式。
#include <iostream>
#include <string>
#include <memory>
class Fruit
{public:Fruit(){}virtual void name() = 0;
};class Apple : public Fruit
{public:Apple(){}virtual void name(){std::cout << "我是一个苹果" << std::endl;}
};class Banana : public Fruit
{public:Banana(){}virtual void name(){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->name();fruit = FruitFactory::create("香蕉");fruit->name();return 0;
}
简单⼯⼚模式:通过参数控制可以⽣产任何产品
优点:简单粗暴,直观易懂。使⽤⼀个⼯⼚⽣产同⼀等级结构下的任意产品
缺点:
1. 所有东西⽣产在⼀起,产品太多会导致代码量庞⼤
- 开闭原则遵循(开放拓展,关闭修改)的不是太好,要新增产品就必须修改⼯⼚⽅法。
工厂方法模式:在简单⼯⼚模式下新增多个⼯⼚,多个产品,每个产品对应⼀个⼯⼚。假设现在有A、B 两种产品,则开两个⼯⼚,⼯⼚ A 负责⽣产产品 A,⼯⼚ B 负责⽣产产品 B,⽤⼾只知道产品的⼯⼚名,⽽不知道具体的产品信息,⼯⼚不需要再接收客⼾的产品类别,⽽只负责⽣产产品。
#include <iostream>
#include <string>
#include <memory>
class Fruit
{public:Fruit(){}virtual void name() = 0;
};class Apple : public Fruit
{public:Apple(){}virtual void name(){std::cout << "我是一个苹果" << std::endl;}
};class Banana : public Fruit
{public:Banana(){}virtual void name(){std::cout << "我是一个香蕉" << std::endl;}
};class FruitFactory
{public:virtual std::shared_ptr<Fruit> create() = 0;
};class AppleFactory : public FruitFactory
{public:std::shared_ptr<Fruit> create() override{return std::make_shared<Apple>();}
};class BananaFactory : public FruitFactory
{public:std::shared_ptr<Fruit> create() override{return std::make_shared<Banana>();}
};int main()
{std::shared_ptr<FruitFactory>ff(new AppleFactory());std::shared_ptr<Fruit> fruit = ff->create();fruit->name();ff.reset(new BananaFactory());fruit = ff->create();fruit->name();return 0;
}
⼯⼚⽅法:定义⼀个创建对象的接⼝,但是由⼦类来决定创建哪种对象,使⽤多个⼯⼚分别⽣产指定的固定产品
优点:
1.减轻了⼯⼚类的负担,将某类产品的⽣产交给指定的⼯⼚来进⾏
2. 开闭原则遵循较好,添加新产品只需要新增产品的⼯⼚即可,不需要修改原先的⼯⼚类
缺点:对于某种可以形成⼀组产品族的情况处理较为复杂,需要创建⼤量的⼯⼚类
抽象工厂模式:⼯⼚⽅法模式通过引⼊⼯⼚等级结构,解决了简单⼯⼚模式中⼯⼚类职责太重的问题,但由于⼯⼚⽅法模式中的每个⼯⼚只⽣产⼀类产品,可能会导致系统中存在⼤量的⼯⼚类,势必会增加系统的开销。此时,我们可以考虑将⼀些相关的产品组成⼀个产品族(位于不同产品等级结构中功能相关联的产品组成的家族),由同⼀个⼯⼚来统⼀⽣产,这就是抽象⼯⼚模式的基本思想。
#include <iostream>
#include <string>
#include <memory>
class Fruit
{public:Fruit(){}virtual void name() = 0;
};class Apple : public Fruit
{public:Apple(){}virtual void name(){std::cout << "我是一个苹果" << std::endl;}
};class Banana : public Fruit
{public:Banana(){}virtual void name(){std::cout << "我是一个香蕉" << std::endl;}
};class Animal
{public:Animal(){}virtual void show() = 0;
};class Dog : public Animal
{public:Dog(){}void show() override{std::cout << "我是一只小狗" << std::endl;}
};class Cat : public Animal
{public:Cat(){}void show() override{std::cout << "我是一只小猫" << std::endl;}
};class Factor //超级工厂
{
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 Factor
{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>();}return std::shared_ptr<Fruit>();}virtual std::shared_ptr<Animal>getAnimal(const std::string& name) override{return std::shared_ptr<Animal>();}
};class AnimalFactory : public Factor
{public:virtual std::shared_ptr<Animal> getAnimal(const std::string& name) override{if(name == "小狗"){return std::make_shared<Dog>();}else{return std::make_shared<Cat>();}return std::shared_ptr<Animal>();}virtual std::shared_ptr<Fruit>getFruit(const std::string& name) override{return std::shared_ptr<Fruit>();}
};class FactoryPoducer //工厂生产者
{public:static std::shared_ptr<Factor> getFactory(const std::string& name){if(name == "水果"){return std::make_shared<FruitFactory>();}else{return std::make_shared<AnimalFactory>();}}
};int main()
{std::shared_ptr<Factor> ft1 = FactoryPoducer::getFactory("水果");std::shared_ptr<Fruit>fruit = ft1->getFruit("苹果");fruit->name();fruit = ft1->getFruit("香蕉");fruit->name();std::shared_ptr<Factor> ft2 = FactoryPoducer::getFactory("动物");std::shared_ptr<Animal>animal = ft2->getAnimal("小狗");animal->show();animal = ft2->getAnimal("小猫");animal->show();return 0;
}
抽象⼯⼚模式适⽤于⽣产多个⼯⼚系列产品衍⽣的设计模式,增加新的产品等级结构复杂,需要对原有系统进⾏较⼤的修改,甚⾄需要修改抽象层代码,违背了“开闭原则”。
5.3、建造者模式
建造者模式是⼀种创建型设计模式, 使⽤多个简单的对象⼀步⼀步构建成⼀个复杂的对象,能够将⼀个复杂的对象的构建与它的表⽰分离,提供⼀种创建对象的最佳⽅式。主要⽤于解决对象的构建过于复杂的问题。
建造者模式主要基于四个核⼼类
实现:
• 抽象产品类:
• 具体产品类:⼀个具体的产品对象类
• 抽象Builder类:创建⼀个产品对象所需的各个部件的抽象接⼝
• 具体产品的Builder类:实现抽象接⼝,构建各个部件
• 指挥者Director类:统⼀组建过程,提供给调⽤者使⽤,通过指挥者来构造产品
#include <iostream>
#include <string>
#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;void showParamaters(){std::string parm = "computer Paramaters:\n";parm += "\tBoard" + _board +"\n";parm += "\tDisplay" + _display + "\n";parm += "\tOs" + _os +"\n";std::cout << parm << std::endl;}
protected:std::string _board;std::string _display;std::string _os;
};//具体产品类
class MacBook : public Computer
{
public:MacBook(){}void SetOs() override {_os = "Mac Book 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;private:
};//具体产品的具体建造者类:实现抽象接口,构建和组装各个部件
class MacBookBuilder : public Builder
{
public:MacBookBuilder():_computer(new MacBook())//初始化_computer{}using ptr = std::shared_ptr<MacBookBuilder>;void buildBoard(const std::string& board) override{_computer->SetBoard(board);}virtual void buildDisplay(const std::string& display) override{_computer->SetDisplay(display);}virtual void buildOs() override{_computer->SetOs();}virtual Computer::ptr build() override{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* builder = new MacBookBuilder();std::unique_ptr<Director>dir(new Director(builder));dir->Construct("三星主板","联想显示器");Computer::ptr computer = builder->build();computer->showParamaters();return 0;
}
5.4、代理模式
代理模式指代理控制对其他对象的访问
, 也就是代理对象控制对原对象的引⽤。在某些情况下,⼀个对象不适合或者不能直接被引⽤访问,⽽代理对象可以在客⼾端和⽬标对象之间起到中介的作⽤。代理模式的结构包括⼀个是真正的你要访问的对象(⽬标类)、⼀个是代理对象。⽬标对象与代理对象实现同⼀个接⼝,先访问代理类再通过代理类访问⽬标对象。代理模式分为静态代理
、动态代理
:
• 静态代理
指的是,在编译时
就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪个被代理类。
• 动态代理
指的是,在运⾏时
才动态⽣成代理类,并将其与被代理类绑定。这意味着,在运⾏时才能确定代理类要代理的是哪个被代理类。
//房东要把一个房子通过中介租出去去理解代理模式
#include <iostream>
#include <string>class RentHouse
{
public:virtual void rentHouse() = 0;
};class Landlord : public RentHouse
{
public:void rentHouse() override{std:: cout << "将房子租出去\n";}
};class Intermediary : public RentHouse
{
public:void rentHouse() override{std:: cout << "发布招租启示\n";std:: cout << "带人看房\n";_landlord.rentHouse();std:: cout << "负责租后维修" << std::endl; }private:Landlord _landlord;
};int main()
{Intermediary intermediary;intermediary.rentHouse();return 0;
}
6、日志系统框架设计
本项⽬实现的是⼀个多⽇志器⽇志系统,主要实现的功能是让程序员能够轻松的将``程序运⾏⽇志信息落地到指定的位置
,且⽀持同步与异步
两种⽅式的⽇志落地⽅式。
项⽬的框架设计将项⽬分为以下⼏个模块来实现.
6.1、模块划分
• ⽇志等级模块:对输出⽇志的等级进⾏划分,以便于控制⽇志的输出,并提供等级枚举转字符串功能。
◦ OFF
:关闭
◦ DEBUG
:调试,调试时的关键信息输出。
◦ INFO
:提⽰,普通的提⽰型⽇志信息。
◦ WARN
:警告,不影响运⾏,但是需要注意⼀下的⽇志。
◦ RROR
:错误,程序运⾏出现错误的⽇志
◦ 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
:表⽰换⾏
◦ 设计思想:设计不同的⼦类,不同的⼦类从⽇志消息中取出不同的数据进⾏处理。
• ⽇志消息落地模块:决定了⽇志的落地⽅向,可以是标准输出,也可以是⽇志⽂件,也可以滚动⽂件输出…
◦ 标准输出
:表⽰将⽇志进⾏标准输出的打印。
◦ ⽇志⽂件输出
:表⽰将⽇志写⼊指定的⽂件末尾。
◦ 滚动⽂件输出
:当前以⽂件⼤⼩进⾏控制,当⼀个⽇志⽂件⼤⼩达到指定⼤⼩,则切换下⼀个⽂件进⾏输出
◦ 后期,也可以扩展远程⽇志输出,创建客⼾端,将⽇志消息发送给远程的⽇志分析服务器。
◦ 设计思想:设计不同的⼦类,不同的⼦类控制不同的⽇志落地⽅向。
• ⽇志器模块:
◦ 此模块是对以上⼏个模块的整合模块,⽤⼾通过⽇志器进⾏⽇志的输出,有效降低⽤⼾的使⽤难度。
◦ 包含有:⽇志消息落地模块对象,⽇志消息格式化模块对象,⽇志输出等级
• ⽇志器管理模块:
◦ 为了降低项⽬开发的⽇志耦合,不同的项⽬组可以有⾃⼰的⽇志器来控制输出格式以及落地⽅向,因此本项⽬是⼀个多⽇志器的⽇志系统。
◦ 管理模块就是对创建的所有⽇志器进⾏统⼀管理
。并提供⼀个默认⽇志器提供标准输出的⽇志输出。
• 异步线程模块:
◦ 实现对⽇志的异步输出功能,⽤⼾只需要将输出⽇志任务放⼊任务池
,异步线程负责⽇志的落地输出功能
,以此提供更加⾼效的⾮阻塞⽇志输出
。
模块分析图
7、代码设计
7.1、实用类设计
• 获取系统时间
//#include <time>
//使用time(nulltre)返回时间戳
• 判断⽂件是否存在
#include <unistd.h>
int access(const char *pathname, int mode);//第一个参数放入路径,第二个参数是权限掩码
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *path, struct stat *bf);
- stat通用windows和linux,而access是linux系统调用接口,不通用,所以使用stat
• 获取⽂件的所在⽬录路径
• 创建⽬录
#include <ctime>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
namespace sqy_log
{namespace util{class Date{public:static time_t now()//获取时间戳{return time(nullptr);}};class File{public:static bool exists(const std::string& pathname){// return access(pathanme.c_str(),F_OK) == 0;//系统调用接口,不具有通用性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){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);return;}//找到了std::string parent_dir = pathname.substr(0,pos+1);if(exists(parent_dir)){idx = pos + 1;continue;}mkdir(parent_dir.c_str(),0777);idx = pos + 1;}}};}
}
#endif
7.2日志等级类设计
⽇志等级总共分为7个等级,分别为:
• OFF
关闭所有⽇志输出
• DRBUG
进⾏debug
时候打印⽇志的等级
• INFO
打印⼀些⽤⼾提⽰信息
• WARING
打印警告信息
• ERROR
打印错误信息
• FATAL
打印致命信息- 导致程序崩溃的信息
#ifndef __LOGLEVEL_HPP_
#define __LOGLEVEL_HPP_#include <iostream>namespace sqy_bit
{class LogLevel{public:enum value{DEBUG,INFO,WARING,ERROR,FATAL,OFF};static const char* toString(LogLevel::value vl){switch(vl){case DEBUG: return "DEBUG";case INFO: return "INFO";case WARING: return "WARING";case ERROR: return "ERROR";case FATAL: return "FATAL";case OFF: return "OFF";}return "UNKNOW";}};}
#endif
7.3、日志消息类的设计
⽇志消息类主要是封装⼀条完整的⽇志消息所需的内容,其中包括⽇志等级、对应的logger name、打印⽇志源⽂件的位置信息(包括⽂件名和⾏号)、线程ID、时间戳信息、具体的⽇志信息等内容。
#ifndef __MESSAGE_H__
#define __MESSAGE_H__
#include "util.hpp"
#include "logLevel.hpp"
#include <thread>namespace sqy_log
{struct LogMsg{time_t _ctime;//日志产生的时间戳LogLevel::value _level;//日志等级size_t _line;//行号std::string _file;//文件名std::string _logger;//日志器名称std::string _payload;//有效消息数据std::thread::id _tid;//线程IDLogMsg(LogLevel::value level,size_t line,std::string file,std::string logger,std::string payload):_ctime(util::Date::now()),_tid(std::this_thread::get_id()),_line(line),_file(file),_logger(logger),_payload(payload),_level(level){}};
}
#endif
7.4、日志输出格式化类设计
⽇志格式化(Formatter)类主要负责格式化⽇志消息。
• pattern成员:保存⽇志输出的格式字符串。
◦ %d
⽇期
◦ %T
缩进
◦ %t
线程id
◦%p
⽇志级别
◦ %c
⽇志器名称
◦ %f
⽂件名
◦%l
⾏号
◦ %m
⽇志消息
◦ %n
换⾏
• std::vector<FormatItem::ptr> items成员:⽤于按序保存格式化字符串对应的⼦格式化对象。
FormatItem类主要负责⽇志消息⼦项的获取及格式化。其包含以下⼦类
• MsgFormatItem
:表⽰要从LogMsg中取出有效⽇志数据
•LevelFormatItem
:表⽰要从LogMsg中取出⽇志等级
• LoggerFormatItem
:表⽰要从LogMsg中取出⽇志器名称
• ThreadFormatItem
:表⽰要从LogMsg中取出线程ID
• TimeFormatItem
:表⽰要从LogMsg中取出时间戳并按照指定格式进⾏格式化
•CFileFormatItem
:表⽰要从LogMsg中取出源码所在⽂件名
• CLineFormatItem
:表⽰要从LogMsg中取出源码所在⾏号
• TabFormatItem
:表⽰⼀个制表符缩进
• NLineFormatItem
:表⽰⼀个换⾏
• OtherFormatItem
:表⽰⾮格式化的原始字符串
格式化子项的实现思想:
作用:从日志消息中取出指定的元素,追加到一块内存空间中
设计思想:
1.抽象一个格式化子项基类
2.基于基类,派生出不同的格式化子项子类主体消息,日志等级,时间子项,文件名,行号,日志器名称,线程ID,制表符,换行,其他这样就可以早父类中定义父类指针的数组,指向不同的格式化子项子类对象
7.4.1、抽象化格式子项基类
class FormatItem
{public:using ptr = std::shared_ptr<FormatItem>;virtual void Format(std::ostream &out, const LogMsg &msg) = 0;
};
7.4.2、有效消息数据子项
class FormatItem
{public:using ptr = std::shared_ptr<FormatItem>;virtual void Format(std::ostream &out, const LogMsg &msg) = 0;
};
7.4.3、日志等级子项
class LevelFormatItem : public FormatItem // 日志等级
{public:virtual void Format(std::ostream &out, const LogMsg &msg){out << LogLevel::toString(msg._level);}
};
7.4.4、日志器名称子项
class LoggerFormatItem : public FormatItem // 日志器名称
{public:virtual void Format(std::ostream &out, const LogMsg &msg){out << msg._logger;}
};
7.4.5、线程Id子项
class ThreadFormatItem : public FormatItem // 线程ID
{public:virtual void Format(std::ostream &out, const LogMsg &msg){out << msg._tid;}
};
7.4.6、时间戳子项
class TimeFormatItem : public FormatItem // 时间戳
{public:TimeFormatItem(const std::string &fmt = "%H:%M:%S") : _time_fmt(fmt){}virtual void Format(std::ostream &out, const LogMsg &msg){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;
};
- 时间子项可以设置
子格式
,在构造函数中需要传递一个子格式字符串
来控制时间子格式;
localtime_r介绍
在LogMsg对象中,时间元素是一个时间戳数字,不方便观察时间信息。我们需要将该时间戳转化为易于观察的时分秒的格式。
localtime_r函数是C标准库中的一个函数,用于将时间戳(表示自1970年1月1日以来的秒数)转换为本地时间的表示。这个函数是线程安全的版本,它接受两个参数:一个指向时间戳的指针和一个指向struct tm类型的指针,它会将转换后的本地时间信息存储在struct tm结构中。
函数原型如下:
struct tm *localtime_r(const time_t *timep, struct tm *result);
timep
参数是指向时间戳的指针;result
参数是指向struct tm类型的指针,用于存储转换后的本地时间信息;localtime_r
函数返回一个指向struct tm结构的指针,同时也将结果存储在result参数中;struct tm
结构包含了年、月、日、时、分、秒等时间信息的成员变量,可用于格式化和输出时间;
struct tm类型
struct tm
是C语言中的一个结构体类型,用于表示日期和时间的各个组成部分。
struct tm
结构包含以下成员变量:
struct tm
{int tm_sec; // 秒(0-59)int tm_min; // 分钟(0-59)int tm_hour; // 小时(0-23)int tm_mday; // 一个月中的日期(1-31)int tm_mon; // 月份(0-11,0代表1月)int tm_year; // 年份(从1900年起的年数,例如,121表示2021年)int tm_wday; // 一周中的星期几(0-6,0代表星期日)int tm_yday; // 一年中的第几天(0-365)int tm_isdst; // 是否为夏令时(正数表示是夏令时,0表示不是,负数表示夏令时信息不可用)
}
strftime介绍
strftime
函数是C标准库中的一个函数,用于将日期和时间按照指定的格式进行格式化,并将结果存储到一个字符数组中
。这个函数在C语言中非常常用,特别是在需要将日期和时间以不同的格式输出到屏幕、文件或其他输出设备时。
函数原型如下:
size_t strftime(char *s, size_t maxsize, const char *format, const struct tm *timeptr);
-
s
:一个指向字符数组的指针,用于存储格式化后的日期和时间字符串; -
maxsize
:指定了字符数组 s 的最大容量,以防止缓冲区溢出; -
format
:一个字符串,用于指定日期和时间的输出格式。该字符串可以包含- 各种格式化控制符,例如%Y表示年份,%m表示月份等等; -
timeptr
:一个指向struct tm
结构的指针,表示待格式化的日期和时间;
返回值: -
strftime
函数返回生成的字符数
(不包括空终止符\0),如果生成的字符数大于maxsize
,则返回0
,表示字符串无法完全存储在给定的缓冲区中.
7.4.7、源码文件名子项
class CFileFormatItem : public FormatItem // 文件名
{public:virtual void Format(std::ostream &out, const LogMsg &msg){out << msg._file;}
};
7.4.8、源码行号子项
class LineFormatItem : public FormatItem // 行号
{public:virtual void Format(std::ostream &out, const LogMsg &msg){out << std::to_string(msg._line);}
};
7.4.9、制表符子项
class TabFormatItem : public FormatItem // 制表符
{public:virtual void Format(std::ostream &out, const LogMsg &msg){out << "\t";}
};
7.4.10、换行子项
class NLineFormatItem : public FormatItem // 换行
{public:virtual void Format(std::ostream &out, const LogMsg &msg){out << "\n";}
};
7.4.11、其他子项
class OtherFormatItem : public FormatItem // 其他
{public:OtherFormatItem(const std::string &str = "") : _str(str){}virtual void Format(std::ostream &out, const LogMsg &msg){out << _str;}private:std::string _str;
};
7.5、日志格式化类的设计
7.5.1、设计思想
日志格式化Formatter
类中提供四个接口:
class Formatter{public:Formatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"): _pattern(pattern){}// 对msg进行格式化void format(std::ostream &out, const LogMsg &msg);std::string format(const LogMsg &msg);private:// 对格式化规则字符串进行解析bool parsePattern();// 根据不同的格式化字符串创建不同的格式化子项对象FormatItem::ptr createItem(const std::string &key, const std::string &val);private:std::string _pattern; // 格式化规则字符串std::vector<FormatItem::ptr> _items;};
}
Formatter
:构造函数,构造一个formatter
对象。函数参数为一个格式化字符串用来初始化成员pattern
;format
:提供两个重载函数,函数作用为将LogMsg
中元素提取出来交由对应的格式化子项处理;可以将LogMsg
进行格式化,并追加到流对象当中,也可以直接返回格式化后的字符串;parsePattern
:用于解析规则字符串_pattern
,createItem
:用于根据不同的格式化字符串创建不同的格式化子项对象;
7.5.2、接口实现
7.5.3、Formatter
// 时间{年-月-日 时:分:秒}缩进 线程ID 缩进 [日志级别] 缩进 [日志名称] 缩进 文件名:行号 缩进 消息换行
Formatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"): _pattern(pattern)
{assert(parsePattern()); // 确保格式化字符串有效
}
7.5.4、format
// 对msg进行格式化
void format(std::ostream &out, const LogMsg &msg)
{for (auto &item : _items){item->format(out, msg);}
}std::string format(const LogMsg &msg)
{std::stringstream ss;format(ss, msg);return ss.str();
}
7.5.5、pattern
7.5.6、函数设计思想
-
函数的主要逻辑是从前往后的处理格式化字符串。以默认格式化字符串"
[[%d{%H:%M:%S}]][[%t]][[%c]][[%f:%l]][%p]%T%m%n
"为例: -
从前往后遍历,如果没有遇到
%
则说明之前的字符都是原始字符串; -
遇到
%
,则看紧随其后的是不是另一个%,如果是,则认为%
就是原始字符串; -
如果%后面紧挨着的是格式化字符(c、f、l、S等),则进行处理;
-
紧随格式化字符之后,如果有{,则认为在{之后、}之前都是子格式内容;
在处理过程中,我们需要将得到的结果保存下来,于是我们可以创建一个
vector
,类型为一个键值对(key,val)
。如果是格式化字符,则key
为该格式化字符,val
为null
;若为原始字符串则key
为null
,val
为原始字符串内容。得到数组之后,根据数组内容,调用createItem
函数创建对应的格式化子项对象,添加到items
成员中。// 对格式化规则字符串进行解析 bool parsePattern() {// ab%%cde[%d{%H:%M:%S}][%p]%T%m%n// 1.对格式化字符串进行解析处理std::vector<std::pair<std::string, std::string>> order;std::string key, val;size_t pos = 0;while (pos < _pattern.size()){// 不是%的话,那么就是原始字符串if (_pattern[pos] != '%') {val.push_back(_pattern[pos++]); continue;}// 如果是%的话,判断后面是否跟%,如果跟的话,那就是原始的%字符if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%'){val.push_back(_pattern[pos + 1]);pos += 2;continue;}// 先进行清空if (!val.empty()){order.push_back(std::make_pair("", val));val.clear();}// 走到这里一定是一个格式化字符pos += 1;if (pos == _pattern.size()){std::cout << "%之后,没有对应的格式化字符!" << std::endl;return false;}key.push_back(_pattern[pos]);pos += 1;if (pos < _pattern.size() && _pattern[pos] == '{'){pos += 1; // 让其指向'{'之后的位置while (pos < _pattern.size() && _pattern[pos] != '}'){val.push_back(_pattern[pos]);pos++;}if (pos == _pattern.size()){std::cout << "子规则{}匹配出错!" << std::endl;return false;}pos += 1;}order.push_back(std::make_pair(key, val));key.clear();val.clear();}// 2.根据解析得到的数据初始化格式化子项数组成员for (auto &it : order){_items.push_back(createItem(it.first, it.second));}return true; }
createItem
根据不同的格式化字符创建不同得格式化子项对象;
// 根据不同的格式化字符串创建不同的格式化子项对象 FormatItem::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<TabFormatItem>();if (key == "t")return std::make_shared<ThreadFormatItem>();if (key == "p")return std::make_shared<LevelFormatItem>();if (key == "c")return std::make_shared<LoggerFormatItem>();if (key == "f")return std::make_shared<CFileFormatItem>();if (key == "l")return std::make_shared<LineFormatItem>();if (key == "m")return std::make_shared<MsgFormatItem>();if (key == "n")return std::make_shared<NLineFormatItem>();if(key.empty()) return std::make_shared<OtherFormatItem>(val);std::cout << "没有对应的格式化字符:%" << key << std::endl;abort();return FormatItem::ptr(); }
7.5.7、日志格式化输出类完整代码
#ifndef __FORMAT_H__
#define __FORMAT_H__#include "logLevel.hpp"
#include "util.hpp"
#include "Message.hpp"
#include <vector>
#include <cassert>
#include <iostream>
#include <sstream>
#include <memory>
namespace sqy_log
{class FormatItem{public:using ptr = std::shared_ptr<FormatItem>;virtual void Format(std::ostream &out, const LogMsg &msg) = 0;};class MsgFormatItem : public FormatItem // 有效消息数据{public:virtual void Format(std::ostream &out, const LogMsg &msg){out << msg._payload;}};class LevelFormatItem : public FormatItem // 日志等级{public:virtual void Format(std::ostream &out, const LogMsg &msg){out << LogLevel::toString(msg._level);}};class LoggerFormatItem : public FormatItem // 日志器名称{public:virtual void Format(std::ostream &out, const LogMsg &msg){out << msg._logger;}};class ThreadFormatItem : public FormatItem // 线程ID{public:virtual void Format(std::ostream &out, const LogMsg &msg){out << msg._tid;}};class TimeFormatItem : public FormatItem // 时间戳{public:TimeFormatItem(const std::string &fmt = "%H:%M:%S") : _time_fmt(fmt){}virtual void Format(std::ostream &out, const LogMsg &msg){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;};class CFileFormatItem : public FormatItem // 文件名{public:virtual void Format(std::ostream &out, const LogMsg &msg){out << msg._file;}};class LineFormatItem : public FormatItem // 行号{public:virtual void Format(std::ostream &out, const LogMsg &msg){out << std::to_string(msg._line);}};class TabFormatItem : public FormatItem // 制表符{public:virtual void Format(std::ostream &out, const LogMsg &msg){out << "\t";}};class NLineFormatItem : public FormatItem // 换行{public:virtual void Format(std::ostream &out, const LogMsg &msg){out << "\n";}};class OtherFormatItem : public FormatItem // 其他{public:OtherFormatItem(const std::string &str = "") : _str(str){}virtual void Format(std::ostream &out, const LogMsg &msg){out << _str;}private:std::string _str;};// ◦ %d ⽇期// ◦ %T 缩进// ◦ %t 线程id// ◦ %p ⽇志级别// ◦ %c ⽇志器名称// ◦ %f ⽂件名// ◦ %l ⾏号// ◦ %m ⽇志消息// ◦ %n 换⾏class Formatter{public:Formatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"): _pattern(pattern){assert(parsePattern());}// 对msg进行格式化void format(std::ostream &out, const LogMsg &msg){for (auto &item : _items){item->Format(out, msg); // 会根据顺序输出}}std::string format(const LogMsg &msg){std::stringstream ss;format(ss, msg);return ss.str();}private:// 对格式化规则字符串进行解析bool parsePattern(){// ab%%cde[%d{%H:%M:%S}][%p]%T%m%n// 1.对格式化字符串进行解析处理std::vector<std::pair<std::string, std::string>> order;std::string key, val;size_t pos = 0;while (pos < _pattern.size()){// 不是%的话,那么就是原始字符串if (_pattern[pos] != '%') {val.push_back(_pattern[pos++]); continue;}// 如果是%的话,判断后面是否跟%,如果跟的话,那就是原始的%字符if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%'){val.push_back(_pattern[pos + 1]);pos += 2;continue;}// 先进行清空if (!val.empty()){order.push_back(std::make_pair("", val));val.clear();}// 走到这里一定是一个格式化字符pos += 1;if (pos == _pattern.size()){std::cout << "%之后,没有对应的格式化字符!" << std::endl;return false;}key.push_back(_pattern[pos]);pos += 1;if (pos < _pattern.size() && _pattern[pos] == '{'){pos += 1; // 让其指向'{'之后的位置while (pos < _pattern.size() && _pattern[pos] != '}'){val.push_back(_pattern[pos]);pos++;}if (pos == _pattern.size()){std::cout << "子规则{}匹配出错!" << std::endl;return false;}pos += 1;}order.push_back(std::make_pair(key, val));key.clear();val.clear();}// 2.根据解析得到的数据初始化格式化子项数组成员for (auto &it : order){_items.push_back(createItem(it.first, it.second));}return true;}// 根据不同的格式化字符串创建不同的格式化子项对象FormatItem::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<TabFormatItem>();if (key == "t")return std::make_shared<ThreadFormatItem>();if (key == "p")return std::make_shared<LevelFormatItem>();if (key == "c")return std::make_shared<LoggerFormatItem>();if (key == "f")return std::make_shared<CFileFormatItem>();if (key == "l")return std::make_shared<LineFormatItem>();if (key == "m")return std::make_shared<MsgFormatItem>();if (key == "n")return std::make_shared<NLineFormatItem>();if(key.empty()) return std::make_shared<OtherFormatItem>(val);std::cout << "没有对应的格式化字符:%" << key << std::endl;abort();return FormatItem::ptr();}private:std::string _pattern; // 格式化规则字符串std::vector<FormatItem::ptr> _items;};
}
#endif
7.6、日志落地模块设计
功能:将格式化完成后的日志消息字符串,输出到指定的位置
扩展:支持同时奖日志落地到不同的位置
位置分类:
1.标准输出:StdoutSink
2.指定文件:FileSink(事后进行日志分析)
3.滚动文件:RollSink(文件按照时间/大小进行滚动切换)
扩展:支持落地方向的扩展
用户可以编写一个新的落地模块,将日志进行其他方向的落地
实现思想:
1.抽象处落地模块类
2.不同落地方向从基类进行派生
3.使用工厂模式进行创建与表示的分离
7.6.1、抽象基类
- 提供一个智能指针对象方便管理;
- 将日志输函数
log
作与析构函数
设置为虚函数
;
class LogSink
{public:using ptr = std::shared_ptr<LogSink>;virtual void log(const char* data,size_t len) = 0;LogSink(){}virtual ~LogSink(){}
};
7.6.2、StdoutSink类设计
// 落地方向:标准输出
class StdOutSink : public LogSink
{
public:void log(const char *data, size_t len){std::cout.write(data, len);}
};
7.6.3、FileSink类设计
类中包含两个成员:
- pathname:文件名,用来指定日志消息输出到哪个文件;
- ofs:文件输出类对象,进行输出操作;
在C++
中,ofstream
是用于文件输出的类,它是 C++
标准库中的一部分,通常与 ifstream
(用于文件输入)一起使用。ofstream
类允许你创建、打开、写入和关闭文本文件。你可以使用它来将数据写入文件,例如文本、数字或二进制数据。
//落地方向:指定文件
class FileSink : public LogSink
{public://构造时传入文件名,并打开文件,将操作句柄管理起来FileSink(const std::string &pathname):_pathname(pathname){//创建日志文件所在目录util::File::createDirectory(util::File::path(pathname));//创建并打开文件_ofs.open(_pathname,std::ios::app | std::ios::binary);//断言文件打开是否成功assert(_ofs.is_open());}//将日志消息写入到文件void log(const char* data,size_t len){_ofs.write(data,len);assert(_ofs.good());}private:std::string _pathname;std::ofstream _ofs;
};
7.6.4、RollBySizeSink类设计
日志文件滚动的条件有两个:文件大小和时间。我们可以选择:
-
日志文件在大于1GB的时候会更换新的文件;
-
每天定点滚动一个日志文件。
本项目基于文件大小的判断滚动生成新的文件。
滚动文件输出的必要性:
-
由于磁盘空间有限,我们不可能一直无限的向一个文件中增加数据;
-
如果一个日志文件的体积太大,一方面是不好打开,另一方面是即使打开了,
由于包含数据巨大,也不利于查找我们需要的信息;
-
所以实际开发中会对单个日志文件的大小也会做一些限制,即当大小超过了某个大小时(如1GB),我们就重新创建一个新的日志文件来滚动写日志。对于那些过期的文件,大部分企业内都有专门的运维人员去定时清理过期的日志文件,或者设置定时任务,定时清理过期日志。
//滚动文件class RollBySizeSink : public LogSink{public://构造时传入文件名,并打开文件,将操作句柄管理起来RollBySizeSink(const std::string &basename,size_t max_size):_basename(basename),_max_fsize(max_size),_cur_fsize(0){std::string pathname = createNewFile();//创建日志文件所在目录util::File::createDirectory(util::File::path(pathname));//创建并打开文件_ofs.open(pathname,std::ios::app | std::ios::binary);}//将日志消息写入到标准输出--写入前判断文件大小,超过了最大大小就切换文件void log (const char* data,size_t len){if(_cur_fsize >= _max_fsize){_ofs.close();//关闭原来打开的文件std::string pathname = createNewFile();_ofs.open(pathname,std::ios::app | std::ios::binary); assert(_ofs.good()); _cur_fsize = 0;//将当前大小更新为0}_ofs.write(data,len);_cur_fsize += len;assert(_ofs.good());}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://通过基础文件名+扩展文件名(以时间生成)组成一个实际的当前输出文件名size_t name_count = 1;std::string _basename;std::ofstream _ofs;size_t _max_fsize;//记录最大大小,当前文件超过了这个大小就要切换文件size_t _cur_fsize;//记录当前文件已经写入的数据大小};
7.6.5、RollByTimeSink设计
本小节主要内容为测试日志落地类是否支持扩展功能。我们新增一个基于时间的滚动文件类RollByTimeSink
。
enum TimeGap
{GAP_SECOND,GAP_MINUTE,GAP_HOUR,GAP_DAY
};class RollByTimeSink : public 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_MINUTE: _gap_size = 60; break;case TimeGap::GAP_HOUR: _gap_size = 3600; break;case TimeGap::GAP_DAY: _gap_size = 3600 * 24; break;}_cur_gap = sqy_log::util::Date::now() % _gap_size;std::string filename = createNewFile();sqy_log::util::File::createDirectory(sqy_log::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 = sqy_log::util::Date::now();if(cur / _gap_size != _cur_gap)//注意这里是除{_ofs.close();std::string filename = createNewFile();_ofs.open(filename,std::ios::app | std::ios::binary);assert(_ofs.is_open());_cur_gap = cur / _gap_size;//更新当前时间}_ofs.write(data,len);assert(_ofs.good());}private://进行大小判断,超过指定大小则创建新文件std::string createNewFile(){//获取时间戳time_t t = sqy_log::util::Date::now();struct tm lt;localtime_r(&t,<);std::stringstream filename;filename << _basename;filename << lt.tm_year + 1900;//因为时间戳从1900开始所以+1900filename << lt.tm_mon + 1;//因为月份从一月开始,所以+1filename << 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 _cur_gap;//当前是第几个时间段size_t _gap_size;//时间段的大小
};
7.6.5、日志落地类工厂设计
- 为了避免用户将来实现自己的落地方向时需要修改源代码,这违背了开闭原则,所以我们采用工厂类的设计;
- 由于不同的落地方向如
StdoutSink
、FileSink
、RollBySizeSink
,它们各自的构造函数所需参数并不相同,无法统一的管理,所以我们采用参数包的方式来解决。
//使用工厂创建日志落地类
class SinkFactory
{public:template<class SingType, typename ...Args>static LogSink::ptr create(Args ...args){return std::make_shared<SingType>(std::forward<Args>(args)...);}
};
7.6.6、日志落地类完整代码
#ifndef __SINK_H__
#define __SINK_H__#include "util.hpp"
#include <memory>
#include <fstream>
#include <assert.h>
#include <sstream>
namespace sqy_log
{class LogSink{public:using ptr = std::shared_ptr<LogSink>;virtual void log(const char* data,size_t len) = 0;LogSink(){}virtual ~LogSink(){}};//落地方向:标准输出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){//创建日志文件所在目录util::File::createDirectory(util::File::path(pathname));//创建并打开文件_ofs.open(_pathname,std::ios::app | std::ios::binary);//断言文件打开是否成功assert(_ofs.is_open());}//将日志消息写入到文件void log(const char* data,size_t len){_ofs.write(data,len);assert(_ofs.good());}private:std::string _pathname;std::ofstream _ofs;};//滚动文件class RollBySizeSink : public LogSink{public://构造时传入文件名,并打开文件,将操作句柄管理起来RollBySizeSink(const std::string &basename,size_t max_size):_basename(basename),_max_fsize(max_size),_cur_fsize(0){std::string pathname = createNewFile();//创建日志文件所在目录util::File::createDirectory(util::File::path(pathname));//创建并打开文件_ofs.open(pathname,std::ios::app | std::ios::binary);}//将日志消息写入到标准输出--写入前判断文件大小,超过了最大大小就切换文件void log (const char* data,size_t len){if(_cur_fsize >= _max_fsize){_ofs.close();//关闭原来打开的文件std::string pathname = createNewFile();_ofs.open(pathname,std::ios::app | std::ios::binary); assert(_ofs.good()); _cur_fsize = 0;//将当前大小更新为0}_ofs.write(data,len);_cur_fsize += len;assert(_ofs.good());}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;//因为时间戳从1900开始所以+1900filename << lt.tm_mon + 1;//因为月份从一月开始,所以+1filename << 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://通过基础文件名+扩展文件名(以时间生成)组成一个实际的当前输出文件名size_t name_count = 1;std::string _basename;std::ofstream _ofs;size_t _max_fsize;//记录最大大小,当前文件超过了这个大小就要切换文件size_t _cur_fsize;//记录当前文件已经写入的数据大小};//使用工厂创建日志落地类class SinkFactory{public:template<class SingType, typename ...Args>static LogSink::ptr create(Args ...args){return std::make_shared<SingType>(std::forward<Args>(args)...);}};}
#endif
7.7、日志器类设计
同步⽇志器:直接对⽇志消息进⾏输出。
异步⽇志器:将⽇志消息放⼊缓冲区,由异步线程进⾏输出。
因此⽇志器类在设计的时候先设计出⼀个Logger基类,在Logger基类的基础上,继承出SyncLogger同步⽇志器和AsyncLogger异步⽇志器。且因为⽇志器模块是对前边多个模块的整合,想要创建⼀个⽇志器,需要设置⽇志器名称,设置⽇志输出等级,设置⽇志器类型,设置⽇志输出格式,设置落地⽅向,且落地⽅向有可能存在多个,整个⽇志器的创建过程较为复杂,为了保持良好的代码⻛格,编写出优雅的代码,因此⽇志器的创建这⾥采⽤了建造者模式来进⾏创建。
功能:对前边所有模块进行整合,向外提供接口完成不同等级日志的输出
管理的成员:
1.格式化模块对象
2.落地模块对象
3.默认的日志输出限制等级(大于等于限制的等级才能输出)
4.互斥锁(保证日志输出过程是线程安全大的,不会出现交叉日志)
5.日志器名称(日志器的唯一标识,以便于查找)
提供的操作:
-
debug
等级日志的输出操作(分别会封装日志消息LogMsg
–各个接口日志等级不同) -
info
等级日志的输出操作 -
waring
等级日志的输出操作 -
error
等级日志的输出操作 -
fatal
等级日志的输出操作
当前日志系统支持同步日志和异步日志两种方式,两个不同的日志器唯一的区别是它们在日志落地方式上有所不同:
同步日志器
:直接对日志消息进行输出;异步日志器
:将日志消息放入缓冲区,由异步线程
进行输出。
因此日志器在设计的时候先设计一个Logger基类
,在Logger基类
的基础上继承出SyncLogger同步日志器
和AsyncLogger异步日志器
。
实现:
1.抽象Logger
基类(派生除同步日志器类&异步日志器类)
2.因为两种不同的日志器,只有落地方式不同,因此将落地操作给抽象处理
不同的日志器调用各自的落地操作进行日志落地
模块关联中使用基类指针对子类日志器对象进行日志管理和操作
7.7.1、Logger类的设计
-
debug
、info
等等接口在设计是,需要传递参数有文件名、行号、参数包。传递文件名与行号是为了避免获取文件名和行号时是在本函数内部。 -
将参数包进行内容提取后保存在字符串中,交由serialize进行处理;
-
serialize函数的功能是,将字符串中的内容进行日志消息格式化,然后最终进行消息落地操作
struct 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()){}//完成构造日志消息对象过程并进行格式化,得到格式化后额日志消息字符串--然后进行落地输出void debug(const std::string&file,size_t line, const std::string& fmt, ...){//通过传入的参数构造除一个日志消息对象,进行日志的格式化,最终落地//1.判断当前的日志是否达到输出等级if(LogLevel::value::DEBUG < _limit_level){return;}//2.对fmt进行格式化字符串和不定参进行字符串组织,得到的日志消息的字符串char* res;va_list ap;va_start(ap,fmt);//初始化ap指针,使其指向count后面的第一个参数int len = vasprintf(&res,fmt.c_str(),ap);if(len == -1){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, ...){if(LogLevel::value::INFO < _limit_level){return;}char* res;va_list ap;va_start(ap,fmt);int len = vasprintf(&res,fmt.c_str(),ap);if(len == -1){std::cout << "vasprintf failed!!\n";return;}va_end(ap);serialize(LogLevel::value::INFO,file,line,res);free(res);}void warn(const std::string&file,size_t line, const std::string& fmt, ...){if(LogLevel::value::WARING < _limit_level){return;}char* res;va_list ap;va_start(ap,fmt);//初始化ap指针,使其指向count后面的第一个参数int len = vasprintf(&res,fmt.c_str(),ap);if(len == -1){std::cout << "vasprintf failed!!\n";return;}va_end(ap);//将ap置空serialize(LogLevel::value::WARING,file,line,res);free(res);//注意释放}void error(const std::string&file,size_t line, const std::string& fmt, ...){if(LogLevel::value::ERROR < _limit_level){return;}char* res;va_list ap;va_start(ap,fmt);int len = vasprintf(&res,fmt.c_str(),ap);if(len == -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, ...){if(LogLevel::value::FATAL< _limit_level){return;}char* res;va_list ap;va_start(ap,fmt);int len = vasprintf(&res,fmt.c_str(),ap);if(len == -1){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,const char * res){//3.构造LogMsg对象LogMsg msg(level,line,file,_logger_name,res);//4.通过格式化工具对LogMsg进行格式化,得到格式化的日志字符串std::stringstream ss;_formatter->format(ss,msg);//5.进行日志落地log(ss.str().c_str(),ss.str().size());}//对log进行抽象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;};
7.7.2、同步日志器类的设计
同步日志器设计较为简单,设计思想是:
- 遍历日志落地数组,以数组中的各种落地方式进行落地操作
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);}}
};
7.7.3、日志器测试代码
//测试Loggerstd::string logger_name = "sync_logger";sqy_log::LogLevel::value limit = sqy_log::LogLevel::value::WARING;sqy_log::Formatter::ptr fmt(new sqy_log::Formatter("[%d{%H:%M:%S}][%c][%f:%l][%p]%T%m%n"));sqy_log::LogSink::ptr stdout_lsp = sqy_log::SinkFactory::create<sqy_log::StdoutSink>();sqy_log::LogSink::ptr file_lsp = sqy_log::SinkFactory::create<sqy_log::FileSink>("./logfile/test.log");sqy_log::LogSink::ptr roll_lsp = sqy_log::SinkFactory::create<sqy_log::RollBySizeSink>("./logfile/roll-",1024*1024);std::vector<sqy_log::LogSink::ptr> sinks = {stdout_lsp,file_lsp,roll_lsp};sqy_log::Logger::ptr logger(new sqy_log::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;
while(cursize < 1024*1024*10)
{logger->fatal(__FILE__,__LINE__,"测试日志-%d",count++);cursize += 100;
}
7.7.4、日志器建造者模式设计
由于日志器测试代码中,在构建一个同步日志器是,需要设置很多的零部件。使用起来过于复杂。
我们需要使用建造者模式来建造日志器,而不要让用户直接去构造日志器,以简化用户的使用复杂度
.
设计思想:
-
抽象一个日志器建造者类:
设置日志器类型
将不同类型(同步&异步)日志器的创建放到同一个日志器建造者类中完成
-
派生除具体的建造者类-----局部日志器建造者&全局日志器建造者类
(后面提娜佳了全局单例管理器之后,将日志器添加到全局管理)
抽象日志器建造者类
- 建造者类中包含成员:
logger_type
日志器类型;logger_name
日志器名称;limit_level
日志输出限制等级;formatter
格式化对象;sinks
日志落地数组;
- 还有构建各个零件的函数;
//使用建造者模式来建造日志器,而不要让用户直接去构造日志器,简化用户的使用复杂度 //抽象一个日志器建造者类(完成日志器对象所需零部件的构建&日志器的构建) //1.设置日志器类型 //2.将不同类型的日志器的创建放到同一个日志器建造者类中完成 enum LoggerType {LOGGER_SYNC,LOGGER_ASYNC };class LoggerBuilder {public:LoggerBuilder():_logger_type(LoggerType::LOGGER_SYNC),_limit_level(LogLevel::value::DEBUG){}void buildLoggerType(LoggerType type);void buildLoggerName(const std::string& name);void buildLoggerLevel(LogLevel::value level);void buildFormatter(const std::string &pattern);template<typename SinkType, typename ...Args>void buildSink(Args &&...args);virtual Logger::ptr build() = 0;protected:LoggerType _logger_type;std::string _logger_name;LogLevel::value _limit_level;Formatter::ptr _formatter;std::vector<LogSink::ptr> _sinks; };
- 建造者类中包含成员:
7.7.5、派生局部日志器建造者
//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<SyncLogger>(_logger_name,_limit_level,_formatter,_sinks);}
};
7.7.6、日志器建造者类测试代码
std::unique_ptr<sqy_log::LoggerBuilder> builder(new sqy_log::LocalLoggerBuilder());
builder->buildFormatter("%m%n");
builder->buildLoggerLevel(sqy_log::LogLevel::value::WARING);
builder->buildLoggerName("sync_logger");
builder->buildLoggerType(sqy_log::LoggerType::LOGGER_SYNC);
builder->buildSink<sqy_log::FileSink>("./logfile/test.log");
builder->buildSink<sqy_log::StdoutSink>();
sqy_log::Logger::ptr logger = builder->build();logger->debug(__FILE__,__LINE__,"%s","测试日志");
logger->info(__FILE__,__LINE__,"%s","测试日志");
logger->warn(__FILE__,__LINE__,"%s","测试日志");
logger->error(__FILE__,__LINE__,"%s","测试日志");
logger->fatal(__FILE__,__LINE__,"%s","测试日志");size_t cursize = 0,count = 0;
while(cursize < 1024*1024*10)
{logger->fatal(__FILE__,__LINE__,"测试日志-%d",count++);cursize += 100;
}
7.7.7、日志器类完整代码
#ifndef __LOGGER_H__
#define __LOGGER_H__#include "util.hpp"
#include "logLevel.hpp"
#include "sink.hpp"
#include "format.hpp"
#include <atomic>
#include <mutex>
#include <cstdarg>namespace sqy_log
{struct 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()){}//完成构造日志消息对象过程并进行格式化,得到格式化后额日志消息字符串--然后进行落地输出void debug(const std::string&file,size_t line, const std::string& fmt, ...){//通过传入的参数构造除一个日志消息对象,进行日志的格式化,最终落地//1.判断当前的日志是否达到输出等级if(LogLevel::value::DEBUG < _limit_level){return;}//2.对fmt进行格式化字符串和不定参进行字符串组织,得到的日志消息的字符串char* res;va_list ap;va_start(ap,fmt);//初始化ap指针,使其指向count后面的第一个参数int len = vasprintf(&res,fmt.c_str(),ap);if(len == -1){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, ...){if(LogLevel::value::INFO < _limit_level){return;}char* res;va_list ap;va_start(ap,fmt);int len = vasprintf(&res,fmt.c_str(),ap);if(len == -1){std::cout << "vasprintf failed!!\n";return;}va_end(ap);serialize(LogLevel::value::INFO,file,line,res);free(res);}void warn(const std::string&file,size_t line, const std::string& fmt, ...){if(LogLevel::value::WARING < _limit_level){return;}char* res;va_list ap;va_start(ap,fmt);//初始化ap指针,使其指向count后面的第一个参数int len = vasprintf(&res,fmt.c_str(),ap);if(len == -1){std::cout << "vasprintf failed!!\n";return;}va_end(ap);//将ap置空serialize(LogLevel::value::WARING,file,line,res);free(res);//注意释放}void error(const std::string&file,size_t line, const std::string& fmt, ...){if(LogLevel::value::ERROR < _limit_level){return;}char* res;va_list ap;va_start(ap,fmt);int len = vasprintf(&res,fmt.c_str(),ap);if(len == -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, ...){if(LogLevel::value::FATAL< _limit_level){return;}char* res;va_list ap;va_start(ap,fmt);int len = vasprintf(&res,fmt.c_str(),ap);if(len == -1){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,const char * res){//3.构造LogMsg对象LogMsg msg(level,line,file,_logger_name,res);//4.通过格式化工具对LogMsg进行格式化,得到格式化的日志字符串std::stringstream ss;_formatter->format(ss,msg);//5.进行日志落地log(ss.str().c_str(),ss.str().size());}//对log进行抽象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);}}};//使用建造者模式来建造日志器,而不要让用户直接去构造日志器,简化用户的使用复杂度//抽象一个日志器建造者类(完成日志器对象所需零部件的构建&日志器的构建)//1.设置日志器类型//2.将不同类型的日志器的创建放到同一个日志器建造者类中完成enum LoggerType{LOGGER_SYNC,LOGGER_ASYNC};class LoggerBuilder{public:LoggerBuilder():_logger_type(LoggerType::LOGGER_SYNC),_limit_level(LogLevel::value::DEBUG){}void buildLoggerType(LoggerType type){_logger_type = type;}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: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<SyncLogger>(_logger_name,_limit_level,_formatter,_sinks);}};
}#endif
7.8、异步日志器类与异步工作器类设计
异步日志器:
思想:为了避免因为写日志得过程阻塞,导致业务线程在写日志得时候影响效率,因此异步得思想就是不让业务线程进行日志实际落地操作,而是将日志消息放到缓冲区(一块指定得内存)中接下来有一个专门得异步线程,去针对缓冲区中得数据进行处理(实际得落地操作)
实现:
1.实现一个线程安全得缓冲区
2.创建一个异步工作线程,专门负责缓冲区日志消息得落地操作
缓冲区详细设计:
1.使用队列,缓存日志消息,逐条处理
要求:不能涉及到空间得频繁申请与释放,否则会降低效率
结果:设计一个环形队列(提前将空间申请好,然后对空间循环利用)
问题:这个缓冲区得操作涉及多线程,因此缓冲区得操作必须涉及线程安全线程安全实现:对于缓冲区得读写加锁因为写日志操作,在实际开发中,并不会分配太多得资源,所以工作线程只需要一个日志器有一个就行设计到得锁冲突:生产者与生产者得互斥 & 生产者与消费者得互斥
问题:锁冲突较为严重,因为所有线程之间都存在互斥关系
解决方案:使用双缓冲区得方式
单个缓冲区得进一步设计:
设计一个缓冲区
:直接存放格式化后得日志消息字符串
好处
:
- 减少了LogMsg对象频繁得构造得消耗
- 可以针对缓冲区中得日志信息,一次性进行IO操作,减少IO次数,提高效率
缓冲区类得设计
:
-
管理一个存放字符串数据得缓冲区(使用vector进行空间管理)
-
当前得写入数据位置得指针(指向可写区域得起始位置,避免数据得写入覆盖)
-
当前得读取数据位置得指针(指向可读数据区域得起始位置,当读取指针与写入指针指向相同位置表示数据取完了)
提供得操作
:
1.向缓冲区中写入数据
2.获取可读数据起始地址得借口
3.获取可读数据长度得接口
4.移动读写位置得接口
5.初始化缓冲区得操作(将读写位置初始化–将一个缓冲区所有数据处理完毕之后)
6.提供交换缓冲区得操作(交换空间地址,并不交换空间数据)
7.8.1、异步缓冲区类设计
#ifndef __BUFFER_H__
#define __BUFFER_H__
#include <vector>
#include <iostream>
#include <cassert>
#include <vector>
#include <algorithm>namespace sqy_log
{#define DEFAULT_BUFFER_SIZE (1024*1024*1)//默认缓冲区的大小#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//固定大小则直接返回// if(len > writeAbleSize()) return;//动态空间,用于极限性能测试--扩容ensureEnoughSize(len);//1.将数据拷贝进缓冲区std::copy(data,data+len, &_buffer[_writer_idx]);//2.将当前写入位置向后偏移moveWriter(len);}//返回可读数据的起始地址const char* begin(){return &_buffer[_reader_idx];}//返回可读数据的长度size_t readAbleSize(){//因为当前实现的缓冲区设计是双缓冲区,处理完交换,不存在空间循环使用return _writer_idx - _reader_idx;}//返回可写数据的长度size_t writeAbleSize(){//对于扩容思路,不存在可写空间大小,这接口仅仅针对固定大小缓冲区设计return _buffer.size() - _writer_idx;}//对buffer实现交换操作void swap(Buffer& buffer){_buffer.swap(buffer._buffer);std::swap(_reader_idx,buffer._reader_idx);std::swap(_writer_idx,buffer._writer_idx);}//判断缓冲区是否为空bool empty(){return _reader_idx == _writer_idx;}//重置读写位置,初始化缓冲区void reset(){_writer_idx = 0;_reader_idx = 0;//与writer_idx相等表示没有数据可读}//对读指针进行向后偏移操作void moveReader(size_t len){assert(len <= readAbleSize());_reader_idx += len;}//对写指针进行向后偏移操作void moveWriter(size_t len){assert(len + _writer_idx <= _buffer.size());_writer_idx += len;}private://对空间扩容void ensureEnoughSize(size_t len){if(len < writeAbleSize()) return;//不需要扩容size_t new_size = 0;if(_buffer.size() < THRESHOLD_BUFFER_SiZE){new_size = _buffer.size() + len;//为什么要加len,是因为有可能翻倍后还是不比len大}else{new_size = _buffer.size() + INCREMENT_BUFFER_SIZE;}_buffer.resize(new_size);}private:std::vector<char> _buffer;size_t _reader_idx;//当前可读数据的指针--本质是下标size_t _writer_idx;//当前可写数据的指针--本质是下标};
}
#endif
7.8.2、异步工作线程设计
1.异步工作使用双缓冲区的思想
外界将入伍数据,添加到输入缓冲区中异步线程对处理缓冲区中的数据进行处理,若处理缓冲区中没有数据了则交换缓冲区
实现:
管理的成员:
1.双缓冲区(生产、消费)
2.互斥锁//保证线程安全
3.条件变量-生产&消费(生产缓冲区没有数据,处理完消费缓冲区数据后就休眠)
4.回调函数(针对缓冲区中数据处理接口-外界传入一个函数,告诉异步工作器数据该如何处理)
提供的操作:
1.停止异步工作器
2.添加数据到缓冲区
私有操作:
创建线程,线程入口函数中,交换缓冲区,对消费缓冲区数据使用回调函数进行处理,处理完后再次交换
#ifndef __LOPPER_HPP__
#define __LOPPER_HPP__
#include <iostream>
#include <functional>
#include <memory>
#include <thread>
#include <mutex>
#include <atomic>
#include <condition_variable>
#include "buffer.hpp"namespace sqy_log
{using Functor = std::function<void(Buffer&)>;enum class AsyncType{ASYNC_SAFE,//安全状态表示,缓冲区满了则阻塞ASYNC_UNSAFE//非安全状态表示无限扩容,用于性能测试};class AsyncLooper{public:AsyncLooper(const Functor& cb,AsyncType lopper_type = AsyncType::ASYNC_SAFE):_stop(false),_thread(std::thread(&AsyncLooper::threadEntry,this)),_callBack(cb),_lopper_type(lopper_type){}using ptr = std::shared_ptr<AsyncLooper>;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(_lopper_type == AsyncType::ASYNC_SAFE)_cond_pro.wait(lock,[&](){return _pro_buf.writeAbleSize() >= len;});//能够走下来表示满足条件,可以向缓冲区添加数据_pro_buf.push(data,len);//唤醒消费者对缓冲区中数据进行处理_cond_con.notify_one();}~AsyncLooper(){stop();}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(_lopper_type == AsyncType::ASYNC_SAFE)_cond_pro.notify_all();}// 3.被唤醒后,对消费者缓冲区进行数据处理_callBack(_con_buf);// 4.初始化消费者缓冲区_con_buf.reset();}}private:Functor _callBack;//回调函数private: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;//异步工作器的工作线程AsyncType _lopper_type;};
}
#endif
7.8.3、异步日志器设计
1.继承于Logger日志器类
对于写日志操作进行函数重写(不再将数据直接写入文件,而是通过异步消息处理器,放到缓冲区中)
2.通过异步消息处理器,进行日志数据的实际落地
管理的成员:
1.异步工作器(异步消息处理器)
完成后,完善日志器建造者,进行异步日志器安全模式的旋转,提供异步日志器的创建
class AsyncLogger : public Logger
{public:AsyncLogger(const std::string& logger_name,LogLevel::value level,Formatter::ptr& formatter,std::vector<LogSink::ptr>& sinks,AsyncType lopper_type):Logger(logger_name, level, formatter,sinks),//因为类中有this指针,所以需要用bind来进行绑定_lopper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::realLog, this, std::placeholders::_1), lopper_type)){}void log(const char* data, size_t len)//将数据写入缓冲区{_lopper->push(data,len);}//设计一个实际落地函数(将缓冲区数据实际落地)void realLog(Buffer& buf){if(_sinks.empty()) return;for(auto& sink : _sinks){sink->log(buf.begin(),buf.readAbleSize());}}private:AsyncLooper::ptr _lopper;
};
7.9、单例日志器管理类设计(单例模式)
-
⽇志的输出,我们希望能够在任意位置都可以进⾏,但是当我们创建了⼀个⽇志器之后,就会受到⽇志器所在作⽤域的访问属性限制。
-
因此,为了突破访问区域的限制,我们创建⼀个⽇志器管理类,且这个类是⼀个单例类,这样的话,我们就可以在任意位置来通过管理器单例获取到指定的⽇志器来进⾏⽇志输出了。
-
基于单例⽇志器管理器的设计思想,我们对于⽇志器建造者类进⾏继承,继承出⼀个全局⽇志器建造者类,实现⼀个⽇志器在创建完毕后,直接将其添加到单例的⽇志器管理器中,以便于能够在任何位置通过⽇志器名称能够获取到指定的⽇志器进⾏⽇志输出。
日志器管理器:
作用1:对所有创建的日志器进行管理
特性:将管理器设计为单例
作用2:可以再程序的任意位置,获取相同的单例对象,获取其中的日志器进行日志输出
拓展:单例管理器创建的时候,默认先创建一个日志器(用于进行标准输出的打印)
目的:让用户在不创建任何日志器的情况下,也能进行标准输出的打印,方便用户设计
管理的成员:
1.默认日志器
2.所管理的日志器数组
3.互斥锁
提供的接口:
1.添加日志器管理
2.判断是否管理了指定名称的日志器
3.获取指定名称的日志器
4.获取默认日志器
7.9.1、日志器管理类设计
class LoggerMangger{public:static LoggerMangger& getInstance(){//在c++11后,针对静态局部变量,编译器在编译的层面实现了线程安全//当静态局部变量没有构造完成之前,其他线程进入就会阻塞static LoggerMangger 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:LoggerMangger(){//注意这里不能用全局的日志器建造者,否则会导致死循环26*std::unique_ptr<sqy_log::LoggerBuilder> builder(new sqy_log::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.9.2、全局建造者类设计
class GlobalLoggerBuilder : 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>();}Logger::ptr logger;if( _logger_type == LoggerType::LOGGER_ASYNC){logger = std::make_shared<AsyncLogger> (_logger_name,_limit_level,_formatter,_sinks,_loop_type);}else{logger = std::make_shared<SyncLogger>(_logger_name,_limit_level,_formatter,_sinks);}LoggerMangger::getInstance().addLogger(logger);return logger;}
};
7.10、⽇志宏&全局接⼝设计(代理模式)
提供全局接口&宏函数,对日志系统接口进行使用便捷性优化
思想:
1.提供获取指定日志器的全局接口(避免用户自己操作单例对象)
2.使用宏函数对日志器的接口进行代理(代理模式)
3.提供宏函数,直接进行日志的标准输出打印(不用获取日志器了)
#ifndef __SQYLOG_H__
#define __SQYLOG_H__
#include "logger.hpp"
namespace sqy_log
{//1.提供获取指定日志器的全局接口,(避免用户自己操作单例对象)Logger::ptr getLogger(const std::string& name){return sqy_log::LoggerMangger::getInstance().getLogger(name);}Logger::ptr rootLogger(){return sqy_log::LoggerMangger::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, ...) sqy_log::rootLogger()->debug(fmt, ##__VA_ARGS__)#define INFO(fmt, ...) sqy_log::rootLogger()->info(fmt, ##__VA_ARGS__)#define WARN(fmt, ...) sqy_log::rootLogger()->warn(fmt, ##__VA_ARGS__)#define ERROR(fmt, ...) sqy_log::rootLogger()->error(fmt, ##__VA_ARGS__)#define FATAL(fmt, ...) sqy_log::rootLogger()->fatal(fmt, ##__VA_ARGS__)}
#endif
7.10.1、全局接口测试
测试代码
void log_test()
{DEBUG("%s", "测试日志");INFO("%s", "测试日志");WARN("%s", "测试日志");ERROR("%s", "测试日志");FATAL("%s", "测试日志");size_t count = 0;while(count < 300000){FATAL("测试日志-%d", count++);}
}
int main()
{log_test();return 0;
}
7.10.2、项目代码结构
7.11性能测试
下⾯对⽇志系统做⼀个性能测试,测试⼀下平均每秒能打印多少条⽇志消息到⽂件。
主要的测试⽅法是:每秒能打印⽇志数 = 打印⽇志条数 / 总的打印⽇志消耗时间
主要测试要素:同步/异步 & 单线程/多线程
• 100w+条指定⻓度的⽇志输出所耗时间
• 每秒可以输出多少条⽇志
• 每秒可以输出多少MB⽇志
测试环境:
• CPU
:Intel® Core™ i5-1035G1 CPU @ 1.00GHz 1.19 GHz
• RAM
:20.0 GB
• ROM
:512G-SSD
• OS
:centos-7.6虚拟机(1CPU核⼼/4G内存)
测试工具的编写
1.可以空值写日志线程数量
2.可以控制写日志的总数量
分别对于同步日志器 & 异步日志器进行各自的性能测试
需要测试单写日志线程的性能
需要测试多写日志线程的性能
实现:
封装一个接口,传入日志器名称,线程数量,日志数量,单条日志大小。
在接口内,创建指定数量的线程,各自负责一部分的日志输出在输出之前计时开始。
在输出完毕后计时结束,所耗时间 = 结束时间-起始时间。
每秒输出量 = 日志数量 / 总耗时
每秒输出大小 = 日志数量 * 单条日志大小 / 总耗时
注意:异步日志输出这里,我们启动非安全模式,纯内存写入(不去考虑实际落地大的时间)
void bench(const std::string& logger_name,size_t thr_count,size_t msg_count, size_t msg_len)
{//1.获取日志器sqy_log::Logger::ptr logger = sqy_log::getLogger(logger_name);if(logger.get() == nullptr){return;}std::cout << "测试日志:" << msg_count << "条,总大小" << (msg_count * msg_len) / 1024 << "KB" << std::endl;//2.组织指定长度的日志消息std::string msg(msg_len - 1, 'A');//3.创建指定数量的线程std::vector<std::thread> threads;size_t msg_per_thr = msg_count / thr_count;//总日志数量/线程数量就是每隔线程要输出的日志数量std::vector<double> cost_array(thr_count);for(int i = 0; i < thr_count; i++){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+1 << ": " << "\t输出日志数量:" << msg_per_thr << ", 耗时:" << cost.count() << "s" << std::endl;});}for(int i = 0; i < thr_count; i++){threads[i].join();}//7.计算总耗时:在多线程中,每个线程都会耗费时间,但是线程时时并发处理的,因此耗时最高的那个就是总时间double max_cost = cost_array[0];for(int i = 0; i < thr_count; i++){max_cost = max_cost < cost_array[i] ? cost_array[i] : max_cost;}size_t msg_per_sec = msg_count / max_cost;size_t size_per_sec = (msg_count * msg_len) / (max_cost * 1024);//8.进行输出打印std::cout << "每秒输出日志数量" << msg_per_sec << "条\n";std::cout << "每秒输出日志大小" << size_per_sec << "KB\n";
}
7.11.1、同步日志器单线程
void sync_bench()
{std::unique_ptr<sqy_log::LoggerBuilder> builder(new sqy_log::GlobalLoggerBuilder());builder->buildFormatter("%m%n");builder->buildLoggerLevel(sqy_log::LogLevel::value::WARING);builder->buildLoggerName("sync_logger");builder->buildLoggerType(sqy_log::LoggerType::LOGGER_SYNC);builder->buildSink<sqy_log::FileSink>("./logfile/async.log");builder->build();bench("sync_logger",1,1000000,100);
}
7.11.2、同步日志器多线程
void sync_bench()
{std::unique_ptr<sqy_log::LoggerBuilder> builder(new sqy_log::GlobalLoggerBuilder());builder->buildFormatter("%m%n");builder->buildLoggerLevel(sqy_log::LogLevel::value::WARING);builder->buildLoggerName("sync_logger");builder->buildLoggerType(sqy_log::LoggerType::LOGGER_SYNC);builder->buildSink<sqy_log::FileSink>("./logfile/async.log");builder->build();bench("sync_logger",3,1000000,100);
}
7.11.3、异步日志器单线程
void async_bench()
{std::unique_ptr<sqy_log::LoggerBuilder> builder(new sqy_log::GlobalLoggerBuilder());builder->buildFormatter("%m%n");builder->buildLoggerLevel(sqy_log::LogLevel::value::WARING);builder->buildLoggerName("async_logger");builder->buildLoggerType(sqy_log::LoggerType::LOGGER_ASYNC);builder->buildEnableUnsafeAsync();builder->buildSink<sqy_log::FileSink>("./logfile/async.log");builder->build();bench("sync_logger",1,1000000,100);
}
7.11.4、异步日志器多线程
void async_bench()
{std::unique_ptr<sqy_log::LoggerBuilder> builder(new sqy_log::GlobalLoggerBuilder());builder->buildFormatter("%m%n");builder->buildLoggerLevel(sqy_log::LogLevel::value::WARING);builder->buildLoggerName("async_logger");builder->buildLoggerType(sqy_log::LoggerType::LOGGER_ASYNC);builder->buildEnableUnsafeAsync();builder->buildSink<sqy_log::FileSink>("./logfile/async.log");builder->build();bench("async_logger",3,1000000,100);
}