C++ - 基于多设计模式下的同步&异步日志系统
日志系统摘要 日志系统是软件开发中重要的调试和维护工具,主要用于: 问题排查:在生产环境无法调试时,通过日志定位问题 流程追踪:帮助理解代码执行流程,特别是分布式/多线程场景 性能优化:避免频繁断点调试,提高问题排查效率 技术实现分为: 同步日志:直接写入文件,简单但性能较差 异步日志:采用生产者-消费者模型,主线程与日志线程分离,提高性能 日志系统实现涉及关键技术: 不定参函数:支持可变参数格式
目录
日志系统介绍
为什么需要日志系统
- ⽣产环境的产品为了保证其稳定性及安全性是不允许开发⼈员附加调试器去排查问题, 可以借助⽇志系统来打印⼀些⽇志帮助开发⼈员解决问题
- 上线客户端的产品出现bug⽆法复现并解决, 可以借助⽇志系统打印⽇志并上传到服务端帮助开发⼈员进⾏分析
- 对于⼀些⾼频操作(如定时器、⼼跳包)在少量调试次数下可能⽆法触发我们想要的⾏为,通过断点的暂停⽅式,我们不得不重复操作⼏⼗次、上百次甚⾄更多,导致排查问题效率是⾮常低下, 可以借助打印⽇志的⽅式查问题
- 在分布式、多线程/多进程代码中, 出现bug⽐较难以定位, 可以借助⽇志系统打印log帮助定位bug
- 帮助⾸次接触项⽬代码的新开发⼈员理解代码的运⾏流程
⽇志系统技术实现
⽇志系统的技术实现主要包括三种类型:
- 利⽤printf、std::cout等输出函数将⽇志信息打印到控制台
- 对于⼤型商业化项⽬, 为了⽅便排查问题,我们⼀般会将⽇志输出到⽂件或者是数据库系统⽅便查询和分析⽇志, 主要分为同步⽇志和异步⽇志⽅式
◦ 同步写⽇志
◦ 异步写⽇志
同步写⽇志
同步⽇志是指当输出⽇志时,必须等待⽇志输出语句执⾏完毕后,才能执⾏后⾯的业务逻辑语句,⽇志输出语句与程序的业务逻辑语句将在同⼀个线程运⾏。每次调⽤⼀次打印⽇志API就对应⼀次系统调⽤write写⽇志⽂件。
在⾼并发场景下,随着⽇志数量不断增加,同步⽇志系统容易产⽣系统瓶颈:
• ⼀⽅⾯,⼤量的⽇志打印陷⼊等量的write系统调⽤,有⼀定系统开销
• 另⼀⽅⾯,使得打印⽇志的进程附带了⼤量同步的磁盘IO,影响程序性能
异步写⽇志
异步⽇志是指在进⾏⽇志输出时,⽇志输出语句与业务逻辑语句并不是在同⼀个线程中运⾏,⽽是有专⻔的线程⽤于进⾏⽇志输出操作。业务线程只需要将⽇志放到⼀个内存缓冲区中不⽤等待即可继续执⾏后续业务逻辑(作为⽇志的⽣产者),⽽⽇志的落地操作交给单独的⽇志线程去完成(作为⽇志的消费者), 这是⼀个典型的⽣产-消费模型。
这样做的好处是即使⽇志没有真的地完成输出也不会影响程序的主业务,可以提⾼程序的性能:
• 主线程调⽤⽇志打印接⼝成为⾮阻塞操作
• 同步的磁盘IO从主线程中剥离出来交给单独的线程完成
前置知识
不定参函数
在初学C语⾔的时候,我们都⽤过printf函数进⾏打印。其中printf函数就是⼀个不定参函数,在函数内部可以根据格式化字符串中格式化字符分别获取不同的参数进⾏数据的格式化。
⽽这种不定参函数在实际的使⽤中也⾮常多⻅,在这⾥简单做⼀介绍:
不定参宏函数
#include<iostream>
#include<cstdarg>
#define LOG(fmt, ...) printf("[%s:%d]" fmt "\n",__FILE__,__LINE__,##__VA_ARGS__)
int main()
{
LOG("%s-%s","hello","zx");
LOG("test");
return 0;
}
在C语言中,如果两个字符串字面量之间只有空白字符(空格、换行、制表符等),编译器会自动将它们合并成一个单一的字符串。例如..." fmt "...。
LOG: 宏的名称。
…: 省略号,表示这是一个可变参数宏。它允许在 fmt 之后传递零个或多个额外的参数。这些额外的参数将用于替换 fmt 中的格式说明符(如 %d, %s 等)。
VA_ARGS: 这是一个特殊的标识符,它代表了宏参数中 … 所匹配的所有可变参数。## (井号井号): 这是一个预处理操作符,叫做“标记粘贴”。在这里它有一个特殊作用:当可变参数 VA_ARGS 为空时(即调用宏时只传了 fmt,没有传任何额外参数),它会删除前面多余的逗号。
C⻛格不定参函数
#include<iostream>
#include<cstdarg>
#include<cstdio>
using namespace std;
//只能针对打印单一类型
void PrintfNum(int count,...)
{
va_list ap;
va_start(ap,count);//让al指向n参数之后的第⼀个可变参数
for(int i = 0;i < count;i++)
{
int num = va_arg(ap,int);//从可变参数中取出⼀个整形参数
cout<<num<<endl;
}
va_end(ap);//清空可变参数列表--其实是将al置空
}
void MyPrintf(const char* fmt,...)
{
va_list ap;
char* res;
va_start(ap,fmt);
int n = vasprintf(&res,fmt,ap);
cout<<res<<endl;
va_end(ap);
}
int main()
{
PrintfNum(5,1,2,3,4,5);
MyPrintf("%s-%d","hello",666);
return 0;
}
vasprintf() 是一个用于格式化输出到动态分配字符串的函数。
C++⻛格不定参函数
#include<iostream>
#include<cstdarg>
#include<memory>
#include<functional>
using namespace std;
void MyPrintf()
{
cout<<endl;
}
template<class T,class ...Args>
void MyPrintf(const T& t,Args&& ...args)
{
cout<<t<<" ";
if(sizeof...(args) < 0)
{
MyPrintf();
}else
{
MyPrintf(forward<Args>(args)...);
}
}
int main()
{
MyPrintf("苹果");
MyPrintf("苹果","香蕉");
MyPrintf("苹果","香蕉","桑葚");
return 0;
}
设计模式
设计模式是前辈们对代码开发经验的总结,是解决特定问题的⼀系列套路。它不是语法规定,⽽是⼀套⽤来提高代码可复⽤性、可维护性、可读性、稳健性以及安全性的解决⽅案。
六大原则:
单⼀职责原则(Single Responsibility Principle);
◦ 类的职责应该单⼀,⼀个⽅法只做⼀件事。职责划分清晰了,每次改动到最⼩单位的⽅法或类。
◦ 使⽤建议:两个完全不⼀样的功能不应该放⼀个类中,⼀个类中应该是⼀组相关性很⾼的函数、数据的封装
◦ ⽤例:⽹络聊天:⽹络通信 & 聊天,应该分割成为⽹络通信类 & 聊天类
开闭原则(Open Closed Principle);
◦ 对扩展开放,对修改封闭
◦ 使⽤建议:对软件实体的改动,最好⽤扩展⽽⾮修改的⽅式。
◦ ⽤例:超时卖货:商品价格—不是修改商品的原来价格,⽽是新增促销价格。
⾥⽒替换原则(Liskov Substitution Principle);
◦ 通俗点讲,就是只要⽗类能出现的地⽅,⼦类就可以出现,⽽且替换为⼦类也不会产⽣任何错误或异常。
◦ 在继承类时,务必重写⽗类中所有的⽅法,尤其需要注意⽗类的protected⽅法,⼦类尽量不要暴露⾃⼰的public⽅法供外界调⽤。
◦ 使⽤建议:⼦类必须完全实现⽗类的⽅法,孩⼦类可以有⾃⼰的个性。覆盖或实现⽗类的⽅法时,输⼊参数可以被放⼤,输出可以缩⼩
◦ ⽤例:跑步运动员类-会跑步,⼦类⻓跑运动员-会跑步且擅⻓⻓跑, ⼦类短跑运动员-会跑步且擅⻓短跑
依赖倒置原则(Dependence Inversion Principle)。
◦ ⾼层模块不应该依赖低层模块,两者都应该依赖其抽象. 不可分割的原⼦逻辑就是低层模式,原
⼦逻辑组装成的就是⾼层模块。
◦ 模块间依赖通过抽象(接⼝)发⽣,具体类之间不直接依赖
◦ 使⽤建议:每个类都尽量有抽象类,任何类都不应该从具体类派⽣。尽量不要重写基类的⽅
法。结合⾥⽒替换原则使⽤。
◦ ⽤例:奔驰⻋司机类–只能开奔驰; 司机类 – 给什么⻋,就开什么⻋; 开⻋的⼈:司机–依
赖于抽象
迪⽶特法则(Law of Demeter),⼜叫“最少知道法则”;
◦ 尽量减少对象之间的交互,从⽽减⼩类之间的耦合。⼀个对象应该对其他对象有最少的了解。对类的低耦合提出了明确的要求:
◦ 只和直接的朋友交流, 朋友之间也是有距离的。⾃⼰的就是⾃⼰的(如果⼀个⽅法放在本类中,既不增加类间关系,也对本类不产⽣负⾯影响,那就放置在本类中)。
◦ ⽤例:⽼师让班⻓点名–⽼师给班⻓⼀个名单,班⻓完成点名勾选,返回结果,⽽不是班⻓点名,⽼师勾选
接⼝隔离原则(Interface Segregation Principle);
◦ 客⼾端不应该依赖它不需要的接⼝,类间的依赖关系应该建⽴在最⼩的接⼝上
◦ 使⽤建议:接⼝设计尽量精简单⼀,但是不要对外暴露没有实际意义的接⼝。
◦ ⽤例:修改密码,不应该提供修改⽤⼾信息接⼝,⽽就是单⼀的最⼩修改密码接⼝,更不要暴露数据库操作
从整体上来理解六⼤设计原则,可以简要的概括为⼀句话,⽤抽象构建框架,⽤实现扩展细节,具体
到每⼀条设计原则,则对应⼀条注意事项:
• 单⼀职责原则告诉我们实现类要职责单⼀;
• ⾥⽒替换原则告诉我们不要破坏继承体系;
• 依赖倒置原则告诉我们要⾯向接⼝编程;
• 接⼝隔离原则告诉我们在设计接⼝的时候要精简单⼀;
• 迪⽶特法则告诉我们要降低耦合;
• 开闭原则是总纲,告诉我们要对扩展开放,对修改关闭。
单例模式
⼀个类只能创建⼀个对象,即单例模式,该设计模式可以保证系统中该类只有⼀个实例,并提供⼀个访问它的全局访问点,该实例被所有程序模块共享。⽐如在某个服务器程序中,该服务器的配置信息存放在⼀个⽂件中,这些配置数据由⼀个单例对象统⼀读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种⽅式简化了在复杂环境下的配置管理。
单例模式有两种实现模式:饿汉模式和懒汉模式
饿汉模式: 程序启动时就会创建⼀个唯⼀的实例对象。 因为单例对象已经确定, 所以⽐较适⽤于多线程环境中, 多线程获取单例对象不需要加锁, 可以有效的避免资源竞争, 提⾼性能。
// 饿汉模式
template <class T>
class Singleton
{
public:
Singleton(const Singleton &) = delete;//显式地禁用类的拷贝构造函数。
Singleton& operator=(const Singleton &) = delete;//显式地禁用类的移动赋值函数。
static Singleton<T>& getInstance()
{
return _eton;
}
private:
Singleton() {}//私有构造函数:目的是为了防止类在外部被任意实例化
~Singleton() {}
private:
static Singleton _eton;
};
template <class T>
Singleton<T> Singleton<T>::_eton;
懒汉模式:第⼀次使⽤要使⽤单例对象的时候创建实例对象。如果单例对象构造特别耗时或者耗费济源(加载插件、加载⽹络资源等), 可以选择懒汉模式, 在第⼀次使⽤的时候才创建对象。这种模式会出现线程安全的问题。
◦ 这⾥介绍的是《Effective C++》⼀书作者Scott Meyers提出的⼀种更加优雅简便的单例模式Meyers' Singleton in C++。
◦C++11 Static local variables特性以确保C++11起,静态变量将能够在满⾜ thread-safe 的前提下唯⼀地被构造和析构。
template <class T>
class Singleton
{
public:
static Singleton<T>& GetInstance()
{
static Singleton _eton;
return _eton;
}
private:
Singleton() {}
~Singleton() {}
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;
};
工厂模式
⼯⼚模式是⼀种创建型设计模式, 它提供了⼀种创建对象的最佳⽅式。在⼯⼚模式中,我们创建对象时不会对上层暴露创建逻辑,⽽是通过使⽤⼀个共同结构来指向新创建的对象,以此实现创建-使⽤的分离。
简单⼯⼚模式: 简单⼯⼚模式实现由⼀个⼯⼚对象通过类型决定创建出来指定产品类的实例。假设有个⼯⼚能⽣产出⽔果,当客⼾需要产品的时候明确告知⼯⼚⽣产哪类⽔果,⼯⼚需要接收⽤⼾提供的类别信息,当新增产品的时候,⼯⼚内部去添加新产品的⽣产⽅式。
class Fruit
{
public:
Fruit() {}
virtual void show() = 0;
};
class Apple : public Fruit
{
public:
Apple() {}
void show() override
{
cout << "I am an apple" << endl;
}
};
class Banana : public Fruit
{
public:
Banana() {}
void show() override
{
cout << "I am a banana" << endl;
}
};
class FruitFactory
{
public:
FruitFactory() {}
static shared_ptr<Fruit> create(const string &name)
{
if (name == "Apple")
return make_shared<Apple>();
if (name == "Banana")
return make_shared<Banana>();
//return shared_ptr<Fruit>();
return nullptr;
}
};
这个模式的结构和管理产品对象的⽅式⼗分简单, 但是它的扩展性⾮常差,当我们需要新增产品的时
候,就需要去修改⼯⼚类新增⼀个类型的产品创建逻辑,违背了开闭原则。
⼯⼚⽅法模式: 在简单⼯⼚模式下新增多个⼯⼚,多个产品,每个产品对应⼀个⼯⼚。假设现在有A、B 两种产品,则开两个⼯⼚,⼯⼚ A 负责⽣产产品 A,⼯⼚ B 负责⽣产产品 B,⽤⼾只知道产品的⼯⼚名,⽽不知道具体的产品信息,⼯⼚不需要再接收客⼾的产品类别,⽽只负责⽣产产品。
//⼯⼚⽅法:定义⼀个创建对象的接⼝,但是由⼦类来决定创建哪种对象,使⽤多个⼯⼚分别⽣产指定的固定产品
// 优点:
// 1. 减轻了⼯⼚类的负担,将某类产品的⽣产交给指定的⼯⼚来进⾏
// 2. 开闭原则遵循较好,添加新产品只需要新增产品的⼯⼚即可,不需要修改原先的⼯⼚类
// 缺点:对于某种可以形成⼀组产品族的情况处理较为复杂,需要创建⼤量的⼯⼚类.
class Fruit
{
public:
Fruit() {}
virtual void show() = 0;
};
class Apple : public Fruit
{
public:
Apple() {}
void show() override
{
cout << "I am an apple" << endl;
}
};
class Banana: public Fruit
{
public:
Banana() {}
void show() override
{
cout << "I am a banana" << endl;
}
};
class FruitFactory
{
public:
FruitFactory() {}
virtual ~FruitFactory() = default;
virtual shared_ptr<Fruit> create() = 0;
};
class AppleFactory:public FruitFactory
{
public:
AppleFactory() {}
shared_ptr<Fruit> create()override
{
return make_shared<Apple>();
}
};
class BananaFactory:public FruitFactory
{
public:
BananaFactory() {}
shared_ptr<Fruit> create()override
{
return make_shared<Banana>();
}
};
⼯⼚⽅法模式每次增加⼀个产品时,都需要增加⼀个具体产品类和⼯⼚类,这会使得系统中类的个数
成倍增加,在⼀定程度上增加了系统的耦合度。
抽象⼯⼚模式: ⼯⼚⽅法模式通过引⼊⼯⼚等级结构,解决了简单⼯⼚模式中⼯⼚类职责太重的问
题,但由于⼯⼚⽅法模式中的每个⼯⼚只⽣产⼀类产品,可能会导致系统中存在⼤量的⼯⼚类,势
必会增加系统的开销。此时,我们可以考虑将⼀些相关的产品组成⼀个产品族(位于不同产品等级
结构中功能相关联的产品组成的家族),由同⼀个⼯⼚来统⼀⽣产,这就是抽象⼯⼚模式的基本思
想。
// 抽象⼯⼚:围绕⼀个超级⼯⼚创建其他⼯⼚。每个⽣成的⼯⼚按照⼯⼚模式提供对象。
// 思想:将⼯⼚抽象成两层,抽象⼯⼚ & 具体⼯⼚⼦类, 在⼯⼚⼦类种⽣产不同类型的⼦产品
class Fruit
{
public:
virtual void show() = 0;
};
class Apple : public Fruit
{
public:
void show() override
{
cout << "I am an apple" << endl;
}
};
class Banana : public Fruit
{
public:
void show() override
{
cout << "I am a banana" << endl;
}
};
class Animal
{
public:
virtual void show() = 0;
};
class Cat : public Animal
{
public:
void show()override
{
cout << "I am a cat" << endl;
}
};
class Dog : public Animal
{
public:
void show()override
{
cout << "I am a dog" << endl;
}
};
class Factory
{
public:
virtual shared_ptr<Animal> GetAnimal(const string &name) = 0;
virtual shared_ptr<Fruit> GetFruit(const string &name) = 0;
};
class AnimalFactory : public Factory
{
public:
virtual shared_ptr<Fruit> GetFruit(const string &name)
{
return shared_ptr<Fruit>();
}
shared_ptr<Animal> GetAnimal(const string &name) override
{
if (name == "Cat"){return make_shared<Cat>();}
else if (name == "Dog")
{
return make_shared<Dog>();
}
return shared_ptr<Animal>();
}
};
class FruitFactory : public Factory
{
public:
virtual shared_ptr<Animal> GetAnimal(const string &name)
{
return shared_ptr<Animal>();
}
shared_ptr<Fruit> GetFruit(const string &name) override
{
if (name == "Apple")
{
return make_shared<Apple>();
}
else if (name == "Banana")
{
return make_shared<Banana>();
}
return shared_ptr<Fruit>();
}
};
class FactoryProducer
{
public:
static shared_ptr<Factory> getFactory(const string &name)
{
if (name == "animal")
return make_shared<AnimalFactory>();
else
return make_shared<FruitFactory>();
}
};
抽象⼯⼚模式适⽤于⽣产多个⼯⼚系列产品衍⽣的设计模式,增加新的产品等级结构复杂,需要对原有系统进⾏较⼤的修改,甚⾄需要修改抽象层代码,违背了“开闭原则”。
建造者模式
建造者模式是⼀种创建型设计模式, 使⽤多个简单的对象⼀步⼀步构建成⼀个复杂的对象,能够将⼀个复杂的对象的构建与它的表⽰分离,提供⼀种创建对象的最佳⽅式。主要⽤于解决对象的构建过于复杂的问题。
建造者模式主要基于四个核⼼类实现:
• 抽象产品类
• 具体产品类:⼀个具体的产品对象类
• 抽象Builder类:创建⼀个产品对象所需的各个部件的抽象接⼝
• 具体产品的Builder类:实现抽象接⼝,构建各个部件
• 指挥者Director类:统⼀组建过程,提供给调⽤者使⽤,通过指挥者来构造产品
/*抽象电脑类*/
class Computer
{
public:
// typedef shared_ptr<Computer> ptr;
using ptr = shared_ptr<Computer>;
void SetDisplay(const string &display) { _display = display; }
void SetBoard(const string &board) { _board = board; }
virtual void SetOs() = 0;
string ToString()
{
string str = "Computer:{[\n";
str += "\tboard = " + _board + ",\n";
str += "\tdisplay = " + _display + ",\n";
str += "\tos = " + _os + ",\n";
str += "\t]}\n";
return str;
}
protected:
string _board;
string _display;
string _os;
};
/*具体产品类*/
class MacBook : public Computer
{
public:
void SetOs() override { _os = "Mac Os X12"; }
};
/*抽象建造者类:包含创建⼀个产品对象的各个部件的抽象接⼝*/
class Builder
{
public:
using ptr = shared_ptr<Builder>;
virtual void SetDisplay(const string &display) = 0;
virtual void SetBoard(const string &board) = 0;
virtual void SetOs() = 0;
virtual Computer::ptr Build() = 0;
};
/*具体产品的具体建造者类:实现抽象接⼝,构建和组装各个部件*/
class MacBuilder : public Builder
{
public:
MacBuilder():_computer(new MacBook()){}
void SetDisplay(const string &display) override { _computer->SetDisplay(display); }
void SetBoard(const string &board) override { _computer->SetBoard(board); }
void SetOs() override { _computer->SetOs(); }
Computer::ptr Build() override { return _computer; }
private:
Computer::ptr _computer;
};
/*指挥者类,提供给调⽤者使⽤,通过指挥者来构造复杂产品*/
class Director
{
public:
Director(shared_ptr<Builder>& builder):_builder(builder){}
void Construct(const string& board,const string& display)
{
_builder->SetBoard(board);
_builder->SetDisplay(display);
_builder->SetOs();
}
private:
Builder::ptr _builder;
};
代理模式
代理模式指代理控制对其他对象的访问, 也就是代理对象控制对原对象的引⽤。在某些情况下,⼀个对象不适合或者不能直接被引⽤访问,⽽代理对象可以在客⼾端和⽬标对象之间起到中介的作⽤。
代理模式的结构包括⼀个是真正的你要访问的对象(⽬标类)、⼀个是代理对象。⽬标对象与代理对象实现同⼀个接⼝,先访问代理类再通过代理类访问⽬标对象。代理模式分为静态代理、动态代理。
静态代理指的是,在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪个被代理类。
动态代理指的是,在运⾏时才动态⽣成代理类,并将其与被代理类绑定。这意味着,在运⾏时才能
确定代理类要代理的是哪个被代理类。
以租房为例,房东将房⼦租出去,但是要租房⼦出去,需要发布招租启⽰, 带⼈看房,负责维修,这
些⼯作中有些操作并⾮房东能完成,因此房东为了图省事,将房⼦委托给中介进⾏租赁。
代理模式实现:
/*房东要把⼀个房⼦通过中介租出去理解代理模式*/
class RentHouse
{
public:
virtual void rentHouse() = 0;
};
/*房东类:将房⼦租出去*/
class LandLord : public RentHouse
{
public:
void rentHouse() { cout << "将房子租出去" << endl; }
};
/*中介代理类:对租房⼦进⾏功能加强,实现租房以外的其他功能*/
class Intermediary : public RentHouse
{
public:
void rentHouse()
{
std::cout << "发布招租启⽰\n";
std::cout << "带⼈看房\n";
_landlord.rentHouse();
std::cout << "负责租后维修\n";
}
private:
LandLord _landlord;
};
日志系统框架设计
本项⽬实现的是⼀个多⽇志器⽇志系统,主要实现的功能是让程序员能够轻松的将程序运⾏⽇志信息落地到指定的位置,且⽀持同步与异步两种⽅式的⽇志落地⽅式。
项⽬的框架设计将项⽬分为以下⼏个模块来实现。
模块划分
⽇志等级模块:对输出⽇志的等级进⾏划分,以便于控制⽇志的输出,并提供等级枚举转字符串功能。
◦ 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:表⽰换⾏
◦ 设计思想:设计不同的⼦类,不同的⼦类从⽇志消息中取出不同的数据进⾏处理。
⽇志消息落地模块:决定了⽇志的落地⽅向,可以是标准输出,也可以是⽇志⽂件,也可以滚动⽂件输出…
◦ 标准输出:表⽰将⽇志进⾏标准输出的打印。
◦ ⽇志⽂件输出:表⽰将⽇志写⼊指定的⽂件末尾。
◦ 滚动⽂件输出:当前以⽂件⼤⼩进⾏控制,当⼀个⽇志⽂件⼤⼩达到指定⼤⼩,则切换下⼀个⽂件进⾏输出
◦ 后期,也可以扩展远程⽇志输出,创建客⼾端,将⽇志消息发送给远程的⽇志分析服务器。
◦ 设计思想:设计不同的⼦类,不同的⼦类控制不同的⽇志落地⽅向。
⽇志器模块:
◦ 此模块是对以上⼏个模块的整合模块,⽤⼾通过⽇志器进⾏⽇志的输出,有效降低⽤⼾的使⽤难度。
◦ 包含有:⽇志消息落地模块对象,⽇志消息格式化模块对象,⽇志输出等级
⽇志器管理模块:
◦ 为了降低项⽬开发的⽇志耦合,不同的项⽬组可以有⾃⼰的⽇志器来控制输出格式以及落地⽅向,因此本项⽬是⼀个多⽇志器的⽇志系统。
◦ 管理模块就是对创建的所有⽇志器进⾏统⼀管理。并提供⼀个默认⽇志器提供标准输出的⽇志输出。
异步线程模块:
◦ 实现对⽇志的异步输出功能,⽤⼾只需要将输出⽇志任务放⼊任务池,异步线程负责⽇志的落
地输出功能,以此提供更加⾼效的⾮阻塞⽇志输出。
模块关系图

