文章目录
- 项目介绍
- 开发环境
- 核心技术
- 日志系统介绍
- 相关技术知识补充
- 日志系统框架设计
- 代码设计
- 项目目录结构
- 功能测试
- 性能测试
- 日志系统框架设计
- 代码设计
- 项目目录结构
- 功能测试
- 性能测试
日志系统:
日志:程序运行过程中所记录的程序运行状态信息
日志的作用:记录程序运行状态信息,以便于程序员能够随时根据状态信息,对系统的运行状态进行分析。
项目不是一个功能型业务型的项目,是一个组件型的项目,是一个库,是给别人用的;所以项目演示时,就展示一下日志系统的性能。
项目介绍
本项目主要实现的是一个日志系统,其支持以下功能:
- 支持多级别日志消息
- 支持同步日志和异步日志
- 支持写入日志到控制台、文件以及滚动文件中
- 支持多线程程序并发写日志
- 支持扩展不同的日志落地到目标地
开发环境
- 操作系统 :Centos 7
- 编辑器: vscode / vim
- 编译器/调试器:g++ / gdb
- 项目自动化构建工具:Makefile
核心技术
- 类层次设计(继承、多态的实际应用)
- C++11语法(多线程库,auto,智能指针,右值引用等)
- 双缓冲区
- 生产者消费者模型
- 多线程
- 设计模式(单例、工厂、代理、建造者等)
日志系统介绍
为什么需要日志系统
- 在生产环境中的产品,为了保证其稳定性以及安全性,是不允许开发人员附加调试器去排查问题的(调试器调试时出现程序崩溃需要根据程序运行数据进行分析,这是不允许的,产品是有隐私的),可以借助日志系统来打印一些日志帮助开发人员解决问题
- 上线客户端的产品出现的Bug无法复现并解决,可以借助日志系统打印日志并且上传到服务端帮助开发人员进行问题分析
- 对于一些高频操作(如定时器,心跳包等)在少量调试次数下可能无法触发我们想要的行为,通过断点暂停的方式,我们需要重复几十次甚至上百次,导致排查问题的效率非常低下,可以借助打印日志的方式排查问题
- 在分布式、多线程/多进程的代码中,出现bug非常难定位,可以借助日志系统打印日志帮助定位bug
- 帮助刚接触项目不久的开发人员理解代码的运行流程
日志系统技术实现
日志系统的技术实现主要包括三种类型:
- 利用printf、std::cout等输出函数将日志信息打印到控制台。-- 1
- 对于大型商业化项目,为了方便排查问题,我们一般会将日志输出到文件或者是数据库系统方便查询和分析日志,主要分为同步日志 和异步日志 方式。
- 同步写日志 -- 2
- 异步写日志 -- 3
同步写日志
同步日志指的是当输出日志时,必须等待日志输出语句执行完毕后,才能执行后面的业务逻辑语句,日志输出语句与程序的业务逻辑语句将在同一个线程中运行。每次调用一次打印日志API就对应一次系统调用write写日志文件。
优点:流程简单,编写代码简单
缺点:在高并发场景下,随着日志数量不断增加,同步日志系统容易产生系统瓶颈:
- ⼀方⾯,⼤量的日志打印陷⼊等量的write系统调用,有⼀定系统开销.
- 另⼀方⾯,使得打印日志的进程附带了⼤量同步的磁盘IO,影响程序性能
异步写日志
异步日志是指在进行日志输出时,日志输出语句与业务逻辑语句并不是在同一个线程中运行,而是有专门的线程用于进行日志输出操作,业务线程只需要将日志放到一个内存缓冲区中,不需要等待即可继续执行后续业务逻辑(作为日志的生产者),而日志的落地操作交给单独的日志线程完成(作为日志的消费者),这是一个典型的生产者消费者模型。
这样的好处是即使日志没有真正的完成输出也不会影响程序的主业务,提高程序的性能
- 主线程调用日志打印接口成为非阻塞操作
- 同步的磁盘IO操作从主线程剥离出来交给单独的线程完成。
相关技术知识补充
不定参函数
在初学C语⾔的时候,我们都⽤过printf函数进⾏打印。其中printf函数就是⼀个不定参函数,在函数内部可以根据格式化字符串中格式化字符分别获取不同的参数进⾏数据的格式化。
不定参宏函数的使用
- 不定参的表示用:"..."
- 不定参的使用:"
__VA_REGS__
"。(在__VA_REGS__
前加 ##,意思是如果__VA_REGS__
为空就取消前面的逗号。)
c
#include <stdio.h>
#define LOG(fmt, ...) printf("[%s:%d]" fmt, __FILE__, __LINE__, __VA_ARGS__);
int main()
{
//编译器内置的宏:__FILE__当前文件名;__LINE__当前行号。
LOG("%s-%d\n", "Anduin", 521);
return 0;
}
如果只有LOG("Anduin"),就会报错;这时候就要加##号。
c
#define LOG(fmt, ...) printf("[%s:%d]" fmt, __FILE__, __LINE__, ##__VA_ARGS__);
C中不定参函数的使用
c
#define _GNU_SOURCE //使用vasprintf函数必须带这个宏
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
//参数类型一样
void printNum(int n, ...)
{
va_list al; // 可变参数列表
va_start(al, n); // 获取n参数之后的第一个不定参数的地址
for (int i = 0; i < n; i++)
{
int num = va_arg(al, int); // 从可变参数列表中获取⼀个整形参数
printf("param[%d]:%d\n", i, num);
}
va_end(al); // 将al置空
}
//参数类型不一样 -- 模拟一下 printf 的实现
void myprintf(const char* fmt,...)
{
va_list ap;
va_start(ap,fmt);
char *res;
//对不定参数的基本使用;給一个一级指针的地址(&res)进去,根据fmt里面的字符串格式,一个一个的取出可变参数列表的每个数。
int ret=vasprintf(&res,fmt,ap);
if(ret!=-1)
{
printf(res);
free(res);
}
va_end(ap);//将ap指针置空
}
int main()
{
printNum(3, 11, 22,33);
printNum(5, 44, 55, 66, 77, 88);
myprintf("%s %d\n","Anduin",521);
return 0;
}
C++中不定参函数的使用
cpp
#include <iostream>
// 实现一个打印操作
// 特化
void xprintf()
{
std::cout<<std::endl;
}
template <typename T,typename ...Args>
void xprintf(const T &v,Args &&...args)
{
std::cout << v;
// 通过 sizeof 计算参数包参数的个数
if((sizeof ...(args))>0)
{
xprintf(std::forward<Args>(args)...);
}
else{
// 参数包没参数了
xprintf();
}
}
int main()
{
xprintf("Anduin","haha",521);
xprintf("Anduin","haha");
xprintf("Anduin");
return 0;
}
设计模式
设计模式是前辈们对代码开发经验的总结,是解决特定问题的⼀系列套路。它不是语法规定,⽽是⼀套⽤来提⾼代码可复⽤性、可维护性、可读性、稳健性以及安全性的解决⽅案。
六⼤原则:
-
单⼀职责原则(SingleResponsibilityPrinciple);
- 类的职责应该单⼀,⼀个类只做⼀件事。职责划分清晰了,每次改动到最⼩单位的⽅法或类。
- 使⽤建议:两个完全不⼀样的功能不应该放⼀个类中,⼀个类中应该是⼀组相关性很⾼的函数、数据的封装
- ⽤例:⽹络聊天:⽹络通信&聊天,应该分割成为网络通信类&聊天类
-
开闭原则(OpenClosedPrinciple )
- 对扩展开放,对修改封闭。
- 使⽤建议:对软件实体的改动,最好⽤扩展⽽⾮修改的⽅式。
- ⽤例:超市卖货:商品价格---不是修改商品的原来价格,⽽是新增促销价格。
-
里氏替换原则(LiskovSubstitutionPrinciple )
- 通俗点讲,就是只要⽗类能出现的地⽅,⼦类就可以出现,⽽且替换为⼦类也不会产⽣任何错误或异常。
- 在继承类时,务必重写⽗类中所有的⽅法,尤其需要注意⽗类的protected⽅法,⼦类尽量不要暴露⾃⼰的public⽅法供外界调⽤。
- 使⽤建议:⼦类必须完全实现⽗类的⽅法,孩⼦类可以有⾃⼰的个性。覆盖或实现⽗类的⽅法时,输⼊参数可以被放⼤,输出可以缩⼩。
- ⽤例:跑步运动员类-会跑步,⼦类⻓跑运动员-会跑步且擅⻓⻓跑,⼦类短跑运动员-会跑步且擅⻓短跑。
-
依赖倒置原则(Dependence Inversion Principle)
- ⾼层模块不应该依赖低层模块,两者都应该依赖其抽象.不可分割的原⼦逻辑就是低层模式,原⼦逻辑组装成的就是⾼层模块。
- 模块间依赖通过抽象(接⼝)发⽣,具体类之间不直接依赖。
- 使⽤建议:每个类都尽量有抽象类,任何类都不应该从具体类派⽣。尽量不要重写基类的⽅法。结合⾥⽒替换原则使⽤。
- ⽤例:奔驰⻋司机类--只能开奔驰;司机类--给什么⻋,就开什么⻋;开⻋的⼈:司机--依赖于抽象。
-
迪⽶特法则(LawofDemeter),⼜叫"最少知道法则"
- 尽量减少对象之间的交互,从⽽减⼩类之间的耦合。⼀个对象应该对其他对象有最少的了解。 对类的低耦合提出了明确要求:1.只和直接的朋友交流,朋友之间也是有距离的。⾃⼰的就是⾃⼰的。(如果⼀个⽅法放在本类中,既不增加类间关系,也对本类不产⽣负⾯影响,那就放置在本类中)
- ⽤例:⽼师让班⻓点名--⽼师给班⻓⼀个名单,班⻓完成点名勾选,返回结果,⽽不是班⻓点名,⽼师勾选。
-
接⼝隔离原则(Interface Segregation Principle );
- 客⼾端不应该依赖它不需要的接⼝,类间的依赖关系应该建⽴在最⼩的接⼝上。
- 使⽤建议:接⼝设计尽量精简单⼀,但是不要对外暴露没有实际意义的接⼝。
- ⽤例:修改密码,不应该提供修改⽤⼾信息接⼝,⽽就是单⼀的最⼩修改密码接⼝,更不要暴露数据库操作。
从整体上来理解六⼤设计原则,可以简要的概括为⼀句话,用抽象构建框架,用实现扩展细节,每⼀条设计原则对应⼀条注意事项:
- 单⼀职责原则告诉我们实现类要职责单⼀;
- 开闭原则是总纲,告诉我们要对扩展开放,对修改关闭;
- ⾥⽒替换原则告诉我们不要破坏继承体系;
- 依赖倒置原则告诉我们要⾯向接⼝编程;
- 迪⽶特法则告诉我们要降低耦合;
- 接⼝隔离原则告诉我们在设计接⼝的时候要精简单⼀。
单例模式
一个类只创建一个对象。 该设计模式可以保证系统中该类只有⼀个实例化对象,并提供⼀个访问它的全局访问点,该实例被所有程序模块共享。⽐如在某个服务器程序中,该服务器的配置信息存放在⼀个⽂件中,这些配置数据由⼀个单例对象统⼀读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种⽅式简化了在复杂环境下的配置管理。
饿汉模式:
程序启动时就会创建⼀个唯⼀的实例对象。因为单例对象已经确定,所以⽐较适⽤于多线程环境中,多线程获取单例对象不需要加锁,可以有效的避免资源竞争,提⾼性能。
cpp
#include <iostream>
class Singleton
{
private:
static Singleton _eton;
Singleton() : _data(99)
{
std::cout << "单例对象构造" << std::endl;
}
Singleton(const Singleton&) = delete;
Singleton& operator= (Singleton&) = delete;
~Singleton(){};
private:
int _data;
public:
static Singleton &getInstance()
{
return _eton;
}
int getData()
{
return _data;
}
};
Singleton Singleton::_eton;
int main()
{
std::cout << Singleton::getInstance().getData() << std::endl;
return 0;
}
懒汉模式:
懒加载,延迟加载的思想,一个对象在用的时候在进行实例化。 如果单例对象构造特别耗时或者耗费资源(加载插件、加载⽹络资源等), 可以选择懒汉模式, 在第⼀次使⽤的时候才创建对象。
- 这⾥介绍的是《Effective C++》⼀书作者ScottMeyers 提出的⼀种更加优雅简便的单例模式 Meyers'Singleton in C++。
- C++11 Static local variables特性以确保C++11起,静态变量将能够在满⾜ thread-safe 的前提下唯⼀地被构造和析构
cpp
#include <iostream>
class Singleton
{
private:
Singleton() : _data(99)
{
std::cout << "单例对象构造" << std::endl;
}
Singleton(const Singleton &) = delete;
Singleton& operator= (Singleton&) = delete;
~Singleton(){};
private:
int _data;
public:
static Singleton &getInstance()
{
static Singleton _eton;
return _eton;
}
int getData()
{
return _data;
}
};
int main()
{
std::cout << Singleton::getInstance().getData() << std::endl;
return 0;
}
注释 27 行:
没注释 27 行:
工厂模式
⼯⼚模式是⼀种创建型设计模式,它提供了⼀种创建对象的最佳⽅式。在⼯⼚模式中,我们创建对象时不会对上层暴露创建逻辑,⽽是通过使⽤⼀个共同结构来指向新创建的对象,以此实现创建-使⽤的分离
工厂模式可以分为:
- 简单工厂模式
简单⼯⼚模式实现由⼀个⼯⼚对象通过类型决定创建出来指定产品类的实例。
假设有个⼯⼚能⽣产出⽔果,当客⼾需要产品的时候明确告知⼯⼚⽣产哪类⽔果,⼯⼚需要接收⽤⼾提供的类别信息,当新增产品的时候,⼯⼚内部去添加新产品的⽣产⽅式。
cpp
#include <iostream>
#include <memory>
// 水果 -- 抽象类
class Fruit
{
public:
virtual void name() = 0;
};
// 水果产品 -- 苹果 香蕉
class Apple : public Fruit
{
public:
void name() override
{
std::cout << "我是一个苹果" << std::endl;
}
};
class Banana : public Fruit
{
public:
void name() override
{
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
return std::make_shared<Banana>();
}
};
int main()
{
std::shared_ptr<Fruit> fruit = FruitFactory::create("苹果");
fruit->name();
fruit = FruitFactory::create("香蕉");
fruit->name();
return 0;
}
-
优点:简单粗暴,直观易懂。
-
缺点:
- 生产产品的代码都写在一起,产品太多会导致代码量庞⼤
- 扩展性差,当新增产品时,需要修改工厂类新增新产品的产品创建逻辑,违背了开闭原则。
- 工厂方法模式
在简单⼯⼚模式下新增多个⼯⼚,多个产品,每个产品对应⼀个⼯⼚。假设现在有 A、B 两种产品,则开两个⼯⼚,⼯⼚ A 负责⽣产产品 A,⼯⼚ B 负责⽣产产品 B,用户只知道产品 的⼯⼚名,⽽不知道具体的产品信息,⼯⼚不需要再接收客户的产品类别,⽽只负责⽣产产品。
cpp
// 工厂方法模式遵循了开闭原则,扩展性好
#include <iostream>
#include <memory>
// 水果类 -- 抽象类
class Fruit
{
public:
virtual void name() = 0;
};
class Apple : public Fruit
{
public:
void name() override
{
std::cout << "我是一个苹果" << std::endl;
}
};
// 水果产品 -- 苹果 香蕉
class Banana : public Fruit
{
public:
void name() override
{
std::cout << "我是一个香蕉" << std::endl;
}
};
// 水果工厂 -- 抽象类
class FruitFactory
{
public:
virtual std::shared_ptr<Fruit> create() = 0;
};
// 苹果工厂
class AppleFactory : public FruitFactory
{
public:
virtual std::shared_ptr<Fruit> create() override // 用来重写基类的同名虚函数
{
return std::make_shared<Apple>();
}
};
// 香蕉工厂
class BananFactory : public FruitFactory
{
public:
virtual 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 BananFactory()); // 重新设置工厂为香蕉工厂
fruit = ff->create(); // 生产香蕉
fruit->name();
return 0;
return 0;
}
但是⼯⼚⽅法模式每次增加⼀个产品时,都需要增加⼀个具体产品类和⼯⼚类,这会使得系统中类的个数成倍增加的,代码就会比较臃肿。
- 抽象工厂模式
⼯⼚⽅法模式通过引⼊⼯⼚等级结构,解决了简单⼯⼚模式中⼯⼚类职责太重的问 题,但由于⼯⼚⽅法模式中的每个⼯⼚只⽣产⼀类产品,可能会导致系统中存在⼤量的⼯⼚类,势必会增加系统的开销。
此时,我们可以考虑将⼀些相关的产品组成⼀个产品族,由同⼀个⼯⼚来统⼀⽣产,这就是抽象⼯⼚模式的基本思想。
cpp
#include<iostream>
#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:
virtual void name() = 0;
};
class Lamp : public Animal
{
public:
virtual void name()
{
std::cout << "我是山羊" << std::endl;
}
};
class Dog : public Animal
{
public:
virtual void name()
{
std::cout << "我是小狗" << std::endl;
}
};
class Factory
{
public:
virtual std::shared_ptr<Fruit> GetFruit(const std::string &name) = 0;
virtual std::shared_ptr<Animal> GetAnimal(const std::string &name) = 0;
};
class FruitFactory : public Factory
{
public:
// 纯虚函数必须重写
virtual std::shared_ptr<Animal> GetAnimal(const std::string &name)
{
return std::shared_ptr<Animal>(); // 返回一个空对象
}
virtual std::shared_ptr<Fruit> GetFruit(const std::string &name)
{
if(name == "苹果")
{
return std::make_shared<Apple>();
}
else if(name == "香蕉")
{
return std::make_shared<Banana>();
}
}
};
class AnimalFactory : public Factory
{
public:
virtual std::shared_ptr<Fruit> GetFruit(const std::string &name)
{
return std::shared_ptr<Fruit>();
}
virtual std::shared_ptr<Animal> GetAnimal(const std::string &name)
{
if(name == "山羊")
{
return std::make_shared<Lamp>();
}
else if(name == "小狗")
{
return std::make_shared<Dog>();
}
}
};
class FactoryProducer
{
public:
static std::shared_ptr<Factory> getFactory(const std::string &name)
{
if(name == "动物")
{
return std::make_shared<AnimalFactory>();
}
else
{
return std::make_shared<FruitFactory>();
}
}
};
int main()
{
std::shared_ptr<Factory> ff = FactoryProducer::getFactory("水果");
std::shared_ptr<Fruit> f = ff->GetFruit("香蕉");
f->name();
f = ff->GetFruit("苹果");
f->name();
std::shared_ptr<Factory> af = FactoryProducer::getFactory("动物");
std::shared_ptr<Animal> a = af->GetAnimal("山羊");
a->name();
a = af->GetAnimal("小狗");
a->name();
return 0;
}
抽象⼯⼚模式适⽤于⽣产多个⼯⼚系列产品的设计模式,增加新的产品时,需要对原有系统进⾏较⼤的修改,甚⾄需要修改抽象层(例如 Factory)的代码,违背了开闭原则。
建造者模式
建造者模式是⼀种创建型设计模式, 使⽤多个简单的对象⼀步⼀步构建成⼀个复杂的对象(可能会按照一定的顺序才能构建成产品),能够将⼀个复杂的对象的构建与它的表⽰分离,提供⼀种创建对象的最佳⽅式。主要⽤于解决对象的构建过于复杂的问题。
建造者模式主要基于五个类实现:
- 抽象产品类
- 具体产品类:⼀个具体的产品对象类 -- 对抽象产品类做一个具象化的实现
- 抽象 Builder 类:创建⼀个产品对象所需的各个部件的抽象接⼝
- 具体产品 Builder 类:实现抽象接⼝,构建各个部件
- 指挥者 Director 类:统⼀组建过程,提供给调⽤者使⽤,通过指挥者类来构造产品
cpp
#include <iostream>
#include <string>
#include <memory>
// 抽象产品类
class Computer
{
public:
Computer() {}
void setBoard(const std::string &board)
{
_board = board;
}
void setDisplay(const std::string &display)
{
_display = display;
}
void showParamaters()
{
std::string computer = "Computer: \n \n";
computer += "\tboard = " + _board + ",\n";
computer += "\tdisplay = " + _display + ",\n";
computer += "\tOS = " + _os + ",\n \n";
std::cout << computer << std::endl;
}
virtual void setOs() = 0;
// 构造电脑所需要的零部件
protected: // 用 private,public 继承看不见,所以写 protected
std::string _board;
std::string _display;
std::string _os;
};
// 具体产品类
class MacBook : public Computer
{
public:
void setOs() override
{
_os = "MAC OS x12";
}
};
// 抽象 Builder 类
class Builder
{
public:
virtual void buildBoard(const std::string &board) = 0;
virtual void buildDisplay(const std::string &display) = 0;
virtual void buildOs() = 0;
virtual std::shared_ptr<Computer> build() = 0;
};
// 具体产品 Builder 类
class MacBookBuilder : public Builder
{
public:
MacBookBuilder() : _computer(new MacBook()) {}
void buildBoard(const std::string &board)
{
_computer->setBoard(board);
}
void buildDisplay(const std::string &display)
{
_computer->setDisplay(display);
}
void buildOs()
{
_computer->setOs();
}
std::shared_ptr<Computer> build()
{
return _computer;
}
private:
std::shared_ptr<Computer> _computer;
};
// 指挥者 Director 类
class Director
{
public:
Director(Builder* builder): _builder(builder) {}
void construct(const std::string& board, const std::string& display)
{
_builder->buildBoard(board);
_builder->buildDisplay(display);
_builder->buildOs();
}
private:
std::shared_ptr<Builder> _builder;
};
int main()
{
Builder* builder = new MacBookBuilder(); // 构造一个具体产品类
std::unique_ptr<Director> director(new Director(builder)); // 构造一个指挥者类
director->construct("华硕主板", "三星显示器");
std::shared_ptr<Computer> computer = builder->build();
computer->showParamaters();
return 0;
}
代理模式
代理模式指代理控制对其他对象的访问,也就是代理对象控制对原对象的引⽤。在某些情况下,⼀个对象不适合或者不能直接被引⽤访问,⽽代理对象可以在客⼾端和⽬标对象之间起到中介的作⽤。
代理模式的结构包括⼀个是真正的你要访问的对象(⽬标类)、⼀个是代理对象。⽬标对象与代理对象实现同⼀个接⼝(代理对象的接口中会包含目标对象的接口,最终使用的是代理对象的接口,所以说实现同一个接口),先访问代理类再通过代理类访问⽬标对象。代理模式分为静态代理、动态代理:
- 静态代理指的是,在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪个被代理类。-- (讲)
- 动态代理指的是,在运⾏时才动态⽣成代理类,并将其与被代理类绑定。这意味着,在运⾏时才能
确定代理类要代理的是哪个被代理类 -- (涉及很多其他知识,不讲)。
以租房为例,房东将房⼦租出去,但是要租房⼦出去,需要发布招租启⽰,带⼈看房,负责维修,这些⼯作中有些操作并⾮房东能完成,因此房东为了图省事,将房⼦委托给中介进⾏租赁。
代理模式 -- 静态代理:
cpp
#include <iostream>
// 抽象租房类
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 << "负责租后维修\n";
}
private:
Landlord _landlord;
};
int main()
{
Intermediary intermediary;
intermediary.rentHouse(); // 通过中介来出租房子
return 0;
}
日志系统框架设计
日志系统:
作用:将一条消息,进行格式化为指定格式的字符串后,写入到指定位置
- 日志要写入指定位置(标准输出,指定文件, 滚动文件等 -- 可扩展 -- 可以写入到其他扩展位置)
日志系统需要支持将日志消息落地到不同的位置---多落地方向 - 日志写入指定位置,支持不同的写入方式(同步,异步)
同步:业务线程自己负责日志的写入(流程简单,但是有可能会因为阻塞导致效率降低) 异步:业务线程将日志放入内存缓冲区,让其他异步线程负责将日志写入指定位置 - 日志输出以日志器为单位,支持多日志器(不同的项目组有不同的日志输出策略)
本项⽬实现的是⼀个多⽇志器⽇志系统,主要实现的功能是让程序员能够轻松的将程序运⾏⽇志信息落地到指定的位置,且⽀持同步与异步两种⽅式的⽇志落地⽅式。项⽬的框架设计将项⽬分为以下⼏个模块来实现。
模块划分
日志等级模块
枚举出日志分为多少个等级---对不同的日志有不同等级标记--以便于控制输出
- OFF:关闭
- DEBUG:调试,调试时的关键信息输出。
- INFO:提示,普通的提⽰型⽇志信息。
- WARN:警告,不影响正常运⾏(运行不会有错误),但是需要注意⼀下的⽇志。
- ERROR:错误,程序运⾏出现错误,但是还能继续运行。这些错误可能会影响部分功能,但是通常不会导致整个程序崩溃的⽇志,如文件读写失败,数据库连接失败。
- FATAL:致命,⼀般是代码异常导致程序⽆法继续推进运⾏的⽇志,即将崩溃或者已经崩溃。
日志消息模块
封装一条日志所需的各种要素(时间,线程ID,文件名,行号,日志等级,消息主体...)
- 时间:描述本条⽇志的输出时间。
- 线程ID:描述本条⽇志是哪个线程输出的。
- ⽇志等级:描述本条⽇志的等级。
- ⽇志数据:本条⽇志的有效载荷数据。
- ⽇志⽂件名:描述本条⽇志在哪个源码⽂件中输出的。
- ⽇志⾏号:描述本条⽇志在源码⽂件的哪⼀⾏输出的。
日志消息格式化模块
按照指定的格式,对于日志消息中关键要素进行组织,最终得到一个指定格式的字符串
系统默认的输出格式: [%d{%H:%M:%S}]%T[%t]%T[%p]%T[%c]%T%f:%l%T%m%n
[12:38:45] [12345678] [FATAL] [root] main.c:178 套接字创建失败...\n
- %d{%H:%M:%S}:表⽰⽇期时间,花括号中的内容表示日期时间的格式。
- %T:表⽰制表符缩进。
- %t:表⽰线程ID。
- %p:表⽰⽇志级别。
- %c:表⽰⽇志器名称,不同的项目组可以创建⾃⼰的⽇志器进⾏⽇志输出,⼩组之间互不影响。
- %f:表⽰⽇志输出时的源代码⽂件名。
- %l:表⽰⽇志输出时的源代码⾏号。
- %m:表⽰⽇志有效载荷数据 。
- %n:表⽰换行。
- 设计思想:设计不同的⼦类,不同的⼦类从⽇志消息中取出不同的数据进⾏处理。
日志落地模块
负责对日志消息进行指定方向的写入输出
- 标准输出:表⽰将⽇志进⾏标准输出的打印。
- ⽇志⽂件输出:表⽰将⽇志写⼊指定的⽂件末尾。
- 滚动⽂件输出:当前以⽂件⼤⼩进⾏控制,当⼀个⽇志⽂件⼤⼩达到指定⼤⼩,则切换下⼀个⽂件进⾏输出 。
- 后期,也可以扩展远程⽇志输出,创建客⼾端,将⽇志消息发送给远程的⽇志分析服务器。
- 设计思想:设计不同的⼦类,不同的⼦类控制不同的⽇志落地⽅向。
日志器模块
对上面几个模块的整合,⽤⼾通过⽇志器进⾏⽇志的输出,有效降低⽤⼾的使⽤难度;包含有:⽇志落地模块对象,⽇志消息格式化模块对象,⽇志限制输出等级
- 同步日志器模块---完成日志的同步输出功能。
- 异步日志器模块---完成日志的异步输出功能
异步线程模块
负责异步日志的实际落地输出功能
- 实现对⽇志的异步输出功能,⽤⼾只需要将输出⽇志任务放⼊任务池,异步线程负责⽇志的落地输出功能,提供了更加⾼效的⾮阻塞的⽇志输出。
单例的日志器管理模块
对日志进行全局的管理,以便于能够在项目的任何位置获取指定的日志器进行日志输出
- 为了降低项⽬开发的⽇志耦合,不同的项⽬组可以有⾃⼰的⽇志器来控制输出格式以及落地⽅向,因此本项⽬是⼀个多⽇志器的⽇志系统。
- 管理模块就是对创建的所有⽇志器进⾏统⼀管理。并提供⼀个默认⽇志器,提供标准输出的⽇志输出。
模块关系图
代码设计
实用类设计
提前完成一些零碎的功能接口:
- 获取系统时间
- 判断文件是否存在
- 获取文件的所在目录路径
- 创建目录
util.hpp:
cpp
# pragma once
#include <iostream>
#include <ctime>
#include <sys/stat.h>
#include <sys/types.h>
#include <string>
namespace log
{
namespace util
{
class Date
{
public:
//获取系统时间
static size_t now()
{
return (size_t)time(nullptr);
}
};
class File
{
public:
// 判断文件是否存在
static bool exists(const std::string& pathname)
{
// stat 获取文件属性,获取成功,说明文件存在;获取失败,则文件不存在
struct stat st;
if (stat(pathname.c_str(), &st) < 0) return false;
return true;
}
// 获取文件的所在目录路径
static std::string path(const std::string& pathname)
{
// ./abc/a.txt 其实也就是获取最后一个 / 的位置,然后从开头进行截取
size_t pos = pathname.find_last_of("/\\"); // 这个 find 调用在 windows 和 linux 下都可以,linux 下路径分隔符为 /;而 \ 为转义字符
// 要表示一个 \ 需要两个 \,也就是查找 / 和 \ 中的任意一个,linux 下路径不会出现 \ ,所以这里没问题
if (pos == std::string::npos) return "."; // 如果没有找到,那么证明这个文件就在当前的目录
return pathname.substr(0, pos + 1); // 把最后一个 / 也截进去
}
// 创建目录
static void createDirectory(const std::string& pathname)
{
// ./abc/bcd/cde
// pos 是 / 的位置,idx是查找的起始位置
size_t pos = 0, idx = 0;
while (idx < pathname.size())
{
// 查找idx 位置起第一个 /
size_t pos = pathname.find_first_of("/\\", idx);
if (pos == std::string::npos)
{
// 创建文件夹,pathname.c_str()表示路径名,0777表示权限
mkdir(pathname.c_str(), 0755);
return;
}
// 截取从开始到/的目录
std::string parent_dir = pathname.substr(0, pos + 1);
// 目录存在就跳过,找下一个
if (exists(parent_dir) == true)
{
idx = pos + 1;
continue;
}
// 目录不存在,创建目录
mkdir(parent_dir.c_str(), 0777);
idx = pos + 1;
}
}
};
}
}
测试:test.cc
cpp
#include "util.hpp"
int main()
{
std::cout << log::util::Date::now() << std::endl;
std::string pathname = "./abc/bcd/a.txt";
log::util::File::createDirectory(log::util::File::path(pathname));
return 0;
}
日志等级模块设计
-
UNKNOW=0,未知等级的日志
-
DEBUG ,调试等级的日志
-
INFO ,提示等级的日志
-
WARN ,警告等级的日志
-
ERROR ,错误等级的日志
-
FATAL ,致命错误等级的日志
-
OFF,关闭所有⽇志输出
每一个项目中都会设置一个默认的日志输出等级,只有输出的日志等级大于等于默认限制等级的时候才可以进行输出。
提供一个接口,将对应等级的枚举,转换为一个对应的字符串
level.hpp:
cpp
/*
1. 定义枚举类,枚举出日志等级
2. 提供转换接口,将枚举转换为对应的字符串
*/
#pragma once
namespace log
{
class LogLevel
{
public:
enum class value
{
UNKNOW = 0,
DEBUG,
INFO,
WARN,
ERROR,
FATAL,
OFF
};
// 转换接口
static const char* toString(LogLevel::value level)
{
switch (level)
{
case LogLevel::value::DEBUG: return "DEBUG";
case LogLevel::value::INFO: return "INFO";
case LogLevel::value::WARN: return "WARN";
case LogLevel::value::ERROR: return "ERROR";
case LogLevel::value::FATAL: return "FATAL";
case LogLevel::value::OFF: return "OFF";
}
return "UNKOWN";
}
};
}
日志消息类设计
意义:存储一条日志消息所需的各项要素
- 日志的输出时间 -- 用于过滤日志输出时间
- 日志等级 -- 用于进行日志过滤分析
- 源文件名称
- 源代码行号 -- 名称和行号:用于定位出错的代码位置
- 线程ID -- 用于过滤出错的线程
- 日志主体消息
- 日志器名称 -- 支持多日志器的同时使用
message.hpp:
cpp
// 定义日志消息类,进行日志信息的存储
#pragma once
#include"util.hpp"
#include"level.hpp"
#include <iostream>
#include <string>
#include <thread>
namespace log
{
struct LogMsg
{
time_t _ctime; // 日志产生的时间戳
LogLevel::value _level; // 日志等级
size_t _line; // 行号
std::thread::id _tid; // 线程id
std::string _file; // 文件名
std::string _logger; // 日志器名称
std::string _payload; // 有效载荷数据
// 构造函数
LogMsg(LogLevel::value level,
size_t line,
const std::string file,
const std::string logger,
const std::string msg) : _ctime(util::Date::now()),
_level(level),
_line(line),
_tid(std::this_thread::get_id()),
_file(file),
_logger(logger),
_payload(msg)
{}
};
}
日志格式化类设计
意义:对日志消息进行格式化,并且组织成指定格式的字符串。
%d ⽇期
%T 缩进
%t 线程id
%p ⽇志级别
%c ⽇志器名称
%f ⽂件名
%l ⾏号
%m ⽇志消息
%n 换⾏
[2024-07-09 17:04][root][1234567][main.c:99][FATAL]:\t创建套接字失败...\n
格式化字符串控制了日志的输出格式;定义格式化字符,是为了让日志系统进行日志格式化更加的灵活方便。
⽇志格式化(Formatter)类包含:
- 格式化字符串:用户定义的输出格式
- 格式化子项数组:对格式化字符串进行解析,保存了日志信息要素的顺序 -- 不同的格式化子项,会从日志消息中取出指定的元素,转化为字符串。
格式化子项:
- 其他信息(非格式化字符)子项:[
- 日期子项:%H%M%S
- 其他信息子项:]
- 其他信息子项:[
- 文件名子项:main.c
- 其他信息子项::
- 行号信息子项:99
- 其他信息子项:]
- 消息主体子项:吃饭睡觉打豆豆
- 换行子项:\n
[12:40:50][main.c:99]吃饭睡觉打豆豆\n
格式化子项的实现思想:从日志消息中取出指定的元素,追加到一块内存空间中。
设计思想:
-
抽象出一个格式化子项的基类
-
基于基类,派生出不同的格式化子项子类:
主体消息、日志等级、时间子项、文件名、行号、日志器名称、线程ID、制表符、换行、非格式化的原始字符串。
这样就可以在父类中定义父类指针(智能指针)的数组,指向不同的格式化子项子类的对象。
FormatItem类主要负责⽇志消息⼦项的获取及格式化;其包含以下⼦类:
- MsgFormatItem:表⽰要从LogMsg中取出有效⽇志数据
- LevelFormatItem:表⽰要从LogMsg中取出⽇志等级
- NameFormatItem:表⽰要从LogMsg中取出⽇志器名称
- ThreadFormatItem:表⽰要从LogMsg中取出线程ID
- TimeFormatItem:表⽰要从LogMsg中取出时间戳并按照指定格式进⾏格式化
- CFileFormatItem:表⽰要从LogMsg中取出源码所在⽂件名
- CLineFormatItem:表⽰要从LogMsg中取出源码所在⾏号
- TabFormatItem:表⽰⼀个制表符缩进
- NLineFormatItem:表⽰⼀个换⾏
- OtherFormatItem:表⽰⾮格式化的原始字符串
格式化子项类:
cpp
# pragma once
#include "level.hpp"
#include "message.hpp"
#include <memory>
namespace log
{
class FormatItem
{
public:
using ptr = std::shared_ptr<FormatItem>;
virtual void format(std::ostream& cout, const LogMsg &msg) = 0;
};
// 派生格式化子项子类 -- 主体消息、日志等级、时间子项、文件名、行号、日志器名称、线程ID、制表符、换行、 其他
// 主体消息
class MsgFormatItem : public FormatItem
{
public:
void format(std::ostream& out, const LogMsg& msg) override
{
out << msg._payload;
}
};
// 日志等级
class LevelFormatItem : public FormatItem
{
public:
void format(std::ostream& out, const LogMsg& msg) override
{
out << LogLevel::toString(msg._level);
}
};
//时间子项
class TimeFormatItem:public FormatItem{
public:
// 设置时间的默认格式
TimeFormatItem(const std::string &fmt = "%H:%M:%S") : _time_fmt(fmt) {}
//虚函数进行重写
void format(std::ostream& out, const LogMsg& msg) override
{
struct tm t; // 时间结构体
localtime_r(&msg._ctime, &t); // 把时间写入 t 中
char tmp[32] = { 0 };
strftime(tmp, 31, _time_fmt.c_str(), &t); // 把格式化的时间写入 tmp 中
out<<tmp;
}
private:
std::string _time_fmt; // 时间格式
};
// 文件名
class FileFormatItem : public FormatItem
{
public:
// 虚函数进行重写
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._file;
}
};
// 行号
class LineFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._line;
}
};
// 日志器名称
class LoggerFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._logger;
}
};
// 线程ID
class ThreadFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._tid;
}
};
// 制表符
class TabFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << "\t";
}
};
// 换行
class NLineFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << "\n";
}
};
// 其他 -- 输出原始字符串 -- abcdef[%d{...}] -- abcdef[ 就是原始字符串,输出
class OtherFormatItem : public FormatItem
{
public:
OtherFormatItem(const std::string &str) : _str(str) {}
void format(std::ostream &out, const LogMsg &msg) override
{
out << _str;
}
private:
std::string _str;
};
}
日志格式化类:
parsePattern
函数思想:
补充说明:
- 原始字符串包含两种,就是 abcd 这种的直接原始字符串,还有 %% 的原始 % 字符串。原始字符串处理完毕后,遇到 % ,则 % 的下一个位置一定是格式化字符。
- 处理一个格式化字符,会连带着它的子规则一起处理
(nullptr 和 null 就是代表 string 为空 "")
cpp
class Formatter
{
public:
Formatter(const std::string& pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n") : _pattern(pattern)
{
assert(parsePattern()); // 解析必须成功,不能失败
}
// 数据放入 io 流中,io 流进行处理
void format(std::ostream& out, const LogMsg& msg)
{
// 遍历格式化子项数组
for (auto &item : _items)
{
item->format(out, msg); // 从 msg 取信息到 out 中
}
}
// 对 LogMsg 进行格式化,返回一个格式化的字符串
std::string format(const LogMsg& msg)
{
std::stringstream ss;
format(ss, msg); // 调用的是上面的 format 重载,void format(std::ostream& out, LogMsg& msg)
return ss.str();
}
private:
//对格式化规则字符串进行解析
bool parsePattern()
{
// 1. 格式化规则字符串解析
//abcd[%d{%H:%M:%S}][%p]%T%m%n
std::vector<std::pair<std::string, std::string>> fmt_order;
size_t pos = 0;
std::string key, val;
while (pos < _pattern.size())
{
// 处理原始字符串 -- 判断是否是 %,不是一定是原始字符
if (_pattern[pos] != '%')
{
val.push_back(_pattern[pos++]);
continue;
}
// 到这里就代表 pos 位置是 % 字符,看后一个是不是 %,%% 处理为一个原始 % 字符
if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%')
{
val.push_back('%');
pos += 2;
continue;
}
// 到这里,说明 % 后是个格式化字符;这时候原始字符串已经处理完毕了,把 val push 进去
if (val.empty() == false) // 可能出现开头就是格式化字符的情况,所以加判断,如果 val 为空,则不添加 -- 是格式化字符的话,上面不会处理,val 是空的
fmt_order.push_back(std::make_pair("", val));
val.clear();
// 这时候是格式化字符的处理
pos += 1; // pos 原本指向 % 位置,+1指向格式化字符的位置
if (pos == _pattern.size())
{
std::cout << "%之后,没有对应的格式化字符!\n";
return false;
}
key = _pattern[pos]; // %d,写入的就是 d,因为上面 pos +1 了
pos += 1; // 再往后走一个,看是否有 {,这时候 pos 指向格式化字符后的位置
bool error_flag = false;
if (pos < _pattern.size() && _pattern[pos] == '{')
{
// 处理子串
pos += 1; // 这时候 pos 指向子规则的起始位置,即 { 的下一个位置
while (pos < _pattern.size() && _pattern[pos] != '}')
{
val.push_back(_pattern[pos++]); // 把子项逐字符放入,带 %
}
if (pos == _pattern.size()) // 走到末尾跳出循环,则代表没有找到 },代表格式错误
{
std::cout << "子规则 {} 匹配出错!\n";
return false;
}
pos += 1; // 这时候 pos 指向 } 位置,往后走一步,就走到下次处理的新位置
}
fmt_order.push_back(std::make_pair(key, val));
key.clear();
val.clear();
}
// 2. 根据解析得到的数据初始化格式化子项数组
for (auto& it : fmt_order)
{
_items.push_back(createItem(it.first, it.second));
}
return true;
}
// key -- 格式化字符(关键字),val -- 关键字对应值
// 例如 %d 对应的子串就是 %H:%M:%S,'[' 则没有 key,就只有 value('[' 本身),这时返回的对象,就用自己填充
// 根据不同的格式化字符创建不同的格式化子项对象
FormatItem::ptr createItem(const std::string& key, const std::string& value)
{
if (key == "d")
return std::make_shared<TimeFormatItem>(value);
if (key == "t")
return std::make_shared<ThreadFormatItem>();
if (key == "c")
return std::make_shared<LoggerFormatItem>();
if (key == "f")
return std::make_shared<FileFormatItem>();
if (key == "l")
return std::make_shared<LineFormatItem>();
if (key == "p")
return std::make_shared<LevelFormatItem>();
if (key == "T")
return std::make_shared<TabFormatItem>();
if (key == "m")
return std::make_shared<MsgFormatItem>();
if (key == "n")
return std::make_shared<NLineFormatItem>();
if (key.empty())
return std::make_shared<OtherFormatItem>(value);
std::cout << "没有对应的格式化字符串:%" << key << std::endl;
abort();
return FormatItem::ptr();
}
private:
std::string _pattern; // 格式化规则字符串
std::vector<FormatItem::ptr> _items; // 格式化子项数组,元素是智能指针
};
测试代码:
cpp
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
int main()
{
log::LogMsg msg(log::LogLevel::value::INFO, 53, "main.c", "root", "格式化功能测试...");
log::Formatter fmt;
// 边缘情况测试
// log::Formatter fmt("abc%%ab%g%g%gc[%d{%H:%M:%S}] %m%n"); // 没有 %g 格式字符
// log::Formatter fmt("abc%%abc[%d{%H:%M:%S}]%m%n");// 测试 %%
// log::Formatter fmt("abc%%abc[%d{%H:%M:%S}]%m%n{"); // 测试花括号
// log::Formatter fmt("abc%%abc[%d{%H:%M:%S}]%m%"); // 末尾只带 %,没有格式化字符
std::string str = fmt.format(msg);
std::cout << str << std::endl;
return 0;
}
日志落地类设计 -- 简单工厂模式
功能:将格式化后的日志消息字符串,输出到指定的位置
扩展:支持同时将日志落地到不同的位置
位置分类:
- 标准输出 -- 不常用,测试的时候用
- 指定文件(事后进行日志分析)
- 滚动文件(文件按时间/大小进行滚动切换)
扩展:支持落地方向的扩展
用户可以自己编写一个新的落地模块,将日志进行其他方向的落地
实现思想:
- 抽象出落地模块类
- 不同落地方向从基类进行派生(使用基类指针,指向子类对象,就可以调用子类对象的接口进行扩展)
- 使用工厂模式进行创建与表示的分离
框架:
cpp
amespace log
{
class LogSink
{
public:
using ptr = std::shared_ptr<LogSink>;
LogSink() {}
virtual ~LogSink() {}
virtual void log(const char* data, size_t len) = 0;
};
// 落地方向:标准输出
class StdoutSink : public LogSink
{
public:
// 将日志消息写入到标准输出
void log(const char* data, size_t len)
{}
};
// 落地方向:指定文件
class FileSink : public LogSink
{
public:
// 构造时传入文件名,并打开文件,将操作句柄 -- _ofs 管理起来
FileSink(const std::string& pathname);
void log(const char* data, size_t len)
{}
private:
std::string _filename;
std::ofstream _ofs; // 输出文件的操作句柄
};
// 落地方向:滚动文件(以大小滚动)
class RollBySizeSink
{
public:
RollBySizeSink(const std::string &basename, size_t max_size);
//将日志消息写入到文件,写入前判断文件大小,超过了最大大小,就要切换文件
void log(const char* data, size_t len)
{}
private:
// 进行大小判断,超过指定大小则创建新文件
void createNewFile();
private:
// 基础文件名 + 扩展文件名(以时间生成 -- 直观,好分析) --》 组成一个实际的当前输出文件名
std::string _basename; // ./log/base- --> ./log/base-20020809132356.log
std::ofstream _ofs;
size_t _max_fsize; // 记录最大大小,当前文件超过了这个大小就要切换文件
size_t _cur_size; // 记录当前文件已经写入的大小
};
// 日志落地的工厂
class SinkFackory
{};
}
code:
cpp
/*日志落地模块的实现
1.抽象落地基类
2.派生子类(根据不同的落地方向进行派生)
3.使用工厂模式进行创建与表示分离
*/
#pragma once
#include "util.hpp"
#include <iostream>
#include <memory>
#include <fstream>
#include <sstream>
#include <cassert>
namespace log
{
class LogSink
{
public:
using ptr = std::shared_ptr<LogSink>;
LogSink() {}
virtual ~LogSink() {}
virtual void log(const char* data, size_t len) = 0;
};
// 落地方向:标准输出
class StdoutSink : public LogSink
{
public:
// 将日志消息写入到标准输出
void log(const char* data, size_t len)
{
// 因为日志输出不一定是字符串,cout 不能指定大小,字符串有 \0 结尾,别的没有
// 因此需要调用write接口,从data位置开始写,写入len长度的数据
std::cout.write(data, len);
}
};
// 落地方向:指定文件
class FileSink : public LogSink
{
public:
// 构造时传入文件名,并打开文件,将操作句柄 -- _ofs 管理起来
FileSink(const std::string& pathname) : _pathname(pathname)
{
// 1. 创建日志文件所在的目录 -- 文件路径可能不存在,以防万一先创建目录
util::File::createDirectory(util::File::path(pathname));
// 2. 创建并打开日志文件
_ofs.open(_pathname, std::ios::binary | std::ios::app); // 二进制可写可追加权限
assert(_ofs.is_open());
}
void log(const char* data, size_t len)
{
_ofs.write(data, len);
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), _name_count(0)
{
std::string pathname = createNewFile();
util::File::createDirectory(util::File::path(pathname));
_ofs.open(pathname, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
//将日志消息写入到文件,写入前判断文件大小,超过了最大大小,就要切换文件
void log(const char* data, size_t len)
{
if (_cur_fsize >= _max_fsize)
{
_ofs.close(); // 切换文件前,关闭文件,防止内存泄漏
std::string pathname = createNewFile();
_ofs.open(pathname, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
_cur_fsize = 0;
}
_ofs.write(data, len);
assert(_ofs.good());
_cur_fsize += len;
}
private:
std::string createNewFile()
{
// 获取系统时间,以时间来构造文件扩展名
time_t t = util::Date::now();
struct tm lt;
localtime_r(&t, <);
std::stringstream filename;
// 从 1900 年 1.1 开始的,所以对应的加上
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; // 用于区别文件名的 -- 因为创建文件可能很快,名字可能会一样
std::string _basename; // ./log/base- --> ./log/base-20020809132356.log
std::ofstream _ofs;
size_t _max_fsize; // 记录最大大小,当前文件超过了这个大小就要切换文件
size_t _cur_fsize; // 记录当前文件已经写入的大小
};
// 日志落地的工厂
//SinkType通过模板参数,可以生产我们需要的落地方式,因为不同落地方式构造的参数不一样,所以需要用到不定参
class SinkFactory
{
public:
template<typename SinkType,typename ...Args>
static LogSink::ptr create(Args && ...args)
{
return std::make_shared<SinkType>(std::forward<Args>(args)...);
}
};
}
测试代码:
cpp
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"
int main()
{
// 日志落地模块的测试
log::LogMsg msg(log::LogLevel::value::INFO, 53, "main.c", "root", "格式化功能测试...");
log::Formatter fmt;
std::string str = fmt.format(msg);
// 设置落地方向
log::LogSink::ptr stdout_lsp = log::SinkFactory::create<log::StdoutSink>(); // 标准输出落地
log::LogSink::ptr file_lsp = log::SinkFactory::create<log::FileSink>("./logfile/test.log"); // 文件落地方式
log::LogSink::ptr roll_lsp = log::SinkFactory::create<log::RollBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式,大小为 1 兆
// 通过指针去控制打印的日志
stdout_lsp->log(str.c_str(), str.size());
file_lsp->log(str.c_str(), str.size());
size_t cursize = 0;
size_t count = 0;
// 用滚动文件的方法希望生产10个文件,一个文件 1 兆
while (cursize < 1024 * 1024 * 10)
{
std::string tmp = std::to_string(count++) + str; // 给日志标号
roll_lsp->log(tmp.c_str(), tmp.size());
cursize += tmp.size();
}
return 0;
}
结果:
扩展一个以时间作为日志文件滚动切换类型的日志落地模块:
在 test.cc 中写的代码:
cpp
/*扩展一个以时间作为日志文件滚动切换类型的日志落地模块
1.以时间进行文件滚动,实际上是以时间段进行滚动
实现思想:以当前系统时间,取模获得时间段大小,可以得到当前时间段是第几个时间段
time(nullptr)%gap;
每次以当前系统时间取模,判断与当前文件的时间段是否一致,不一致代表不是同一个时间段
*/
// 使用枚举来确定时间段的大小
enum class TimeGap
{
GAP_SECOND,
GAP_MINUTE,
GAP_HOUR,
GAP_DAY,
};
class RollByTimeSink : public log::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 = _gap_size==1?log::util::Date::now():log::util::Date::now() / _gap_size; // 获取当前是第几个时间段;前面是 _gap_size == 1 的情况
// 创建文件
std::string filename = createNewFile();
log::util::File::createDirectory(log::util::File::path(filename));
_ofs.open(filename, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
// 将日志消息写入到标准输出,判断当前时间是否是当前文件的时间段,不是就要切换文件。
void log(const char *Date, size_t len)
{
time_t cur = log::util::Date::now(); // 获取当前系统时间,时间戳
if ((cur / _gap_size) != _cur_gap)//(每次写日志时判断当前的时间段与上次的时间段是否是一致得,一致的话就写入,不一致就创建新文件)
{
_ofs.close(); // 打开文件,就必须关闭文件(这里关闭以前的文件)
std::string pathname = createNewFile(); // 创建新文件
_cur_gap = _gap_size==1?log::util::Date::now():log::util::Date::now() / _gap_size; // 获取当前是第几个时间段
_ofs.open(pathname, std::ios::binary | std::ios::app); // 打开文件 二进制可写可追加权限
assert(_ofs.is_open()); // 打开失败就报错
}
_ofs.write(Date, len);
assert(_ofs.good()); // 检测文件流状态和文件读写过程是否正常
}
protected:
// 创建一个新文件,不需要用户去创建,所有我们把权限设置为私有
std::string createNewFile()
{
// 获取系统时间,以时间来构造文件名的扩展名
time_t t = log::util::Date::now();
struct tm lt;
localtime_r(&t, <);
std::stringstream filename;
filename << _basename;
filename << lt.tm_year + 1900;
filename << lt.tm_mon + 1;
filename << lt.tm_mday;
filename << lt.tm_hour;
filename << lt.tm_min;
filename << lt.tm_sec;
filename << ".log";
return filename.str();
}
private:
std::string _basename; // 基本文件名
std::ofstream _ofs; // 会默认以写的方式打开文件
size_t _cur_gap; // 当前是第几个时间段
size_t _gap_size; // 时间段的大小
};
三秒,写三个文件:
日志器模块设计 -- 涉及建造者模式
功能:对前面所有功能进行整合,向外提供接口完成不同等级日志的输出。
管理的成员:
- 格式化模块对象
- 落地模块对象
- 默认的日志输出限制等级(大于等于限制输出等级的日志才能输出)
- 互斥锁(保证日志输出过程的线程安全,不会出现交叉日志)
- 日志名称(日志器的唯一标识,方便查找)
提供的操作:
- debug等级日志的输出操作(分别封装日志消息LogMsg------各个接口日志等级不同)
- info等级日志的输出操作
- warn等级日志的输出操作
- error等级日志的输出操作
- fatal等级日志的输出操作
实现:
- 实现Logger基类(派生出同步日志器和异步日志器)
- 因为两种日志器的落地方式不同,需要将落地操作给抽象出来,不同的日志器调用不同的落地操作进行日志落地
- 模块关联过程中使用基类指针对子类日志器对象进行日志管理和操作
当前日志系统支持同步日志&异步日志,它们的不同点在于日志的落地方式上不同:
- 同步日志器:直接对日志消息进行输出
- 异步日志器:先将日志消息放到缓冲区,然后异步线程进行输出
因此 :日志器类在设计的时候,先要设计一个Logger的基类,在Logger基类的基础上,继承出同步日志器(SyncLogger)和异步日志器(AsyncLoggrr)。
同步日志器
框架:
cpp
/*完成日志器模块
1. 抽象日志器基类
2. 派生出不同的子类(同步日志器嘞 & 异步日志器类)
*/
#pragma once
#include "util.hpp"
#include "level.hpp"
#include "format.hpp"
#include "sink.hpp"
#include <thread>
#include <atomic>
#include <mutex>
namespace log
{
class Logger
{
public:
using ptr = std::shared_ptr<Logger>;
void debug(const std::string& file, size_t line, const std::string& fmt, ...);
void info(const std::string& file, size_t line, const std::string& fmt, ...);
void warn(const std::string& file, size_t line, const std::string& fmt, ...);
void error(const std::string& file, size_t line, const std::string& fmt, ...);
void fatal(const std::string& file, size_t line, const std::string& fmt, ...);
protected:
// 抽象接口完成实际的落地输出 -- 不同日志器会有不同的实际落地方式
virtual void log(const char* data, size_t len) = 0;
private:
std::mutex _mutex; // 互斥锁
std::string _logger_name; // 日志器名称
std::atomic<LogLevel::value> _limit_level; // 限制日志等级, atomic原子操作
Formatter::ptr _formatter; // 格式化模块对象
std::vector<LogSink::ptr> _sinks; // 用一个数组来存放日志落地方向 -- 一个日志器中可能有多个日志落地方向
};
class SynLogger : public Logger
{
protected:
void log(const char* data, size_t len);
};
}
Logger.hpp:
cpp
/*完成日志器模块
1. 抽象日志器基类
2. 派生出不同的子类(同步日志器嘞 & 异步日志器类)
*/
#pragma once
#include "util.hpp"
#include "level.hpp"
#include "sink.hpp"
#include "format.hpp"
#include <memory>
#include <mutex>
#include <atomic>
#include <cstdarg>
namespace log
{
// 设计日志器基类
class Logger
{
// 公有
public:
// 基类指针,用来控制继承子类的对象
using ptr = std::shared_ptr<Logger>;
// 构造函数
Logger(const std::string &logger_name, LogLevel::value level, Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks) : _logger_name(logger_name), _limit_level(level), _formatter(formatter), _sliks(sinks.begin(), sinks.end()) {}
// 操作方法
//获取日志器名称
const std::string& name(){
return _logger_name;
}
// 构造日志消息对象并进行格式化,得到格式化后的日志消息字符串--然后进行落地输出,5个等级
void debug(const std::string &file, size_t line, const std::string &fmt, ...)
{ // 日志的输出操作
// 1.判断当前的日志是否达到输出等级,没达到等级就 return
if (LogLevel::value::DEBUG < _limit_level)
{
return;
}
// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf failed!!\n";
return;
}
va_end(ap);
// 代码一样,可以封装成一个函数
serialize(LogLevel::value::DEBUG, file, line, res);
free(res); // 将指针释放掉,防止内存泄漏
}
void info(const std::string &file, size_t line, const std::string &fmt, ...)
{ // 日志的输出操作
// 1.判断当前的日志是否达到输出等级
if (LogLevel::value::INFO < _limit_level)
{
return;
} // 没有达到输出等级
// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf failed!!\n";
return;
}
va_end(ap); // 将ap指针置空
// 代码一样,可以封装成一个函数
serialize(LogLevel::value::INFO, file, line, res);
free(res); // 将指针释放掉
}
void warn(const std::string &file, size_t line, const std::string &fmt, ...)
{ // 日志的输出操作
// 1.判断当前的日志是否达到输出等级
if (LogLevel::value::WARN < _limit_level)
{
return;
} // 没有达到输出等级
// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf failed!!\n";
return;
}
va_end(ap); // 将ap指针置空
// 代码一样,可以封装成一个函数
serialize(LogLevel::value::WARN, file, line, res);
free(res); // 将指针释放掉
}
void error(const std::string &file, size_t line, const std::string &fmt, ...)
{ // 日志的输出操作
// 1.判断当前的日志是否达到输出等级
if (LogLevel::value::ERROR < _limit_level)
{
return;
} // 没有达到输出等级
// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf failed!!\n";
return;
}
va_end(ap); // 将ap指针置空
// 代码一样,可以封装成一个函数
serialize(LogLevel::value::ERROR, file, line, res);
free(res); // 将指针释放掉
}
void fatal(const std::string &file, size_t line, const std::string &fmt, ...)
{ // 日志的输出操作
// 1.判断当前的日志是否达到输出等级
if (LogLevel::value::FATAL < _limit_level)
{
return;
} // 没有达到输出等级
// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf failed!!\n";
return;
}
va_end(ap); // 将ap指针置空
// 代码一样,可以封装成一个函数
serialize(LogLevel::value::FATAL, file, line, res);
free(res); // 将指针释放掉
}
void serialize(LogLevel::value level, const std::string &file, size_t line, char *str)
{
// 3.构造LogMsg对象
LogMsg msg(level, line, file, _logger_name, str); // 传入等级、行号、文件、日志器、有效信息
// 4.通过格式化工具对LogMsg进行格式化,得到格式化后的日志字符串
std::stringstream ss;
_formatter->format(ss, msg);
// 5.进行日志落地
log(ss.str().c_str(), ss.str().size()); // 日志字符串和长度,调用派生类的 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; // 限制日志等级,atomic原子操作的意思是该操作执行过程中不能被中断,该操作要么不执行,要么全部执行,不存在执行一部分的情况。
Formatter::ptr _formatter; // 控制格式化模块的对象
std::vector<LogSink::ptr> _sliks; // 这是一个数组,数组里存放日志落地方式的对象
};
// 派生出同步日志器
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 (_sliks.empty())
{
return;
}
// 不是空
for (auto &sink : _sliks)
{
sink->log(data, len); // 调用 sink 中的 log
}
}
};
}
测试代码:
cpp
// 测试代码
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "logger.hpp"
#include <unistd.h>
int main()
{
// 日志器模块:同步日志器
std::string logger_name = "sync_logger";
log::LogLevel::value limit = log::LogLevel::value::WARN;
log::Formatter::ptr fmt(new log::Formatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"));
log::LogSink::ptr stdout_lsp = log::SinkFactory::create<log::StdoutSink>(); // 标准输出落地
log::LogSink::ptr file_lsp = log::SinkFactory::create<log::FileSink>("./logfile/test.log"); // 文件落地方式
log::LogSink::ptr roll_lsp = log::SinkFactory::create<log::RollBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式
std::vector<log::LogSink::ptr> sinks = {stdout_lsp, file_lsp, roll_lsp};
log::Logger::ptr logger(new 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;
size_t count = 0;
while (cursize < 1024 * 1024 * 10)
{
logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
cursize += 20;
}
return 0;
}
日志器建造者类:
框架:
cpp
// 使用建造者模式来建造日志器,而不要让用户直接去构造日志器,简化用户的使用成本
// 1. 抽象一个日志器建造者类
// 1 - 设置日志器类型
// 2 - 将不同类型日志器的创建都放到同一个日志器建造者类中完成
enum class LoggerType
{
LOGGER_SYNC, // 同步日志器
LOGGER_ASYNC // 异步日志器
};
class LoggerBuilder
{
public:
void buildLoggerType(LoggerType type);
void buildLoggerName(std::string &name);
void buildLoggerLevel(LogLevel::value level);
// 构造一个格式化器
void buildLoggerFormatter(const std::string &pattern);
// 一个日志器可以有多个不同的落地方式
template <typename SinkType, typename... Args>
void buildSink(Args &&...arg);
// 完成我们的日志器构建
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;
};
code:
cpp
enum class 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(std::string &name)
{
_logger_name = name;
}
void buildLoggerLevel(LogLevel::value level)
{
_limit_level = level;
}
// 构造一个格式化器
void buildLoggerFormatter(const std::string &pattern)
{
_formatter = std::make_shared<Formatter>(pattern);
}
// 一个日志器可以有多个不同的落地方式
template <typename SinkType, typename... Args>
void buildSink(Args &&...arg)
{
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);
}
};
}
我们这个建造者模式没有指挥者。因为我们构造对象的零部件没有顺序的要求,只管构造就可以了,所有只要建造者。
测试代码:
cpp
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "logger.hpp"
#include <unistd.h>
int main()
{
//同步日志器建造者模式的测试
//先要构造一个建造者出来
std::unique_ptr<log::LoggerBuilder> builder(new log::LocalLoggerBuilder());
//建造者构建零部件
builder->buildLoggerType(log::LoggerType::LOGGER_SYNC);
builder->buildLoggerName("sync_logger");
builder->buildLoggerLevel(log::LogLevel::value::WARN);
builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");
builder->buildSink<log::StdoutSink>(); // 标准输出落地
builder->buildSink<log::FileSink>("./logfile/test.log"); // 文件落地方式
builder->buildSink<log::RollBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式
//零部件构建好后,用建造者建筑对象
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;
size_t count = 0;
while (cursize < 1024 * 1024 * 10)
{
logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
cursize += 20;
}
return 0;
}
异步日志器
思想:为了避免写日志的过程中阻塞,导致影响业务线程的执行效率。异步的思想就是不让业务线程进行日志的实际落地,而是将日志消息放到缓冲区(一块指定的内存)中接下来有一个专门的异步线程,去针对缓冲区中的数据进行处理(实际的落地操作)
实现:
- 实现一个线程安全的缓冲区
- 创建一个异步工作线程,专门用来负责缓冲区中日志信息的落地操作。
缓冲区详情设计:
1.使用队列缓存日志消息,逐条处理
- 要求:不能涉及空间的频繁申请与释放,否则会降低效率。
- 结果:设计一个环形队列(提前将空间申请好,然后对空间循环利用)
- 存在问题:这个缓冲区的操作会涉及到多线程,因此缓冲区的操作必须保证线程安全。
- 线程安全实现:对于缓冲区的读写加锁
- 因此写日志操作,在实际开发中,不好分配太多资源,工作线程只需要一个日志器就行
- 涉及到的锁冲突:生产者与生产者之间的互斥&生产者与消费者的互斥。
- 问题:锁冲突较为严重,所有线程之间都存在互斥关系
- 解决方案:双缓冲区
两个缓冲区,写入和处理:
固定时间,对两个缓冲区做交换。
单个缓冲区设计
设计一个缓冲区:直接存放格式化后的日志消息字符串,而不是放的 LogMsg 都西昂
好处:
- 减少了LogMsg对象频繁的构造的消耗
- 可以针对缓冲区中的日志消息,一次性进行IO操作,减少IO次数,提高效率
缓冲区类的设计:
1.管理一个存放字符串数据的缓冲区(使用vecotor进行空间管理)
2.当前的写入数据位置的指针(指向可写区域的起始位置,避免数据的写入覆盖)
3.当前的读取数据位置的指针(指向可读数据区域的起始位置,当读取指针与写入指针指向相同位置
提供的操作:
1.向缓冲区写入数据
2.获取可读数据起始地址的接口
3.获取可读数据长度的接口
4.移动读写位置的接口
5.初始化缓冲区的操作(将读写位置初始化------将一个缓冲区所有数据处理完毕之后)
6.提供交换缓冲区的接口(交换空间地址,并不交换空间数据)
框架:
cpp
/*实现异步日志缓冲区*/
#include "util.hpp"
#include <vector>
namespace logslearn
{
// 定义宏,表示缓冲区的大小
#define DEFAULT_BUFFER_SIZE (1 * 1024 * 1024)
#define THRESHOLD_BUFFER_SIZE (8 * 1024 * 1024)
#define INCREMENT_BUFFER_SIZE (1 * 1024 * 1024)
// 异步缓冲区
class Buffer
{
public:
// 构造函数
Buffer() {}
// 1.向缓冲区写入数据
void push(const char *data, size_t len);
// 2.返回可读数据起始地址的接口
const char *begin();
// 3.返回可读数据的长度的接口;返回可写数据的长度的接口
size_t readAbleSize();
size_t writeAbleSize();
// 4.移动读写指针进行向后偏移的接口
void moveWriter(size_t len);
void moveReader(size_t len);
// 5.重置读写位置,初始化缓冲区的操作
void reset();
// 6.交换缓冲区的接口
void swap( Buffer &buffer);
// 判断缓冲区是否为空
bool empty();
private:
// 1.存放字符串数据的缓冲区
std::vector<char> _buffer;
// 2.当前可写数据的指针--本质是下标
size_t _reader_idx;
// 3.当前可读数据的指针
size_t _writer_idx;
};
}
buffer.hpp:
cpp
// 实现异步日志缓冲区
#pragma once
#include "util.hpp"
#include <vector>
#include <cassert>
namespace log
{
#define DEFAULT_BUFFER_SIZE (10 * 1024 * 1024)
#define THRESHOLD_BUFFER_SIZE (80 * 1024 * 1024) // 阈值大小,小于阈值翻倍增长,达到阈值线性增长
#define INCREMENT_BUFFER_SIZE (1 * 1024 * 1024)
class Buffer
{
public:
Buffer() : _buffer(DEFAULT_BUFFER_SIZE), _reader_idx(0), _writer_idx(0) {}
// 向缓冲区写入数据,容量不够就扩容(两种方式,极限测试的时候使用扩容,实际使用过程中固定空间大小,空间不够阻塞)
void push(const char* data, size_t len)
{
// 缓冲区剩余空间不够的情况下:扩容。
// // 1.固定大小,直接返回
// if (len > writeAbleSize())
// return;
// 2.动态空间,用于极限测试--扩容
ensureEnoughSize(len);
// 将数据拷贝到缓冲区
std::copy(data, data + len, &_buffer[_writer_idx]);
// 将写入位置向后偏移
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);
}
// 移动读写指针进行向后偏移的接口
void moveWriter(size_t len)
{
assert((len + _writer_idx) <= _buffer.size());
_writer_idx += len;
}
void moveReader(size_t len)
{
assert(len <= readAbleSize());
_reader_idx += len;
}
// 重置读写位置,初始化缓冲区
void reset()
{
// 读写为 0
_writer_idx = 0; // 缓冲区所有空间都是空闲的
_reader_idx = 0; //_reader_idx与_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);
}
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() * 2;
}
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;
};
}
测试代码:
测试思想:读取文件数据,一点一点写入缓冲区,最终将缓冲区数据写入文件,判断生成的新文件是否和读取的文件一致
code:
cpp
int main()
{
// 异步日志器缓冲区测试
// 读取文件数据,一点一点写入缓冲区,最终将缓冲区数据写入文件,判断生成的新文件与源文件是否一致
std::ifstream ifs("./logfile/test.log", std::ios::binary); // 打开一个文件
if (ifs.is_open() == false)
{
return -1;
} // 文件打开失败返回-1
// 让读写位置跳转到末尾
ifs.seekg(0, std::ios::end);
// 获取当前读写位置相对于起始位置的偏移量
size_t fsize = ifs.tellg();
// 重新让指针跳转到起始位置
ifs.seekg(0, std::ios::beg);
std::string body;
body.resize(fsize);
ifs.read(&body[0], fsize);
if (ifs.good() == false)
{
std::cout << "read error!\n";
return -1;
}
// 打开文件,也要关闭
ifs.close();
std::cout << fsize << std::endl; // 读取文件的数据大小
log::Buffer buffer; // 定义一个缓冲区
for (int i = 0; i < body.size(); i++)
{
buffer.push(&body[i], 1);
}
std::cout << buffer.readAbleSize() << std::endl; // buffer里面可读的数据大小
std::ofstream ofs("./logfile/tem.log", std::ios::binary);
size_t rsize = buffer.readAbleSize();
for (int i = 0; i < rsize; i++)
{
ofs.write(buffer.begin(), 1);
if (ofs.good() == false)
{
std::cout << "writer error!\n";
return -1;
}
buffer.moveReader(1);
}
ofs.close(); // 关闭文件
return 0;
}
md5sum 可以验证文件的完整性,tem.log和test.log文件的md5值一样,说明文件内容一模一样。
异步工作器的设计 -- 双缓冲区思想
异步工作器:
异步工作使用双缓冲区的思想
- 外界将任务数据,添加到输入缓冲区中
- 异步线程对处理缓冲区中的数据进行处理,若处理缓冲区中没有数据了则交换缓冲区
管理的成员:
1.双缓冲区(生产,消费)
2.互斥锁 -- 保证线程安全
3.条件变量-生产&消费(生产缓冲区没有数据,处理完消费缓冲区数据后就休眠)
4.回调函数(针对缓冲区中数据的处理接口-外界传入一个函数,告诉异步工作器数据该如何处理)
-
提供的操作:
a.停止异步工作器
b.添加数据到缓冲区
-
私有操作:
创建线程,线程入口函数中,交换缓冲区,对消费缓冲区数据使用回调函数进行处理,处理完后再次交换
框架:
cpp
/*
实现异步工作器
*/
#pragma once
#include "buffer.hpp" //缓冲区
#include <thread> //线程库
#include <mutex> //互斥锁
#include <condition_variable> //条件变量
#include <functional> //包装器
#include <atomic> //原子类型
namespace logslearn
{
// 异步工作器类
using Functor = std::function<void(Buffer &)>;
class AsyncLooper
{
public:
using ptr = std::shared_ptr<AsyncLooper>;
AsyncLooper();
void stop();
void push(const char *data, size_t len);
private:
void threadEntry(); // 线程入口函数
private:
Functor _callBack; // 具体对缓冲区数据进行处理的回调函数,由异步工作器使用者传入。
private:
std::atomic<bool> _stop; // 让工作器停止标准变成原子性操作,提高了线程安全
Buffer _pro_buf; // 生产缓冲区
Buffer _con_buf; // 消费缓冲区
std::mutex _mutex; // 互斥锁
std::condition_variable _cond_pro; // 两个pcb的等待队列,这是生产者,等待队列的条件变量
std::condition_variable _cond_con; // 这是消费者,等待队列的条件变量
std::thread _thread; // 异步工作器对应的工作线程
};
}
looper.hpp:
cpp
/*
实现异步工作器
*/
#pragma once
#include "buffer.hpp" //缓冲区
#include <thread> //线程库
#include <mutex> //互斥锁
#include <condition_variable> //条件变量
#include <functional> //包装器
#include <atomic> //原子类型
namespace log
{
// 异步工作器类
using Functor = std::function<void(Buffer &)>;
enum class AsyncType
{
ASYNC_SAFE, // 安全状态,表示缓冲区满了就阻塞,避免了资源耗尽的风险
ASUNC_UNSAFE // 非安全状态,不考虑资源耗尽的情况,可以无限扩容,常用与测试
};
class AsyncLooper
{
public:
using ptr = std::shared_ptr<AsyncLooper>;
AsyncLooper(const Functor &cb, AsyncType looper_type = AsyncType::ASUNC_UNSAFE)
: _looper_type(looper_type), _stop(false), _thread(std::thread(&AsyncLooper::threadEntry, this)),
_callBack(cb) {}
~AsyncLooper() { stop(); }
void stop()
{
_stop = true;
_cond_con.notify_all(); // 唤醒所有的工作线程
_thread.join(); // 等待工作线程退出
}
void push(const char *data, size_t len)
{
// 1.无限扩容-非安全(极限压力测试的情况下使用)2.固定大小
std::unique_lock<std::mutex> lock(_mutex);
// 条件变量空值,若缓冲区剩余空间大小大于数据长度,就可以添加数据
// 如果是安全状态就把这个代码加上,非安全状态就把这个代码屏蔽
if (_looper_type == AsyncType::ASYNC_SAFE)
_cond_pro.wait(lock, [&]()
{ return _pro_buf.writeAbleSize() >= len; }); // 被唤醒之后,如果缓冲区有剩余空间,则添加数据;否则继续休眠(lambda表达式结果为假)
// 能够走下来代表满足了条件,可以向缓冲区添加数据
_pro_buf.push(data, len);
// 唤醒一个消费者对缓冲区中的数据进行处理
_cond_con.notify_one();
}
private:
void threadEntry() // 线程入口函数
{
while (!_stop)
{
// 要为互斥锁设置一个生命周期,将缓冲区交换完毕后就解锁(不对数据的处理过程加锁保护)
{
// 1.判断生产缓冲区里有没有数据,有则交换,无则阻塞
std::unique_lock<std::mutex> lock(_mutex);
//退出标志被设置,且生产缓冲区无数据,这时候在退出,否则有可能会造成生产缓冲区有数据,但是没有被完全处理
if (_stop && _pro_buf.empty()) break;
// 若退出前被唤醒,或者有数据被唤醒,则返回真,继续向下运行,否则重新陷入休眠
_cond_con.wait(lock, [&]()
{ return !_pro_buf.empty() || _stop; }); //_stop是真表示程序退出,把剩余的数据进行交换
// 等待完毕,消费者与生产者进行地址交换
_con_buf.swap(_pro_buf);
// 2.唤醒生产者
if (_looper_type == AsyncType::ASYNC_SAFE)
_cond_pro.notify_all();
}
// 3.被唤醒后,对消费缓冲区进行数据处理
_callBack(_con_buf);
// 4.初始化消费缓冲区
_con_buf.reset();
}
}
private:
Functor _callBack; // 具体对缓冲区数据进行处理的回调函数,由异步工作器使用者传入。
private:
AsyncType _looper_type; // 默认是安全模式
std::atomic<bool> _stop; // 让工作器停止标志变成原子性操作,提高了线程安全
Buffer _pro_buf; // 生产缓冲区
Buffer _con_buf; // 消费缓冲区
std::mutex _mutex; // 互斥锁
std::condition_variable _cond_pro; // 生产者,等待队列的条件变量
std::condition_variable _cond_con; // 消费者,等待队列的条件变量
std::thread _thread; // 异步工作器对应的工作线程
};
}
异步日志器设计
1.继承于Logger日志器类 对于写日志操作进行函数重写(不再将数据写入文件,而是通过异步消息处理器,放到缓冲区中)
2.通过异步消息处理器,进行日志数据的实际落地
管理成员:
异步工作器(异步消息处理器)
完成后,完善日志器建造者,进行异步日志器安全模式的选择,提供异步日志器的创建
框架:
cpp
//派生出异步日志器
class AsyncLogger:public Logger{
public:
AsyncLogger( const std::string &logger_name, LogLevel::value level, Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks,AsyncType looper_type) :
Logger(logger_name, level, formatter, sinks) {}
//将数据写入缓冲区
void log(const char*data,size_t len);
//设计一个实际落地函数(将缓冲区里的数据进行落地)
void realLog(Buffer &buf);
private:
AsyncLooper::ptr _looper;
};
完善日志器建造者功能:
cpp
// 使用建造者模式来建造日志器,而不要让用户直接去构造日志器,简化用户的使用成本
// 1. 抽象一个日志器建造者类
// 1 - 设置日志器类型
// 2 - 将不同类型日志器的创建都放到同一个日志器建造者类中完成
enum class LoggerType
{
LOGGER_SYNC, // 同步日志器
LOGGER_ASYNC // 异步日志器
};
class LoggerBuilder
{
public:
LoggerBuilder() : _logger_type(LoggerType::LOGGER_SYNC),
_limit_level(LogLevel::value::DEBUG),
_looper_type(AsyncType::ASYNC_SAFE) {}
void buildLoggerType(LoggerType type)
{
_logger_type = type;
}
void buildLoggerName(const std::string &name)
{
_logger_name = name;
}
void buildLoggerLevel(LogLevel::value level)
{
_limit_level = level;
}
void buildEnableUnSafeAsync()
{
_looper_type = AsyncType::ASUNC_UNSAFE;
}
// 构造一个格式化器
void buildLoggerFormatter(const std::string &pattern)
{
_formatter = std::make_shared<Formatter>(pattern);
}
// 一个日志器可以有多个不同的落地方式
template <typename SinkType, typename... Args>
void buildSink(Args &&...args)
{
LogSink::ptr psink = SinkFactory::create<SinkType>(std::forward<Args>(args)...);
_sinks.push_back(psink);
}
// 完成我们的日志器构建
virtual Logger::ptr build() = 0;
protected:
AsyncType _looper_type;
LoggerType _logger_type;
std::string _logger_name;
LogLevel::value _limit_level;
Formatter::ptr _formatter;
std::vector<LogSink::ptr> _sinks;
};
// 2.派生出具体的建造者类------局部日志器的建造者 & 全局日志器建造者(后边添加了全局单例管理之后,将日志器添加全局管理)
// 局部日志器的建造者
class LocalLoggerBuilder : public LoggerBuilder
{
public:
Logger::ptr build() override
{
// 必须要有日志器名称
assert(_logger_name.empty() == false);
// 必须要有格式化器,没有就要创建
if (_formatter.get() == nullptr)
{
_formatter = std::make_shared<Formatter>();
}
// 如果没有落地方式,默认添加一个标准输出的落地方式
if (_sinks.empty())
{
buildSink<StdoutSink>();
}
// 类型为异步日志器
if (_logger_type == LoggerType::LOGGER_ASYNC)
{
// 返回异步日志器对象
return std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);
}
return std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);
}
};
}
测试代码:
cpp
int main()
{
//异步日志器的测试
//异步日志器和异步工作器进行联调
// //先要构造一个建造者出来
std::unique_ptr<log::LoggerBuilder> builder(new log::LocalLoggerBuilder());
//建造者构建零部件
builder->buildLoggerType(log::LoggerType::LOGGER_ASYNC);
builder->buildLoggerName("async_logger");
builder->buildLoggerLevel(log::LogLevel::value::WARN);
builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");
builder->buildSink<log::StdoutSink>(); // 标准输出落地
builder->buildSink<log::FileSink>("./logfile/async.log"); // 文件落地方式
// builder->buildSink<log::RoolBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式
//零部件构建好后,用建造者建筑对象
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 count = 0;
while (count < 100000)
{
logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
}
return 0;
}
日志器管理模块 -- 单例模式
⽇志的输出,我们希望能够在任意位置都可以进⾏,但是当我们创建了⼀个⽇志器之后,就会受到⽇志器所在作⽤域的访问区域限制。
因此,为了突破访问区域的限制,我们创建⼀个⽇志器管理类,且这个类是⼀个单例类,这样的话, 我们就可以在任意位置来通过管理器单例获取到指定的⽇志器来进⾏⽇志输出了。
日志器管理器:
作用1:对所有创建的日志器进行管理
特性:将管理器设计为单例
作用2:可以在程序的任意位置,获取相同的单例对象,获取其中的日志器进行日志输出
拓展:单例管理器创建的时候,默认先创建一个日志器(用于进行标准输出的打印)
拓展目的:让用户在不创建任何日志器的情况下,也能进行标准输出的打印,方便用户使用
设计:
管理的成员:
1.默认日志器
2.所管理的日志器数组
3.互斥锁
提供的接口:
1添加日志器管理
2.判断是否管理了指定名称的日志器
3.获取指定名称的日志器
4.获取默认日志器
日志管理器设计
在 looger.hpp 中:
框架:
cpp
// 日志器管理模块
class LoggerManager
{
public:
// 1添加日志器管理
void addLogger(Logger::ptr &logger);
// 2.判断是否管理了指定名称的日志器
bool hasLogger(const std::string &name);
// 3.获取指定名称的日志器
Logger::ptr getLogger(const std::string &name);
// 4.获取默认日志器
Logger::ptr rootLogger();
// 5. 获取单例句柄
static LoggerManager &getInstance();
private:
// 构造函数私有化
LoggerManager() {}
private:
// 1.默认日志器
Logger::ptr _root_logger;
// 2.所管理的日志器 -- 日志器名称和日志器对象(智能指针)的映射
std::unordered_map<std::string, Logger::ptr> _loggers;
// 3.互斥锁
std::mutex _mutex;
};
code:
cpp
// 日志器管理模块
class LoggerManager
{
public:
// 1添加日志器管理
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));
}
// 2.判断是否管理了指定名称的日志器
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;
}
// 3.获取指定名称的日志器
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;
}
// 4.获取默认日志器
Logger::ptr rootLogger()
{
return _root_logger;
}
// 5. 获取单例句柄
static LoggerManager &getInstance()
{
// 在c++11之后,针对静态局部变量,编译器在编译的层面上实现了线程安全
// 当静态局部变量在没有构造完成之前,其他的线程进入就会阻塞
static LoggerManager eton;
return eton;
}
private:
// 构造函数私有化
LoggerManager()
{
// 构造一个日志器建造者
std::unique_ptr<log::LoggerBuilder> builder(new log::LocalLoggerBuilder());
builder->buildLoggerName("root");
_root_logger = builder->build();
// 把默认构造的日志器管理起来
_loggers.insert(std::make_pair("root", _root_logger));
}
private:
// 1.默认日志器
Logger::ptr _root_logger;
// 2.所管理的日志器
std::unordered_map<std::string, Logger::ptr> _loggers;
// 3.互斥锁
std::mutex _mutex;
};
设计一个全局的日志器建造者
在局部的日志器建造者上增加一个功能:将日志器添加到单例对象中;就是全局的日志器建造者
cpp
class GlobalLoggerBuilder : public LoggerBuilder
{
public:
Logger::ptr build() override
{
// 必须要有日志器名称
assert(_logger_name.empty() == false);
// 必须要有formatter//必须要有格式化器,没有就要创建
if (_formatter.get() == nullptr)
{
_formatter = std::make_shared<Formatter>();
}
// 如果没有落地方式就给它添加一个标准输出的默认落地方式
if (_sinks.empty())
{
buildSink<StdoutSink>();
}
//默认日志器
Logger::ptr logger;
// 如果类型为LOGGER_ASYNC,那么日志器为异步日志器
if (_logger_type == LoggerType::LOGGER_ASYNC)
{
// 返回异步日志器对象
logger=std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);
}else{
// 返回同步日志器的对象
logger=std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks); // 日志器名字,等级,格式化,落地方式
}
//把日志器添加到日志器管理器中
LoggerManager::getInstance().addLogger(logger);
// 返回同步日志器的对象
return logger;
}
};
测试日志器管理器的接口和全局建造者类
cpp
void test_log(){
//日志器管理器
log::Logger::ptr logger=log::LoggerManager::getInstance().getLogger("async_logger");
//测试日志打印
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 count = 0;
while (count < 100000)
{
logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
}
}
int main()
{
//测试日志器管理模块
//先要构造一个建造者出来
//全局建造者构造日志器
std::unique_ptr<log::LoggerBuilder> builder(new log::GlobalLoggerBuilder());
//建造者构建零部件
builder->buildLoggerName("async_logger");
builder->buildLoggerLevel(log::LogLevel::value::WARN);
builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");
builder->buildLoggerType(log::LoggerType::LOGGER_ASYNC);
builder->buildEnableUnSafeAsync();//切换模式
builder->buildSink<log::StdoutSink>(); // 标准输出落地
builder->buildSink<log::FileSink>("./logfile/async.log"); // 文件落地方式
builder->build();
// builder->buildSink<log::RoolBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式
test_log();
return 0;
}
宏函数和全局接口设计 -- 涉及代理模式
提供全局接口&宏函数, 对日志系统接口,进行使用便捷性优化
思想:
1.提供获取指定日志器的全局接口(避免用户自己操作单例对象)
2.使用宏函数对日志器的接口进行代理(代理模式)
3.提供宏函数,直接通过默认日志器进行日志的标准输出打印(不要获取日志器了)
新建一个.h,文件,文件里面放我们写的.hpp(各个模块文件)
log.h:
方便外界使用者进行调用,用户使用时直接包含该头文件就行,不需要包含各个模块的头文件了。
cpp
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "logger.hpp"
#include <unistd.h>
#include "buffer.hpp"
#include "looper.hpp"
#include <fstream>
namespace log
{
// 1.提供获取指定日志器的全局接口(避免用户自己操作单例对象)
Logger::ptr getLogger(const std::string &name) // 指定日志器
{
return log::LoggerManager::getInstance().getLogger(name);
}
Logger::ptr rootLogger() // 默认日志器
{
return log::LoggerManager::getInstance().rootLogger();
}
// 2.使用宏函数对日志器的接口进行代理(代理模式)
#define debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define info(fmt, ...) info(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define warn(fmt, ...) warn(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define error(fmt, ...) error(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define fatal(fmt, ...) fatal(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
// 3.提供宏函数,直接通过默认日志器进行日志的标准输出打印(不要获取日志器了)
#define DEBUG(fmt, ...) log::rootLogger()->debug(fmt, ##__VA_ARGS__)
#define INFO(fmt, ...) log::rootLogger()->info(fmt, ##__VA_ARGS__)
#define WARN(fmt, ...) log::rootLogger()->warn(fmt, ##__VA_ARGS__)
#define ERROR(fmt, ...) log::rootLogger()->error(fmt, ##__VA_ARGS__)
#define FATAL(fmt, ...) log::rootLogger()->fatal(fmt, ##__VA_ARGS__)
}
对宏函数与全局接口进行功能测试
测试第一组宏是否正确:
cpp
#include "log.h"
void test_log(){
//日志器管理器
log::Logger::ptr logger=log::LoggerManager::getInstance().getLogger("async_logger");
//测试日志打印
logger->debug("%s", "测试日志");
logger->info( "%s", "测试日志");
logger->warn("%s", "测试日志");
logger->error( "%s", "测试日志");
logger->fatal( "%s", "测试日志");
size_t count = 0;
while (count < 100000)
{
logger->fatal( "测试日志-%d", count++);
}
}
int main()
{
//测试日志器管理模块
//先要构造一个建造者出来
//全局建造者构造日志器
std::unique_ptr<log::LoggerBuilder> builder(new log::GlobalLoggerBuilder());
//建造者构建零部件
builder->buildLoggerName("async_logger");
builder->buildLoggerLevel(log::LogLevel::value::WARN);
builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");
builder->buildLoggerType(log::LoggerType::LOGGER_ASYNC);
builder->buildEnableUnSafeAsync();//切换模式
builder->buildSink<log::StdoutSink>(); // 标准输出落地
builder->buildSink<log::FileSink>("./logfile/async.log"); // 文件落地方式
builder->build();
// builder->buildSink<log::RoolBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式
test_log();
return 0;
}
测试第二组宏:
cpp
#include "log.h"
void test_log(){
// 使用默认日志器打印
DEBUG("%s", "测试日志");
INFO("%s", "测试日志");
WARN("%s", "测试日志");
ERROR("%s", "测试日志");
FATAL("%s", "测试日志");
size_t count = 0;
while (count < 100000)
{
FATAL("测试日志-%d", count++);
}
}
int main()
{
test_log();
return 0;
}
项目目录结构
example:如何使用项目的具体样例
logs:项目的各个模块,项目源码
pratice:练习代码,项目前置学习的代码
bench:用来进行性能测试
整理完成后,目录结构如下:
功能测试
cpp
#include "../logs/log.h"
//进行功能测试
void test_log(const std::string &name){
INFO( "%s", "测试开始");
log::Logger::ptr logger=log::LoggerManager::getInstance().getLogger(name);
//测试日志打印
logger->debug( "%s", "测试日志");
logger->info( "%s", "测试日志");
logger->warn( "%s", "测试日志");
logger->error( "%s", "测试日志");
logger->fatal("%s", "测试日志");
INFO( "%s", "测试结束");
}
int main()
{
std::unique_ptr<log::LoggerBuilder> builder(new log::GlobalLoggerBuilder());
//建造者构建零部件
builder->buildLoggerName("async_logger");
builder->buildLoggerLevel(log::LogLevel::value::DEBUG);
builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%p]%m%n");
builder->buildLoggerType(log::LoggerType::LOGGER_SYNC);
builder->buildSink<log::StdoutSink>(); // 标准输出落地
builder->buildSink<log::FileSink>("./logfile/async.log"); // 文件落地方式
builder->buildSink<log::RollBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式
builder->build();
test_log("async_logger");
return 0;
}
性能测试
测试环境:
CPU:11th Gen Intel® Core™ i7-11800H @ 2.30GHz
RAM:16G DDR4 3200
ROM:512GB-SSD
OS:CentOS7.6(2核2G)
测试三要素:
1.测试环境
2.测试方法
3.测试结果
测试工具的编写:
1.可以控制写日志线程数量
2.可以控制写日志的总数量
分别对于同步日志器 & 异步日志器进行各自的性能测试,
需要测试单写日志线程的性能
需要测试多写日志线程的性能
实现:
封装一个接口,传入日志器名称,线程数量,日志数量,单条日志大小 在接口内,创建指定数量的线程,各自负责一部分日志的输出,在输出之前计时开始,在输出完毕后计时结束。
所耗时间=结束时间-起始时间
每秒输出量 =日志数量/总耗时
每秒输出大小 =日志数量*单条日志大小/总耗时
注意:异步日志输出这里,我们启动非安全模式,纯内存写入(不去考虑实际落地的时间)
项目性能测试工具实现
在对项目测试之前,需要编写测试工具,测试工具的具体实现放到bench目录下。
cpp
#include "../logs/log.h"
// c++11提供的时间特性
#include <chrono>
// 设计性能测试功能
// logger_name日志器的名字,thr_count线程数的个数,msg_counr日志消息的总条数,len日志消息的长度
void bench(const std::string &logger_name, size_t thr_count, size_t msg_count, size_t msg_len)
{
// 1.获取日志器
log::Logger::ptr logger = log::getLogger(logger_name);
// 如果没找到日志器就返回空
if (logger.get() == nullptr)
{
return;
}
// 2.组织指定长度的日志消息
// 留一个字符,放换行符
std::string msg(msg_len - 1, 'A');
// 3.创建指定数量的线程
// 创建一个存放线程的数组
std::vector<std::thread> threads;
// 存放每个线程打印日志需要消耗的时间
std::vector<double> cost_arry(thr_count);> 日志系统:
>
> 日志:程序运行过程中所记录的程序运行状态信息
>
> 日志的作用:记录程序运行状态信息,以便于程序员能够随时根据状态信息,对系统的运行状态进行分析。
项目不是一个功能型业务型的项目,是一个组件型的项目,是一个库,是给别人用的;所以项目演示时,就展示一下日志系统的性能。
# 项目介绍
本项目主要实现的是一个日志系统,其支持以下功能:
- 支持多级别日志消息
- 支持同步日志和异步日志
- 支持写入日志到控制台、文件以及滚动文件中
- 支持多线程程序并发写日志
- 支持扩展不同的日志落地到目标地
# 开发环境
- 操作系统 :Centos 7
- 编辑器: vscode / vim
- 编译器/调试器:g++ / gdb
- 项目自动化构建工具:Makefile
# 核心技术
- 类层次设计(继承、多态的实际应用)
- C++11语法(多线程库,auto,智能指针,右值引用等)
- 双缓冲区
- 生产者消费者模型
- 多线程
- 设计模式(单例、工厂、代理、建造者等)
# 日志系统介绍
## 为什么需要日志系统
* 在生产环境中的产品,为了保证其稳定性以及安全性,是不允许开发人员附加调试器去排查问题的(调试器调试时出现程序崩溃需要根据程序运行数据进行分析,这是不允许的,产品是有隐私的),可以借助日志系统来打印一些日志帮助开发人员解决问题
* 上线客户端的产品出现的Bug无法复现并解决,可以借助日志系统打印日志并且上传到服务端帮助开发人员进行问题分析
* 对于一些高频操作(如定时器,心跳包等)在少量调试次数下可能无法触发我们想要的行为,通过断点暂停的方式,我们需要重复几十次甚至上百次,导致排查问题的效率非常低下,可以借助打印日志的方式排查问题
* 在分布式、多线程/多进程的代码中,出现bug非常难定位,可以借助日志系统打印日志帮助定位bug
* 帮助刚接触项目不久的开发人员理解代码的运行流程
## 日志系统技术实现
> **日志系统的技术实现主要包括三种类型:**
>
> 1. 利用printf、std::cout等输出函数将日志信息打印到控制台。-- 1
> 2. 对于大型商业化项目,为了方便排查问题,我们一般会将日志输出到文件或者是数据库系统方便查询和分析日志,主要分为**同步日志**和**异步日志**方式。
> * 同步写日志 -- 2
> * 异步写日志 -- 3
### 同步写日志
**同步日志指的是当输出日志时,必须等待日志输出语句执行完毕后,才能执行后面的业务逻辑语句,日志输出语句与程序的业务逻辑语句将在同一个线程中运行。每次调用一次打印日志API就对应一次系统调用write写日志文件**。
![image-20240827114052891](https://anduin.oss-cn-nanjing.aliyuncs.com/image-20240827114052891.png)
优点:流程简单,编写代码简单
缺点:在高并发场景下,随着日志数量不断增加,同步日志系统容易产生系统瓶颈:
1. ⼀方⾯,⼤量的日志打印陷⼊等量的write系统调用,有⼀定系统开销.
2. 另⼀方⾯,使得打印日志的进程附带了⼤量同步的磁盘IO,影响程序性能
### 异步写日志
**异步日志是指在进行日志输出时,日志输出语句与业务逻辑语句并不是在同一个线程中运行,而是有专门的线程用于进行日志输出操作,业务线程只需要将日志放到一个内存缓冲区中,不需要等待即可继续执行后续业务逻辑(作为日志的生产者),而日志的落地操作交给单独的日志线程完成(作为日志的消费者),这是一个典型的生产者消费者模型**。
![image-20240827120755639](https://anduin.oss-cn-nanjing.aliyuncs.com/image-20240827120755639.png)
这样的好处是即使日志没有真正的完成输出也不会影响程序的主业务,提高程序的性能
1. 主线程调用日志打印接口成为非阻塞操作
2. 同步的磁盘IO操作从主线程剥离出来交给单独的线程完成。
# 相关技术知识补充
## 不定参函数
> 在初学C语⾔的时候,我们都⽤过printf函数进⾏打印。其中printf函数就是⼀个不定参函数,在函数内部可以根据格式化字符串中格式化字符分别获取不同的参数进⾏数据的格式化。
### 不定参宏函数的使用
1. 不定参的表示用:"..."
2. 不定参的使用:" `__VA_REGS__`"。(在 `__VA_REGS__` 前加 ##,意思是如果 `__VA_REGS__` 为空就取消前面的逗号。)
```c
#include <stdio.h>
#define LOG(fmt, ...) printf("[%s:%d]" fmt, __FILE__, __LINE__, __VA_ARGS__);
int main()
{
//编译器内置的宏:__FILE__当前文件名;__LINE__当前行号。
LOG("%s-%d\n", "Anduin", 521);
return 0;
}
如果只有LOG("Anduin"),就会报错;这时候就要加##号。
c
#define LOG(fmt, ...) printf("[%s:%d]" fmt, __FILE__, __LINE__, ##__VA_ARGS__);
C中不定参函数的使用
c
#define _GNU_SOURCE //使用vasprintf函数必须带这个宏
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
//参数类型一样
void printNum(int n, ...)
{
va_list al; // 可变参数列表
va_start(al, n); // 获取n参数之后的第一个不定参数的地址
for (int i = 0; i < n; i++)
{
int num = va_arg(al, int); // 从可变参数列表中获取⼀个整形参数
printf("param[%d]:%d\n", i, num);
}
va_end(al); // 将al置空
}
//参数类型不一样 -- 模拟一下 printf 的实现
void myprintf(const char* fmt,...)
{
va_list ap;
va_start(ap,fmt);
char *res;
//对不定参数的基本使用;給一个一级指针的地址(&res)进去,根据fmt里面的字符串格式,一个一个的取出可变参数列表的每个数。
int ret=vasprintf(&res,fmt,ap);
if(ret!=-1)
{
printf(res);
free(res);
}
va_end(ap);//将ap指针置空
}
int main()
{
printNum(3, 11, 22,33);
printNum(5, 44, 55, 66, 77, 88);
myprintf("%s %d\n","Anduin",521);
return 0;
}
C++中不定参函数的使用
cpp
#include <iostream>
// 实现一个打印操作
// 特化
void xprintf()
{
std::cout<<std::endl;
}
template <typename T,typename ...Args>
void xprintf(const T &v,Args &&...args)
{
std::cout << v;
// 通过 sizeof 计算参数包参数的个数
if((sizeof ...(args))>0)
{
xprintf(std::forward<Args>(args)...);
}
else{
// 参数包没参数了
xprintf();
}
}
int main()
{
xprintf("Anduin","haha",521);
xprintf("Anduin","haha");
xprintf("Anduin");
return 0;
}
设计模式
设计模式是前辈们对代码开发经验的总结,是解决特定问题的⼀系列套路。它不是语法规定,⽽是⼀套⽤来提⾼代码可复⽤性、可维护性、可读性、稳健性以及安全性的解决⽅案。
六⼤原则:
-
单⼀职责原则(SingleResponsibilityPrinciple);
- 类的职责应该单⼀,⼀个类只做⼀件事。职责划分清晰了,每次改动到最⼩单位的⽅法或类。
- 使⽤建议:两个完全不⼀样的功能不应该放⼀个类中,⼀个类中应该是⼀组相关性很⾼的函数、数据的封装
- ⽤例:⽹络聊天:⽹络通信&聊天,应该分割成为网络通信类&聊天类
-
开闭原则(OpenClosedPrinciple )
- 对扩展开放,对修改封闭。
- 使⽤建议:对软件实体的改动,最好⽤扩展⽽⾮修改的⽅式。
- ⽤例:超市卖货:商品价格---不是修改商品的原来价格,⽽是新增促销价格。
-
里氏替换原则(LiskovSubstitutionPrinciple )
- 通俗点讲,就是只要⽗类能出现的地⽅,⼦类就可以出现,⽽且替换为⼦类也不会产⽣任何错误或异常。
- 在继承类时,务必重写⽗类中所有的⽅法,尤其需要注意⽗类的protected⽅法,⼦类尽量不要暴露⾃⼰的public⽅法供外界调⽤。
- 使⽤建议:⼦类必须完全实现⽗类的⽅法,孩⼦类可以有⾃⼰的个性。覆盖或实现⽗类的⽅法时,输⼊参数可以被放⼤,输出可以缩⼩。
- ⽤例:跑步运动员类-会跑步,⼦类⻓跑运动员-会跑步且擅⻓⻓跑,⼦类短跑运动员-会跑步且擅⻓短跑。
-
依赖倒置原则(Dependence Inversion Principle)
- ⾼层模块不应该依赖低层模块,两者都应该依赖其抽象.不可分割的原⼦逻辑就是低层模式,原⼦逻辑组装成的就是⾼层模块。
- 模块间依赖通过抽象(接⼝)发⽣,具体类之间不直接依赖。
- 使⽤建议:每个类都尽量有抽象类,任何类都不应该从具体类派⽣。尽量不要重写基类的⽅法。结合⾥⽒替换原则使⽤。
- ⽤例:奔驰⻋司机类--只能开奔驰;司机类--给什么⻋,就开什么⻋;开⻋的⼈:司机--依赖于抽象。
-
迪⽶特法则(LawofDemeter),⼜叫"最少知道法则"
- 尽量减少对象之间的交互,从⽽减⼩类之间的耦合。⼀个对象应该对其他对象有最少的了解。 对类的低耦合提出了明确要求:1.只和直接的朋友交流,朋友之间也是有距离的。⾃⼰的就是⾃⼰的。(如果⼀个⽅法放在本类中,既不增加类间关系,也对本类不产⽣负⾯影响,那就放置在本类中)
- ⽤例:⽼师让班⻓点名--⽼师给班⻓⼀个名单,班⻓完成点名勾选,返回结果,⽽不是班⻓点名,⽼师勾选。
-
接⼝隔离原则(Interface Segregation Principle );
- 客⼾端不应该依赖它不需要的接⼝,类间的依赖关系应该建⽴在最⼩的接⼝上。
- 使⽤建议:接⼝设计尽量精简单⼀,但是不要对外暴露没有实际意义的接⼝。
- ⽤例:修改密码,不应该提供修改⽤⼾信息接⼝,⽽就是单⼀的最⼩修改密码接⼝,更不要暴露数据库操作。
从整体上来理解六⼤设计原则,可以简要的概括为⼀句话,用抽象构建框架,用实现扩展细节,每⼀条设计原则对应⼀条注意事项:
- 单⼀职责原则告诉我们实现类要职责单⼀;
- 开闭原则是总纲,告诉我们要对扩展开放,对修改关闭;
- ⾥⽒替换原则告诉我们不要破坏继承体系;
- 依赖倒置原则告诉我们要⾯向接⼝编程;
- 迪⽶特法则告诉我们要降低耦合;
- 接⼝隔离原则告诉我们在设计接⼝的时候要精简单⼀。
单例模式
一个类只创建一个对象。 该设计模式可以保证系统中该类只有⼀个实例化对象,并提供⼀个访问它的全局访问点,该实例被所有程序模块共享。⽐如在某个服务器程序中,该服务器的配置信息存放在⼀个⽂件中,这些配置数据由⼀个单例对象统⼀读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种⽅式简化了在复杂环境下的配置管理。
饿汉模式:
程序启动时就会创建⼀个唯⼀的实例对象。因为单例对象已经确定,所以⽐较适⽤于多线程环境中,多线程获取单例对象不需要加锁,可以有效的避免资源竞争,提⾼性能。
cpp
#include <iostream>
class Singleton
{
private:
static Singleton _eton;
Singleton() : _data(99)
{
std::cout << "单例对象构造" << std::endl;
}
Singleton(const Singleton&) = delete;
Singleton& operator= (Singleton&) = delete;
~Singleton(){};
private:
int _data;
public:
static Singleton &getInstance()
{
return _eton;
}
int getData()
{
return _data;
}
};
Singleton Singleton::_eton;
int main()
{
std::cout << Singleton::getInstance().getData() << std::endl;
return 0;
}
懒汉模式:
懒加载,延迟加载的思想,一个对象在用的时候在进行实例化。 如果单例对象构造特别耗时或者耗费资源(加载插件、加载⽹络资源等), 可以选择懒汉模式, 在第⼀次使⽤的时候才创建对象。
- 这⾥介绍的是《Effective C++》⼀书作者ScottMeyers 提出的⼀种更加优雅简便的单例模式 Meyers'Singleton in C++。
- C++11 Static local variables特性以确保C++11起,静态变量将能够在满⾜ thread-safe 的前提下唯⼀地被构造和析构
cpp
#include <iostream>
class Singleton
{
private:
Singleton() : _data(99)
{
std::cout << "单例对象构造" << std::endl;
}
Singleton(const Singleton &) = delete;
Singleton& operator= (Singleton&) = delete;
~Singleton(){};
private:
int _data;
public:
static Singleton &getInstance()
{
static Singleton _eton;
return _eton;
}
int getData()
{
return _data;
}
};
int main()
{
std::cout << Singleton::getInstance().getData() << std::endl;
return 0;
}
注释 27 行:
没注释 27 行:
工厂模式
⼯⼚模式是⼀种创建型设计模式,它提供了⼀种创建对象的最佳⽅式。在⼯⼚模式中,我们创建对象时不会对上层暴露创建逻辑,⽽是通过使⽤⼀个共同结构来指向新创建的对象,以此实现创建-使⽤的分离
工厂模式可以分为:
- 简单工厂模式
简单⼯⼚模式实现由⼀个⼯⼚对象通过类型决定创建出来指定产品类的实例。
假设有个⼯⼚能⽣产出⽔果,当客⼾需要产品的时候明确告知⼯⼚⽣产哪类⽔果,⼯⼚需要接收⽤⼾提供的类别信息,当新增产品的时候,⼯⼚内部去添加新产品的⽣产⽅式。
cpp
#include <iostream>
#include <memory>
// 水果 -- 抽象类
class Fruit
{
public:
virtual void name() = 0;
};
// 水果产品 -- 苹果 香蕉
class Apple : public Fruit
{
public:
void name() override
{
std::cout << "我是一个苹果" << std::endl;
}
};
class Banana : public Fruit
{
public:
void name() override
{
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
return std::make_shared<Banana>();
}
};
int main()
{
std::shared_ptr<Fruit> fruit = FruitFactory::create("苹果");
fruit->name();
fruit = FruitFactory::create("香蕉");
fruit->name();
return 0;
}
-
优点:简单粗暴,直观易懂。
-
缺点:
- 生产产品的代码都写在一起,产品太多会导致代码量庞⼤
- 扩展性差,当新增产品时,需要修改工厂类新增新产品的产品创建逻辑,违背了开闭原则。
- 工厂方法模式
在简单⼯⼚模式下新增多个⼯⼚,多个产品,每个产品对应⼀个⼯⼚。假设现在有 A、B 两种产品,则开两个⼯⼚,⼯⼚ A 负责⽣产产品 A,⼯⼚ B 负责⽣产产品 B,用户只知道产品 的⼯⼚名,⽽不知道具体的产品信息,⼯⼚不需要再接收客户的产品类别,⽽只负责⽣产产品。
cpp
// 工厂方法模式遵循了开闭原则,扩展性好
#include <iostream>
#include <memory>
// 水果类 -- 抽象类
class Fruit
{
public:
virtual void name() = 0;
};
class Apple : public Fruit
{
public:
void name() override
{
std::cout << "我是一个苹果" << std::endl;
}
};
// 水果产品 -- 苹果 香蕉
class Banana : public Fruit
{
public:
void name() override
{
std::cout << "我是一个香蕉" << std::endl;
}
};
// 水果工厂 -- 抽象类
class FruitFactory
{
public:
virtual std::shared_ptr<Fruit> create() = 0;
};
// 苹果工厂
class AppleFactory : public FruitFactory
{
public:
virtual std::shared_ptr<Fruit> create() override // 用来重写基类的同名虚函数
{
return std::make_shared<Apple>();
}
};
// 香蕉工厂
class BananFactory : public FruitFactory
{
public:
virtual 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 BananFactory()); // 重新设置工厂为香蕉工厂
fruit = ff->create(); // 生产香蕉
fruit->name();
return 0;
return 0;
}
但是⼯⼚⽅法模式每次增加⼀个产品时,都需要增加⼀个具体产品类和⼯⼚类,这会使得系统中类的个数成倍增加的,代码就会比较臃肿。
- 抽象工厂模式
⼯⼚⽅法模式通过引⼊⼯⼚等级结构,解决了简单⼯⼚模式中⼯⼚类职责太重的问 题,但由于⼯⼚⽅法模式中的每个⼯⼚只⽣产⼀类产品,可能会导致系统中存在⼤量的⼯⼚类,势必会增加系统的开销。
此时,我们可以考虑将⼀些相关的产品组成⼀个产品族,由同⼀个⼯⼚来统⼀⽣产,这就是抽象⼯⼚模式的基本思想。
cpp
#include<iostream>
#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:
virtual void name() = 0;
};
class Lamp : public Animal
{
public:
virtual void name()
{
std::cout << "我是山羊" << std::endl;
}
};
class Dog : public Animal
{
public:
virtual void name()
{
std::cout << "我是小狗" << std::endl;
}
};
class Factory
{
public:
virtual std::shared_ptr<Fruit> GetFruit(const std::string &name) = 0;
virtual std::shared_ptr<Animal> GetAnimal(const std::string &name) = 0;
};
class FruitFactory : public Factory
{
public:
// 纯虚函数必须重写
virtual std::shared_ptr<Animal> GetAnimal(const std::string &name)
{
return std::shared_ptr<Animal>(); // 返回一个空对象
}
virtual std::shared_ptr<Fruit> GetFruit(const std::string &name)
{
if(name == "苹果")
{
return std::make_shared<Apple>();
}
else if(name == "香蕉")
{
return std::make_shared<Banana>();
}
}
};
class AnimalFactory : public Factory
{
public:
virtual std::shared_ptr<Fruit> GetFruit(const std::string &name)
{
return std::shared_ptr<Fruit>();
}
virtual std::shared_ptr<Animal> GetAnimal(const std::string &name)
{
if(name == "山羊")
{
return std::make_shared<Lamp>();
}
else if(name == "小狗")
{
return std::make_shared<Dog>();
}
}
};
class FactoryProducer
{
public:
static std::shared_ptr<Factory> getFactory(const std::string &name)
{
if(name == "动物")
{
return std::make_shared<AnimalFactory>();
}
else
{
return std::make_shared<FruitFactory>();
}
}
};
int main()
{
std::shared_ptr<Factory> ff = FactoryProducer::getFactory("水果");
std::shared_ptr<Fruit> f = ff->GetFruit("香蕉");
f->name();
f = ff->GetFruit("苹果");
f->name();
std::shared_ptr<Factory> af = FactoryProducer::getFactory("动物");
std::shared_ptr<Animal> a = af->GetAnimal("山羊");
a->name();
a = af->GetAnimal("小狗");
a->name();
return 0;
}
抽象⼯⼚模式适⽤于⽣产多个⼯⼚系列产品的设计模式,增加新的产品时,需要对原有系统进⾏较⼤的修改,甚⾄需要修改抽象层(例如 Factory)的代码,违背了开闭原则。
建造者模式
建造者模式是⼀种创建型设计模式, 使⽤多个简单的对象⼀步⼀步构建成⼀个复杂的对象(可能会按照一定的顺序才能构建成产品),能够将⼀个复杂的对象的构建与它的表⽰分离,提供⼀种创建对象的最佳⽅式。主要⽤于解决对象的构建过于复杂的问题。
建造者模式主要基于五个类实现:
- 抽象产品类
- 具体产品类:⼀个具体的产品对象类 -- 对抽象产品类做一个具象化的实现
- 抽象 Builder 类:创建⼀个产品对象所需的各个部件的抽象接⼝
- 具体产品 Builder 类:实现抽象接⼝,构建各个部件
- 指挥者 Director 类:统⼀组建过程,提供给调⽤者使⽤,通过指挥者类来构造产品
cpp
#include <iostream>
#include <string>
#include <memory>
// 抽象产品类
class Computer
{
public:
Computer() {}
void setBoard(const std::string &board)
{
_board = board;
}
void setDisplay(const std::string &display)
{
_display = display;
}
void showParamaters()
{
std::string computer = "Computer: \n \n";
computer += "\tboard = " + _board + ",\n";
computer += "\tdisplay = " + _display + ",\n";
computer += "\tOS = " + _os + ",\n \n";
std::cout << computer << std::endl;
}
virtual void setOs() = 0;
// 构造电脑所需要的零部件
protected: // 用 private,public 继承看不见,所以写 protected
std::string _board;
std::string _display;
std::string _os;
};
// 具体产品类
class MacBook : public Computer
{
public:
void setOs() override
{
_os = "MAC OS x12";
}
};
// 抽象 Builder 类
class Builder
{
public:
virtual void buildBoard(const std::string &board) = 0;
virtual void buildDisplay(const std::string &display) = 0;
virtual void buildOs() = 0;
virtual std::shared_ptr<Computer> build() = 0;
};
// 具体产品 Builder 类
class MacBookBuilder : public Builder
{
public:
MacBookBuilder() : _computer(new MacBook()) {}
void buildBoard(const std::string &board)
{
_computer->setBoard(board);
}
void buildDisplay(const std::string &display)
{
_computer->setDisplay(display);
}
void buildOs()
{
_computer->setOs();
}
std::shared_ptr<Computer> build()
{
return _computer;
}
private:
std::shared_ptr<Computer> _computer;
};
// 指挥者 Director 类
class Director
{
public:
Director(Builder* builder): _builder(builder) {}
void construct(const std::string& board, const std::string& display)
{
_builder->buildBoard(board);
_builder->buildDisplay(display);
_builder->buildOs();
}
private:
std::shared_ptr<Builder> _builder;
};
int main()
{
Builder* builder = new MacBookBuilder(); // 构造一个具体产品类
std::unique_ptr<Director> director(new Director(builder)); // 构造一个指挥者类
director->construct("华硕主板", "三星显示器");
std::shared_ptr<Computer> computer = builder->build();
computer->showParamaters();
return 0;
}
代理模式
代理模式指代理控制对其他对象的访问,也就是代理对象控制对原对象的引⽤。在某些情况下,⼀个对象不适合或者不能直接被引⽤访问,⽽代理对象可以在客⼾端和⽬标对象之间起到中介的作⽤。
代理模式的结构包括⼀个是真正的你要访问的对象(⽬标类)、⼀个是代理对象。⽬标对象与代理对象实现同⼀个接⼝(代理对象的接口中会包含目标对象的接口,最终使用的是代理对象的接口,所以说实现同一个接口),先访问代理类再通过代理类访问⽬标对象。代理模式分为静态代理、动态代理:
- 静态代理指的是,在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪个被代理类。-- (讲)
- 动态代理指的是,在运⾏时才动态⽣成代理类,并将其与被代理类绑定。这意味着,在运⾏时才能
确定代理类要代理的是哪个被代理类 -- (涉及很多其他知识,不讲)。
以租房为例,房东将房⼦租出去,但是要租房⼦出去,需要发布招租启⽰,带⼈看房,负责维修,这些⼯作中有些操作并⾮房东能完成,因此房东为了图省事,将房⼦委托给中介进⾏租赁。
代理模式 -- 静态代理:
cpp
#include <iostream>
// 抽象租房类
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 << "负责租后维修\n";
}
private:
Landlord _landlord;
};
int main()
{
Intermediary intermediary;
intermediary.rentHouse(); // 通过中介来出租房子
return 0;
}
日志系统框架设计
日志系统:
作用:将一条消息,进行格式化为指定格式的字符串后,写入到指定位置
- 日志要写入指定位置(标准输出,指定文件, 滚动文件等 -- 可扩展 -- 可以写入到其他扩展位置)
日志系统需要支持将日志消息落地到不同的位置---多落地方向 - 日志写入指定位置,支持不同的写入方式(同步,异步)
同步:业务线程自己负责日志的写入(流程简单,但是有可能会因为阻塞导致效率降低) 异步:业务线程将日志放入内存缓冲区,让其他异步线程负责将日志写入指定位置 - 日志输出以日志器为单位,支持多日志器(不同的项目组有不同的日志输出策略)
本项⽬实现的是⼀个多⽇志器⽇志系统,主要实现的功能是让程序员能够轻松的将程序运⾏⽇志信息落地到指定的位置,且⽀持同步与异步两种⽅式的⽇志落地⽅式。项⽬的框架设计将项⽬分为以下⼏个模块来实现。
模块划分
日志等级模块
枚举出日志分为多少个等级---对不同的日志有不同等级标记--以便于控制输出
- OFF:关闭
- DEBUG:调试,调试时的关键信息输出。
- INFO:提示,普通的提⽰型⽇志信息。
- WARN:警告,不影响正常运⾏(运行不会有错误),但是需要注意⼀下的⽇志。
- ERROR:错误,程序运⾏出现错误,但是还能继续运行。这些错误可能会影响部分功能,但是通常不会导致整个程序崩溃的⽇志,如文件读写失败,数据库连接失败。
- FATAL:致命,⼀般是代码异常导致程序⽆法继续推进运⾏的⽇志,即将崩溃或者已经崩溃。
日志消息模块
封装一条日志所需的各种要素(时间,线程ID,文件名,行号,日志等级,消息主体...)
- 时间:描述本条⽇志的输出时间。
- 线程ID:描述本条⽇志是哪个线程输出的。
- ⽇志等级:描述本条⽇志的等级。
- ⽇志数据:本条⽇志的有效载荷数据。
- ⽇志⽂件名:描述本条⽇志在哪个源码⽂件中输出的。
- ⽇志⾏号:描述本条⽇志在源码⽂件的哪⼀⾏输出的。
日志消息格式化模块
按照指定的格式,对于日志消息中关键要素进行组织,最终得到一个指定格式的字符串
系统默认的输出格式: [%d{%H:%M:%S}]%T[%t]%T[%p]%T[%c]%T%f:%l%T%m%n
[12:38:45] [12345678] [FATAL] [root] main.c:178 套接字创建失败...\n
- %d{%H:%M:%S}:表⽰⽇期时间,花括号中的内容表示日期时间的格式。
- %T:表⽰制表符缩进。
- %t:表⽰线程ID。
- %p:表⽰⽇志级别。
- %c:表⽰⽇志器名称,不同的项目组可以创建⾃⼰的⽇志器进⾏⽇志输出,⼩组之间互不影响。
- %f:表⽰⽇志输出时的源代码⽂件名。
- %l:表⽰⽇志输出时的源代码⾏号。
- %m:表⽰⽇志有效载荷数据 。
- %n:表⽰换行。
- 设计思想:设计不同的⼦类,不同的⼦类从⽇志消息中取出不同的数据进⾏处理。
日志落地模块
负责对日志消息进行指定方向的写入输出
- 标准输出:表⽰将⽇志进⾏标准输出的打印。
- ⽇志⽂件输出:表⽰将⽇志写⼊指定的⽂件末尾。
- 滚动⽂件输出:当前以⽂件⼤⼩进⾏控制,当⼀个⽇志⽂件⼤⼩达到指定⼤⼩,则切换下⼀个⽂件进⾏输出 。
- 后期,也可以扩展远程⽇志输出,创建客⼾端,将⽇志消息发送给远程的⽇志分析服务器。
- 设计思想:设计不同的⼦类,不同的⼦类控制不同的⽇志落地⽅向。
日志器模块
对上面几个模块的整合,⽤⼾通过⽇志器进⾏⽇志的输出,有效降低⽤⼾的使⽤难度;包含有:⽇志落地模块对象,⽇志消息格式化模块对象,⽇志限制输出等级
- 同步日志器模块---完成日志的同步输出功能。
- 异步日志器模块---完成日志的异步输出功能
异步线程模块
负责异步日志的实际落地输出功能
- 实现对⽇志的异步输出功能,⽤⼾只需要将输出⽇志任务放⼊任务池,异步线程负责⽇志的落地输出功能,提供了更加⾼效的⾮阻塞的⽇志输出。
单例的日志器管理模块
对日志进行全局的管理,以便于能够在项目的任何位置获取指定的日志器进行日志输出
- 为了降低项⽬开发的⽇志耦合,不同的项⽬组可以有⾃⼰的⽇志器来控制输出格式以及落地⽅向,因此本项⽬是⼀个多⽇志器的⽇志系统。
- 管理模块就是对创建的所有⽇志器进⾏统⼀管理。并提供⼀个默认⽇志器,提供标准输出的⽇志输出。
模块关系图
代码设计
实用类设计
提前完成一些零碎的功能接口:
- 获取系统时间
- 判断文件是否存在
- 获取文件的所在目录路径
- 创建目录
util.hpp:
cpp
# pragma once
#include <iostream>
#include <ctime>
#include <sys/stat.h>
#include <sys/types.h>
#include <string>
namespace log
{
namespace util
{
class Date
{
public:
//获取系统时间
static size_t now()
{
return (size_t)time(nullptr);
}
};
class File
{
public:
// 判断文件是否存在
static bool exists(const std::string& pathname)
{
// stat 获取文件属性,获取成功,说明文件存在;获取失败,则文件不存在
struct stat st;
if (stat(pathname.c_str(), &st) < 0) return false;
return true;
}
// 获取文件的所在目录路径
static std::string path(const std::string& pathname)
{
// ./abc/a.txt 其实也就是获取最后一个 / 的位置,然后从开头进行截取
size_t pos = pathname.find_last_of("/\\"); // 这个 find 调用在 windows 和 linux 下都可以,linux 下路径分隔符为 /;而 \ 为转义字符
// 要表示一个 \ 需要两个 \,也就是查找 / 和 \ 中的任意一个,linux 下路径不会出现 \ ,所以这里没问题
if (pos == std::string::npos) return "."; // 如果没有找到,那么证明这个文件就在当前的目录
return pathname.substr(0, pos + 1); // 把最后一个 / 也截进去
}
// 创建目录
static void createDirectory(const std::string& pathname)
{
// ./abc/bcd/cde
// pos 是 / 的位置,idx是查找的起始位置
size_t pos = 0, idx = 0;
while (idx < pathname.size())
{
// 查找idx 位置起第一个 /
size_t pos = pathname.find_first_of("/\\", idx);
if (pos == std::string::npos)
{
// 创建文件夹,pathname.c_str()表示路径名,0777表示权限
mkdir(pathname.c_str(), 0755);
return;
}
// 截取从开始到/的目录
std::string parent_dir = pathname.substr(0, pos + 1);
// 目录存在就跳过,找下一个
if (exists(parent_dir) == true)
{
idx = pos + 1;
continue;
}
// 目录不存在,创建目录
mkdir(parent_dir.c_str(), 0777);
idx = pos + 1;
}
}
};
}
}
测试:test.cc
cpp
#include "util.hpp"
int main()
{
std::cout << log::util::Date::now() << std::endl;
std::string pathname = "./abc/bcd/a.txt";
log::util::File::createDirectory(log::util::File::path(pathname));
return 0;
}
日志等级模块设计
-
UNKNOW=0,未知等级的日志
-
DEBUG ,调试等级的日志
-
INFO ,提示等级的日志
-
WARN ,警告等级的日志
-
ERROR ,错误等级的日志
-
FATAL ,致命错误等级的日志
-
OFF,关闭所有⽇志输出
每一个项目中都会设置一个默认的日志输出等级,只有输出的日志等级大于等于默认限制等级的时候才可以进行输出。
提供一个接口,将对应等级的枚举,转换为一个对应的字符串
level.hpp:
cpp
/*
1. 定义枚举类,枚举出日志等级
2. 提供转换接口,将枚举转换为对应的字符串
*/
#pragma once
namespace log
{
class LogLevel
{
public:
enum class value
{
UNKNOW = 0,
DEBUG,
INFO,
WARN,
ERROR,
FATAL,
OFF
};
// 转换接口
static const char* toString(LogLevel::value level)
{
switch (level)
{
case LogLevel::value::DEBUG: return "DEBUG";
case LogLevel::value::INFO: return "INFO";
case LogLevel::value::WARN: return "WARN";
case LogLevel::value::ERROR: return "ERROR";
case LogLevel::value::FATAL: return "FATAL";
case LogLevel::value::OFF: return "OFF";
}
return "UNKOWN";
}
};
}
日志消息类设计
意义:存储一条日志消息所需的各项要素
- 日志的输出时间 -- 用于过滤日志输出时间
- 日志等级 -- 用于进行日志过滤分析
- 源文件名称
- 源代码行号 -- 名称和行号:用于定位出错的代码位置
- 线程ID -- 用于过滤出错的线程
- 日志主体消息
- 日志器名称 -- 支持多日志器的同时使用
message.hpp:
cpp
// 定义日志消息类,进行日志信息的存储
#pragma once
#include"util.hpp"
#include"level.hpp"
#include <iostream>
#include <string>
#include <thread>
namespace log
{
struct LogMsg
{
time_t _ctime; // 日志产生的时间戳
LogLevel::value _level; // 日志等级
size_t _line; // 行号
std::thread::id _tid; // 线程id
std::string _file; // 文件名
std::string _logger; // 日志器名称
std::string _payload; // 有效载荷数据
// 构造函数
LogMsg(LogLevel::value level,
size_t line,
const std::string file,
const std::string logger,
const std::string msg) : _ctime(util::Date::now()),
_level(level),
_line(line),
_tid(std::this_thread::get_id()),
_file(file),
_logger(logger),
_payload(msg)
{}
};
}
日志格式化类设计
意义:对日志消息进行格式化,并且组织成指定格式的字符串。
%d ⽇期
%T 缩进
%t 线程id
%p ⽇志级别
%c ⽇志器名称
%f ⽂件名
%l ⾏号
%m ⽇志消息
%n 换⾏
[2024-07-09 17:04][root][1234567][main.c:99][FATAL]:\t创建套接字失败...\n
格式化字符串控制了日志的输出格式;定义格式化字符,是为了让日志系统进行日志格式化更加的灵活方便。
⽇志格式化(Formatter)类包含:
- 格式化字符串:用户定义的输出格式
- 格式化子项数组:对格式化字符串进行解析,保存了日志信息要素的顺序 -- 不同的格式化子项,会从日志消息中取出指定的元素,转化为字符串。
格式化子项:
- 其他信息(非格式化字符)子项:[
- 日期子项:%H%M%S
- 其他信息子项:]
- 其他信息子项:[
- 文件名子项:main.c
- 其他信息子项::
- 行号信息子项:99
- 其他信息子项:]
- 消息主体子项:吃饭睡觉打豆豆
- 换行子项:\n
[12:40:50][main.c:99]吃饭睡觉打豆豆\n
格式化子项的实现思想:从日志消息中取出指定的元素,追加到一块内存空间中。
设计思想:
-
抽象出一个格式化子项的基类
-
基于基类,派生出不同的格式化子项子类:
主体消息、日志等级、时间子项、文件名、行号、日志器名称、线程ID、制表符、换行、非格式化的原始字符串。
这样就可以在父类中定义父类指针(智能指针)的数组,指向不同的格式化子项子类的对象。
FormatItem类主要负责⽇志消息⼦项的获取及格式化;其包含以下⼦类:
- MsgFormatItem:表⽰要从LogMsg中取出有效⽇志数据
- LevelFormatItem:表⽰要从LogMsg中取出⽇志等级
- NameFormatItem:表⽰要从LogMsg中取出⽇志器名称
- ThreadFormatItem:表⽰要从LogMsg中取出线程ID
- TimeFormatItem:表⽰要从LogMsg中取出时间戳并按照指定格式进⾏格式化
- CFileFormatItem:表⽰要从LogMsg中取出源码所在⽂件名
- CLineFormatItem:表⽰要从LogMsg中取出源码所在⾏号
- TabFormatItem:表⽰⼀个制表符缩进
- NLineFormatItem:表⽰⼀个换⾏
- OtherFormatItem:表⽰⾮格式化的原始字符串
格式化子项类:
cpp
# pragma once
#include "level.hpp"
#include "message.hpp"
#include <memory>
namespace log
{
class FormatItem
{
public:
using ptr = std::shared_ptr<FormatItem>;
virtual void format(std::ostream& cout, const LogMsg &msg) = 0;
};
// 派生格式化子项子类 -- 主体消息、日志等级、时间子项、文件名、行号、日志器名称、线程ID、制表符、换行、 其他
// 主体消息
class MsgFormatItem : public FormatItem
{
public:
void format(std::ostream& out, const LogMsg& msg) override
{
out << msg._payload;
}
};
// 日志等级
class LevelFormatItem : public FormatItem
{
public:
void format(std::ostream& out, const LogMsg& msg) override
{
out << LogLevel::toString(msg._level);
}
};
//时间子项
class TimeFormatItem:public FormatItem{
public:
// 设置时间的默认格式
TimeFormatItem(const std::string &fmt = "%H:%M:%S") : _time_fmt(fmt) {}
//虚函数进行重写
void format(std::ostream& out, const LogMsg& msg) override
{
struct tm t; // 时间结构体
localtime_r(&msg._ctime, &t); // 把时间写入 t 中
char tmp[32] = { 0 };
strftime(tmp, 31, _time_fmt.c_str(), &t); // 把格式化的时间写入 tmp 中
out<<tmp;
}
private:
std::string _time_fmt; // 时间格式
};
// 文件名
class FileFormatItem : public FormatItem
{
public:
// 虚函数进行重写
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._file;
}
};
// 行号
class LineFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._line;
}
};
// 日志器名称
class LoggerFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._logger;
}
};
// 线程ID
class ThreadFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._tid;
}
};
// 制表符
class TabFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << "\t";
}
};
// 换行
class NLineFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << "\n";
}
};
// 其他 -- 输出原始字符串 -- abcdef[%d{...}] -- abcdef[ 就是原始字符串,输出
class OtherFormatItem : public FormatItem
{
public:
OtherFormatItem(const std::string &str) : _str(str) {}
void format(std::ostream &out, const LogMsg &msg) override
{
out << _str;
}
private:
std::string _str;
};
}
日志格式化类:
parsePattern
函数思想:
补充说明:
- 原始字符串包含两种,就是 abcd 这种的直接原始字符串,还有 %% 的原始 % 字符串。原始字符串处理完毕后,遇到 % ,则 % 的下一个位置一定是格式化字符。
- 处理一个格式化字符,会连带着它的子规则一起处理
(nullptr 和 null 就是代表 string 为空 "")
cpp
class Formatter
{
public:
Formatter(const std::string& pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n") : _pattern(pattern)
{
assert(parsePattern()); // 解析必须成功,不能失败
}
// 数据放入 io 流中,io 流进行处理
void format(std::ostream& out, const LogMsg& msg)
{
// 遍历格式化子项数组
for (auto &item : _items)
{
item->format(out, msg); // 从 msg 取信息到 out 中
}
}
// 对 LogMsg 进行格式化,返回一个格式化的字符串
std::string format(const LogMsg& msg)
{
std::stringstream ss;
format(ss, msg); // 调用的是上面的 format 重载,void format(std::ostream& out, LogMsg& msg)
return ss.str();
}
private:
//对格式化规则字符串进行解析
bool parsePattern()
{
// 1. 格式化规则字符串解析
//abcd[%d{%H:%M:%S}][%p]%T%m%n
std::vector<std::pair<std::string, std::string>> fmt_order;
size_t pos = 0;
std::string key, val;
while (pos < _pattern.size())
{
// 处理原始字符串 -- 判断是否是 %,不是一定是原始字符
if (_pattern[pos] != '%')
{
val.push_back(_pattern[pos++]);
continue;
}
// 到这里就代表 pos 位置是 % 字符,看后一个是不是 %,%% 处理为一个原始 % 字符
if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%')
{
val.push_back('%');
pos += 2;
continue;
}
// 到这里,说明 % 后是个格式化字符;这时候原始字符串已经处理完毕了,把 val push 进去
if (val.empty() == false) // 可能出现开头就是格式化字符的情况,所以加判断,如果 val 为空,则不添加 -- 是格式化字符的话,上面不会处理,val 是空的
fmt_order.push_back(std::make_pair("", val));
val.clear();
// 这时候是格式化字符的处理
pos += 1; // pos 原本指向 % 位置,+1指向格式化字符的位置
if (pos == _pattern.size())
{
std::cout << "%之后,没有对应的格式化字符!\n";
return false;
}
key = _pattern[pos]; // %d,写入的就是 d,因为上面 pos +1 了
pos += 1; // 再往后走一个,看是否有 {,这时候 pos 指向格式化字符后的位置
bool error_flag = false;
if (pos < _pattern.size() && _pattern[pos] == '{')
{
// 处理子串
pos += 1; // 这时候 pos 指向子规则的起始位置,即 { 的下一个位置
while (pos < _pattern.size() && _pattern[pos] != '}')
{
val.push_back(_pattern[pos++]); // 把子项逐字符放入,带 %
}
if (pos == _pattern.size()) // 走到末尾跳出循环,则代表没有找到 },代表格式错误
{
std::cout << "子规则 {} 匹配出错!\n";
return false;
}
pos += 1; // 这时候 pos 指向 } 位置,往后走一步,就走到下次处理的新位置
}
fmt_order.push_back(std::make_pair(key, val));
key.clear();
val.clear();
}
// 2. 根据解析得到的数据初始化格式化子项数组
for (auto& it : fmt_order)
{
_items.push_back(createItem(it.first, it.second));
}
return true;
}
// key -- 格式化字符(关键字),val -- 关键字对应值
// 例如 %d 对应的子串就是 %H:%M:%S,'[' 则没有 key,就只有 value('[' 本身),这时返回的对象,就用自己填充
// 根据不同的格式化字符创建不同的格式化子项对象
FormatItem::ptr createItem(const std::string& key, const std::string& value)
{
if (key == "d")
return std::make_shared<TimeFormatItem>(value);
if (key == "t")
return std::make_shared<ThreadFormatItem>();
if (key == "c")
return std::make_shared<LoggerFormatItem>();
if (key == "f")
return std::make_shared<FileFormatItem>();
if (key == "l")
return std::make_shared<LineFormatItem>();
if (key == "p")
return std::make_shared<LevelFormatItem>();
if (key == "T")
return std::make_shared<TabFormatItem>();
if (key == "m")
return std::make_shared<MsgFormatItem>();
if (key == "n")
return std::make_shared<NLineFormatItem>();
if (key.empty())
return std::make_shared<OtherFormatItem>(value);
std::cout << "没有对应的格式化字符串:%" << key << std::endl;
abort();
return FormatItem::ptr();
}
private:
std::string _pattern; // 格式化规则字符串
std::vector<FormatItem::ptr> _items; // 格式化子项数组,元素是智能指针
};
测试代码:
cpp
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
int main()
{
log::LogMsg msg(log::LogLevel::value::INFO, 53, "main.c", "root", "格式化功能测试...");
log::Formatter fmt;
// 边缘情况测试
// log::Formatter fmt("abc%%ab%g%g%gc[%d{%H:%M:%S}] %m%n"); // 没有 %g 格式字符
// log::Formatter fmt("abc%%abc[%d{%H:%M:%S}]%m%n");// 测试 %%
// log::Formatter fmt("abc%%abc[%d{%H:%M:%S}]%m%n{"); // 测试花括号
// log::Formatter fmt("abc%%abc[%d{%H:%M:%S}]%m%"); // 末尾只带 %,没有格式化字符
std::string str = fmt.format(msg);
std::cout << str << std::endl;
return 0;
}
日志落地类设计 -- 简单工厂模式
功能:将格式化后的日志消息字符串,输出到指定的位置
扩展:支持同时将日志落地到不同的位置
位置分类:
- 标准输出 -- 不常用,测试的时候用
- 指定文件(事后进行日志分析)
- 滚动文件(文件按时间/大小进行滚动切换)
扩展:支持落地方向的扩展
用户可以自己编写一个新的落地模块,将日志进行其他方向的落地
实现思想:
- 抽象出落地模块类
- 不同落地方向从基类进行派生(使用基类指针,指向子类对象,就可以调用子类对象的接口进行扩展)
- 使用工厂模式进行创建与表示的分离
框架:
cpp
amespace log
{
class LogSink
{
public:
using ptr = std::shared_ptr<LogSink>;
LogSink() {}
virtual ~LogSink() {}
virtual void log(const char* data, size_t len) = 0;
};
// 落地方向:标准输出
class StdoutSink : public LogSink
{
public:
// 将日志消息写入到标准输出
void log(const char* data, size_t len)
{}
};
// 落地方向:指定文件
class FileSink : public LogSink
{
public:
// 构造时传入文件名,并打开文件,将操作句柄 -- _ofs 管理起来
FileSink(const std::string& pathname);
void log(const char* data, size_t len)
{}
private:
std::string _filename;
std::ofstream _ofs; // 输出文件的操作句柄
};
// 落地方向:滚动文件(以大小滚动)
class RollBySizeSink
{
public:
RollBySizeSink(const std::string &basename, size_t max_size);
//将日志消息写入到文件,写入前判断文件大小,超过了最大大小,就要切换文件
void log(const char* data, size_t len)
{}
private:
// 进行大小判断,超过指定大小则创建新文件
void createNewFile();
private:
// 基础文件名 + 扩展文件名(以时间生成 -- 直观,好分析) --》 组成一个实际的当前输出文件名
std::string _basename; // ./log/base- --> ./log/base-20020809132356.log
std::ofstream _ofs;
size_t _max_fsize; // 记录最大大小,当前文件超过了这个大小就要切换文件
size_t _cur_size; // 记录当前文件已经写入的大小
};
// 日志落地的工厂
class SinkFackory
{};
}
code:
cpp
/*日志落地模块的实现
1.抽象落地基类
2.派生子类(根据不同的落地方向进行派生)
3.使用工厂模式进行创建与表示分离
*/
#pragma once
#include "util.hpp"
#include <iostream>
#include <memory>
#include <fstream>
#include <sstream>
#include <cassert>
namespace log
{
class LogSink
{
public:
using ptr = std::shared_ptr<LogSink>;
LogSink() {}
virtual ~LogSink() {}
virtual void log(const char* data, size_t len) = 0;
};
// 落地方向:标准输出
class StdoutSink : public LogSink
{
public:
// 将日志消息写入到标准输出
void log(const char* data, size_t len)
{
// 因为日志输出不一定是字符串,cout 不能指定大小,字符串有 \0 结尾,别的没有
// 因此需要调用write接口,从data位置开始写,写入len长度的数据
std::cout.write(data, len);
}
};
// 落地方向:指定文件
class FileSink : public LogSink
{
public:
// 构造时传入文件名,并打开文件,将操作句柄 -- _ofs 管理起来
FileSink(const std::string& pathname) : _pathname(pathname)
{
// 1. 创建日志文件所在的目录 -- 文件路径可能不存在,以防万一先创建目录
util::File::createDirectory(util::File::path(pathname));
// 2. 创建并打开日志文件
_ofs.open(_pathname, std::ios::binary | std::ios::app); // 二进制可写可追加权限
assert(_ofs.is_open());
}
void log(const char* data, size_t len)
{
_ofs.write(data, len);
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), _name_count(0)
{
std::string pathname = createNewFile();
util::File::createDirectory(util::File::path(pathname));
_ofs.open(pathname, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
//将日志消息写入到文件,写入前判断文件大小,超过了最大大小,就要切换文件
void log(const char* data, size_t len)
{
if (_cur_fsize >= _max_fsize)
{
_ofs.close(); // 切换文件前,关闭文件,防止内存泄漏
std::string pathname = createNewFile();
_ofs.open(pathname, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
_cur_fsize = 0;
}
_ofs.write(data, len);
assert(_ofs.good());
_cur_fsize += len;
}
private:
std::string createNewFile()
{
// 获取系统时间,以时间来构造文件扩展名
time_t t = util::Date::now();
struct tm lt;
localtime_r(&t, <);
std::stringstream filename;
// 从 1900 年 1.1 开始的,所以对应的加上
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; // 用于区别文件名的 -- 因为创建文件可能很快,名字可能会一样
std::string _basename; // ./log/base- --> ./log/base-20020809132356.log
std::ofstream _ofs;
size_t _max_fsize; // 记录最大大小,当前文件超过了这个大小就要切换文件
size_t _cur_fsize; // 记录当前文件已经写入的大小
};
// 日志落地的工厂
//SinkType通过模板参数,可以生产我们需要的落地方式,因为不同落地方式构造的参数不一样,所以需要用到不定参
class SinkFactory
{
public:
template<typename SinkType,typename ...Args>
static LogSink::ptr create(Args && ...args)
{
return std::make_shared<SinkType>(std::forward<Args>(args)...);
}
};
}
测试代码:
cpp
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"
int main()
{
// 日志落地模块的测试
log::LogMsg msg(log::LogLevel::value::INFO, 53, "main.c", "root", "格式化功能测试...");
log::Formatter fmt;
std::string str = fmt.format(msg);
// 设置落地方向
log::LogSink::ptr stdout_lsp = log::SinkFactory::create<log::StdoutSink>(); // 标准输出落地
log::LogSink::ptr file_lsp = log::SinkFactory::create<log::FileSink>("./logfile/test.log"); // 文件落地方式
log::LogSink::ptr roll_lsp = log::SinkFactory::create<log::RollBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式,大小为 1 兆
// 通过指针去控制打印的日志
stdout_lsp->log(str.c_str(), str.size());
file_lsp->log(str.c_str(), str.size());
size_t cursize = 0;
size_t count = 0;
// 用滚动文件的方法希望生产10个文件,一个文件 1 兆
while (cursize < 1024 * 1024 * 10)
{
std::string tmp = std::to_string(count++) + str; // 给日志标号
roll_lsp->log(tmp.c_str(), tmp.size());
cursize += tmp.size();
}
return 0;
}
结果:
扩展一个以时间作为日志文件滚动切换类型的日志落地模块:
在 test.cc 中写的代码:
cpp
/*扩展一个以时间作为日志文件滚动切换类型的日志落地模块
1.以时间进行文件滚动,实际上是以时间段进行滚动
实现思想:以当前系统时间,取模获得时间段大小,可以得到当前时间段是第几个时间段
time(nullptr)%gap;
每次以当前系统时间取模,判断与当前文件的时间段是否一致,不一致代表不是同一个时间段
*/
// 使用枚举来确定时间段的大小
enum class TimeGap
{
GAP_SECOND,
GAP_MINUTE,
GAP_HOUR,
GAP_DAY,
};
class RollByTimeSink : public log::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 = _gap_size==1?log::util::Date::now():log::util::Date::now() / _gap_size; // 获取当前是第几个时间段;前面是 _gap_size == 1 的情况
// 创建文件
std::string filename = createNewFile();
log::util::File::createDirectory(log::util::File::path(filename));
_ofs.open(filename, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
// 将日志消息写入到标准输出,判断当前时间是否是当前文件的时间段,不是就要切换文件。
void log(const char *Date, size_t len)
{
time_t cur = log::util::Date::now(); // 获取当前系统时间,时间戳
if ((cur / _gap_size) != _cur_gap)//(每次写日志时判断当前的时间段与上次的时间段是否是一致得,一致的话就写入,不一致就创建新文件)
{
_ofs.close(); // 打开文件,就必须关闭文件(这里关闭以前的文件)
std::string pathname = createNewFile(); // 创建新文件
_cur_gap = _gap_size==1?log::util::Date::now():log::util::Date::now() / _gap_size; // 获取当前是第几个时间段
_ofs.open(pathname, std::ios::binary | std::ios::app); // 打开文件 二进制可写可追加权限
assert(_ofs.is_open()); // 打开失败就报错
}
_ofs.write(Date, len);
assert(_ofs.good()); // 检测文件流状态和文件读写过程是否正常
}
protected:
// 创建一个新文件,不需要用户去创建,所有我们把权限设置为私有
std::string createNewFile()
{
// 获取系统时间,以时间来构造文件名的扩展名
time_t t = log::util::Date::now();
struct tm lt;
localtime_r(&t, <);
std::stringstream filename;
filename << _basename;
filename << lt.tm_year + 1900;
filename << lt.tm_mon + 1;
filename << lt.tm_mday;
filename << lt.tm_hour;
filename << lt.tm_min;
filename << lt.tm_sec;
filename << ".log";
return filename.str();
}
private:
std::string _basename; // 基本文件名
std::ofstream _ofs; // 会默认以写的方式打开文件
size_t _cur_gap; // 当前是第几个时间段
size_t _gap_size; // 时间段的大小
};
三秒,写三个文件:
日志器模块设计 -- 涉及建造者模式
功能:对前面所有功能进行整合,向外提供接口完成不同等级日志的输出。
管理的成员:
- 格式化模块对象
- 落地模块对象
- 默认的日志输出限制等级(大于等于限制输出等级的日志才能输出)
- 互斥锁(保证日志输出过程的线程安全,不会出现交叉日志)
- 日志名称(日志器的唯一标识,方便查找)
提供的操作:
- debug等级日志的输出操作(分别封装日志消息LogMsg------各个接口日志等级不同)
- info等级日志的输出操作
- warn等级日志的输出操作
- error等级日志的输出操作
- fatal等级日志的输出操作
实现:
- 实现Logger基类(派生出同步日志器和异步日志器)
- 因为两种日志器的落地方式不同,需要将落地操作给抽象出来,不同的日志器调用不同的落地操作进行日志落地
- 模块关联过程中使用基类指针对子类日志器对象进行日志管理和操作
当前日志系统支持同步日志&异步日志,它们的不同点在于日志的落地方式上不同:
- 同步日志器:直接对日志消息进行输出
- 异步日志器:先将日志消息放到缓冲区,然后异步线程进行输出
因此 :日志器类在设计的时候,先要设计一个Logger的基类,在Logger基类的基础上,继承出同步日志器(SyncLogger)和异步日志器(AsyncLoggrr)。
同步日志器
框架:
cpp
/*完成日志器模块
1. 抽象日志器基类
2. 派生出不同的子类(同步日志器嘞 & 异步日志器类)
*/
#pragma once
#include "util.hpp"
#include "level.hpp"
#include "format.hpp"
#include "sink.hpp"
#include <thread>
#include <atomic>
#include <mutex>
namespace log
{
class Logger
{
public:
using ptr = std::shared_ptr<Logger>;
void debug(const std::string& file, size_t line, const std::string& fmt, ...);
void info(const std::string& file, size_t line, const std::string& fmt, ...);
void warn(const std::string& file, size_t line, const std::string& fmt, ...);
void error(const std::string& file, size_t line, const std::string& fmt, ...);
void fatal(const std::string& file, size_t line, const std::string& fmt, ...);
protected:
// 抽象接口完成实际的落地输出 -- 不同日志器会有不同的实际落地方式
virtual void log(const char* data, size_t len) = 0;
private:
std::mutex _mutex; // 互斥锁
std::string _logger_name; // 日志器名称
std::atomic<LogLevel::value> _limit_level; // 限制日志等级, atomic原子操作
Formatter::ptr _formatter; // 格式化模块对象
std::vector<LogSink::ptr> _sinks; // 用一个数组来存放日志落地方向 -- 一个日志器中可能有多个日志落地方向
};
class SynLogger : public Logger
{
protected:
void log(const char* data, size_t len);
};
}
Logger.hpp:
cpp
/*完成日志器模块
1. 抽象日志器基类
2. 派生出不同的子类(同步日志器嘞 & 异步日志器类)
*/
#pragma once
#include "util.hpp"
#include "level.hpp"
#include "sink.hpp"
#include "format.hpp"
#include <memory>
#include <mutex>
#include <atomic>
#include <cstdarg>
namespace log
{
// 设计日志器基类
class Logger
{
// 公有
public:
// 基类指针,用来控制继承子类的对象
using ptr = std::shared_ptr<Logger>;
// 构造函数
Logger(const std::string &logger_name, LogLevel::value level, Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks) : _logger_name(logger_name), _limit_level(level), _formatter(formatter), _sliks(sinks.begin(), sinks.end()) {}
// 操作方法
//获取日志器名称
const std::string& name(){
return _logger_name;
}
// 构造日志消息对象并进行格式化,得到格式化后的日志消息字符串--然后进行落地输出,5个等级
void debug(const std::string &file, size_t line, const std::string &fmt, ...)
{ // 日志的输出操作
// 1.判断当前的日志是否达到输出等级,没达到等级就 return
if (LogLevel::value::DEBUG < _limit_level)
{
return;
}
// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf failed!!\n";
return;
}
va_end(ap);
// 代码一样,可以封装成一个函数
serialize(LogLevel::value::DEBUG, file, line, res);
free(res); // 将指针释放掉,防止内存泄漏
}
void info(const std::string &file, size_t line, const std::string &fmt, ...)
{ // 日志的输出操作
// 1.判断当前的日志是否达到输出等级
if (LogLevel::value::INFO < _limit_level)
{
return;
} // 没有达到输出等级
// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf failed!!\n";
return;
}
va_end(ap); // 将ap指针置空
// 代码一样,可以封装成一个函数
serialize(LogLevel::value::INFO, file, line, res);
free(res); // 将指针释放掉
}
void warn(const std::string &file, size_t line, const std::string &fmt, ...)
{ // 日志的输出操作
// 1.判断当前的日志是否达到输出等级
if (LogLevel::value::WARN < _limit_level)
{
return;
} // 没有达到输出等级
// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf failed!!\n";
return;
}
va_end(ap); // 将ap指针置空
// 代码一样,可以封装成一个函数
serialize(LogLevel::value::WARN, file, line, res);
free(res); // 将指针释放掉
}
void error(const std::string &file, size_t line, const std::string &fmt, ...)
{ // 日志的输出操作
// 1.判断当前的日志是否达到输出等级
if (LogLevel::value::ERROR < _limit_level)
{
return;
} // 没有达到输出等级
// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf failed!!\n";
return;
}
va_end(ap); // 将ap指针置空
// 代码一样,可以封装成一个函数
serialize(LogLevel::value::ERROR, file, line, res);
free(res); // 将指针释放掉
}
void fatal(const std::string &file, size_t line, const std::string &fmt, ...)
{ // 日志的输出操作
// 1.判断当前的日志是否达到输出等级
if (LogLevel::value::FATAL < _limit_level)
{
return;
} // 没有达到输出等级
// 2.对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf failed!!\n";
return;
}
va_end(ap); // 将ap指针置空
// 代码一样,可以封装成一个函数
serialize(LogLevel::value::FATAL, file, line, res);
free(res); // 将指针释放掉
}
void serialize(LogLevel::value level, const std::string &file, size_t line, char *str)
{
// 3.构造LogMsg对象
LogMsg msg(level, line, file, _logger_name, str); // 传入等级、行号、文件、日志器、有效信息
// 4.通过格式化工具对LogMsg进行格式化,得到格式化后的日志字符串
std::stringstream ss;
_formatter->format(ss, msg);
// 5.进行日志落地
log(ss.str().c_str(), ss.str().size()); // 日志字符串和长度,调用派生类的 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; // 限制日志等级,atomic原子操作的意思是该操作执行过程中不能被中断,该操作要么不执行,要么全部执行,不存在执行一部分的情况。
Formatter::ptr _formatter; // 控制格式化模块的对象
std::vector<LogSink::ptr> _sliks; // 这是一个数组,数组里存放日志落地方式的对象
};
// 派生出同步日志器
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 (_sliks.empty())
{
return;
}
// 不是空
for (auto &sink : _sliks)
{
sink->log(data, len); // 调用 sink 中的 log
}
}
};
}
测试代码:
cpp
// 测试代码
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "logger.hpp"
#include <unistd.h>
int main()
{
// 日志器模块:同步日志器
std::string logger_name = "sync_logger";
log::LogLevel::value limit = log::LogLevel::value::WARN;
log::Formatter::ptr fmt(new log::Formatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"));
log::LogSink::ptr stdout_lsp = log::SinkFactory::create<log::StdoutSink>(); // 标准输出落地
log::LogSink::ptr file_lsp = log::SinkFactory::create<log::FileSink>("./logfile/test.log"); // 文件落地方式
log::LogSink::ptr roll_lsp = log::SinkFactory::create<log::RollBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式
std::vector<log::LogSink::ptr> sinks = {stdout_lsp, file_lsp, roll_lsp};
log::Logger::ptr logger(new 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;
size_t count = 0;
while (cursize < 1024 * 1024 * 10)
{
logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
cursize += 20;
}
return 0;
}
日志器建造者类:
框架:
cpp
// 使用建造者模式来建造日志器,而不要让用户直接去构造日志器,简化用户的使用成本
// 1. 抽象一个日志器建造者类
// 1 - 设置日志器类型
// 2 - 将不同类型日志器的创建都放到同一个日志器建造者类中完成
enum class LoggerType
{
LOGGER_SYNC, // 同步日志器
LOGGER_ASYNC // 异步日志器
};
class LoggerBuilder
{
public:
void buildLoggerType(LoggerType type);
void buildLoggerName(std::string &name);
void buildLoggerLevel(LogLevel::value level);
// 构造一个格式化器
void buildLoggerFormatter(const std::string &pattern);
// 一个日志器可以有多个不同的落地方式
template <typename SinkType, typename... Args>
void buildSink(Args &&...arg);
// 完成我们的日志器构建
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;
};
code:
cpp
enum class 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(std::string &name)
{
_logger_name = name;
}
void buildLoggerLevel(LogLevel::value level)
{
_limit_level = level;
}
// 构造一个格式化器
void buildLoggerFormatter(const std::string &pattern)
{
_formatter = std::make_shared<Formatter>(pattern);
}
// 一个日志器可以有多个不同的落地方式
template <typename SinkType, typename... Args>
void buildSink(Args &&...arg)
{
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);
}
};
}
我们这个建造者模式没有指挥者。因为我们构造对象的零部件没有顺序的要求,只管构造就可以了,所有只要建造者。
测试代码:
cpp
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "logger.hpp"
#include <unistd.h>
int main()
{
//同步日志器建造者模式的测试
//先要构造一个建造者出来
std::unique_ptr<log::LoggerBuilder> builder(new log::LocalLoggerBuilder());
//建造者构建零部件
builder->buildLoggerType(log::LoggerType::LOGGER_SYNC);
builder->buildLoggerName("sync_logger");
builder->buildLoggerLevel(log::LogLevel::value::WARN);
builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");
builder->buildSink<log::StdoutSink>(); // 标准输出落地
builder->buildSink<log::FileSink>("./logfile/test.log"); // 文件落地方式
builder->buildSink<log::RollBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式
//零部件构建好后,用建造者建筑对象
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;
size_t count = 0;
while (cursize < 1024 * 1024 * 10)
{
logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
cursize += 20;
}
return 0;
}
异步日志器
思想:为了避免写日志的过程中阻塞,导致影响业务线程的执行效率。异步的思想就是不让业务线程进行日志的实际落地,而是将日志消息放到缓冲区(一块指定的内存)中接下来有一个专门的异步线程,去针对缓冲区中的数据进行处理(实际的落地操作)
实现:
- 实现一个线程安全的缓冲区
- 创建一个异步工作线程,专门用来负责缓冲区中日志信息的落地操作。
缓冲区详情设计:
1.使用队列缓存日志消息,逐条处理
- 要求:不能涉及空间的频繁申请与释放,否则会降低效率。
- 结果:设计一个环形队列(提前将空间申请好,然后对空间循环利用)
- 存在问题:这个缓冲区的操作会涉及到多线程,因此缓冲区的操作必须保证线程安全。
- 线程安全实现:对于缓冲区的读写加锁
- 因此写日志操作,在实际开发中,不好分配太多资源,工作线程只需要一个日志器就行
- 涉及到的锁冲突:生产者与生产者之间的互斥&生产者与消费者的互斥。
- 问题:锁冲突较为严重,所有线程之间都存在互斥关系
- 解决方案:双缓冲区
两个缓冲区,写入和处理:
固定时间,对两个缓冲区做交换。
单个缓冲区设计
设计一个缓冲区:直接存放格式化后的日志消息字符串,而不是放的 LogMsg 都西昂
好处:
- 减少了LogMsg对象频繁的构造的消耗
- 可以针对缓冲区中的日志消息,一次性进行IO操作,减少IO次数,提高效率
缓冲区类的设计:
1.管理一个存放字符串数据的缓冲区(使用vecotor进行空间管理)
2.当前的写入数据位置的指针(指向可写区域的起始位置,避免数据的写入覆盖)
3.当前的读取数据位置的指针(指向可读数据区域的起始位置,当读取指针与写入指针指向相同位置
提供的操作:
1.向缓冲区写入数据
2.获取可读数据起始地址的接口
3.获取可读数据长度的接口
4.移动读写位置的接口
5.初始化缓冲区的操作(将读写位置初始化------将一个缓冲区所有数据处理完毕之后)
6.提供交换缓冲区的接口(交换空间地址,并不交换空间数据)
框架:
cpp
/*实现异步日志缓冲区*/
#include "util.hpp"
#include <vector>
namespace logslearn
{
// 定义宏,表示缓冲区的大小
#define DEFAULT_BUFFER_SIZE (1 * 1024 * 1024)
#define THRESHOLD_BUFFER_SIZE (8 * 1024 * 1024)
#define INCREMENT_BUFFER_SIZE (1 * 1024 * 1024)
// 异步缓冲区
class Buffer
{
public:
// 构造函数
Buffer() {}
// 1.向缓冲区写入数据
void push(const char *data, size_t len);
// 2.返回可读数据起始地址的接口
const char *begin();
// 3.返回可读数据的长度的接口;返回可写数据的长度的接口
size_t readAbleSize();
size_t writeAbleSize();
// 4.移动读写指针进行向后偏移的接口
void moveWriter(size_t len);
void moveReader(size_t len);
// 5.重置读写位置,初始化缓冲区的操作
void reset();
// 6.交换缓冲区的接口
void swap( Buffer &buffer);
// 判断缓冲区是否为空
bool empty();
private:
// 1.存放字符串数据的缓冲区
std::vector<char> _buffer;
// 2.当前可写数据的指针--本质是下标
size_t _reader_idx;
// 3.当前可读数据的指针
size_t _writer_idx;
};
}
buffer.hpp:
cpp
// 实现异步日志缓冲区
#pragma once
#include "util.hpp"
#include <vector>
#include <cassert>
namespace log
{
#define DEFAULT_BUFFER_SIZE (10 * 1024 * 1024)
#define THRESHOLD_BUFFER_SIZE (80 * 1024 * 1024) // 阈值大小,小于阈值翻倍增长,达到阈值线性增长
#define INCREMENT_BUFFER_SIZE (1 * 1024 * 1024)
class Buffer
{
public:
Buffer() : _buffer(DEFAULT_BUFFER_SIZE), _reader_idx(0), _writer_idx(0) {}
// 向缓冲区写入数据,容量不够就扩容(两种方式,极限测试的时候使用扩容,实际使用过程中固定空间大小,空间不够阻塞)
void push(const char* data, size_t len)
{
// 缓冲区剩余空间不够的情况下:扩容。
// // 1.固定大小,直接返回
// if (len > writeAbleSize())
// return;
// 2.动态空间,用于极限测试--扩容
ensureEnoughSize(len);
// 将数据拷贝到缓冲区
std::copy(data, data + len, &_buffer[_writer_idx]);
// 将写入位置向后偏移
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);
}
// 移动读写指针进行向后偏移的接口
void moveWriter(size_t len)
{
assert((len + _writer_idx) <= _buffer.size());
_writer_idx += len;
}
void moveReader(size_t len)
{
assert(len <= readAbleSize());
_reader_idx += len;
}
// 重置读写位置,初始化缓冲区
void reset()
{
// 读写为 0
_writer_idx = 0; // 缓冲区所有空间都是空闲的
_reader_idx = 0; //_reader_idx与_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);
}
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() * 2;
}
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;
};
}
测试代码:
测试思想:读取文件数据,一点一点写入缓冲区,最终将缓冲区数据写入文件,判断生成的新文件是否和读取的文件一致
code:
cpp
int main()
{
// 异步日志器缓冲区测试
// 读取文件数据,一点一点写入缓冲区,最终将缓冲区数据写入文件,判断生成的新文件与源文件是否一致
std::ifstream ifs("./logfile/test.log", std::ios::binary); // 打开一个文件
if (ifs.is_open() == false)
{
return -1;
} // 文件打开失败返回-1
// 让读写位置跳转到末尾
ifs.seekg(0, std::ios::end);
// 获取当前读写位置相对于起始位置的偏移量
size_t fsize = ifs.tellg();
// 重新让指针跳转到起始位置
ifs.seekg(0, std::ios::beg);
std::string body;
body.resize(fsize);
ifs.read(&body[0], fsize);
if (ifs.good() == false)
{
std::cout << "read error!\n";
return -1;
}
// 打开文件,也要关闭
ifs.close();
std::cout << fsize << std::endl; // 读取文件的数据大小
log::Buffer buffer; // 定义一个缓冲区
for (int i = 0; i < body.size(); i++)
{
buffer.push(&body[i], 1);
}
std::cout << buffer.readAbleSize() << std::endl; // buffer里面可读的数据大小
std::ofstream ofs("./logfile/tem.log", std::ios::binary);
size_t rsize = buffer.readAbleSize();
for (int i = 0; i < rsize; i++)
{
ofs.write(buffer.begin(), 1);
if (ofs.good() == false)
{
std::cout << "writer error!\n";
return -1;
}
buffer.moveReader(1);
}
ofs.close(); // 关闭文件
return 0;
}
md5sum 可以验证文件的完整性,tem.log和test.log文件的md5值一样,说明文件内容一模一样。
异步工作器的设计 -- 双缓冲区思想
异步工作器:
异步工作使用双缓冲区的思想
- 外界将任务数据,添加到输入缓冲区中
- 异步线程对处理缓冲区中的数据进行处理,若处理缓冲区中没有数据了则交换缓冲区
管理的成员:
1.双缓冲区(生产,消费)
2.互斥锁 -- 保证线程安全
3.条件变量-生产&消费(生产缓冲区没有数据,处理完消费缓冲区数据后就休眠)
4.回调函数(针对缓冲区中数据的处理接口-外界传入一个函数,告诉异步工作器数据该如何处理)
-
提供的操作:
a.停止异步工作器
b.添加数据到缓冲区
-
私有操作:
创建线程,线程入口函数中,交换缓冲区,对消费缓冲区数据使用回调函数进行处理,处理完后再次交换
框架:
cpp
/*
实现异步工作器
*/
#pragma once
#include "buffer.hpp" //缓冲区
#include <thread> //线程库
#include <mutex> //互斥锁
#include <condition_variable> //条件变量
#include <functional> //包装器
#include <atomic> //原子类型
namespace logslearn
{
// 异步工作器类
using Functor = std::function<void(Buffer &)>;
class AsyncLooper
{
public:
using ptr = std::shared_ptr<AsyncLooper>;
AsyncLooper();
void stop();
void push(const char *data, size_t len);
private:
void threadEntry(); // 线程入口函数
private:
Functor _callBack; // 具体对缓冲区数据进行处理的回调函数,由异步工作器使用者传入。
private:
std::atomic<bool> _stop; // 让工作器停止标准变成原子性操作,提高了线程安全
Buffer _pro_buf; // 生产缓冲区
Buffer _con_buf; // 消费缓冲区
std::mutex _mutex; // 互斥锁
std::condition_variable _cond_pro; // 两个pcb的等待队列,这是生产者,等待队列的条件变量
std::condition_variable _cond_con; // 这是消费者,等待队列的条件变量
std::thread _thread; // 异步工作器对应的工作线程
};
}
looper.hpp:
cpp
/*
实现异步工作器
*/
#pragma once
#include "buffer.hpp" //缓冲区
#include <thread> //线程库
#include <mutex> //互斥锁
#include <condition_variable> //条件变量
#include <functional> //包装器
#include <atomic> //原子类型
namespace log
{
// 异步工作器类
using Functor = std::function<void(Buffer &)>;
enum class AsyncType
{
ASYNC_SAFE, // 安全状态,表示缓冲区满了就阻塞,避免了资源耗尽的风险
ASUNC_UNSAFE // 非安全状态,不考虑资源耗尽的情况,可以无限扩容,常用与测试
};
class AsyncLooper
{
public:
using ptr = std::shared_ptr<AsyncLooper>;
AsyncLooper(const Functor &cb, AsyncType looper_type = AsyncType::ASUNC_UNSAFE)
: _looper_type(looper_type), _stop(false), _thread(std::thread(&AsyncLooper::threadEntry, this)),
_callBack(cb) {}
~AsyncLooper() { stop(); }
void stop()
{
_stop = true;
_cond_con.notify_all(); // 唤醒所有的工作线程
_thread.join(); // 等待工作线程退出
}
void push(const char *data, size_t len)
{
// 1.无限扩容-非安全(极限压力测试的情况下使用)2.固定大小
std::unique_lock<std::mutex> lock(_mutex);
// 条件变量空值,若缓冲区剩余空间大小大于数据长度,就可以添加数据
// 如果是安全状态就把这个代码加上,非安全状态就把这个代码屏蔽
if (_looper_type == AsyncType::ASYNC_SAFE)
_cond_pro.wait(lock, [&]()
{ return _pro_buf.writeAbleSize() >= len; }); // 被唤醒之后,如果缓冲区有剩余空间,则添加数据;否则继续休眠(lambda表达式结果为假)
// 能够走下来代表满足了条件,可以向缓冲区添加数据
_pro_buf.push(data, len);
// 唤醒一个消费者对缓冲区中的数据进行处理
_cond_con.notify_one();
}
private:
void threadEntry() // 线程入口函数
{
while (!_stop)
{
// 要为互斥锁设置一个生命周期,将缓冲区交换完毕后就解锁(不对数据的处理过程加锁保护)
{
// 1.判断生产缓冲区里有没有数据,有则交换,无则阻塞
std::unique_lock<std::mutex> lock(_mutex);
//退出标志被设置,且生产缓冲区无数据,这时候在退出,否则有可能会造成生产缓冲区有数据,但是没有被完全处理
if (_stop && _pro_buf.empty()) break;
// 若退出前被唤醒,或者有数据被唤醒,则返回真,继续向下运行,否则重新陷入休眠
_cond_con.wait(lock, [&]()
{ return !_pro_buf.empty() || _stop; }); //_stop是真表示程序退出,把剩余的数据进行交换
// 等待完毕,消费者与生产者进行地址交换
_con_buf.swap(_pro_buf);
// 2.唤醒生产者
if (_looper_type == AsyncType::ASYNC_SAFE)
_cond_pro.notify_all();
}
// 3.被唤醒后,对消费缓冲区进行数据处理
_callBack(_con_buf);
// 4.初始化消费缓冲区
_con_buf.reset();
}
}
private:
Functor _callBack; // 具体对缓冲区数据进行处理的回调函数,由异步工作器使用者传入。
private:
AsyncType _looper_type; // 默认是安全模式
std::atomic<bool> _stop; // 让工作器停止标志变成原子性操作,提高了线程安全
Buffer _pro_buf; // 生产缓冲区
Buffer _con_buf; // 消费缓冲区
std::mutex _mutex; // 互斥锁
std::condition_variable _cond_pro; // 生产者,等待队列的条件变量
std::condition_variable _cond_con; // 消费者,等待队列的条件变量
std::thread _thread; // 异步工作器对应的工作线程
};
}
异步日志器设计
1.继承于Logger日志器类 对于写日志操作进行函数重写(不再将数据写入文件,而是通过异步消息处理器,放到缓冲区中)
2.通过异步消息处理器,进行日志数据的实际落地
管理成员:
异步工作器(异步消息处理器)
完成后,完善日志器建造者,进行异步日志器安全模式的选择,提供异步日志器的创建
框架:
cpp
//派生出异步日志器
class AsyncLogger:public Logger{
public:
AsyncLogger( const std::string &logger_name, LogLevel::value level, Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks,AsyncType looper_type) :
Logger(logger_name, level, formatter, sinks) {}
//将数据写入缓冲区
void log(const char*data,size_t len);
//设计一个实际落地函数(将缓冲区里的数据进行落地)
void realLog(Buffer &buf);
private:
AsyncLooper::ptr _looper;
};
完善日志器建造者功能:
cpp
// 使用建造者模式来建造日志器,而不要让用户直接去构造日志器,简化用户的使用成本
// 1. 抽象一个日志器建造者类
// 1 - 设置日志器类型
// 2 - 将不同类型日志器的创建都放到同一个日志器建造者类中完成
enum class LoggerType
{
LOGGER_SYNC, // 同步日志器
LOGGER_ASYNC // 异步日志器
};
class LoggerBuilder
{
public:
LoggerBuilder() : _logger_type(LoggerType::LOGGER_SYNC),
_limit_level(LogLevel::value::DEBUG),
_looper_type(AsyncType::ASYNC_SAFE) {}
void buildLoggerType(LoggerType type)
{
_logger_type = type;
}
void buildLoggerName(const std::string &name)
{
_logger_name = name;
}
void buildLoggerLevel(LogLevel::value level)
{
_limit_level = level;
}
void buildEnableUnSafeAsync()
{
_looper_type = AsyncType::ASUNC_UNSAFE;
}
// 构造一个格式化器
void buildLoggerFormatter(const std::string &pattern)
{
_formatter = std::make_shared<Formatter>(pattern);
}
// 一个日志器可以有多个不同的落地方式
template <typename SinkType, typename... Args>
void buildSink(Args &&...args)
{
LogSink::ptr psink = SinkFactory::create<SinkType>(std::forward<Args>(args)...);
_sinks.push_back(psink);
}
// 完成我们的日志器构建
virtual Logger::ptr build() = 0;
protected:
AsyncType _looper_type;
LoggerType _logger_type;
std::string _logger_name;
LogLevel::value _limit_level;
Formatter::ptr _formatter;
std::vector<LogSink::ptr> _sinks;
};
// 2.派生出具体的建造者类------局部日志器的建造者 & 全局日志器建造者(后边添加了全局单例管理之后,将日志器添加全局管理)
// 局部日志器的建造者
class LocalLoggerBuilder : public LoggerBuilder
{
public:
Logger::ptr build() override
{
// 必须要有日志器名称
assert(_logger_name.empty() == false);
// 必须要有格式化器,没有就要创建
if (_formatter.get() == nullptr)
{
_formatter = std::make_shared<Formatter>();
}
// 如果没有落地方式,默认添加一个标准输出的落地方式
if (_sinks.empty())
{
buildSink<StdoutSink>();
}
// 类型为异步日志器
if (_logger_type == LoggerType::LOGGER_ASYNC)
{
// 返回异步日志器对象
return std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);
}
return std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);
}
};
}
测试代码:
cpp
int main()
{
//异步日志器的测试
//异步日志器和异步工作器进行联调
// //先要构造一个建造者出来
std::unique_ptr<log::LoggerBuilder> builder(new log::LocalLoggerBuilder());
//建造者构建零部件
builder->buildLoggerType(log::LoggerType::LOGGER_ASYNC);
builder->buildLoggerName("async_logger");
builder->buildLoggerLevel(log::LogLevel::value::WARN);
builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");
builder->buildSink<log::StdoutSink>(); // 标准输出落地
builder->buildSink<log::FileSink>("./logfile/async.log"); // 文件落地方式
// builder->buildSink<log::RoolBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式
//零部件构建好后,用建造者建筑对象
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 count = 0;
while (count < 100000)
{
logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
}
return 0;
}
日志器管理模块 -- 单例模式
⽇志的输出,我们希望能够在任意位置都可以进⾏,但是当我们创建了⼀个⽇志器之后,就会受到⽇志器所在作⽤域的访问区域限制。
因此,为了突破访问区域的限制,我们创建⼀个⽇志器管理类,且这个类是⼀个单例类,这样的话, 我们就可以在任意位置来通过管理器单例获取到指定的⽇志器来进⾏⽇志输出了。
日志器管理器:
作用1:对所有创建的日志器进行管理
特性:将管理器设计为单例
作用2:可以在程序的任意位置,获取相同的单例对象,获取其中的日志器进行日志输出
拓展:单例管理器创建的时候,默认先创建一个日志器(用于进行标准输出的打印)
拓展目的:让用户在不创建任何日志器的情况下,也能进行标准输出的打印,方便用户使用
设计:
管理的成员:
1.默认日志器
2.所管理的日志器数组
3.互斥锁
提供的接口:
1添加日志器管理
2.判断是否管理了指定名称的日志器
3.获取指定名称的日志器
4.获取默认日志器
日志管理器设计
在 looger.hpp 中:
框架:
cpp
// 日志器管理模块
class LoggerManager
{
public:
// 1添加日志器管理
void addLogger(Logger::ptr &logger);
// 2.判断是否管理了指定名称的日志器
bool hasLogger(const std::string &name);
// 3.获取指定名称的日志器
Logger::ptr getLogger(const std::string &name);
// 4.获取默认日志器
Logger::ptr rootLogger();
// 5. 获取单例句柄
static LoggerManager &getInstance();
private:
// 构造函数私有化
LoggerManager() {}
private:
// 1.默认日志器
Logger::ptr _root_logger;
// 2.所管理的日志器 -- 日志器名称和日志器对象(智能指针)的映射
std::unordered_map<std::string, Logger::ptr> _loggers;
// 3.互斥锁
std::mutex _mutex;
};
code:
cpp
// 日志器管理模块
class LoggerManager
{
public:
// 1添加日志器管理
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));
}
// 2.判断是否管理了指定名称的日志器
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;
}
// 3.获取指定名称的日志器
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;
}
// 4.获取默认日志器
Logger::ptr rootLogger()
{
return _root_logger;
}
// 5. 获取单例句柄
static LoggerManager &getInstance()
{
// 在c++11之后,针对静态局部变量,编译器在编译的层面上实现了线程安全
// 当静态局部变量在没有构造完成之前,其他的线程进入就会阻塞
static LoggerManager eton;
return eton;
}
private:
// 构造函数私有化
LoggerManager()
{
// 构造一个日志器建造者
std::unique_ptr<log::LoggerBuilder> builder(new log::LocalLoggerBuilder());
builder->buildLoggerName("root");
_root_logger = builder->build();
// 把默认构造的日志器管理起来
_loggers.insert(std::make_pair("root", _root_logger));
}
private:
// 1.默认日志器
Logger::ptr _root_logger;
// 2.所管理的日志器
std::unordered_map<std::string, Logger::ptr> _loggers;
// 3.互斥锁
std::mutex _mutex;
};
设计一个全局的日志器建造者
在局部的日志器建造者上增加一个功能:将日志器添加到单例对象中;就是全局的日志器建造者
cpp
class GlobalLoggerBuilder : public LoggerBuilder
{
public:
Logger::ptr build() override
{
// 必须要有日志器名称
assert(_logger_name.empty() == false);
// 必须要有formatter//必须要有格式化器,没有就要创建
if (_formatter.get() == nullptr)
{
_formatter = std::make_shared<Formatter>();
}
// 如果没有落地方式就给它添加一个标准输出的默认落地方式
if (_sinks.empty())
{
buildSink<StdoutSink>();
}
//默认日志器
Logger::ptr logger;
// 如果类型为LOGGER_ASYNC,那么日志器为异步日志器
if (_logger_type == LoggerType::LOGGER_ASYNC)
{
// 返回异步日志器对象
logger=std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);
}else{
// 返回同步日志器的对象
logger=std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks); // 日志器名字,等级,格式化,落地方式
}
//把日志器添加到日志器管理器中
LoggerManager::getInstance().addLogger(logger);
// 返回同步日志器的对象
return logger;
}
};
测试日志器管理器的接口和全局建造者类
cpp
void test_log(){
//日志器管理器
log::Logger::ptr logger=log::LoggerManager::getInstance().getLogger("async_logger");
//测试日志打印
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 count = 0;
while (count < 100000)
{
logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
}
}
int main()
{
//测试日志器管理模块
//先要构造一个建造者出来
//全局建造者构造日志器
std::unique_ptr<log::LoggerBuilder> builder(new log::GlobalLoggerBuilder());
//建造者构建零部件
builder->buildLoggerName("async_logger");
builder->buildLoggerLevel(log::LogLevel::value::WARN);
builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");
builder->buildLoggerType(log::LoggerType::LOGGER_ASYNC);
builder->buildEnableUnSafeAsync();//切换模式
builder->buildSink<log::StdoutSink>(); // 标准输出落地
builder->buildSink<log::FileSink>("./logfile/async.log"); // 文件落地方式
builder->build();
// builder->buildSink<log::RoolBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式
test_log();
return 0;
}
宏函数和全局接口设计 -- 涉及代理模式
提供全局接口&宏函数, 对日志系统接口,进行使用便捷性优化
思想:
1.提供获取指定日志器的全局接口(避免用户自己操作单例对象)
2.使用宏函数对日志器的接口进行代理(代理模式)
3.提供宏函数,直接通过默认日志器进行日志的标准输出打印(不要获取日志器了)
新建一个.h,文件,文件里面放我们写的.hpp(各个模块文件)
log.h:
方便外界使用者进行调用,用户使用时直接包含该头文件就行,不需要包含各个模块的头文件了。
cpp
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "logger.hpp"
#include <unistd.h>
#include "buffer.hpp"
#include "looper.hpp"
#include <fstream>
namespace log
{
// 1.提供获取指定日志器的全局接口(避免用户自己操作单例对象)
Logger::ptr getLogger(const std::string &name) // 指定日志器
{
return log::LoggerManager::getInstance().getLogger(name);
}
Logger::ptr rootLogger() // 默认日志器
{
return log::LoggerManager::getInstance().rootLogger();
}
// 2.使用宏函数对日志器的接口进行代理(代理模式)
#define debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define info(fmt, ...) info(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define warn(fmt, ...) warn(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define error(fmt, ...) error(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define fatal(fmt, ...) fatal(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
// 3.提供宏函数,直接通过默认日志器进行日志的标准输出打印(不要获取日志器了)
#define DEBUG(fmt, ...) log::rootLogger()->debug(fmt, ##__VA_ARGS__)
#define INFO(fmt, ...) log::rootLogger()->info(fmt, ##__VA_ARGS__)
#define WARN(fmt, ...) log::rootLogger()->warn(fmt, ##__VA_ARGS__)
#define ERROR(fmt, ...) log::rootLogger()->error(fmt, ##__VA_ARGS__)
#define FATAL(fmt, ...) log::rootLogger()->fatal(fmt, ##__VA_ARGS__)
}
对宏函数与全局接口进行功能测试
测试第一组宏是否正确:
cpp
#include "log.h"
void test_log(){
//日志器管理器
log::Logger::ptr logger=log::LoggerManager::getInstance().getLogger("async_logger");
//测试日志打印
logger->debug("%s", "测试日志");
logger->info( "%s", "测试日志");
logger->warn("%s", "测试日志");
logger->error( "%s", "测试日志");
logger->fatal( "%s", "测试日志");
size_t count = 0;
while (count < 100000)
{
logger->fatal( "测试日志-%d", count++);
}
}
int main()
{
//测试日志器管理模块
//先要构造一个建造者出来
//全局建造者构造日志器
std::unique_ptr<log::LoggerBuilder> builder(new log::GlobalLoggerBuilder());
//建造者构建零部件
builder->buildLoggerName("async_logger");
builder->buildLoggerLevel(log::LogLevel::value::WARN);
builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n");
builder->buildLoggerType(log::LoggerType::LOGGER_ASYNC);
builder->buildEnableUnSafeAsync();//切换模式
builder->buildSink<log::StdoutSink>(); // 标准输出落地
builder->buildSink<log::FileSink>("./logfile/async.log"); // 文件落地方式
builder->build();
// builder->buildSink<log::RoolBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式
test_log();
return 0;
}
测试第二组宏:
cpp
#include "log.h"
void test_log(){
// 使用默认日志器打印
DEBUG("%s", "测试日志");
INFO("%s", "测试日志");
WARN("%s", "测试日志");
ERROR("%s", "测试日志");
FATAL("%s", "测试日志");
size_t count = 0;
while (count < 100000)
{
FATAL("测试日志-%d", count++);
}
}
int main()
{
test_log();
return 0;
}
项目目录结构
example:如何使用项目的具体样例
logs:项目的各个模块,项目源码
pratice:练习代码,项目前置学习的代码
bench:用来进行性能测试
整理完成后,目录结构如下:
功能测试
cpp
#include "../logs/log.h"
//进行功能测试
void test_log(const std::string &name){
INFO( "%s", "测试开始");
log::Logger::ptr logger=log::LoggerManager::getInstance().getLogger(name);
//测试日志打印
logger->debug( "%s", "测试日志");
logger->info( "%s", "测试日志");
logger->warn( "%s", "测试日志");
logger->error( "%s", "测试日志");
logger->fatal("%s", "测试日志");
INFO( "%s", "测试结束");
}
int main()
{
std::unique_ptr<log::LoggerBuilder> builder(new log::GlobalLoggerBuilder());
//建造者构建零部件
builder->buildLoggerName("async_logger");
builder->buildLoggerLevel(log::LogLevel::value::DEBUG);
builder->buildLoggerFormatter("[%d{%H:%M:%S}][%t][%c][%p]%m%n");
builder->buildLoggerType(log::LoggerType::LOGGER_SYNC);
builder->buildSink<log::StdoutSink>(); // 标准输出落地
builder->buildSink<log::FileSink>("./logfile/async.log"); // 文件落地方式
builder->buildSink<log::RollBySizeSink>("./logfile/roll-", 1024 * 1024); // 滚动文件落地方式
builder->build();
test_log("async_logger");
return 0;
}
性能测试
测试环境:
CPU:11th Gen Intel® Core™ i7-11800H @ 2.30GHz
RAM:16G DDR4 3200
ROM:512GB-SSD
OS:CentOS7.6(2核2G)
测试三要素:
1.测试环境
2.测试方法
3.测试结果
测试工具的编写:
1.可以控制写日志线程数量
2.可以控制写日志的总数量
分别对于同步日志器 & 异步日志器进行各自的性能测试,
需要测试单写日志线程的性能
需要测试多写日志线程的性能
实现:
封装一个接口,传入日志器名称,线程数量,日志数量,单条日志大小 在接口内,创建指定数量的线程,各自负责一部分日志的输出,在输出之前计时开始,在输出完毕后计时结束。
所耗时间=结束时间-起始时间
每秒输出量 =日志数量/总耗时
每秒输出大小 =日志数量*单条日志大小/总耗时
注意:异步日志输出这里,我们启动非安全模式,纯内存写入(不去考虑实际落地的时间)
项目性能测试工具实现
在对项目测试之前,需要编写测试工具,测试工具的具体实现放到bench目录下。
cpp
#include "../logs/log.h"
// c++11提供的时间特性
#include <chrono>
// 设计性能测试功能
// logger_name日志器的名字,thr_count线程数的个数,msg_counr日志消息的总条数,len日志消息的长度
void bench(const std::string &logger_name, size_t thr_count, size_t msg_count, size_t msg_len)
{
// 1.获取日志器
log::Logger::ptr logger = log::getLogger(logger_name);
// 如果没找到日志器就返回空
if (logger.get() == nullptr)
{
return;
}
// 2.组织指定长度的日志消息
// 留一个字符,放换行符
std::string msg(msg_len - 1, 'A');
// 3.创建指定数量的线程
// 创建一个存放线程的数组
std::vector<std::thread> threads;
// 存放每个线程打印日志需要消耗的时间
std::vector<double> cost_arry(thr_count);
// 每个线程需要打印的日志数=总日志数/线程数
size_t msg_per_thr = msg_count / thr_count;
// 创建指定数量的线程,push_back()构造然后拷贝,插入元素到末尾,emplace_back()构造并插入元素到末尾
// 打印测试日志总条数,总大小
std::cout << "\t测试日志:" << msg_count << "条,\t总大小:" << (msg_count * msg_len) / 1024 << "KB\n";
for (int i = 0; i < thr_count; i++)
{
// 插入元素时用lambad表达式
threads.emplace_back([&, i]()
{
// 4.线程函数内部开始计时,高精度获得当前的系统时间
auto start=std::chrono::high_resolution_clock::now();
// 5.开始循环写日志
for(int i=0;i<msg_per_thr;i++){
//打印日志
logger->fatal("%s",msg.c_str());
}
// 6.线程函数内部结束计时,高精度获得当前的系统时间
auto end=std::chrono::high_resolution_clock::now();
//每个线程需要的时间
std::chrono::duration<double> cost=end-start;
cost_arry[i]=cost.count();
std::cout<<"\t线程"<<i<<":"<<"\t输出数量日志:"<<msg_per_thr<<",\t耗时:"<<cost.count()<<"s"<<std::endl; });
}
// 要记住,创建线程那么就要等待线程退出
for (int i = 0; i < thr_count; i++)
{
threads[i].join();
}
// 7.计算总耗时:在多线程中,每个线程都会耗时间,但是线程是并发运行处理的,因此耗时最高的线程就是总时间。
// 创建的子线程已经全部退出了
double max_cost = cost_arry[0];
for (int i = 0; i < thr_count; i++)
{
max_cost = max_cost < cost_arry[i] ? cost_arry[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 << "\t总耗时:" << max_cost << "s\n";
std::cout << "\t每秒输出日志数量:" << msg_per_sec << "条\n";
std::cout << "\t每秒输出日志大小:" << size_per_sec << "KB\n";
}
测试同步日志器:
cpp
void sync_bench()
{
// 创建一个同步日志器建造者
std::unique_ptr<log::LoggerBuilder> builder(new log::GlobalLoggerBuilder());
// 建造者构建零部件
builder->buildLoggerName("sync_logger");
builder->buildLoggerFormatter("%m%n");
builder->buildLoggerType(log::LoggerType::LOGGER_SYNC);
builder->buildSink<log::FileSink>("./logfile/sync.log"); // 文件落地方式
// builder->buildSink<log::StdoutSink>(); // 标准输出落地
// builder->buildSink<log::RoolBySizeSink>("./logfile/roll-sync-", 1024 * 1024); // 滚动文件落地方式
builder->build();
// 测试单线程情况
bench("sync_logger", 1, 1000000, 100);
// bench("sync_logger", 3, 1000000, 100); // 多线程
}
单线程:
多线程:
测试异步日志器:
本质:多线程和单线程都是向内存写数据要考虑 cpu和内存的性能,不去考虑磁盘落地的情况。
单线程:
cpp
void async_bench()
{
// 创建一个同步日志器建造者
std::unique_ptr<log::LoggerBuilder> builder(new log::GlobalLoggerBuilder());
// 建造者构建零部件
builder->buildLoggerName("async_logger");
builder->buildLoggerFormatter("%m%n");
builder->buildLoggerType(log::LoggerType::LOGGER_ASYNC);
builder->buildEnableUnSafeAsync();//开启非安全模式------------主要是为了将实际落地时间排除在外
builder->buildSink<log::FileSink>("./logfile/async.log"); // 文件落地方式
// builder->buildSink<log::StdoutSink>(); // 标准输出落地
// builder->buildSink<log::RoolBySizeSink>("./logfile/roll-sync-", 1024 * 1024); // 滚动文件落地方式
builder->build();
// 测试单线程情况
bench("async_logger", 1, 1000000, 100);
//bench("async_logger", 3, 1000000, 100);
}
多线程:
cpu和内存的性能越好打印日志越快,日志的多少和线程数无关(不会因为落地而阻塞)。
同步日志器:多线程<单线程
异步日志器:多线程>单线程
// 每个线程需要打印的日志数=总日志数/线程数
size_t msg_per_thr = msg_count / thr_count;
// 创建指定数量的线程,push_back()构造然后拷贝,插入元素到末尾,emplace_back()构造并插入元素到末尾
// 打印测试日志总条数,总大小
std::cout << "\t测试日志:" << msg_count << "条,\t总大小:" << (msg_count * msg_len) / 1024 << "KB\n";
for (int i = 0; i < thr_count; i++)
{
// 插入元素时用lambad表达式
threads.emplace_back([&, i]()
{
// 4.线程函数内部开始计时,高精度获得当前的系统时间
auto start=std::chrono::high_resolution_clock::now();
// 5.开始循环写日志
for(int i=0;i<msg_per_thr;i++){
//打印日志
logger->fatal("%s",msg.c_str());
}
// 6.线程函数内部结束计时,高精度获得当前的系统时间
auto end=std::chrono::high_resolution_clock::now();
//每个线程需要的时间
std::chrono::duration<double> cost=end-start;
cost_arry[i]=cost.count();
std::cout<<"\t线程"<<i<<":"<<"\t输出数量日志:"<<msg_per_thr<<",\t耗时:"<<cost.count()<<"s"<<std::endl; });
}
// 要记住,创建线程那么就要等待线程退出
for (int i = 0; i < thr_count; i++)
{
threads[i].join();
}
// 7.计算总耗时:在多线程中,每个线程都会耗时间,但是线程是并发运行处理的,因此耗时最高的线程就是总时间。
// 创建的子线程已经全部退出了
double max_cost = cost_arry[0];
for (int i = 0; i < thr_count; i++)
{
max_cost = max_cost < cost_arry[i] ? cost_arry[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 << "\t总耗时:" << max_cost << "s\n";
std::cout << "\t每秒输出日志数量:" << msg_per_sec << "条\n";
std::cout << "\t每秒输出日志大小:" << size_per_sec << "KB\n";
}
测试同步日志器:
```cpp
void sync_bench()
{
// 创建一个同步日志器建造者
std::unique_ptr<log::LoggerBuilder> builder(new log::GlobalLoggerBuilder());
// 建造者构建零部件
builder->buildLoggerName("sync_logger");
builder->buildLoggerFormatter("%m%n");
builder->buildLoggerType(log::LoggerType::LOGGER_SYNC);
builder->buildSink<log::FileSink>("./logfile/sync.log"); // 文件落地方式
// builder->buildSink<log::StdoutSink>(); // 标准输出落地
// builder->buildSink<log::RoolBySizeSink>("./logfile/roll-sync-", 1024 * 1024); // 滚动文件落地方式
builder->build();
// 测试单线程情况
bench("sync_logger", 1, 1000000, 100);
// bench("sync_logger", 3, 1000000, 100); // 多线程
}
单线程:
多线程:
测试异步日志器:
本质:多线程和单线程都是向内存写数据要考虑 cpu和内存的性能,不去考虑磁盘落地的情况。
单线程:
cpp
void async_bench()
{
// 创建一个同步日志器建造者
std::unique_ptr<log::LoggerBuilder> builder(new log::GlobalLoggerBuilder());
// 建造者构建零部件
builder->buildLoggerName("async_logger");
builder->buildLoggerFormatter("%m%n");
builder->buildLoggerType(log::LoggerType::LOGGER_ASYNC);
builder->buildEnableUnSafeAsync();//开启非安全模式------------主要是为了将实际落地时间排除在外
builder->buildSink<log::FileSink>("./logfile/async.log"); // 文件落地方式
// builder->buildSink<log::StdoutSink>(); // 标准输出落地
// builder->buildSink<log::RoolBySizeSink>("./logfile/roll-sync-", 1024 * 1024); // 滚动文件落地方式
builder->build();
// 测试单线程情况
bench("async_logger", 1, 1000000, 100);
//bench("async_logger", 3, 1000000, 100);
}
多线程:
cpu和内存的性能越好打印日志越快,日志的多少和线程数无关(不会因为落地而阻塞)。
同步日志器:多线程<单线程
异步日志器:多线程>单线程