代码设计
实⽤类设计
提前完成⼀些零碎的功能接⼝,以便于项⽬中会⽤到。
• 获取系统时间
• 判断⽂件是否存在
• 获取⽂件的所在⽬录路径
• 创建⽬录
// 1. 获取系统时间
class Date
{
public:
static time_t now() { return time(nullptr); }
};
class File
{
public:
// 2.判断文件是否存在
static bool existes(const std::string &pathname)
{
struct stat st;
// stat检查一个文件是否存在,并输出其大小和类型。
// 成功返回0,错误返回-1
return stat(pathname.c_str(), &st) == 0;
}
// 3.创建⽬录
static void create_directory(const std::string &pathname)
{
//./abc/de/test.c
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);
if (existes(parent_dir))
{
idx = pos + 1;
continue;
}
mkdir(parent_dir.c_str(), 0777);
idx = pos + 1;
}
}
// 4.获取⽂件所在⽬录
static std::string path(const std::string &pathname)
{
//./abc/test.cc
if (pathname.empty())
return "."; // 如果文件目录为空,则表示在当前目录
auto pos = pathname.find_last_of("/\\");
//.abc
if (pos == std::string::npos)
return ".";
return pathname.substr(0, pos + 1); //./adc/
}
};
日志等级类设计
⽇志等级总共分为7个等级,分别为:
• DRBUG 进⾏debug时候打印⽇志的等级
• INFO 打印⼀些⽤⼾提⽰信息
• WARN 打印警告信息
• ERROR 打印错误信息
• FATAL 打印致命信息- 导致程序崩溃的信息
• OFF 关闭所有⽇志输出
class LogLevel
{
public:
enum class Value
{
UNKNOW = 0,
DEBUG,
INFO,
WARN,
ERROR,
FATAL,
OFF
};
static std::string ToString(const Value& value)
{
switch(value)
{
case Value::DEBUG:return "DEBUG";
case Value::INFO:return "INFO";
case Value::WARN:return "WARN";
case Value::ERROR:return "ERROR";
case Value::FATAL:return "FATAL";
case Value::OFF:return "OFF";
}
return "UNKNOW";
}
};
日志消息类设计
⽇志消息类主要是封装⼀条完整的⽇志消息所需的内容,其中包括⽇志等级、对应的logger name、打印⽇志源⽂件的位置信息(包括⽂件名和⾏号)、线程ID、时间戳信息、具体的⽇志信息等内容。
struct LogMsg
{
using ptr = std::shared_ptr<LogMsg>;
size_t _line;//行号
size_t _ctime;//当前时间
std::thread::id _tid;//线程ID
std::string _name;//日志器名称
std::string _file;//文件名
std::string _payload;//日志消息--有效载荷
LogLevel::Value _value;//日志等级
LogMsg(){}
LogMsg(std::string name,std::string file,size_t line,std::string payload,LogLevel::Value value)
:_name(name)
,_file(file)
,_line(line)
,_payload(payload)
,_value(value)
,_ctime(Date::now())
,_tid(std::this_thread::get_id())
{}
};
⽇志输出格式化类设计
⽇志格式化(Formatter)类主要负责格式化⽇志消息。其主要包含以下内容
pattern成员:保存⽇志输出的格式字符串。
◦ %d ⽇期
◦ %T 缩进
◦ %t 线程id
◦ %p ⽇志级别
◦ %c ⽇志器名称
◦ %f ⽂件名
◦ %l ⾏号
◦ %m ⽇志消息
◦ %n 换⾏
std::vectorFormatItem::ptr items成员:⽤于按序保存格式化字符串对应的⼦格式化对象。
FormatItem类主要负责⽇志消息⼦项的获取及格式化。其包含以下⼦类
• MsgFormatItem :表⽰要从LogMsg中取出有效⽇志数据
• LevelFormatItem :表⽰要从LogMsg中取出⽇志等级
• NameFormatItem :表⽰要从LogMsg中取出⽇志器名称
• ThreadFormatItem :表⽰要从LogMsg中取出线程ID
• TimeFormatItem :表⽰要从LogMsg中取出时间戳并按照指定格式进⾏格式化
• CFileFormatItem :表⽰要从LogMsg中取出源码所在⽂件名
• CLineFormatItem :表⽰要从LogMsg中取出源码所在⾏号
• TabFormatItem :表⽰⼀个制表符缩进
• NLineFormatItem :表⽰⼀个换⾏
• OtherFormatItem :表⽰⾮格式化的原始字符串
⽰例:“[%d{%H:%M:%S}] %m%n”
pattern = "[%d{%H:%M:%S}] %m%n"
items = {
{OtherFormatItem(), "["},
{TimeFormatItem(), "%H:%M:%S"},
{OtherFormatItem(), "]"},
{MsgFormatItem (), ""},
{NLineFormatItem (), ""}
}
LogMsg msg = {
size_t _line = 22;
size_t _ctime = 12345678;
std::thread::id _tid = 0x12345678;
std::string _name = "logger";
std::string _file = "main.cpp";
std::string _payload = "创建套接字失败";
LogLevel::value _level = ERROR;
};
格式化的过程其实就是按次序从Msg中取出需要的数据进⾏字符串的连接的过程。
最终组织出来的格式化消息: "[22:32:54] 创建套接字失败\n
#ifndef _M_FORMATTER_H
#define _M_FORMATTER_H
/*
⽇志格式化(Formatter)类主要负责格式化⽇志消息。
FormatItem类主要负责⽇志消息⼦项的获取及格式化。其包含以下⼦类
• MsgFormatItem :表⽰要从LogMsg中取出有效⽇志数据
• LevelFormatItem :表⽰要从LogMsg中取出⽇志等级
• NameFormatItem :表⽰要从LogMsg中取出⽇志器名称
• ThreadFormatItem :表⽰要从LogMsg中取出线程ID
• TimeFormatItem :表⽰要从LogMsg中取出时间戳并按照指定格式进⾏格式化
• CFileFormatItem :表⽰要从LogMsg中取出源码所在⽂件名
• CLineFormatItem :表⽰要从LogMsg中取出源码所在⾏号
• TabFormatItem :表⽰⼀个制表符缩进
• NLineFormatItem :表⽰⼀个换⾏
• OtherFormatItem :表⽰⾮格式化的原始字符串
*/
#include "message.hpp"
#include <iostream>
#include <vector>
#include <ctime>
#include <cassert>
#include <sstream>
#include <memory>
namespace zxlog
{
class FormatItem
{
public:
using ptr = std::shared_ptr<FormatItem>;
virtual void Format(std::ostream &os, const LogMsg &msg) = 0;
};
// 表⽰要从LogMsg中取出有效⽇志数据
class MsgFormatItem : public FormatItem
{
public:
void Format(std::ostream &os, const LogMsg &msg) override
{
os << msg._payload;
}
};
// 表⽰要从LogMsg中取出⽇志等级
class LevelFormatItem : public FormatItem
{
public:
void Format(std::ostream &os, const LogMsg &msg) override
{
os << LogLevel::ToString(msg._value);
}
};
// 表⽰要从LogMsg中取出⽇志器名称
class NameFormatItem : public FormatItem
{
public:
void Format(std::ostream &os, const LogMsg &msg) override
{
os << msg._name;
}
};
// 表⽰要从LogMsg中取出线程ID
class ThreadFormatItem : public FormatItem
{
public:
void Format(std::ostream &os, const LogMsg &msg) override
{
os << msg._tid;
}
};
// 表⽰要从LogMsg中取出时间戳并按照指定格式进⾏格式化
class TimeFormatItem : public FormatItem
{
private:
std::string _format;
public:
TimeFormatItem(const std::string &format = "%H:%M:%S") : _format(format)
{
if (format.empty())
_format = format;
}
void Format(std::ostream &os, const LogMsg &msg) override
{
struct tm t;
localtime_r(&msg._ctime, &t); // 获取当前时间放入struct tm结构体中
char tmp[128];
strftime(tmp, 127, _format.c_str(), &t); // 按照指定格式读取struct tm结构体中的时间
os << tmp;
}
};
// 表⽰要从LogMsg中取出源码所在⽂件名
class CFileFormatItem : public FormatItem
{
public:
void Format(std::ostream &os, const LogMsg &msg) override
{
os << msg._file;
}
};
// 表⽰要从LogMsg中取出源码所在⾏号
class CLineFormatItem : public FormatItem
{
public:
void Format(std::ostream &os, const LogMsg &msg) override
{
os << msg._line;
}
};
// 表⽰⼀个制表符缩进
class TabFormatItem : public FormatItem
{
public:
void Format(std::ostream &os, const LogMsg &msg) override
{
os << "\t";
}
};
// 表⽰⼀个换⾏
class NLineFormatItem : public FormatItem
{
public:
void Format(std::ostream &os, const LogMsg &msg) override
{
os << "\n";
}
};
// 表⽰⾮格式化的原始字符串
class OtherFormatItem : public FormatItem
{
private:
std::string _str;
public:
OtherFormatItem(const std::string &str = "") : _str(str) {}
void Format(std::ostream &os, const LogMsg &msg) override
{
os << _str;
}
};
class Formatter
{
public:
// 时间{年-月-日 时:分:秒}缩进 线程ID 缩进 [日志级别] 缩进 [日志名称] 缩进 文件名:行号 缩进 消息换行
Formatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%p][%c][%f:%l] %m%n")
: _pattern(pattern)
{
assert(parsePattern());
}
const std::string Format(LogMsg &msg)
{
std::stringstream ss;
for (auto item : _items)
{
item->Format(ss, msg);
}
return ss.str();
}
std::ostream &Format(std::ostream &os, LogMsg &msg)
{
for (auto item : _items)
{
item->Format(os, msg);
}
return os;
}
private:
/*
%d 日期
%T 缩进
%t 线程id
%p 日志级别
%c 日志器名称
%f 文件名
%l 行号
%m 日志消息
%n 换行
*/
FormatItem::ptr createItem(std::string &key, std::string &value)
{
// if(key == "m") return FormatItem::ptr(new MsgFormatItem());
if (key == "m")
return std::make_shared<MsgFormatItem>();
if (key == "d")
return std::make_shared<TimeFormatItem>(value);
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<NameFormatItem>();
if (key == "f")
return std::make_shared<CFileFormatItem>();
if (key == "l")
return std::make_shared<CLineFormatItem>();
if (key == "n")
return std::make_shared<NLineFormatItem>();
return std::make_shared<OtherFormatItem>(value);
}
// 对格式化规则字符串进行解析
bool parsePattern()
{
std::vector<std::pair<std::string, std::string>> fmt_order;
size_t pos = 0;
std::string key, val;
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]);
pos += 2;
continue;
}
// 走到这里就一定是格式化字符,代表原始字符串处理完毕
if (!val.empty())
{
fmt_order.push_back(std::make_pair("", val));
val.clear();
}
// 这个时候就是格式字符串的处理了
pos += 1;
if (pos == _pattern.size())
{
std::cout << "%子后,没有对应匹配的格式化字符\n";
return false;
}
key = _pattern[pos];
pos += 1;
if (pos < _pattern.size() && _pattern[pos] == '{')
{
pos++; // 此时pos指向{后一个字符,指向子规则的起始位置
while (pos < _pattern[pos] && _pattern[pos] != '}')
{
val.push_back(_pattern[pos++]);
}
if (pos == _pattern.size())
{
std::cout << "子规则{}匹配错误!\n";
return false;
}
pos += 1;
}
fmt_order.push_back(std::make_pair(key, val));
val.clear();
key.clear();
}
if (!val.empty())
{
fmt_order.push_back(std::make_pair(key, val));
}
for (auto &fmt : fmt_order)
{
_items.push_back(createItem(fmt.first, fmt.second));
//std::cout << fmt.first << ":" << fmt.second << std::endl;
}
return true;
}
private:
std::string _pattern;
std::vector<FormatItem::ptr> _items;
};
}
#endif
日志落地(LogSink)类设计(简单工厂模式)
⽇志落地类主要负责落地日志消息到目的地。
它主要包括以下内容:
• Formatter⽇志格式化器:主要是负责格式化⽇志消息,
• mutex互斥锁:保证多线程⽇志落地过程中的线程安全,避免出现交叉输出的情况。
这个类⽀持可扩展,其成员函数log设置为纯虚函数,当我们需要增加⼀个log输出⽬标, 可以增加⼀个类继承⾃该类并重写log⽅法实现具体的落地⽇志逻辑。
⽬前实现了三个不同⽅向上的⽇志落地:
• 标准输出:StdoutSink
• 固定文件:FileSink
• 滚动文件:RollSink
滚动⽇志⽂件输出的必要性:
▪ 由于机器磁盘空间有限, 我们不可能⼀直⽆限地向⼀个⽂件中增加数据
▪ 如果⼀个⽇志⽂件体积太⼤,⼀⽅⾯是不好打开,另⼀⽅⾯是即时打开了由于包含数据巨⼤,也不利于查找我们需要的信息
▪ 所以实际开发中会对单个⽇志⽂件的⼤⼩也会做⼀些控制,即当⼤⼩超过某个⼤⼩时(如1GB),我们就重新创建⼀个新的⽇志⽂件来滚动写⽇志。 对于那些过期的⽇志, ⼤部分企业内部都有专⻔的运维⼈员去定时清理过期的⽇志,或者设置系统定时任务,定时清理过期⽇志。
⽇志⽂件的滚动思想:
⽇志⽂件滚动的条件有两个:文件大小和时间。我们可以选择:
▪ ⽇志⽂件在⼤于 1GB 的时候会更换新的⽂件
▪ 每天定点滚动⼀个⽇志⽂件
本项目基于文件大小的判断滚动生成新的文件
/*
日志落地方向的设计 -- 使用简单工厂模式
1.控制台输出 StdoutSink
2.固定文件输出 FileSink
3.滚动文件输出(按照大小或者时间滚动)
*/
#ifndef _M_SINK_H_
#define _M_SINK_H_
#include "until.hpp"
#include "message.hpp"
#include "formatter.hpp"
#include <memory>
#include <iostream>
#include <fstream>
namespace zxlog
{
// 基类
class LogSink
{
public:
using ptr = std::shared_ptr<LogSink>;
virtual void log(const char *data, size_t len) = 0;
};
// 标准输出,输出到控制台
class StdoutSink : public LogSink
{
public:
void log(const char *data, size_t len)
{
std::cout.write(data, len);
}
};
// 固定文件输出
class FileSink : public LogSink
{
public:
FileSink(const std::string &filename) : _filename(filename)
{
// 打开文件,如果文件不存在,需要自己创建
// if(File::existes(_filename) == false)//existes判断文件时候存在
// create_directory创建的是目录
File::create_directory(File::path(_filename));
// 打开文件
_ofs.open(_filename, std::ios_base::binary | std::ios_base::app);
assert(_ofs.is_open());
}
const std::string &File() { return _filename; }
void log(const char *data, size_t len)
{
_ofs.write(data, len); // 写入文件
if (_ofs.good() == false)
{
std::cout << "文件输出失败!" << std::endl;
}
}
private:
std::string _filename;
std::ofstream _ofs;
};
class RollSink : public LogSink
{
public:
RollSink(const std::string &basename, size_t max_size)
: _basename(basename), _max_size(max_size), _cur_size(0)
{
File::create_directory(File::path(_basename));
}
void log(const char *data, size_t len)
{
InitFileName();
_ofs.write(data,len);
if (_ofs.good() == false){std::cout << "文件输出失败!" << std::endl;}
_cur_size += len;
}
private:
//创建并且打开文件
void InitFileName()
{
if(_ofs.is_open() == false|| _cur_size >= _max_size)//判断当前文件大小是否超过最大文件大小
{
_ofs.close();//关闭原来的文件
std::string name = CreateFileName();//创建文件名称
_ofs.open(name,std::ios_base::binary|std::ios_base::app);//打开文件
assert(_ofs.is_open());
_cur_size = 0;
}
}
//创建文件名称
std::string CreateFileName()
{
time_t t = time(nullptr);
struct tm ct;
localtime_r(&t,&ct);//获取年月日时分秒
std::stringstream ss;
ss<<ct.tm_year + 1900;
ss<<_basename;
ss<<ct.tm_mon + 1;
ss<<ct.tm_mday;
ss<<ct.tm_hour;
ss<<ct.tm_min;
ss<<ct.tm_sec;
ss<<".log";
return ss.str();
}
private:
std::string _basename; // 基础名称
std::ofstream _ofs; // 写入的文件流
size_t _cur_size; // 当前文件大小
size_t _max_size; // 最大文件大小
};
//SinkFactory简单工厂模式设计
class SinkFactory{
public:
//不同的落地方式类的构造参数不同,所以可以使用模板来设计
template<class SinkType,class ...Args>
static LogSink::ptr create(Args&& ...args)
{
return std::make_shared<SinkType>(std::forward<Args>(args)...);
}
};
}
#endif
日志器类(Logger)设计(建造者模式)
⽇志器主要是⽤来和前端交互, 当我们需要使⽤⽇志系统打印log的时候, 只需要创建Logger对象,调⽤该对象debug、info、warn、error、fatal等⽅法输出⾃⼰想打印的⽇志即可,⽀持解析可变参数列表和输出格式, 即可以做到像使⽤printf函数⼀样打印⽇志。
当前⽇志系统⽀持同步⽇志 & 异步⽇志两种模式,两个不同的⽇志器唯⼀不同的地⽅在于他们在⽇志的落地⽅式上有所不同:
同步⽇志器:直接对⽇志消息进⾏输出。
异步⽇志器:将⽇志消息放⼊缓冲区,由异步线程进⾏输出。
因此⽇志器类在设计的时候先设计出⼀个Logger基类,在Logger基类的基础上,继承出SyncLogger同
步⽇志器和AsyncLogger异步⽇志器。
且因为⽇志器模块是对前边多个模块的整合,想要创建⼀个⽇志器,需要设置⽇志器名称,设置⽇志输出等级,设置⽇志器类型,设置⽇志输出格式,设置落地⽅向,且落地⽅向有可能存在多个,整个⽇志器的创建过程较为复杂,为了保持良好的代码⻛格,编写出优雅的代码,因此⽇志器的创建这⾥采⽤了建造者模式来进⾏创建。
class Logger
{
public:
// 日志器类型(同步/异步)
enum class LoggerType
{
LOGGER_SYNC = 0,
LOGGER_ASYNC
};
using ptr = std::shared_ptr<Logger>;
Logger(const std::string &logger_name, LogLevel::Value limit_level,
Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks)
: _logger_name(logger_name),
_limit_level(limit_level),
_formatter(formatter),
_sinks(sinks.begin(), sinks.end())
{
}
/*将日志信息进行格式化,再进行落地输出*/
void debug(const char *file, size_t line, const char *fmt, ...)
{
/*先封装一个日志信息,在进行格式化,最后在落地输出*/
// 1.判断日志等级是否大于该等级,如果大于,就不输出
if (LogLevel::Value::DEBUG < _limit_level)
{
return;
}
// 2.提取日志信息的有效载荷
char *res;
va_list al;
va_start(al, fmt);
int len = vasprintf(&res, fmt, al);
if (len < 0)
{
std::cout << "日志信息格式化失败!";
return;
}
Serialize(LogLevel::Value::DEBUG, file, line, res);
va_end(al);
}
void info(const char *file, size_t line, const char *fmt, ...)
{
if (LogLevel::Value::INFO < _limit_level)
{
return;
}
// 2.提取日志信息的有效载荷
char *res;
va_list al;
va_start(al, fmt);
int len = vasprintf(&res, fmt, al);
if (len < 0)
{
std::cout << "日志信息格式化失败!";
return;
}
Serialize(LogLevel::Value::INFO, file, line, res);
va_end(al);
}
void error(const char *file, size_t line, const char *fmt, ...)
{
if (LogLevel::Value::ERROR < _limit_level)
{
return;
}
// 2.提取日志信息的有效载荷
char *res;
va_list al;
va_start(al, fmt);
int len = vasprintf(&res, fmt, al);
if (len < 0)
{
std::cout << "日志信息格式化失败!";
return;
}
Serialize(LogLevel::Value::ERROR, file, line, res);
va_end(al);
}
void warn(const char *file, size_t line, const char *fmt, ...)
{
if (LogLevel::Value::WARN < _limit_level)
{
return;
}
// 2.提取日志信息的有效载荷
char *res;
va_list al;
va_start(al, fmt);
int len = vasprintf(&res, fmt, al);
if (len < 0)
{
std::cout << "日志信息格式化失败!";
return;
}
Serialize(LogLevel::Value::WARN, file, line, res);
va_end(al);
}
void fatal(const char *file, size_t line, const char *fmt, ...)
{
if (LogLevel::Value::FATAL < _limit_level)
{
return;
}
// 2.提取日志信息的有效载荷
char *res;
va_list al;
va_start(al, fmt);
int len = vasprintf(&res, fmt, al);
if (len < 0)
{
std::cout << "日志信息格式化失败!";
return;
}
Serialize(LogLevel::Value::FATAL, file, line, res);
va_end(al);
}
protected:
// 抽象接口完成实际的落地输出,不同的日志器有不同的具体的落地方式
virtual void Log(const char *data, size_t len) = 0;
void Serialize(LogLevel::Value level, const char *file, size_t line, std::string str)
{
// 3.封装日志信息
LogMsg msg(_logger_name, file, line, str, level);
// 4.格式化日志信息
std::stringstream ss;
_formatter->Format(ss, msg);
// 5.进行落地
Log(ss.str().c_str(), ss.str().size());
}
protected:
std::mutex _mutex; // 对sinks加锁,多个线程使用同一个日志器时,会有锁冲突
const std::string _logger_name; // 日志器名称
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 limit_level,
Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks)
: Logger(logger_name, limit_level, formatter, sinks)
{
}
private:
// 同步日志器,将日志内容直接落地到磁盘
void Log(const char *data, size_t len)
{
std::unique_lock<std::mutex> lock(_mutex);
if (_sinks.empty())
return;
for (auto &sink : _sinks)
{
sink->log(data, len);
}
}
};
//抽象出一个日志器的建造类 -- 目的是建造一个日志器
class LoggerBuilder
{
public:
using ptr = std::shared_ptr<LoggerBuilder>;
// 默认日志器类型是同步日志器,限制等级是DEBUG
LoggerBuilder() : _logger_type(Logger::LoggerType::LOGGER_SYNC), _limit_level(LogLevel::Value::DEBUG) {}
void BuildLoggerName(const std::string &name) { _logger_name = name; }
void BuileLoggerType(const Logger::LoggerType &type) { _logger_type = type; }
// void BuiledFormatter(const Formatter::ptr &formatter)
// {
// _formatter = formatter;
// }
void BuiledFormatter(const std::string &pattern)
{
_formatter = std::make_shared<Formatter>(pattern);
}
template<class SinkType,class ... Args>
void BuildSink(Args&& ... args)
{
LogSink::ptr sink = SinkFactory::create<SinkType>(std::forward<Args>(args)...);
_sinks.push_back(sink);
}
virtual Logger::ptr builder() = 0;
protected:
Logger::LoggerType _logger_type;
std::string _logger_name;
Formatter::ptr _formatter;
LogLevel::Value _limit_level;
std::vector<LogSink::ptr> _sinks;
};
//派生出具体的建造者类 -- 局部日志器建造者 && 全局日志器建造者 ???
class LocalLoggerBuilder:public LoggerBuilder
{
public:
//建造一个日志器出来
Logger::ptr builder()
{
//为真,则照常运行;为假,则终止程序
assert(!_logger_name.empty());//判断是否有日志器名称,日志器名称是日志器的唯一标识
//判断日志信息格式化是否为空
if(_formatter.get() == nullptr){_formatter = std::make_shared<Formatter>();}
//判断落地方式是否为空
if(_sinks.empty())
{
BuildSink<StdoutSink>();
}
if(_logger_type == Logger::LoggerType::LOGGER_ASYNC){}
return std::make_shared<SyncLogger>(_logger_name,_limit_level,_formatter,_sinks);
}
};
双缓冲区异步任务处理器(AsyncLooper)设计
设计思想:异步处理线程 + 数据池
使⽤者将需要完成的任务添加到任务池中,由异步线程来完成任务的实际执⾏操作。
任务池的设计思想:双缓冲区阻塞数据池
优势:避免了空间的频繁申请释放,且尽可能的减少了⽣产者与消费者之间锁冲突的概率,提⾼了任务处理效率。
在任务池的设计中,有很多备选⽅案,⽐如循环队列等等,但是不管是哪⼀种都会涉及到锁冲突的情况,因为在⽣产者与消费者模型中,任何两个角色之间都具有互斥关系,因此每⼀次的任务添加与取出都有可能涉及锁的冲突,⽽双缓冲区不同,双缓冲区是处理器将⼀个缓冲区中的任务全部处理完毕后,然后交换两个缓冲区,重新对新的缓冲区中的任务进⾏处理,虽然同时多线程写⼊也会冲突,但是冲突并不会像每次只处理⼀条的时候频繁(减少了生产者与消费者之间的锁冲突),且不涉及到空间的频繁申请释放所带来的消耗。
在业务缓冲区中,我们一般采用的是队列,先进先出的原则符合我们缓冲区存取数据的逻辑,但是
STL容器里面的队列底层是链表结构,如果作为我们的缓冲区,会有频繁的空间申请和释放,也是一种资源的消耗,这个STL容器里面的队列不行,我们可以使用环形队列(固定好空间大小),但是这里出现了一个问题,那就锁冲突的频繁,生产者与生产者的锁冲突,生产者与消费者的锁冲突,消费者和消费者不存在锁冲突(一般只有一个工作线程写入数据到磁盘),因此就有了双缓冲的思想,减少了生产者和消费者的锁冲突,只有在pop pool空了,同时push pool有数据时,交换缓冲区(交换缓冲区地址或者数据),才会产生一次锁冲突。
注意:设计缓冲区时,往里面存放的数据不是单个单个的
LogMsg日志信息对象,而是格式化好后的数据直接放入缓冲区。
- 这样有两个好处:减少了LogMsg对象频繁的拷贝和构造的消耗,同时直接存放字符串数据有利于进行一次性IO操作,减少了IO次数,提高效率。

单缓冲区的设计:
class Buffer
{
public:
Buffer() : _read_idx(0), _write_idx(0), _buffer(BUFFER_DEFAULT_SIZE) {}
// 判断缓冲区是否为空
bool empty() { return _write_idx == _read_idx; }
// 获取可读数据的大小
size_t readAbleSize() { return _write_idx - _read_idx; }
// 获取可写数据的大小,这个只针对于固定缓冲区的情况
size_t writeAbleSize() { return _buffer.size() - _write_idx; }
// 交换两个缓冲区
void swap(Buffer &buffer)
{
_buffer.swap(buffer._buffer);
std::swap(_read_idx, buffer._read_idx);
std::swap(_write_idx, buffer._write_idx);
}
// 向缓冲区进行写入操作
void push(const char *data, size_t len)
{
// 剩余空间不足有两种情况:1.阻塞 2.扩容(用于极限测试)
// 1.阻塞 固定大小,直接返回
// if(len > writeAbleSize())return;
// 2.扩容(用于极限测试)
EnsureEnoughSize(len);
// 将数据写入到缓冲区
std::copy(data, data + len, &_buffer[_write_idx]);
// 移动可写位置
moveWriter(len);
}
// 获取可读位置的起始地址
const char *begin() { return &_buffer[_read_idx]; }
//
void pop(size_t len)
{
assert(len < readAbleSize());
_read_idx += len;//直接移动指针,不需要删除数据
}
// 重置缓冲区
void reset() { _read_idx = _write_idx = 0; }
void moveReader(size_t len)
{
assert(len+_read_idx <= readAbleSize());//这里需要等于,如果刚好read到末尾,_read_idx + len 应该是大于等于_write_idx的
_read_idx += len;
}
private:
// 移动读写指针的位置
void moveWriter(size_t len)
{
assert(len <= writeAbleSize());
_write_idx += len;
}
void EnsureEnoughSize(size_t len)
{
if (len <= writeAbleSize())
return;
size_t new_size = 0;
if (_buffer.size() < BUFFER_THRESHOLD_SIZE)
new_size += _buffer.size() * 2 + len; // 没有超过阈值,则翻倍增长
else
new_size += _buffer.size() + BUFFER_INCRESE_SIZE + len; // 超过阈值,则线性增长
_buffer.resize(new_size);
}
private:
size_t _read_idx; // 表示当前可读数据的位置的指针
size_t _write_idx; // 表示当前可写数据的位置的指针
std::vector<char> _buffer; // 存放字符串数据的缓冲区(不能用string,string遇到/0就停止了)
};
双缓冲区的设计:只需要将日志写入输入缓冲区,交换输入输出缓冲区,对于输入缓冲区的处理,转化为对输出缓冲区数据的处理,使用一个单独的线程来处理这一段的数据。避免了因日志输出而导致进程阻塞住。
//异步线程工作器
class AsyncLooper
{
public:
enum class AsyncType
{
ASYNC_SAFE,
ASYNC_UNSAFE
};
using Functor = std::function<void(Buffer &buffer)>;
using ptr = std::shared_ptr<AsyncLooper>;
AsyncLooper(const Functor &cb,AsyncType& looper_type) : _callback(cb),
// ThreadEntry函数会传一个参数,this指针
_thread(std::thread(&AsyncLooper::ThreadEntry, this)),
_stop(false),
_looper_type(looper_type)
{}
~AsyncLooper() { stop(); }
void stop() // 停止异步工作器
{
_stop = true; // 停止异步工作器,将_stop标志设为false
_con_cond.notify_all(); // 唤醒所有的工作线程,让消费线程处理剩余数据
_thread.join(); // 等待线程
}
void push(const char *data, size_t len) // 向输入缓冲区输入数据
{
// 1.固定大小 2.扩容--用于极限测试
std::unique_lock<std::mutex> lock(_mutex);
// 生产者往缓冲区写入数据时,必须保证可写数据大小大于等于数据长度
//如果是安全的话,才需要使用判断可写空间大小
if(_looper_type == AsyncType::ASYNC_SAFE)
_pro_cond.wait(lock, [&]()
{ return _pro_buffer.writeAbleSize() >= len; });
// 向缓冲区里写入数据
_pro_buffer.push(data, len);
// 唤醒消费者对数据进行处理
_con_cond.notify_one();
}
private:
void ThreadEntry() // 线程入口函数
{
while (!_stop)
{
// 对锁设置生命周期,只有在交换缓冲区时,会有生产者和消费者的锁冲突,处理消费缓冲区数据时不需要加锁
{
std::unique_lock<std::mutex> lock(_mutex);
// 判断生产缓冲区中有无数据,有则交换,无则阻塞
_con_cond.wait(lock, [&]()
{ return _stop || !_pro_buffer.empty(); });
// 交换缓冲区,程序刚开始时,消费者缓冲区中没有数据
if (!_pro_buffer.empty()) // 万一_stop满足条件时,_pro_buffer也可能为空
_con_buffer.swap(_pro_buffer);
// 唤醒生产者(有可能生产者以为缓冲区满了,处于阻塞状态)
//是安全状态才会阻塞,才需要去唤醒
if(_looper_type == AsyncType::ASYNC_SAFE)
_pro_cond.notify_all();
}
// 对消费缓冲区进行处理
_callback(_con_buffer);
// 重置消费缓冲区
_con_buffer.reset();
}
}
private:
// 回调函数,告诉异步工作器输出缓冲区的数据该如何处理
Functor _callback;
private:
AsyncType _looper_type;
std::atomic<bool> _stop; // 停止异步工作线程,使用atmoic保证原子性
std::mutex _mutex; // 互斥锁,保证线程安全
Buffer _pro_buffer; // 输入缓冲区
Buffer _con_buffer; // 输出缓冲区
std::thread _thread; // 工作线程
std::condition_variable _pro_cond; // 条件变量
std::condition_variable _con_cond;
};
异步日志器(AsyncLogger)设计
异步⽇志器类继承⾃日志器类, 并在同步日志器类上拓展了异步消息处理器。当我们需要异步输出日
志的时候, 需要创建异步日志器和消息处理器, 调⽤异步⽇志器的log、error、info、fatal等函数输
出不同级别日志。
- log函数为重写Logger类的函数, 主要实现将⽇志数据加⼊异步队列缓冲区中
- realLog函数主要由异步线程进⾏调⽤(是为异步消息处理器设置的回调函数),完成⽇志的实际落地
⼯作。
class AsyncLogger:public Logger
{
public:
AsyncLogger(const std::string &logger_name,zxlog::LogLevel::Value& limit_level,zxlog::Formatter::ptr& formatter,std::vector<zxlog::LogSink::ptr>& sinks,AsyncLooper::AsyncType async_type)
:Logger(logger_name,limit_level,formatter,sinks),
_looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::realLog, this, std::placeholders::_1), async_type))
{}
void Log(const char* data,size_t len)override
{
//将数据写入输入缓冲区
_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;//双缓冲区
};
单例日志器管理类设计(单例模式)
- ⽇志的输出,我们希望能够在任意位置都可以进⾏,但是当我们创建了⼀个⽇志器之后,就会受到⽇志器所在作⽤域的访问属性限制。
- 因此,为了突破访问区域的限制,我们创建⼀个⽇志器管理类,且这个类是⼀个单例类,这样的话,我们就可以在任意位置来通过管理器单例获取到指定的⽇志器来进⾏⽇志输出了。
- 基于单例⽇志器管理器的设计思想,我们对于⽇志器建造者类进⾏继承,继承出⼀个全局⽇志器建造者类,实现⼀个⽇志器在创建完毕后,直接将其添加到单例的⽇志器管理器中,以便于能够在任何位置通过⽇志器名称能够获取到指定的⽇志器进⾏⽇志输出。
总结:把日志器管理类设置为单例模式,可以对日志器进行管理,并且在程序的任一位置,对同一个日志器进行访问,突破了作用域的限制。
拓展:可以自己默认先创建一个日志器,放入日志管理类中进行管理,让用户在不创建日志器的情况下,可以进行简单的日志的标准输出打印。
class loggerManager
{
private:
std::mutex _mutex;
Logger::ptr _root_logger;
std::unordered_map<std::string, Logger::ptr> _loggers;
private:
loggerManager()
{
std::unique_ptr<LocalLoggerBuilder> slb(new LocalLoggerBuilder());
slb->BuildLoggerName("root");
_root_logger = slb->builder();
_loggers.insert(std::make_pair("root", _root_logger));
}
loggerManager(const loggerManager &) = delete;
loggerManager &operator=(const loggerManager &) = delete;
public:
static loggerManager &getInstance()
{
static loggerManager lm;
return lm;
}
bool hasLogger(const std::string &name)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _loggers.find(name);
if (it == _loggers.end())
{
return false;
}
return true;
}
void addLogger(const std::string &name, const Logger::ptr logger)
{
std::unique_lock<std::mutex> lock(_mutex);
_loggers.insert(std::make_pair(name, logger));
}
Logger::ptr getLogger(const std::string &name)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _loggers.find(name);
if (it == _loggers.end())
{
return Logger::ptr();
}
return it->second;
}
Logger::ptr rootLogger()
{
std::unique_lock<std::mutex> lock(_mutex);
return _root_logger;
}
};
// 全局日志器建造者 -- 方便用户将日志器添加到单例管理对象中
class GlobalLoggerBuilder : public LoggerBuilder
{
public:
Logger::ptr builder()
{
if (_logger_name.empty())
return Logger::ptr();
//判断该日志器是否在单例管理对象中,有则不添加
assert(loggerManager::getInstance().hasLogger(_logger_name) == false);
if (_formatter.get() == nullptr)
{
_formatter = std::make_shared<Formatter>();
}
if (_sinks.empty())
{
_sinks.push_back(std::make_shared<StdoutSink>());
}
Logger::ptr logger;
if (_logger_type == AsyncLogger::LoggerType::LOGGER_ASYNC)
{
logger = std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);
}
else
{
logger = std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);
}
//将全局日志器添加到单例对象中
loggerManager::getInstance().addLogger(_logger_name,logger);
return logger;
}
};
日志宏&全局接口设计(代理模式)
- 提供全局的日志器获取接口,避免用户自己操作单例对象。
- 使⽤代理模式通过全局函数或宏函数来代理
Logger类的log、debug、info、warn、error、fatal等接⼝,以便于控制源码⽂件名称和⾏号的输出控制,简化用户操作。 - 当仅需标准输出日志的时候可以通过主日志器来打印日志。 且操作时只需要通过宏函数直接进行输出即可,不用再获取日志器了。
//1.提供全局的日志器获取接口
Logger::ptr getLogger(std::string& name)
{
return LoggerManager::getInstance().getLogger(name);
}
Logger::ptr rootLogger()
{
return 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 LOGD(fmt,...) DEBUG(zxlog::rootLogger(),fmt,#__VA_ARGS__)
#define LOGI(fmt,...) INFO(zxlog::rootLogger(),fmt,#__VA_ARGS__)
#define LOGW(fmt,...) WARN(zxlog::rootLogger(),fmt,#__VA_ARGS__)
#define LOGE(fmt,...) ERROR(zxlog::rootLogger(),fmt,#__VA_ARGS__)
#define LOGF(fmt,...) FATAL(zxlog::rootLogger(),fmt,#__VA_ARGS__)
//4.提供宏函数,直接通过指定日志器对数据的落地
#define DEBUG(logger,fmt,...) (logger)->debug(fmt,...)
#define INFO(logger,fmt,...) (logger)->info(fmt,...)
#define WARN(logger,fmt,...) (logger)->warn(fmt,...)
#define ERROR(logger,fmt,...) (logger)->error(fmt,...)
#define FATAL(logger,fmt,...) (logger)->fatal(fmt,...)
功能用例
#include "zxlog.hpp"
void loggerTest(const std::string &logger_name) {
zxlog::Logger::ptr lp = zxlog::getLogger(logger_name);
LOGF("------------example--------------------");
lp->debug("%s", "logger->debug");
lp->info("%s", "logger->info");
lp->warn("%s", "logger->warn");
lp->error("%s", "logger->error");
lp->fatal("%s", "logger->fatal");
DEBUG(lp, "%s", "LOG_DEBUG");
INFO(lp, "%s", "LOG_INFO");
WARN(lp, "%s", "LOG_WARN");
ERROR(lp, "%s", "LOG_ERROR");
FATAL(lp, "%s", "LOG_FATAL");
LOGF("---------------------------------------");
std::string log_msg = "hello bitejiuyeke-";
size_t fsize = 0;
size_t count = 0;
while(count < 100000) {
std::string msg = log_msg + std::to_string(count++);
lp->error("%s", msg.c_str());
}
}
int main(int argc, char *argv[])
{
//实例化全局⽇志器建造者
zxlog::BuildLogger::ptr builder = std::make_shared<zxlog::GlobalBuildLogger>();
builder->BuildLoggerName("SyncLogger");
builder->BuildLoggerType(zxlog::Logger::LoggerType::SYNC_LOGGER);
builder->BuildLoggerLevel(zxlog::LogLevel::Value::DEBUG);
builder->BuildSinks<zxlog::StdoutSink>();
builder->BuildSinks<zxlog::FileSink>("tmp.log");
builder->BuildSinks<zxlog::RollSink>("a/test",1024*1024)
;
builder->Builder();
loggerTest("SyncLogger");
return 0;
}
更多推荐



所有评论(0)