文章目录
- 项目介绍
- 地址:https://gitee.com/royal-never-give-up/c-log-system
- 开发环境
- 核心技术
- 为什么需要日志系统
- 同步日志
- 异步日志
- 知识补充
- 不定参宏函数
- `__FILE__`
- `__LINE__`
- `__VA_ARGS__`
- C使用
- C++使用
- 左值右值
- `sizeof...()` 运算符
- 完美转发
- 完整例子
- `sizeof...()` 运算符获取可变参数包中参数的数量
- 设计模式
- 六大原则
- 工厂模式
- 简单工厂
- 工厂方法
- 抽象工厂
- 建造者模式
- `Computer` 类:提供_board`、 `_display` 和 `_os属性和设置属性的方法,成员变量设置为公有,派生类可以直接访问成员变量
- `Winbook` 类:公有继承父类`Computer` 类,virtual 可以省略,同时父类成员变量是公有,所以可以直接修改父类成员,同时要重写父类抽象函数
- `Builder` 类:抽象基类,不能实例化,提供访问接口
- `WinbookBuilder` 类:构建 `Winbook` 电脑对象,重写访问接口需要构建Winbook 对象的智能指针,不能是computer 因为setos 无法实例化,之后用指针将computer 对象中的成员变量实例化
- `Director` 类:指导 `Builder` 类构建电脑对象,不能实例化 `Builder` 类对象,因该在构造函数传入具体的 `build` 的子类对象
- `main` 函数:创建 `Director` 对象并传入 `WinbookBuilder` 对象,调用 `build` 方法构建电脑,然后获取构建好的电脑对象并显示其信息
- 代理模式
- 日志系统框架设计
- 代码设计
- 实用类设计-util.hpp
- 实现
- 测试
- access-检查文件是否存在
- stat-检查文件是否存在,范围更广
- `find_last_of`-查找指定字符或字符串最后一次出现的位置
- `mkdir` -创建目录
- #ifndef` 和 `#endif
- 日志等级类设计-level.hpp
- 日志消息类设计-message.hpp
- 日志输出格式化设计-format.hpp
- 整体框架
- 格式化子项数组:
- **基类 `FormatItem`**
- 派生类 **`FormatItem` 的不同格式化子项**
- `ostream` 和 `istream`
- `localtime_r` 和 `strftime` 函数
- 格式化字符串:
- **`Formatter` **实现格式化字符串
- `using ptr = std::shared_ptr<FormatItem>;` 和 `std::shared_ptr<FormatItem> ptr;`
- 完整代码和测试
- 日志落地类设计(工厂模式)-sink.hpp
- 基类`LogSink `
- 派生类`StdoutSink `-标准输出
- 派生类`FileSink `-指定文件
- `ofstream` 文件类
- 关于string变量到char* 变量的传参-str.c_str()
- 派生类`RollBySizeSink`-滚动文件
- 工厂类
- 完整代码和测试
- 日志器类设计(建造者模式)-logger.hpp
- 整体框架
- Logger基类-logger.hpp
- `name` 函数:获取日志器名称
- 日志记录函数(`debug`、`info`、`warn`、`error`、`fatal`):记录不同级别的日志消息,`va_list` 类型的变量处理可变参数,具体实现交给`logMessage` 函数
- `logMessage` 函数:实际处理日志消息的函数,可以简化日志记录函数,首先判断日志等级,之后将格式化字符串和可变参数列表组合,LogMsg对象包含所有信息,之后再格式化消息,在落地
- `log` 纯虚函数
- `atomic` 类-原子操作
- `va_list` -处理可变数量的参数
- `vasprintf` 函数-生成一个格式化的字符串
- `SynchLogger` 派生类-同步日志器-logger.hpp
- `unique_lock` 类-锁
- `Logger`类和`SynchLogger`完整代码和测试
- 异步日志器
- `Buffer`类-缓冲区设计-在buffer.hpp
- `push`:向缓冲区中写入长度为 `len` 的数据
- `begin()`:返回可读数据的起始地址
- `readAbleSize()`:返回可读数据的长度
- `writeAbleSize()`:返回可写空间的长度
- `moveWriter()`:将 `_write_idx` 向后移动 `len`
- `bufferReset()`:重置
- `bufferEmpty()`:判断缓冲区是否为空
- `ensureEnoughSize()`:确保缓冲区有足够的空间来存储长度为 `len` 的数据,不够就扩容
- `copy` 函数-把一个范围内的元素复制到另一个范围
- `AsynchLogger`派生类-异步日志器-logger.hpp
- `bind`函数
- `placeholders`-命名空间
- `LoggerBuilder`类-建造者-logger.hpp
- 全局建造者单例模式-懒汉模式-logger.hpp
- 完整代码和测试
- `AsynchLooper`类-异步工作器-looper.hpp
- logger.hpp-完整代码和测试
- `condition_variable` 类-条件变量
- `thread ` 类-线程
- `join()`:阻塞当前线程,直到被调用线程执行完毕
- `detach()`:线程分离,允许线程独立执行,无法再对该线程进行 `join()` 或 `detach()` 操作
- 日志宏全局接口设计
- 项目总结
- 性能测试
- 测试环境 :
- 测试代码
- bench-测试主要代码
- ``emplace_back``-末尾添加元素
- `chrono`-计时
- 测试结果
项目介绍
- 支持多级别的日志消息:不同的日志有不同的优先级
- 支持同步日志和异步日志
- 支持写入日志到控制台,文件和滚动文件中:当文件写到一定体积后,新的内容写到别的文件中
- 支持多线程程序并发写日志
- 支持扩展不同的日志存放位置
地址:https://gitee.com/royal-never-give-up/c-log-system
开发环境
- centos7
- vscode/vim
核心技术
- 继承多态
- C++11(多线程,auto,智能指针,右值引用)
- 双缓冲区
- 生产消费者模型
- 多线程
- 单例模式,工厂模式,代理模式
为什么需要日志系统
-
⽣产环境的产品为了保证其稳定性及安全性是不允许开发⼈员附加调试器去排查问题,可以借助⽇志系统来打印⼀些⽇志帮助开发⼈员解决问题
-
上线客⼾端的产品出现bug⽆法复现并解决,可以借助⽇志系统打印⽇志并上传到服务端帮助开发⼈员进⾏分析
-
对于⼀些⾼频操作(如定时器、⼼跳包)在少量调试次数下可能⽆法触发我们想要的⾏为,通过断点的暂停⽅式,我们不得不重复操作⼏⼗次、上百次甚⾄更多,导致排查问题效率是⾮常低下,可以借助打印⽇志的⽅式查问题
-
在分布式、多线程/多进程代码中,出现bug⽐较难以定位,可以借助⽇志系统打印log帮助定位bug
-
帮助⾸次接触项⽬代码的新开发⼈员理解代码的运⾏流程
同步日志
顺序是:业务线程->调用磁盘IO->回到业务线程
异步日志
顺序是:业务线程->将日志放到缓冲区->回到业务线程,日志线程->将缓冲区中日志拿出来->回到日志线程。
业务线程是生产者,日志线程是消费者
知识补充
不定参宏函数
__FILE__
__FILE__
是一个宏,用于获取当前源文件的文件名(包括路径),是字符串类型
__LINE__
__LINE__
是一个宏,用于获取当前代码所在的行号
__VA_ARGS__
__VA_ARGS__
是一个宏,用于在宏中处理可变数量的参数。
C使用
#define LOG(fmt, ...) printf("[%s:%d] " fmt, __FILE__, __LINE__, __VA_ARGS__);
LOG(fmt, ...)
:LOG
是宏的名称,fmt
是一个普通的宏参数;...
是 C 语言中的可变参数列表,来接受任意数量的参数printf("[%s:%d] " fmt, __FILE__, __LINE__, __VA_ARGS__);
:这是宏的替换体[%s:%d]
:这是日志信息的前缀,用于显示文件名和行号。__FILE__
和__LINE__
:这是 C 语言的预定义宏,__FILE__
会被替换为当前源文件的文件名,__LINE__
会被替换为当前代码所在的行号。fmt
:是宏的第一个参数,用于指定输出的格式字符串。__VA_ARGS__
:这是一个可变参数宏,用于替换宏定义中...
所代表的可变参数。
LOG("%s-%d\n", "Hello World", 666);
fmt
被替换为"%s-%d\n"
。__VA_ARGS__
被替换为"Hello World", 666
。- 最终
LOG
宏会被替换为printf("[%s:%d] %s-%d\n", __FILE__, __LINE__, "Hello World", 666);
,该语句会输出包含文件名、行号、"Hello World"
和数字666
的日志信息。
LOG("Hello World");
- 会报错,
fmt
被替换为"Hello World"
。 - 但是
__VA_ARGS__
就变成了NULL
解决方法:##____VA_ARGS__
__,##
运算符将前一个标识符与可变参数(__VA_ARGS__
)合并,只有当参数列表不为空时才插入前缀
#define LOG(fmt, ...) printf("[%s:%d] " fmt, __FILE__, __LINE__, ##__VA_ARGS__);
C++使用
左值右值
左值是持久内存地址的表达式,变量、数组元素、对象成员等
int& ref;
是错误的
类型 &引用名 = 左值;
int num = 10; // num 是一个左值
int &ref = num; // 声明一个左值引用 ref,引用 num
右值是临时对象、字面量、表达式的结果,通常只能出现在赋值语句的右边,实现移动语义和完美转发
int num = 10; int&& rref = num;
是错误的
类型 &&引用名 = 右值;
int&& rr1 = 10;
double&& rr2 = x + y;
int&& ret = Add(3, 4);// 函数的返回值是一个临时变量,是一个右值
sizeof...()
运算符
获取可变参数包中参数的数量
sizeof...(args)
获取参数包args中的参数数量,要用括号
完美转发
将参数以原始的左值或右值属性传递给另一个函数,避免不必要的对象拷贝,例如,如果传入的参数是右值,我们希望传递给下一个函数时它仍然是右值;如果传入的是左值,传递时也保持为左值
T&&
作为模板参数时,它既可以绑定到左值,也可以绑定到右值
- 如果传入的参数是左值,
T
会被推导为左值引用类型,T&&
最终是一个左值引用。 - 如果传入的参数是右值,
T
会被推导为非引用类型,T&&
就是一个右值引用。
forward 根据转发引用的推导结果,将参数以原始的左值或右值属性转发给其他函数
template <typename T>
void wrapper(T&& arg) {
otherFunction(std::forward<T>(arg));
}
将参数 arg
以原始的左值或右值属性转发给 otherFunction
完整例子
#include <iostream>
using namespace std;
// 处理无参数的情况,输出换行符
void myprintf()
{
cout << "\n";
}
// 可变参数模板函数
template <class T, class... Args>
void myprintf(const T &val, Args &&...args)//传入字符串是常量要加const
{
// 先打印 val
cout << val;
if (sizeof...(args) > 0)
{
cout << " "; // 为了让输出更美观,添加空格分隔参数
// 使用 std::forward 进行完美转发
myprintf(std::forward<Args>(args)...);
}
else
{
myprintf(); // 递归结束,输出换行符
cout<<"没有了"<<endl;//也可以这样子
}
}
int main()
{
myprintf();
myprintf("hello");
myprintf("hello ", "world");
myprintf("hello ", "world", 66);
return 0;
}
-
...
:参数包展开运算符。它表示Args
是一个可变参数模板参数包,这个参数包可以包含零个或多个模板类型参数。 -
Args
:是参数包的名称,可以命名别的,可包含参数例如int
、double
、string
等,和C语言不一样不能省略。 -
sizeof...()
运算符获取可变参数包中参数的数量 -
forward<Args>(args)...
完美转发,使用时要记得展开
设计模式
六大原则
1、单一职责原则
定义:一个类应该只负责一项职责
2.开闭原则
定义:扩展新功能时不应该修改原有代码,用多态实现
3.里氏替换原则
定义:子类继承父类中所有方法
4.依赖倒置原则
定义:高层模块不应该依赖底层模块,两者都应该依赖其抽象;抽象不应该依赖具体,而具体应该依赖抽象
实现:每个类都应该派生于抽象类,而不应该派生于具体类
5.接口隔离原则
定义:类之间的依赖的关系应该建立在最少的接口上,不提供没有必要的接口
6.迪米特法则(最少知道原则)
定义:一个对象应该对其他对象保持最少程度了解,即尽量减少对象之间的耦合
工厂模式
将创建对象时封装起来,将创建-使用分离
简单工厂
三种水果,苹果,香蕉,他们分别包括价格和种类,常规设计二个类,现在设计父类水果,之后苹果,香蕉分别继承水果。
每次增加一个产品时,都需要增加一个具体类和对象实现工厂
#include <iostream>
#include <memory>
#include <string>
using namespace std;
class Fruit
{
public:
// 父类指针指向new出来的子类对象,父类的析构函数必须要加virtual
// 否则delete时就会调用父类的析构函数,极大可能会导致 内存泄漏或其他潜在风险
virtual ~Fruit() {}
virtual void name() = 0;
};
class Apple : public Fruit
{
public:
virtual void name() override
{
cout << "我是苹果\n";
}
};
class Banana : public Fruit
{
public:
virtual void name() override
{
cout << "我是香蕉\n";
}
};
class FruitFactory
{
public:
static std::shared_ptr<Fruit> create(const std::string& str)
{
if (str == "APPLE")
return std::make_shared<Apple>();
else if (str == "BANANA")
return std::make_shared<Banana>();
else
return std::shared_ptr<Fruit>(); // 空的智能指针
//不是创建一个新的 Fruit 对象,不能用make_shared代替
}
};
int main()
{
std::shared_ptr<Fruit> sp = FruitFactory::create("APPLE");
sp->name();
sp = FruitFactory::create("BANANA");
sp->name();
return 0;
}
virtual ~Fruit() {}
:类指针指向派生类对象,并且通过基类指针删除派生类对象时,如果基类析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数,这可能导致派生类中动态分配的资源无法释放,从而造成内存泄漏,所以多态通常会把基类析构换成虚函数virtual void name() = 0;
:纯虚函数,基类virtual 不能省,后面 = 0 让抽象类不能实例化,必须要被继承virtual void name() override
:virtual 派生类virtual 可以省略,override 用于检查派生类是否重写基类虚函数static std::shared_ptr<Fruit> create(const std::string& str)
:static 让函数可以直接调用,返回类型是指向Fruit 的智能指针,指向派生类对象的指针可以隐式转换为指向基类对象的指针- 如果是"APPLE" 创建
Apple
对象返回指向Apple
的智能指针,std::shared_ptr<Apple>
会隐式转换为std::shared_ptr<Fruit>
类型返回 - 如果不是上面字符,返回空的
std::shared_ptr<Fruit>
智能指针,而不是创建一个Fruit
对象并返回指向它的智能指针
- 如果是"APPLE" 创建
工厂方法
还是水果,每一个水果都对应一个工厂
#include <iostream>
#include <memory>
#include <string>
// 抽象水果类
class Fruit {
public:
virtual ~Fruit() {}
virtual void name() = 0;
};
// 苹果类
class Apple : public Fruit {
public:
void name() override {
std::cout << "我是苹果\n";
}
};
// 香蕉类
class Banana : public Fruit {
public:
void name() override {
std::cout << "我是香蕉\n";
}
};
// 抽象工厂类
class FruitFactory {
public:
virtual std::shared_ptr<Fruit> create() = 0;
virtual ~FruitFactory() {}
};
// 苹果工厂类
class AppleFactory : public FruitFactory {
public:
std::shared_ptr<Fruit> create() override {
return std::make_shared<Apple>();
}
};
// 香蕉工厂类
class BananaFactory : public FruitFactory {
public:
std::shared_ptr<Fruit> create() override {
return std::make_shared<Banana>();
}
};
int main() {
// 创建苹果工厂
std::shared_ptr<FruitFactory> appleFactory = std::make_shared<AppleFactory>();
// 通过苹果工厂创建苹果对象
std::shared_ptr<Fruit> apple = appleFactory->create();
apple->name();
// 创建香蕉工厂
std::shared_ptr<FruitFactory> bananaFactory = std::make_shared<BananaFactory>();
// 通过香蕉工厂创建香蕉对象
std::shared_ptr<Fruit> banana = bananaFactory->create();
banana->name();
return 0;
}
FruitFactory
类定义纯虚函数,因为后面有具体的工厂,纯虚函数的声明格式是在函数声明后加上= 0
std::shared_ptr<Fruit> create() override
:create
函数重写了基类FruitFactory
中的纯虚函数create()
。return std::make_shared<Apple>();
:由于Apple
是Fruit
的派生类,std::shared_ptr<Apple>
可以隐式转换为std::shared_ptr<Fruit>
抽象工厂
将工厂再封装下,和工厂方法差不多
#include <iostream>
#include <memory>
#include <string>
// 抽象水果类
class Fruit {
public:
virtual ~Fruit() {}
virtual void name() = 0;
};
// 中国苹果类
class ChineseApple : public Fruit {
public:
void name() override {
std::cout << "我是中国苹果\n";
}
};
// 中国香蕉类
class ChineseBanana : public Fruit {
public:
void name() override {
std::cout << "我是中国香蕉\n";
}
};
// 美国苹果类
class AmericanApple : public Fruit {
public:
void name() override {
std::cout << "我是美国苹果\n";
}
};
// 美国香蕉类
class AmericanBanana : public Fruit {
public:
void name() override {
std::cout << "我是美国香蕉\n";
}
};
// 抽象水果工厂类
class FruitFactory {
public:
virtual std::shared_ptr<Fruit> createApple() = 0;
virtual std::shared_ptr<Fruit> createBanana() = 0;
virtual ~FruitFactory() {}
};
// 中国水果工厂类
class ChineseFruitFactory : public FruitFactory {
public:
std::shared_ptr<Fruit> createApple() override {
return std::make_shared<ChineseApple>();
}
std::shared_ptr<Fruit> createBanana() override {
return std::make_shared<ChineseBanana>();
}
};
// 美国水果工厂类
class AmericanFruitFactory : public FruitFactory {
public:
std::shared_ptr<Fruit> createApple() override {
return std::make_shared<AmericanApple>();
}
std::shared_ptr<Fruit> createBanana() override {
return std::make_shared<AmericanBanana>();
}
};
int main() {
// 创建中国水果工厂
std::shared_ptr<FruitFactory> chineseFactory = std::make_shared<ChineseFruitFactory>();
// 通过中国水果工厂创建中国苹果对象
std::shared_ptr<Fruit> chineseApple = chineseFactory->createApple();
chineseApple->name();
// 通过中国水果工厂创建中国香蕉对象
std::shared_ptr<Fruit> chineseBanana = chineseFactory->createBanana();
chineseBanana->name();
// 创建美国水果工厂
std::shared_ptr<FruitFactory> americanFactory = std::make_shared<AmericanFruitFactory>();
// 通过美国水果工厂创建美国苹果对象
std::shared_ptr<Fruit> americanApple = americanFactory->createApple();
americanApple->name();
// 通过美国水果工厂创建美国香蕉对象
std::shared_ptr<Fruit> americanBanana = americanFactory->createBanana();
americanBanana->name();
return 0;
}
建造者模式
将对象的创建和表示分离,通过多个步骤创建对象。
- **产品:
Computer
类及其子类Winbook
- 抽象建造者:定义创建产品各个部分的抽象方法
- 具体建造者:实现了抽象建造者接口,负责具体的步骤,
WinbookBuilder
是具体建造者 - 指挥者:安排产品的构建步骤,
Director
类是指挥者
#include <iostream>
#include <memory>
#include <string>
using namespace std;
// 每个电脑都必须要有一个OS,因此我们把OS定为纯虚函数,强制子类重写
class Computer
{
public:
virtual ~Computer() {}
void setBoard(const string &board)
{
_board = board;
}
void setDisplay(const string &display)
{
_display = display;
}
virtual void setOS() = 0;
void show()
{
cout << "board:\t" << _board << "\n";
cout << "display:\t" << _display << "\n";
cout << "os:\t" << _os << "\n";
}
string _board;
string _display;
string _os;
};
class Winbook : public Computer
{
public:
virtual void setOS() override
{
_os = "Win 10";
}
};
class Builder
{
public:
virtual ~Builder() {}
virtual void BuilderBoard(const string &board) = 0;
virtual void BuilderDisplay(const string &display) = 0;
virtual void BuilderOS() = 0;
virtual std::shared_ptr<Computer> get() = 0;
};
class WinbookBuilder : public Builder
{
public:
WinbookBuilder() : _computer(std::make_shared<Winbook>()) {}
virtual void BuilderBoard(const string &board)
{
_computer->setBoard(board);
}
virtual void BuilderDisplay(const string &display)
{
_computer->setDisplay(display);
}
virtual void BuilderOS()
{
_computer->setOS();
}
virtual shared_ptr<Computer> get()
{
return _computer;
}
private:
shared_ptr<Winbook> _computer;
};
class Director
{
public:
Director(const shared_ptr<Builder> &builder)
: _builder(builder) {}
void build(const string &board, const string &display)
{
_builder->BuilderBoard(board);
_builder->BuilderDisplay(display);
_builder->BuilderOS();
}
shared_ptr<Builder> get()
{
return _builder;
}
private:
shared_ptr<Builder> _builder;
};
int main()
{
// 步骤1:创建具体的建造者对象
auto winBuilder = std::make_shared<WinbookBuilder>();
// 步骤2:创建指挥者对象,并将建造者对象传递给指挥者
Director director(winBuilder);
// 步骤3:指挥者调用建造者的方法来构建电脑
director.build("Z主板", "小米显示器");
// 步骤4:通过建造者获取构建好的电脑对象
auto builder = director.get();
auto sp = builder->get();
// 步骤5:调用电脑对象的 show 方法,显示电脑的信息
sp->show();
return 0;
}
-
Computer
类:提供_board、
_display和
_os属性和设置属性的方法,成员变量设置为公有,派生类可以直接访问成员变量 -
Winbook
类:公有继承父类Computer
类,virtual 可以省略,同时父类成员变量是公有,所以可以直接修改父类成员,同时要重写父类抽象函数 -
Builder
类:抽象基类,不能实例化,提供访问接口 -
WinbookBuilder
类:构建Winbook
电脑对象,重写访问接口需要构建Winbook 对象的智能指针,不能是computer 因为setos 无法实例化,之后用指针将computer 对象中的成员变量实例化 -
Director
类:指导Builder
类构建电脑对象,不能实例化Builder
类对象,因该在构造函数传入具体的build
的子类对象 -
main
函数:创建Director
对象并传入WinbookBuilder
对象,调用build
方法构建电脑,然后获取构建好的电脑对象并显示其信息
作为基类将析构函数变为虚函数,防止内存泄漏
接口测试
void testWinbook() {
Winbook winbook;
winbook.setBoard("Winbook Board");
winbook.setDisplay("Winbook Display");
winbook.setOS();
winbook.show();
}
void testWinbookBuilder() {
WinbookBuilder builder;
builder.BuilderBoard("Builder Board");
builder.BuilderDisplay("Builder Display");
builder.BuilderOS();
auto computer = builder.get();
computer->show();
}
void testDirector() {
auto builder = std::make_shared<WinbookBuilder>();
Director director(builder);
director.build("Director Board", "Director Display");
auto computer = builder->get();
computer->show();
}
代理模式
允许一个对象(代理对象)替代另一个对象(目标对象)来提供服务
#include <iostream>
using namespace std;
class RentHouse
{
public:
virtual void rent()=0;
};
class Landlord:public RentHouse//房东
{
public:
virtual void rent() override
{
cout<<"把房子租出去\n";
}
};
class Intermediary:public RentHouse//中介
{
public:
virtual void rent()
{
cout<<"发布招租启示\n";
cout<<"带人看房\n";
_landlord.rent();
cout<<"负责维修\n";
}
private:
Landlord _landlord;
};
int main()
{
Intermediary intermediary;
intermediary.rent();
return 0;
}
RentHouse
:是抽象类,不能被实例化,纯虚函数rent
Landlord
:实现了rent
方法Intermediary
:Intermediary
类充当了Landlord
类的代理
日志系统框架设计
代码设计
实用类设计-util.hpp
- 获取系统时间
- 传入文件绝对路径,获取文件所在目录的绝对路径
- 判断文件是否存在
- 传入一个绝对路径,根据路径依次创建目录
实现
//util.hpp
#ifndef UTIL_H
#define UTIL_H
#include <iostream>
#include <ctime>
#include <string>
#include <unistd.h>
#include <sys/stat.h>
// 这个文件中包含一些通用工具
namespace MySpace
{
class util
{
public:
// 1、获取当前时间戳
static size_t getCurTime()
{
return (size_t)time(nullptr);
}
// 2、获取文件目录
static std::string getDirectory(const std::string& pathname)
{
int pos = pathname.find_last_of("/\\");//查找斜杠或者反斜杠
if (pos == std::string::npos)//没找到
return std::string("./");
return pathname.substr(0, pos + 1);
}
// 3、判断文件是否存在
static bool isExist(const std::string& pathname)
{
struct stat st;
return (stat(pathname.c_str(), &st) == 0);
//return (access(pathname.c_str(), F_OK) == 0);//上面接口更广泛
}
// 4、创建一个目录
static void createDirectory(const std::string& pathname){
// ./abc/a
size_t pos = 0, idx = 0;//pos:用于记录路径中分隔符(/ 或 \)位置,idx:作为循环的索引
while (idx < pathname.size()) {
pos = pathname.find_first_of("/\\", idx);//从索引 idx 开始,在路径中查找第一个出现的分隔符
if (pos == std::string::npos) {
mkdir(pathname.c_str(), 0777);
break; // 找到末尾,退出循环
}
//截取从路径开头到分隔符位置(包含分隔符)的子字符串
std::string parent_dir = pathname.substr(0 , pos + 1);
if (!isExist(parent_dir.c_str())) {
mkdir(parent_dir.c_str() , 0777);
}
idx = pos + 1;
}
}
};
}
#endif
getCurTime
函数:time
函数返回系统时间秒数,返回值强转下getDirectory
函数:路径pathname
中提取出目录部分,用find_last_of
找到最后一个斜杠或反斜杠,然后截取从开头到最后一个斜杠或反斜杠isExist
函数:判断指定路径pathname
的文件或目录是否存在createDirectory
函数:pos = pathname.find_first_of("/\\", idx)
查找从索引idx
开始的第一个分隔符位置。- 当找不到分隔符(
pos
等于std::string::npos
)时,说明已经到达路径的末尾,使用mkdir
函数创建最后的目录,并通过break
语句退出循环。 - 如果找到了分隔符,则截取从路径开头到分隔符位置(包含分隔符)的子字符串作为父目录
parent_dir
。 - 使用
isExist
函数检查父目录是否存在,如果不存在,则使用mkdir
函数创建该父目录。 - 最后更新
idx
为pos + 1
,继续下一轮循环查找下一个分隔符。
测试
// 测试 getCurTime 函数
size_t timeStamp = MySpace::util::getCurTime();
// 测试 getDirectory 函数
string path1 = "/home/user/documents/file.txt";
string result1 = MySpace::util::getDirectory(path1);
// 测试 isExist 函数
string existingFile = "/etc/passwd";
bool existsResult1 = MySpace::util::isExist(existingFile);
// 测试 createDirectory 函数
string testDirPath = "./abc/def/ert";
MySpace::util::createDirectory(testDirPath);
access-检查文件是否存在
#include <unistd.h>
int access(const char *pathname, int mode);
//使用
string pathname;
if(access(pathname.c_str(), F_OK) == 0){...}
pathname
需要要检查的文件或目录的路径名,字符串mode
:指定检查模式,填写F_OK
:检查文件或目录是否存在- 返回0表示存在,返回-1表示不存在
stat-检查文件是否存在,范围更广
#include <sys/stat.h>
int stat(const char *pathname, struct stat *buf);
//使用
string pathname;
struct stat st;
if(stat(pathname.c_str(), &st) == 0){...}
pathname
参数需要获取目录的路径名buf
是一个指向struct stat
结构体的指针,用于存储获取到的文件信息- 成功返回0,失败返回-1
find_last_of
-查找指定字符或字符串最后一次出现的位置
size_t find_last_of(charT c, size_t pos = npos) const;
size_t find_last_of(const basic_string& str, size_t pos = npos) const;
//使用
string str = "hello, world"
// 查找字符串 \"lo\" 中任意字符最后一次出现在位置
size_t pos1 = str.find_last_of("lo");
- 查找字符
c
最后一次出现的位置,或者字符串 str 中包含的任意字符最后出现位置 - 不指定
pos
,则默认从后往前搜索 - 找到返回最后一次出现的位置,没找到返回npos
mkdir
-创建目录
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
pathname
:要创建的目录的路径名,可以填绝对路径和相对路径(在当前目录下创建目录)mode
:指定新创建目录的权限模式,一般写 0777- 成功返回0,失败返回-1
#ifndef和
#endif
//名为 example.h 的头文件
#ifndef EXAMPLE_H
#define EXAMPLE_H
// 代码块
#endif
#ifndef
:即 “if not defined” 的缩写,检查宏名字(EXAMPLE_H)是否未被定义,如果未被定义则,编译#ifndef
和#endif
之间的代码块;如果已定义,则跳过该代码块- 上面代码都可以用
#pragma once
代替
日志等级类设计-level.hpp
枚举实现不同的等级,只有输出的日志等级大于日志器的默认限制等级才可以进行日志输出
-
OFF : 最高等级,可用于禁止所有日志输出
-
DEBUG : 调试等级日志
-
INFO : 提示等级日志
-
WARN : 警告等级日志
-
ERROR : 错误等级日志
-
FATAL : 致命错误等级日志
-
定义枚举类,枚举出日志等级
-
提供转换接口:将美剧转换为对应字符串
实现
//level.hpp
#pragma once
#include <string>
namespace MySpace
{
class LogLevel
{
public:
enum value
{
DEBUG,
INFO,
WARN,
ERROR,
FATAL,
OFF
};
static const std::string toString(value level){
switch (level){
case DEBUG: return "DEBUG";
case INFO : return "INFO";
case WARN : return "WARN";
case ERROR: return "ERROR";
case FATAL: return "FATAL";
case OFF : return "OFF";
}
return "UNKNOW";
}
};
}
测试
MySpace::LogLevel::value debugLevel = MySpace::LogLevel::DEBUG;
std::string debugStr = MySpace::LogLevel::toString(debugLevel);
日志消息类设计-message.hpp
存储日志的各个属性信息
- 日志的输出时间
- 日志的等级
- 源文件名称
- 源代码行号
- 线程ID
- 日志主体消息
- 日志器名称 ,允许多日志器同时使用
实现
//message.hpp
#pragma once
#include "level.hpp"
#include "util.hpp"
#include <ctime>
#include <iostream>
#include <string>
#include <thread>
namespace MySpace {
class LogMsg {
public:
time_t _ctime; // 日志产生的时间戳
LogLevel::value _level; // 日志等级
std::string _file; // 源文件名称
size_t _line; // 源文件行号
std::thread::id _tid; // 线程ID
std::string _payload; // 有效载荷,日志主题消息
std::string _logger; // 日志器
LogMsg(LogLevel::value level
, size_t line
, const std::string file
, const std::string logger
, const std::string msg)
: _level(level)
, _ctime(util::getCurTime())
, _line(line)
, _file(file)
, _logger(logger)
, _payload(msg)
, _tid(std::this_thread::get_id())
{}
};
}
- 将上面内容设置,注意std::thread::id _tid; 中id不能省略,我们存储的是线程id不是线程对象
- std::this_thread::get_id():返回的标识符类型是
std::thread::id
测试
// 创建一个 LogMsg 对象
MySpace::LogMsg logMsg(MySpace::LogLevel::INFO
, 10
, "test.cpp"
, "test_logger"
, "This is a test log message.");
// 输出 LogMsg 对象的各个成员信息
std::cout << "Log Time: " << logMsg._ctime << std::endl;
日志输出格式化设计-format.hpp
控制日志的输出格式
整体框架
通过格式化字符串定义日志的输出格式为下面方法,
[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n //默认格式
[时间][线程ID][日志器名称][文件名:行号][日志级别] 日志主体消息
如果用户不希望显示线程id 也可以修改为 [%d{%H:%M:%S}][%c][%f:%l][%p]%T%m%n
。
将上面每个部分拆分交给格式化子项数组完成。
格式化子项数组:
将不同的格式化规则封装在不同的 FormatItem
子类中,并通过 Formatter
类进行统一管理和调用
基类 FormatItem
基类作为抽象类,后面的格式化子项类都有相同的操作方式,方便在 Formatter
类中统一调用
//format.hpp
//namespace MySpace
class FormatItem {
public:
virtual void format(std::ostream& out, LogMsg& msg) = 0;
};
- 纯虚函数
format
:将日志消息按照特定的格式输出到流中,具体让子类实现,也是为了统一接口
派生类 FormatItem
的不同格式化子项
各个子类按照特定格式输出的任务
//format.hpp
//namespace MySpace
class payloadFormatItem : public FormatItem{
public:
virtual void format(std::ostream& out, LogMsg& msg) override{
out<<msg._payload;
}
};
class levelFormatItem : public FormatItem{
public:
virtual void format(std::ostream& out, LogMsg& msg) override{
//日志等级在LogLevel域中定义
//不能用to_string 没有定义这个枚举类型
out<<LogLevel::toString(msg._level);
}
};
class ctimeFormatItem : public FormatItem{
public:
ctimeFormatItem(const std::string &fmt)
: fmt_time(fmt)
{}
virtual void format(std::ostream& out, LogMsg& msg) override {
struct tm t;
localtime_r(&msg._ctime, &t);
char buff[32] = {0};
strftime(buff, sizeof(buff), fmt_time.c_str(), &t);
out << buff;
}
private:
std::string fmt_time; // %H:%M:%S
};
class fileFormatItem : public FormatItem{
public:
virtual void format(std::ostream& out, LogMsg& msg) override{
out<<msg._file;
}
};
class lineFormatItem : public FormatItem{
public:
virtual void format(std::ostream& out, LogMsg& msg) override{
out<<std::to_string(msg._line);
}
};
class tidFormatItem : public FormatItem{
public:
virtual void format(std::ostream& out, LogMsg& msg) override{
out<<msg._tid;
}
};
class loggerFormatItem : public FormatItem{
public:
virtual void format(std::ostream& out, LogMsg& msg) override{
out<<msg._logger;
}
};
class TabFormatItem : public FormatItem{
public:
virtual void format(std::ostream& out, LogMsg& msg) override{
out<<'\t';
}
};
class NewLineFormatItem : public FormatItem{
public:
virtual void format(std::ostream& out, LogMsg& msg) override{
out<<'\n';
}
};
//[]
class OtherFormatItem : public FormatItem{
public:
OtherFormatItem(const std::string& str)
:_str(str)
{}
virtual void format(std::ostream& out, LogMsg& msg) override{
out<<_str;
}
private:
std::string _str;
};
payloadFormatItem
:输出主体消息。levelFormatItem
:输出日志级别,日志级别从枚举类型转化字符串类型。ctimeFormatItem
:把 时间戳转化为指定格式字符串fileFormatItem
:输出 文件名。lineFormatItem
:输出 行号。tidFormatItem
:输出 线程 ID。loggerFormatItem
:输出 日志器名称。TabFormatItem
:输出缩进。NewLineFormatItem
:输出换行。OtherFormatItem
:输出字符串。
ostream
和 istream
std::cout
是std::ostream
类的一个实例对象,同理cin 是istream 的实例对象- 当需要自定义类型输出时,用
ostream
localtime_r
和 strftime
函数
localtime_r
是一个用于将 time_t
类型的时间戳转换为本地时间的函数
struct tm *localtime_r(const time_t *timep, struct tm *result);
//使用
struct tm t;
time_t timeValue = time(nullptr);
localtime_r(&timeValue, &t);
-
timep
:指向time_t
类型的指针 -
result
:指向tm
结构体的指针-
struct tm { int tm_sec; // 秒,范围从0到60(包含60,用于表示闰秒) int tm_min; // 分钟,范围从0到59 int tm_hour; // 小时,范围从0到23 int tm_mday; // 一个月中的第几天,范围从1到31 int tm_mon; // 月份,从0开始计数,0表示1月,11表示12月 int tm_year; // 从1900年开始计数的年份,例如2023年表示为123 int tm_wday; // 一周中的第几天,从0开始计数,0表示星期日,6表示星期六 int tm_yday; // 一年中的第几天,从0开始计数,0表示1月1日 int tm_isdst; // 夏令时标志,非零表示夏令时,零表示非夏令时,-1表示不确定 };
-
-
成功返回指向
result
的指针,失败返回空
strftime
函数用于将 tm
结构体表示的时间信息按照指定的格式转换为字符串
size_t strftime(char *buff, size_t max, const char *format, const struct tm *tm);
//使用
char buffer[80];
struct tm timeinfo;
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &timeinfo);
buff
:指向用于存储格式化后时间字符串的字符数组的指针。max
:字符数组s
的最大长度,防止缓冲区溢出。format
:指向格式化字符串的指针,包含了格式化指令,用于指定输出字符串的格式。%Y
:四位数的年份。%m
:两位数的月份(01 - 12)。%d
:两位数的日期(01 - 31)。%H
:24 小时制的小时数(00 - 23)。%M
:分钟数(00 - 59)。%S
:秒数(00 - 59)。
tm
:指向tm
结构体的指针,包含了要格式化的时间信息。- 格式化后没超出长度就返回写入buff 的字符数,否则返回0
格式化字符串:
**Formatter
**实现格式化字符串
解析用户提供的字符串,根据该字符串创建相应的格式化子项,然后将日志消息按照指定格式输出
//format.hpp
/*
%d 表示日期, 子格式 {%H:%M:%S}
%t 表示鲜橙ID
%c 表示日志器名称
%f 表示源码文件名
%l 表示源码行号
%p 表示日志级别
%T 表示制表符缩进
%m 表示主体消息
%n 表示换行
*/
//namespace MySpace
class Formatter{
public:
Formatter(const std::string& pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n")
:_pattern(pattern)
{
assert(parsePattern());
}
//对msg格式化
void format(std::ostream& out, LogMsg& msg){
for (auto &item : _items) {
item->format(out, msg);
}
}
std::string format(LogMsg &msg) {
std::ostringstream out;
format(out, msg);
return out.str();
}
//对格式化字符串进行解析
bool parsePattern(){
//1.对格式化字符串解析
std::vector<std::pair<std::string, std::string>> fmt_order;
size_t pos = 0;
std::string key, val;
while(pos<_pattern.size()){
//处理原始字符abc[]
if (_pattern[pos] != '%') {
val.push_back(_pattern[pos++]); continue;
}
//处理%%
if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%') {
val.push_back('%'); pos += 2; continue;
}
//原始字符串处理完毕
fmt_order.push_back(std::make_pair("", val));
val.clear();
//处理格式化字符{}
pos += 1;
if (pos == _pattern.size()) {
std::cout << "%之后没有对应的格式化字符" << std::endl;
return false;
}
key = _pattern[pos];
pos += 1;
if (pos < _pattern.size() && _pattern[pos] == '{') {
pos += 1;
// 这时pos指向子规则的起始位置
while (pos < _pattern.size() && _pattern[pos] != '}') {
val.push_back(_pattern[pos++]);
}
// 若走到了末尾,还没有找到},则说明格式是错误的,跳出循环
if (pos == _pattern.size()) {
std::cout << "子规则{}匹配出错" << std::endl;
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;
}
private:
//根据不同的格式化字符创建不同的格式化子项对象
std::shared_ptr<FormatItem> createItem(const std::string &key, const std::string &val){
if (key == "d") return std::make_shared<ctimeFormatItem>(val);
if (key == "t") return std::make_shared<tidFormatItem>();
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<payloadFormatItem>();
if (key == "n") return std::make_shared<NewLineFormatItem>();
return std::make_shared<OtherFormatItem>(val);
}
private:
std::string _pattern;//格式化规则字符串
std::vector<std::shared_ptr<FormatItem>> _items;// 格式化子项数组
};
- 构造:用字符串
_pattern
接收,定义了一个默认的,可以改,初始化的时候就解析字符串,如果字符串不规范就停止初始化 format
将日志消息按照特定的格式输出到流中,默认有输出流和日志消息两个参数parsePattern
用于解析格式化模式字符串,结果存储在 fmt_order 中,包含格式化指令和对应的参数createItem
根据格式化指令的键值对创建相应的FormatItem
对象_pattern
:存储用户传入的格式化字符串,默认值为[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n
。这个字符串定义了日志消息的输出格式_items
:是一个存储FormatItem
智能指针的向量。每个FormatItem
对象代表一个格式化子项,负责将日志消息的特定部分按照指定格式输出。
using ptr = std::shared_ptr<FormatItem>;
和 std::shared_ptr<FormatItem> ptr;
using ptr = std::shared_ptr<FormatItem>;
:类型别名声明,ptr
就等价于std::shared_ptr<FormatItem>
也可以用typedef
std::shared_ptr<FormatItem> ptr;
:变量声明
完整代码和测试
//format.hpp
#pragma once
#include "level.hpp"
#include "util.hpp"
#include "message.hpp"
#include <vector>
#include <iostream>
#include <unordered_map>
#include <sstream>
#include <assert.h>
namespace MySpace{
class FormatItem {
public:
virtual void format(std::ostream& out, LogMsg& msg) = 0;
};
class payloadFormatItem : public FormatItem{
public:
virtual void format(std::ostream& out, LogMsg& msg) override{
out<<msg._payload;
}
};
class levelFormatItem : public FormatItem{
public:
virtual void format(std::ostream& out, LogMsg& msg) override{
//日志等级在LogLevel域中定义
//不能用to_string 没有定义这个枚举类型
out<<LogLevel::toString(msg._level);
}
};
class ctimeFormatItem : public FormatItem{
public:
ctimeFormatItem(const std::string &fmt)
: fmt_time(fmt)
{}
virtual void format(std::ostream& out, LogMsg& msg) override {
struct tm t;
localtime_r(&msg._ctime, &t);
char buff[32] = {0};
strftime(buff, sizeof(buff), fmt_time.c_str(), &t);
out << buff;
}
private:
std::string fmt_time; // %H:%M:%S
};
class fileFormatItem : public FormatItem{
public:
virtual void format(std::ostream& out, LogMsg& msg) override{
out<<msg._file;
}
};
class lineFormatItem : public FormatItem{
public:
virtual void format(std::ostream& out, LogMsg& msg) override{
out<<std::to_string(msg._line);
}
};
class tidFormatItem : public FormatItem{
public:
virtual void format(std::ostream& out, LogMsg& msg) override{
out<<msg._tid;
}
};
class loggerFormatItem : public FormatItem{
public:
virtual void format(std::ostream& out, LogMsg& msg) override{
out<<msg._logger;
}
};
class TabFormatItem : public FormatItem{
public:
virtual void format(std::ostream& out, LogMsg& msg) override{
out<<'\t';
}
};
class NewLineFormatItem : public FormatItem{
public:
virtual void format(std::ostream& out, LogMsg& msg) override{
out<<'\n';
}
};
//[]
class OtherFormatItem : public FormatItem{
public:
OtherFormatItem(const std::string& str)
:_str(str)
{}
virtual void format(std::ostream& out, LogMsg& msg) override{
out<<_str;
}
private:
std::string _str;
};
/*
%d 表示日期, 子格式 {%H:%M:%S}
%t 表示鲜橙ID
%c 表示日志器名称
%f 表示源码文件名
%l 表示源码行号
%p 表示日志级别
%T 表示制表符缩进
%m 表示主体消息
%n 表示换行
*/
class Formatter{
public:
Formatter(const std::string& pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n")
:_pattern(pattern)
{
assert(parsePattern());
}
//对msg格式化
void format(std::ostream& out, LogMsg& msg){
for (auto &item : _items) {
item->format(out, msg);
}
}
std::string format(LogMsg &msg) {
std::ostringstream out;
format(out, msg);
return out.str();
}
//对格式化字符串进行解析
bool parsePattern(){
//1.对格式化字符串解析
std::vector<std::pair<std::string, std::string>> fmt_order;
size_t pos = 0;
std::string key, val;
while(pos<_pattern.size()){
//处理原始字符abc[]
if (_pattern[pos] != '%') {
val.push_back(_pattern[pos++]); continue;
}
//处理%%
if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%') {
val.push_back('%'); pos += 2; continue;
}
//原始字符串处理完毕
fmt_order.push_back(std::make_pair("", val));
val.clear();
//处理格式化字符{}
pos += 1;
if (pos == _pattern.size()) {
std::cout << "%之后没有对应的格式化字符" << std::endl;
return false;
}
key = _pattern[pos];
pos += 1;
if (pos < _pattern.size() && _pattern[pos] == '{') {
pos += 1;
// 这时pos指向子规则的起始位置
while (pos < _pattern.size() && _pattern[pos] != '}') {
val.push_back(_pattern[pos++]);
}
// 若走到了末尾,还没有找到},则说明格式是错误的,跳出循环
if (pos == _pattern.size()) {
std::cout << "子规则{}匹配出错" << std::endl;
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;
}
private:
//根据不同的格式化字符创建不同的格式化子项对象
std::shared_ptr<FormatItem> createItem(const std::string &key, const std::string &val){
if (key == "d") return std::make_shared<ctimeFormatItem>(val);
if (key == "t") return std::make_shared<tidFormatItem>();
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<payloadFormatItem>();
if (key == "n") return std::make_shared<NewLineFormatItem>();
return std::make_shared<OtherFormatItem>(val);
}
private:
std::string _pattern;//格式化规则字符串
std::vector<std::shared_ptr<FormatItem>> _items;// 格式化子项数组
};
}
测试
#include <iostream>
#include <sstream>
#include "format.hpp"
int main() {
// 创建一个 LogMsg 对象
MySpace::LogMsg logMsg(MySpace::LogLevel::INFO, 11, "test.cpp", "root", "This is a test message");
// 创建一个 Formatter 对象
MySpace::Formatter formatter;
//MySpace::Formatter formatter("abc[%d]{%H%M%S}");
// 解析格式化字符串
std::ostringstream oss;
// 对 LogMsg 进行格式化
formatter.format(oss, logMsg);
// 输出格式化后的日志
std::cout << oss.str() << std::endl;
return 0;
}
//输出
//[16:43:30][1][root][test.cpp:11][INFO] This is a test message
日志落地类设计(工厂模式)-sink.hpp
将格式化完成后的日志消息,输出到指定位置,标准输出,指定文件,滚动文件(大小,时间),数据库,服务器
抽象出落地模块作为基类,从不同的落地方向从基类进行派生,用工厂模式将创建和表示分离
基类LogSink
统一日志输出接口
//sink.hpp
//namespace MySpace
class LogSink {
public:
virtual void log(const std::string& data, size_t len) = 0;
};
- 派生类都必须实现
log
函数,可以通过基类指针或引用调用log
函数
派生类StdoutSink
-标准输出
把日志信息输出到控制台
//sink.hpp
//namespace MySpace
// 落地方向: 标准输出
class StdoutSink : public LogSink {
public:
// 将日志消息写到标准输出,定长输出
void log(const std::string& data, size_t len) override {
//从0开始截取长度为len
std::string str(data, 0,len);
std::cout << str.c_str()<< std::endl;
}
};
- 从
data
的起始位置(索引为 0)开始截取长度为len
的子字符串,然后输出到控制台
派生类FileSink
-指定文件
将日志消息写入指定的文件
//sink.hpp
//namespace MySpace
// 落地方向: 指定文件
class FileSink : public LogSink {
public:
// 构造时传入文件名
FileSink(const std::string &pathname) {
// 1、 创建日志文件所在的目录
util::createDirectory(util::getDirectory(pathname));
// 2、 创建并打开日志文件
_ofs.open(pathname, std::ios::binary | std::ios::app);
}
void log(const std::string& data, size_t len) override {
_ofs.write(data.c_str(), len);
if (_ofs.fail()) {
std::cerr << "Failed to write to file." << std::endl;
}
}
private:
std::string _pathname;
std::ofstream _ofs;
};
- 创建
FileSink
对象,用户可以指定文件名字,然后找到文件目录,打开文件 - 之后将日志消息写入文件
ofstream
文件类
#include <fstream>
ofstream::ofstream(const char* filename, ios_base::openmode mode = ios_base::out);
//使用,在当前目录下创建文件example.txt
std::ofstream file("example.txt");
上面实例化对象,下面是一些接口使用
ofs.open(file_name, mode) // 打开文件
ofs.is_open() // 判断文件是否打开成功,成功返回true
ofs.write(data, len) // 将指定长度 len 的数据 data 写入到与 ofs 关联的文件,data 通常是一个字符数组或指向字符数据的指针
ofs.good() // 若文件读或写失败,某些字段会被设置,调用good()返回false
file_name
:要打开的文件名,可以是相对路径或绝对路径mode
:打开文件的模式,二进制用按位或操作(|)std::ios_base::out
打开文件,没有就创建std::ios_base::app
追加文件std::ios_base::binary
二进制模式
关于string变量到char* 变量的传参-str.c_str()
string str = "abc";
const char* cstr = str.c_str();
str.c_str()
直接将str 从string 类型变成了 const char*
类型
派生类RollBySizeSink
-滚动文件
日志文件写入量不断增大,RollBySizeSink
类的主要作用就是当日志文件达到指定的最大大小时,自动创建新的日志文件来继续记录日志,从而实现日志文件的滚动存储
//sink.hpp
//namespace MySpace
// 落地方向: 滚动文件,按大小
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::createDirectory(util::getDirectory(pathname));
_ofs.open(pathname, std::ios::binary | std::ios::app);
}
void log(const std::string& data, size_t len) override{
if (_cur_fsize + len >= _max_fsize) {
_ofs.close(); // 关闭原来已经打开的文件
std::string pathname = createNewFile();
_cur_fsize = 0;
util::createDirectory(util::getDirectory(pathname));
_ofs.open(pathname, std::ios::binary | std::ios::app);
}
_ofs.write(data.c_str(), len);
_cur_fsize += len;
}
private:
//根据时间创建新的滚动文件
std::string createNewFile(){
_name_count += 1;
// 获取系统时间,以时间来构建文件扩展名
time_t t = util::getCurTime();
struct tm lt;
localtime_r(&t, <);
std::string filename;
filename += _basename;
filename += std::to_string(lt.tm_year + 1900);
filename += "-";
filename += std::to_string(lt.tm_mon + 1);
filename += "-";
filename += std::to_string(lt.tm_mday);
filename += " ";
filename += std::to_string(lt.tm_hour);
filename += ":";
filename += std::to_string(lt.tm_min);
filename += ":";
filename += std::to_string(lt.tm_sec);
filename += "-";
filename += std::to_string(_name_count);
filename += ".log";
return filename;
}
private:
std::string _basename; // 基础文件名
std::ofstream _ofs; // 操作句柄
size_t _max_fsize; // 记录文件允许存储最大数据量,超过大小就要切换文件
size_t _cur_fsize; // 记录当前文件已经写入数据大小
size_t _name_count; // 滚动文件数量
};
- 构造时,需要用户确定文件名和文件大小,
createNewFile
函数创建新的日志文件,同时创建该文件所在的目录,最后以二进制追加模式打开文件 - 写入日志前,检查当前文件的已写入数据大小
_cur_fsize
加上要写入的数据大小len
是否超过最大文件大小_max_fsize
,如果超过最大文件大小,关闭当前文件,调用createNewFile
函数创建新的日志文件,重置当前文件大小为 0,创建新文件所在的目录并打开新文件,将日志数据写入文件,并更新当前文件的已写入数据大小 - 根据当前时间和文件计数生成新的文件名
工厂类
将不同 LogSink
派生类的创建逻辑封装,外部代码只需调用工厂类的 create
方法并传入相应的参数,就能得到所需的 LogSink
对象
//sink.hpp
//namespace MySpace
class SinkFactory {
public:
template<class T, class ...Args>
static std::shared_ptr<LogSink> create(Args&&... args) {
return std::make_shared<T>(std::forward<Args>(args)...);
}
};
完整代码和测试
//sink.hpp
#pragma once
#include "level.hpp"
#include "util.hpp"
#include "message.hpp"
#include <string>
#include <iostream>
#include <assert.h>
#include <fstream>
namespace MySpace{
class LogSink {
public:
virtual void log(const std::string& data, size_t len) = 0;
};
// 落地方向: 标准输出
class StdoutSink : public LogSink {
public:
// 将日志消息写到标准输出,定长输出
void log(const std::string& data, size_t len) override {
//从0开始截取长度为len
std::string str(data, 0,len);
std::cout << str.c_str()<< std::endl;
}
};
// 落地方向: 指定文件
class FileSink : public LogSink {
public:
// 构造时传入文件名
FileSink(const std::string &pathname) {
// 1、 创建日志文件所在的目录
util::createDirectory(util::getDirectory(pathname));
// 2、 创建并打开日志文件
_ofs.open(pathname, std::ios::binary | std::ios::app);
}
void log(const std::string& data, size_t len) override {
_ofs.write(data.c_str(), len);
if (_ofs.fail()) {
std::cerr << "Failed to write to file." << std::endl;
}
}
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::createDirectory(util::getDirectory(pathname));
_ofs.open(pathname, std::ios::binary | std::ios::app);
}
void log(const std::string& data, size_t len) override{
if (_cur_fsize + len >= _max_fsize) {
_ofs.close(); // 关闭原来已经打开的文件
std::string pathname = createNewFile();
_cur_fsize = 0;
util::createDirectory(util::getDirectory(pathname));
_ofs.open(pathname, std::ios::binary | std::ios::app);
}
_ofs.write(data.c_str(), len);
_cur_fsize += len;
}
private:
//根据时间创建新的滚动文件
std::string createNewFile(){
_name_count += 1;
// 获取系统时间,以时间来构建文件扩展名
time_t t = util::getCurTime();
struct tm lt;
localtime_r(&t, <);
std::string filename;
filename += _basename;
filename += std::to_string(lt.tm_year + 1900);
filename += "-";
filename += std::to_string(lt.tm_mon + 1);
filename += "-";
filename += std::to_string(lt.tm_mday);
filename += " ";
filename += std::to_string(lt.tm_hour);
filename += ":";
filename += std::to_string(lt.tm_min);
filename += ":";
filename += std::to_string(lt.tm_sec);
filename += "-";
filename += std::to_string(_name_count);
filename += ".log";
return filename;
}
private:
std::string _basename; // 基础文件名
std::ofstream _ofs; // 操作句柄
size_t _max_fsize; // 记录文件允许存储最大数据量,超过大小就要切换文件
size_t _cur_fsize; // 记录当前文件已经写入数据大小
size_t _name_count; // 滚动文件数量
};
class SinkFactory {
public:
template<class T, class ...Args>
static std::shared_ptr<LogSink> create(Args&&... args) {
return std::make_shared<T>(std::forward<Args>(args)...);
}
};
}
测试
#include <iostream>
#include <sstream>
#include "format.hpp"
#include "sink.hpp"
int main() {
// 创建一个 LogMsg 对象
MySpace::LogMsg logMsg(MySpace::LogLevel::INFO, 11, "test.cpp", "root", "This is a test message");
// 创建一个 Formatter 对象
MySpace::Formatter formatter;
// 对 LogMsg 进行格式化
std::string str = formatter.format(logMsg);
//三种输出方式
std::shared_ptr<MySpace::LogSink> stdout_ptr = MySpace::SinkFactory::create<MySpace::StdoutSink>();
std::shared_ptr<MySpace::LogSink> file_ptr = MySpace::SinkFactory::create<MySpace::FileSink>("./logfile/test.log");
std::shared_ptr<MySpace::LogSink> roll_ptr = MySpace::SinkFactory::create<MySpace::RollBySizeSink>("./logfile/roll-", 1024*1024);
stdout_ptr->log(str.c_str(), str.size());
file_ptr->log(str.c_str(), str.size());
roll_ptr->log(str.c_str(), str.size());
return 0;
}
//输出
//屏幕输出信息,当前路径下创建logfile目录,在该目录下创建指定文件和滚动文件
日志器类设计(建造者模式)-logger.hpp
从上面的测试可以看出,如果不对资源整合用户使用就是这么麻烦,所以对前面模块整合,向外提供接口完成不同等级的日志输出,支持多个落地方向,
- 同步日志器:直接对日志消息进行输出
- 异步日志器,将日志消息放入缓冲区中,由异步线程进行日志输出
整体框架
Logger基类-logger.hpp
抽象Logger基类,派生出同步日志器和异步日志器,两种日志器的落地方式不同所以将落地方式抽象
//logger.hpp
class Logger {
public:
Logger(const std::string &logger_name
, MySpace::LogLevel::value limit_level
, std::shared_ptr<MySpace::Formatter > formatter
, std::vector<std::shared_ptr<MySpace::LogSink >> sinks)
{}
//获取日志器名称
const std::string &name(){ return _logger_name; }
/* 构造日志消息对象过程, 并得到格式化后的日志消息字符串-- 然后进行落地输出*/
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:
void logMessage(MySpace::LogLevel::value level, const std::string& file, size_t line, const std::string &fmt, va_list ap) {}
/* 抽象接口完成实际的落地输出 -- 不同的日志器会有不同的实际落地方式 */
virtual void log(const std::string& data, size_t len) = 0;
protected:
std::mutex _mutex;
std::string _logger_name;
std::atomic<MySpace::LogLevel::value> _limit_level;
std::shared_ptr<MySpace::Formatter> _formatter;
std::vector<std::shared_ptr<MySpace::LogSink>> _sinks;
};
成员变量
- _mutex :一个日志器可能会被多个线程同时访问,确保同一时间日志落地的时候不会有其他线程干扰
- _logger_name :日志器名称
- _limit_level :日志输出等级,原子操作,比锁消耗性能低
- _fomatter :将LogMsg对象格式化成指定字符串
- _sinks :确定落地位置
下面是成员函数具体实现
class Logger {
public:
Logger(const std::string &logger_name
, MySpace::LogLevel::value limit_level
, std::shared_ptr<MySpace::Formatter > formatter
, std::vector<std::shared_ptr<MySpace::LogSink >> sinks)
:_logger_name(logger_name)
, _limit_level(limit_level)
, _formatter(formatter)
, _sinks(sinks.begin(), sinks.end())
{}
//获取日志器名称
const std::string &name(){ return _logger_name; }
/* 构造日志消息对象过程, 并得到格式化后的日志消息字符串-- 然后进行落地输出*/
void debug(const std::string& file, size_t line, const std::string &fmt, ...){
va_list ap;
va_start(ap, fmt);
logMessage(LogLevel::value::DEBUG, file, line, fmt, ap);
va_end(ap);
}
void info (const std::string& file, size_t line, const std::string &fmt, ...){
va_list ap;
va_start(ap, fmt);
logMessage(LogLevel::value::INFO, file, line, fmt, ap);
va_end(ap);
}
void warn (const std::string& file, size_t line, const std::string &fmt, ...){
va_list ap;
va_start(ap, fmt);
logMessage(LogLevel::value::WARN, file, line, fmt, ap);
va_end(ap);
}
void error(const std::string& file, size_t line, const std::string &fmt, ...){
va_list ap;
va_start(ap, fmt);
logMessage(LogLevel::value::ERROR, file, line, fmt, ap);
va_end(ap);
}
void fatal(const std::string& file, size_t line, const std::string &fmt, ...){
// 2、 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
logMessage(LogLevel::value::FATAL, file, line, fmt, ap);
va_end(ap);
}
protected:
void logMessage(MySpace::LogLevel::value level, const std::string& file, size_t line, const std::string &fmt, va_list ap) {
/* 通过传入的参数构造出一个日志消息对象,进行日志格式化,最终落地*/
// 1、 判断当前日志等级是否达到输出标准
if (level < _limit_level)
return;
// 2、 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
char *res = nullptr;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1) {
std::cout << "vasprintf failed! " << std::endl;
return;
}
// 3、 构造LogMsg对象
LogMsg msg(level, line, file, _logger_name, res);
// 4、 通过格式化工具对LogMsg进行格式化,获得格式化后的日志字符串
std::string real_message = _formatter->format(msg);
// 5、 进行日志落地
log(real_message.c_str(), real_message.size());
free(res);// vasprintf() 内部开辟空间了,是动态申请的,需要我们手动释放
}
/* 抽象接口完成实际的落地输出 -- 不同的日志器会有不同的实际落地方式 */
virtual void log(const std::string& data, size_t len) = 0;
protected:
std::mutex _mutex;
std::string _logger_name;
std::atomic<MySpace::LogLevel::value> _limit_level;
std::shared_ptr<MySpace::Formatter> _formatter;
std::vector<std::shared_ptr<MySpace::LogSink>> _sinks;
};
-
构造:用户指定日志器名称,日志器默认最低日志输出等级,格式化模块对象,落地器对象数组,每个日志接收器负责将格式化后的日志消息输出到不同的目标,如文件、控制台
-
name
函数:获取日志器名称 -
日志记录函数(
debug
、info
、warn
、error
、fatal
):记录不同级别的日志消息,va_list
类型的变量处理可变参数,具体实现交给logMessage
函数 -
logMessage
函数:实际处理日志消息的函数,可以简化日志记录函数,首先判断日志等级,之后将格式化字符串和可变参数列表组合,LogMsg对象包含所有信息,之后再格式化消息,在落地 -
log
纯虚函数
atomic
类-原子操作
#include <atomic>
std::atomic<int> atomicInt;
创建了一个 std::atomic 对象 atomicInt
va_list
-处理可变数量的参数
#include <cstdarg>
void xxx(int count, ...)
va_list ap;
va_start(ap, count);
va_end(ap);
va_start(ap, count)
:这个宏用于初始化va_list
类型的变量ap
,count
是函数中...
前一个变量va_end(ap)
:用于清理va_list
相关的资源
vasprintf
函数-生成一个格式化的字符串
根据格式化字符串和可变参数列表生成一个格式化的字符串,并将结果存储在动态分配的内存中
int vasprintf(char **strp, const char *fmt, va_list ap);
//使用
void print_formatted_string(const char *fmt, ...){
va_list ap;
va_start(ap, fmt);
char *result = nullptr;
int ret = vasprintf(&result, fmt, ap);
if (ret == -1) {
perror("vasprintf");
return;
}
//其他代码
free(result);
va_end(ap);
}
SynchLogger
派生类-同步日志器-logger.hpp
日志消息应如何落地
class SynchLogger : public Logger {
public:
SynchLogger(const std::string &logger_name
, MySpace::LogLevel::value limit_level
, std::shared_ptr<MySpace::Formatter> formatter
, std::vector<std::shared_ptr<MySpace::LogSink>> sinks)
: Logger(logger_name, limit_level, formatter, sinks)
{}
protected:
/* 同步日志器,是将日志直接通过落地模块 句柄进行日志落地 */
void log(const std::string& data, size_t len) override{
std::unique_lock<std::mutex> lock(_mutex);
if (_sinks.empty()) return;
for (auto &sink : _sinks) {
sink->log(data, len);
}
}
};
- 构造:全部传给基类
log
:互斥锁可以保证同一时间只有一个线程能够执行日志落地操作,把日志消息通过_sinks
里的各个日志接收器输出到指定位置
unique_lock
类-锁
#include <mutex>
std::unique_lock<std::mutex> lock;
std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx);
- 锁在离开作用域时自动释放
Logger
类和SynchLogger
完整代码和测试
//logger.hpp
#pragma once
#include <iostream>
#include <unordered_map>
#include <string>
#include <vector>
#include <mutex>
#include <memory>
#include <atomic>
#include <cstdarg>
#include <condition_variable>
#include "buffer.hpp"
#include "format.hpp"
#include "level.hpp"
#include "looper.hpp"
#include "message.hpp"
#include "sink.hpp"
#include "util.hpp"
namespace MySpace{
class Logger {
public:
Logger(const std::string &logger_name
, MySpace::LogLevel::value limit_level
, std::shared_ptr<MySpace::Formatter > formatter
, std::vector<std::shared_ptr<MySpace::LogSink >> sinks)
:_logger_name(logger_name)
, _limit_level(limit_level)
, _formatter(formatter)
, _sinks(sinks.begin(), sinks.end())
{}
//获取日志器名称
const std::string &name(){ return _logger_name; }
/* 构造日志消息对象过程, 并得到格式化后的日志消息字符串-- 然后进行落地输出*/
void debug(const std::string& file, size_t line, const std::string &fmt, ...){
va_list ap;
va_start(ap, fmt);
logMessage(LogLevel::value::DEBUG, file, line, fmt, ap);
va_end(ap);
}
void info (const std::string& file, size_t line, const std::string &fmt, ...){
va_list ap;
va_start(ap, fmt);
logMessage(LogLevel::value::INFO, file, line, fmt, ap);
va_end(ap);
}
void warn (const std::string& file, size_t line, const std::string &fmt, ...){
va_list ap;
va_start(ap, fmt);
logMessage(LogLevel::value::WARN, file, line, fmt, ap);
va_end(ap);
}
void error(const std::string& file, size_t line, const std::string &fmt, ...){
va_list ap;
va_start(ap, fmt);
logMessage(LogLevel::value::ERROR, file, line, fmt, ap);
va_end(ap);
}
void fatal(const std::string& file, size_t line, const std::string &fmt, ...){
// 2、 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
logMessage(LogLevel::value::FATAL, file, line, fmt, ap);
va_end(ap);
}
protected:
void logMessage(MySpace::LogLevel::value level, const std::string& file, size_t line, const std::string &fmt, va_list ap) {
/* 通过传入的参数构造出一个日志消息对象,进行日志格式化,最终落地*/
// 1、 判断当前日志等级是否达到输出标准
if (level < _limit_level)
return;
// 2、 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
char *res = nullptr;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1) {
std::cout << "vasprintf failed! " << std::endl;
return;
}
// 3、 构造LogMsg对象
LogMsg msg(level, line, file, _logger_name, res);
// 4、 通过格式化工具对LogMsg进行格式化,获得格式化后的日志字符串
std::string real_message = _formatter->format(msg);
// 5、 进行日志落地
log(real_message.c_str(), real_message.size());
free(res);// vasprintf() 内部开辟空间了,是动态申请的,需要我们手动释放
}
/* 抽象接口完成实际的落地输出 -- 不同的日志器会有不同的实际落地方式 */
virtual void log(const std::string& data, size_t len) = 0;
protected:
std::mutex _mutex;
std::string _logger_name;
std::atomic<MySpace::LogLevel::value> _limit_level;
std::shared_ptr<MySpace::Formatter> _formatter;
std::vector<std::shared_ptr<MySpace::LogSink>> _sinks;
};
enum LoggerType {
LOGGER_SYNCH, //同步日志器
LOGGER_ASYNCH //异步日志器
};
class SynchLogger : public Logger {
public:
SynchLogger(const std::string &logger_name
, MySpace::LogLevel::value limit_level
, std::shared_ptr<MySpace::Formatter> formatter
, std::vector<std::shared_ptr<MySpace::LogSink>> sinks)
: Logger(logger_name, limit_level, formatter, sinks)
{}
protected:
/* 同步日志器,是将日志直接通过落地模块 句柄进行日志落地 */
void log(const std::string& data, size_t len) override{
std::unique_lock<std::mutex> lock(_mutex);
if (_sinks.empty()) return;
for (auto &sink : _sinks) {
sink->log(data, len);
}
}
};
}
测试
#include "logger.hpp"
#include "format.hpp"
#include "sink.hpp"
#include <iostream>
#include <memory>
#include <vector>
// 主测试函数
int main() {
// 创建 Formatter
auto formatter = std::make_shared<MySpace::Formatter >("[%d{%H:%M:%S}][%c][%f:%l][%p]%T%m%n");
//三种输出方式
std::shared_ptr<MySpace::LogSink> stdout_ptr = MySpace::SinkFactory::create<MySpace::StdoutSink>();
std::shared_ptr<MySpace::LogSink> file_ptr = MySpace::SinkFactory::create<MySpace::FileSink>("./logfile/test.log");
std::shared_ptr<MySpace::LogSink> roll_ptr = MySpace::SinkFactory::create<MySpace::RollBySizeSink>("./logfile/roll-", 1024*1024);
std::vector<std::shared_ptr<MySpace::LogSink>> sinks = {stdout_ptr,file_ptr,roll_ptr};
// 创建 logger,日志等级是WARN,所以等级小于WARN的不会输出
MySpace::SynchLogger logger("TestLogger", MySpace::LogLevel::value::WARN, formatter, sinks);
// 测试不同级别的日志记录
logger.debug(__FILE__, __LINE__, "This is a debug message");
logger.info(__FILE__, __LINE__, "This is an info message");
logger.warn(__FILE__, __LINE__, "This is a warn message");
logger.error(__FILE__, __LINE__, "This is an error message");
logger.fatal(__FILE__, __LINE__, "This is a fatal message");
return 0;
}
//终端输出,因为设置等级为WARN,所以等级小于WARN的不会输出
[00:31:42][TestLogger][test.cpp:27][WARN] This is a warn message
[00:31:42][TestLogger][test.cpp:28][ERROR] This is an error message
[00:31:42][TestLogger][test.cpp:29][FATAL] This is a fatal message
//当前路径下创建logfile目录,在该目录下创建指定文件和滚动文件
//然后在文件当中输出和终端一样的内容
异步日志器
为了避免写日志过程阻塞,使用异步日志器,首先将消息放到缓冲区,之后会有异步线程处理日志落地。所以首先设计缓冲区,之后设计异步类,最后用建造者负责具体的建造步骤。
Buffer
类-缓冲区设计-在buffer.hpp
使用双缓冲区,避免空间的频繁申请和释放,分为任务写入缓冲区和任务处理缓冲区,业务线程将日志写入缓冲区,异步工作线程处理任务缓冲区,当写入缓冲区满了后和任务处理缓冲区交换。
//buffer.hpp
#pragma once
#include "level.hpp"
#include <vector>
#include <iostream>
#include <assert.h>
#define DEFAULT_BUFFER_SIZE (1 * 1024 * 1024)//1M大小
#define THRESHOLD_BUFFER_SIZE (8 * 1024 * 1024)//8M大小
#define INCREMENT_BUFFER_SIZE (1 * 1024 * 1024)//1M大小
namespace MySpace{
class Buffer{
public:
Buffer()
: _buffer(DEFAULT_BUFFER_SIZE)
, _write_idx(0)
, _read_idx(0)
{}
// 向缓冲区写入数据
void push(const char* data, size_t len){
// 缓冲区剩余空间不够的情况:
// if (len > writeAbleSize()) return; //情况一满了返回
ensureEnoughSize(len); //情况二扩容
// 1、将数据拷贝进缓冲区
std::copy(data, data + len, &_buffer[_write_idx]);
// 2、将当前写入数据向后偏移
moveWriter(len);
}
// 返回可读数据的起始地址
const char* begin() { return &_buffer[_read_idx]; }
// 返回可读数据的长度
size_t readAbleSize() { return _write_idx-_read_idx; }
// 返回可写空间的长度
size_t writeAbleSize() { return _buffer.size()-_write_idx; }
// 对读写指针进行向后偏移操作
void moveWriter(size_t len) { assert(len+_write_idx <= writeAbleSize()); _write_idx += len; }
// 对读写指针进行向后偏移操作
void moveReader(size_t len) { assert(len <= readAbleSize()); _read_idx += len; }
// 重制读写位置,初始化缓冲区
void bufferReset() { _read_idx = 0; _write_idx = 0; }
// 对buffer实现交换的操作
void bufferSwap(Buffer &buffer){
_buffer.swap(buffer._buffer);
std::swap(_read_idx, buffer._read_idx);
std::swap(_write_idx, buffer._write_idx);
}
// 判断缓冲区是否为空
bool bufferEmpty() { return _read_idx == _write_idx; }
// 对空间进行扩容操作
void ensureEnoughSize(size_t len){
if (len <= writeAbleSize()) return;
size_t new_size = 0;
while (writeAbleSize() < len) {
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 _read_idx; // 当前可读数据的指针
size_t _write_idx; // 当前可写数据的指针
};
}
-
成员变量:用vector接收字符,用读和写当作两个指针
-
构造:vector默认大小是1M,初始化读和写的位置为0
-
push
:向缓冲区中写入长度为len
的数据 -
begin()
:返回可读数据的起始地址 -
readAbleSize()
:返回可读数据的长度 -
writeAbleSize()
:返回可写空间的长度 -
moveWriter()
:将_write_idx
向后移动len
-
moveReader()
:将_read_idx
向后移动len
-
bufferReset()
:重置 -
bufferSwap()
:将当前缓冲区对象与另一个Buffer
对象进行交换 -
bufferEmpty()
:判断缓冲区是否为空 -
ensureEnoughSize()
:确保缓冲区有足够的空间来存储长度为len
的数据,不够就扩容
copy
函数-把一个范围内的元素复制到另一个范围
#include <algorithm>
template< class InputIt, class OutputIt >
OutputIt copy( InputIt first, InputIt last, OutputIt d_first );
//使用
std::vector<int, 5> source = {1, 2, 3, 4, 5};
std::vector<int, 5> destination;
// 复制 source 数组的元素到 destination 数组
std::copy(source.begin(), source.end(), destination.begin());
first
、last
:是源元素的范围d_first
:指定了复制元素的目标范围起始位置
AsynchLogger
派生类-异步日志器-logger.hpp
//logger.hpp
#pragma once
#include <iostream>
#include <unordered_map>
#include <string>
#include <vector>
#include <mutex>
#include <memory>
#include <atomic>
#include <cstdarg>
#include <condition_variable>
#include "buffer.hpp"
#include "format.hpp"
#include "level.hpp"
#include "looper.hpp"
#include "message.hpp"
#include "sink.hpp"
#include "util.hpp"
namespace MySpace{
class Logger {
public:
Logger(const std::string &logger_name
, MySpace::LogLevel::value limit_level
, std::shared_ptr<MySpace::Formatter > formatter
, std::vector<std::shared_ptr<MySpace::LogSink >> sinks)
:_logger_name(logger_name)
, _limit_level(limit_level)
, _formatter(formatter)
, _sinks(sinks.begin(), sinks.end())
{}
//获取日志器名称
const std::string &name(){ return _logger_name; }
/* 构造日志消息对象过程, 并得到格式化后的日志消息字符串-- 然后进行落地输出*/
void debug(const std::string& file, size_t line, const std::string &fmt, ...){
va_list ap;
va_start(ap, fmt);
logMessage(LogLevel::value::DEBUG, file, line, fmt, ap);
va_end(ap);
}
void info (const std::string& file, size_t line, const std::string &fmt, ...){
va_list ap;
va_start(ap, fmt);
logMessage(LogLevel::value::INFO, file, line, fmt, ap);
va_end(ap);
}
void warn (const std::string& file, size_t line, const std::string &fmt, ...){
va_list ap;
va_start(ap, fmt);
logMessage(LogLevel::value::WARN, file, line, fmt, ap);
va_end(ap);
}
void error(const std::string& file, size_t line, const std::string &fmt, ...){
va_list ap;
va_start(ap, fmt);
logMessage(LogLevel::value::ERROR, file, line, fmt, ap);
va_end(ap);
}
void fatal(const std::string& file, size_t line, const std::string &fmt, ...){
// 2、 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
logMessage(LogLevel::value::FATAL, file, line, fmt, ap);
va_end(ap);
}
protected:
void logMessage(MySpace::LogLevel::value level, const std::string& file, size_t line, const std::string &fmt, va_list ap) {
/* 通过传入的参数构造出一个日志消息对象,进行日志格式化,最终落地*/
// 1、 判断当前日志等级是否达到输出标准
if (level < _limit_level)
return;
// 2、 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
char *res = nullptr;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1) {
std::cout << "vasprintf failed! " << std::endl;
return;
}
// 3、 构造LogMsg对象
LogMsg msg(level, line, file, _logger_name, res);
// 4、 通过格式化工具对LogMsg进行格式化,获得格式化后的日志字符串
std::string real_message = _formatter->format(msg);
// 5、 进行日志落地
log(real_message.c_str(), real_message.size());
free(res);// vasprintf() 内部开辟空间了,是动态申请的,需要我们手动释放
}
/* 抽象接口完成实际的落地输出 -- 不同的日志器会有不同的实际落地方式 */
virtual void log(const std::string& data, size_t len) = 0;
protected:
std::mutex _mutex;
std::string _logger_name;
std::atomic<MySpace::LogLevel::value> _limit_level;
std::shared_ptr<MySpace::Formatter> _formatter;
std::vector<std::shared_ptr<MySpace::LogSink>> _sinks;
};
enum LoggerType {
LOGGER_SYNCH, //同步日志器
LOGGER_ASYNCH //异步日志器
};
class AsynchLogger : public Logger {
public:
AsynchLogger(const std::string &logger_name
, LogLevel::value level
, std::shared_ptr<Formatter> &formatter
, std::vector<std::shared_ptr<LogSink>> &sinks)
: Logger(logger_name, level, formatter, sinks)
, _looper(std::make_shared<AsynchLooper>(std::bind(&AsynchLogger::realLog, this, std::placeholders::_1)))
{}
/* 将数据写入缓冲区*/
virtual void log(const std::string& data, size_t len) override{
_looper->push(data.c_str(), len);
}
/* 设计一个实际落地函数(将缓冲区中的数据落地) */
void realLog(Buffer &buf) {
if (_sinks.empty()) return;
for (auto &sink : _sinks) {
sink->log(buf.begin(), buf.readAbleSize());
}
}
private:
std::shared_ptr<AsynchLooper> _looper;
};
}
bind
函数
注意区分套接字中的bind,C++中bind把一个可调用对象(像函数、成员函数、函数对象等)和它的部分参数绑定在一起,生成一个新的可调用对象
#include <functional>
template< class F, class... Args >
bind( F&& f, Args&&... args );
//使用
// 绑定 add 函数,固定第一个参数为 3,且还需要一个参数
int add(int a, int b){}
auto addThree = std::bind(add, 3, std::placeholders::_1);
需要注意的是,普通函数可以直接调用,成员函数由于和类紧密相关,所以调用时,传函数变成传函数指针,同时还要提供一个对象的指针
placeholders
-命名空间
一个命名空间,指定新可调用对象传入的参数在原可调用对象中的位置
#include <functional>
void test(int a, int b, int c){}
auto boundFunc = std::bind(test, std::placeholders::_2, std::placeholders::_3, std::placeholders::_1);
boundFunc(3, 1, 2);
- 创建了一个新的可调用对象
boundFunc
boundFunc(3, 1, 2)
:test
函数的第一个参数绑定的是std::placeholders::_2
,所以调用boundFunc
时传入的第二个参数1
会作为test
函数的第一个参数
LoggerBuilder
类-建造者-logger.hpp
建造者分为局部建造者(LocalLoggerBuilder)和全局建造者(GlobalLoggerBuilder),目的是为了传入相关参数就能会返回对应的日志器,和局部相比,全局日志器会自动添加构建出来的日志器到日志管理器单例对象当中
全局建造者单例模式-懒汉模式-logger.hpp
对于全局建造者我们新增懒汉模式,当全局需要实例化一个全局建造者类时,我们新增一个实例
完整代码和测试
//logger.hpp
#pragma once
#include <iostream>
#include <unordered_map>
#include <string>
#include <vector>
#include <mutex>
#include <memory>
#include <atomic>
#include <cstdarg>
#include <condition_variable>
#include "buffer.hpp"
#include "format.hpp"
#include "level.hpp"
#include "looper.hpp"
#include "message.hpp"
#include "sink.hpp"
#include "util.hpp"
namespace MySpace{
class Logger {
public:
Logger(const std::string &logger_name
, MySpace::LogLevel::value limit_level
, std::shared_ptr<MySpace::Formatter > formatter
, std::vector<std::shared_ptr<MySpace::LogSink >> sinks)
:_logger_name(logger_name)
, _limit_level(limit_level)
, _formatter(formatter)
, _sinks(sinks.begin(), sinks.end())
{}
//获取日志器名称
const std::string &name(){ return _logger_name; }
/* 构造日志消息对象过程, 并得到格式化后的日志消息字符串-- 然后进行落地输出*/
void debug(const std::string& file, size_t line, const std::string &fmt, ...){
va_list ap;
va_start(ap, fmt);
logMessage(LogLevel::value::DEBUG, file, line, fmt, ap);
va_end(ap);
}
void info (const std::string& file, size_t line, const std::string &fmt, ...){
va_list ap;
va_start(ap, fmt);
logMessage(LogLevel::value::INFO, file, line, fmt, ap);
va_end(ap);
}
void warn (const std::string& file, size_t line, const std::string &fmt, ...){
va_list ap;
va_start(ap, fmt);
logMessage(LogLevel::value::WARN, file, line, fmt, ap);
va_end(ap);
}
void error(const std::string& file, size_t line, const std::string &fmt, ...){
va_list ap;
va_start(ap, fmt);
logMessage(LogLevel::value::ERROR, file, line, fmt, ap);
va_end(ap);
}
void fatal(const std::string& file, size_t line, const std::string &fmt, ...){
// 2、 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
va_list ap;
va_start(ap, fmt);
logMessage(LogLevel::value::FATAL, file, line, fmt, ap);
va_end(ap);
}
protected:
void logMessage(MySpace::LogLevel::value level, const std::string& file, size_t line, const std::string &fmt, va_list ap) {
/* 通过传入的参数构造出一个日志消息对象,进行日志格式化,最终落地*/
// 1、 判断当前日志等级是否达到输出标准
if (level < _limit_level)
return;
// 2、 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串
char *res = nullptr;
int ret = vasprintf(&res, fmt.c_str(), ap);
if (ret == -1) {
std::cout << "vasprintf failed! " << std::endl;
return;
}
// 3、 构造LogMsg对象
LogMsg msg(level, line, file, _logger_name, res);
// 4、 通过格式化工具对LogMsg进行格式化,获得格式化后的日志字符串
std::string real_message = _formatter->format(msg);
// 5、 进行日志落地
log(real_message.c_str(), real_message.size());
free(res);// vasprintf() 内部开辟空间了,是动态申请的,需要我们手动释放
}
/* 抽象接口完成实际的落地输出 -- 不同的日志器会有不同的实际落地方式 */
virtual void log(const std::string& data, size_t len) = 0;
protected:
std::mutex _mutex;
std::string _logger_name;
std::atomic<MySpace::LogLevel::value> _limit_level;
std::shared_ptr<MySpace::Formatter> _formatter;
std::vector<std::shared_ptr<MySpace::LogSink>> _sinks;
};
enum LoggerType {
LOGGER_SYNCH, //同步日志器
LOGGER_ASYNCH //异步日志器
};
class SynchLogger : public Logger {
public:
SynchLogger(const std::string &logger_name
, MySpace::LogLevel::value limit_level
, std::shared_ptr<MySpace::Formatter> formatter
, std::vector<std::shared_ptr<MySpace::LogSink>> sinks)
: Logger(logger_name, limit_level, formatter, sinks)
{}
protected:
/* 同步日志器,是将日志直接通过落地模块 句柄进行日志落地 */
void log(const std::string& data, size_t len) override{
std::unique_lock<std::mutex> lock(_mutex);
if (_sinks.empty()) return;
for (auto &sink : _sinks) {
sink->log(data, len);
}
}
};
class AsynchLogger : public Logger {
public:
AsynchLogger(const std::string &logger_name
, LogLevel::value level
, std::shared_ptr<Formatter> &formatter
, std::vector<std::shared_ptr<LogSink>> &sinks)
: Logger(logger_name, level, formatter, sinks)
, _looper(std::make_shared<AsynchLooper>(std::bind(&AsynchLogger::realLog, this, std::placeholders::_1)))
{}
/* 将数据写入缓冲区*/
virtual void log(const std::string& data, size_t len) override{
_looper->push(data.c_str(), len);
}
/* 设计一个实际落地函数(将缓冲区中的数据落地) */
void realLog(Buffer &buf) {
if (_sinks.empty()) return;
for (auto &sink : _sinks) {
sink->log(buf.begin(), buf.readAbleSize());
}
}
private:
std::shared_ptr<AsynchLooper> _looper;
};
class LoggerBuilder {
public:
LoggerBuilder()
: _logger_type(LoggerType::LOGGER_SYNCH)
, _limit_level(LogLevel::value::DEBUG)
{}
void buildLoggerType(LoggerType type) { _logger_type = type; }
void buildLoggerName(const std::string &name) { _logger_name = name; }
void buildLoggerLevel(LogLevel::value level) { _limit_level = level; }
void buildLoggerFormatter(const std::string &pattern) { _formatter.reset(new Formatter(pattern)); }
template<typename SinkType, typename ...Args>
void buildSink(Args&&... args) { _sinks.push_back(SinkFactory::create<SinkType>(std::forward<Args>(args)...)); }
virtual std::shared_ptr<Logger> build() = 0; // 建造日志器
protected:
LoggerType _logger_type;
std::string _logger_name;
LogLevel::value _limit_level; // 需要频繁访问
std::shared_ptr<Formatter> _formatter;
std::vector<std::shared_ptr<LogSink>> _sinks;
};
class LocalLoggerBuilder : public LoggerBuilder {
public:
virtual std::shared_ptr<Logger> 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_ASYNCH) {//异步日志器
return std::make_shared<AsynchLogger>(_logger_name, _limit_level, _formatter, _sinks);
}
//同步日志器
return std::make_shared<SynchLogger>(_logger_name, _limit_level, _formatter, _sinks);
}
};
//日志器建造者-懒汉模式
class LoggerManager {
public:
static LoggerManager& getInstance(){
//声明,静态局部变量没有构造完成之前,其他线程就会阻塞
static LoggerManager eton;
return eton;
}
//添加
void addLogger(std::shared_ptr<Logger> &logger){
//防止重复添加
if(findLogger(logger->name()))return;
std::unique_lock<std::mutex>(_mutex);
_loggers.insert(std::make_pair(logger->name(), logger));
}
//查找
bool findLogger(const std::string &name){
std::unique_lock<std::mutex>(_mutex);
//没找到
if(_loggers.find(name) == _loggers.end()){
return false;
}
return true;
}
//获取
std::shared_ptr<Logger> getLogger(const std::string &name){
std::unique_lock<std::mutex>(_mutex);
//没找到
if(_loggers.find(name) == _loggers.end()){
return std::shared_ptr<Logger>();
}
return _loggers.find(name)->second;
}
std::shared_ptr<Logger> rootLogger() { return _root_logger; }
private:
//构造函数私有
LoggerManager() {
std::shared_ptr<LoggerBuilder> LoggerBuilder(new MySpace::LocalLoggerBuilder());
LoggerBuilder->buildLoggerName("root");
_root_logger = LoggerBuilder->build();
_loggers.insert(std::make_pair("root", _root_logger));
}
LoggerManager(const LoggerManager&) = delete; //删除拷贝构造
std::mutex _mutex;
std::shared_ptr<Logger> _root_logger; // 默认日志器
std::unordered_map<std::string, std::shared_ptr<Logger>> _loggers;
};
/* 全局日志器建造者 -- 在局部的基础上新增:自动添加日志器到单例对象中 */
class GlobalLoggerBuilder : public LoggerBuilder {
public:
virtual std::shared_ptr<Logger> build() override {
assert(!_logger_name.empty()); // 必须有日志器名称
if (_formatter.get() == nullptr) { _formatter = std::make_shared<Formatter>(); }
if (_sinks.empty()) { buildSink<StdoutSink>(); }
std::shared_ptr<Logger> logger;
if (_logger_type == LoggerType::LOGGER_ASYNCH) {
logger = std::make_shared<AsynchLogger>(_logger_name, _limit_level, _formatter, _sinks);
} else {
logger = std::make_shared<SynchLogger>(_logger_name, _limit_level, _formatter, _sinks);
}
LoggerManager::getInstance().addLogger(logger); // 新增
return logger;
}
};
}
测试
#include "logger.hpp"
#include "format.hpp"
#include "sink.hpp"
#include <iostream>
#include <memory>
#include <vector>
// 主测试函数
int main() {
// 创建 LocalLoggerBuilder 对象
MySpace::LocalLoggerBuilder localBuilder;
// 设置日志器名称
localBuilder.buildLoggerName("TestLogger");
// 设置日志级别
localBuilder.buildLoggerLevel(MySpace::LogLevel::value::DEBUG);
// 设置格式化器模式
localBuilder.buildLoggerFormatter("%m%n");
// 添加一个 StdoutSink
localBuilder.buildSink<MySpace::StdoutSink>();
// 构建日志器
std::shared_ptr<MySpace::Logger> logger = localBuilder.build();
// 测试日志记录函数
logger->info(__FILE__, __LINE__, "This is a test info message");
logger->debug(__FILE__, __LINE__, "This is a test debug message");
return 0;
}
//输出This is a test info message This is a test debug message
AsynchLooper
类-异步工作器-looper.hpp
外界将任务数据放到缓冲区中,异步线程对缓冲区中数据进行处理,使用的是生产消费模型
push
:是生产者不断将数据写到_produce_buffer
生产缓冲区threadEntry
:是消费者不断检查_produce_buffer
生产缓冲区是否有数据。如果有数据,将其交换到_consumer_buffer
消费缓冲区,并调用回调函数_callBack
进行处理
//looper.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <mutex>
#include <memory>
#include <atomic>
#include <cstdarg>
#include <condition_variable>
#include "buffer.hpp"
#include "format.hpp"
#include "level.hpp"
#include "message.hpp"
#include "sink.hpp"
#include "util.hpp"
namespace MySpace{
class AsynchLooper {
public:
AsynchLooper(const std::function<void(Buffer &)> &cb)
:_stop(false)
, _thread(std::thread(&AsynchLooper::threadEntry, this))
, _callBack(cb)
{}
~AsynchLooper(){
_stop = true; // 退出标志设置为true
_consumer_cond.notify_all(); // 唤醒所有工作线程
_thread.join(); // 等待工作线程退出
}
void push(const char *data, size_t len) {
std::unique_lock<std::mutex> lock(_mutex);
//缓冲区满了就阻塞
_produce_cond.wait(lock, [&](){ return _produce_buffer.writeAbleSize() >= len; });
//向缓冲区添加数据
_produce_buffer.push(data, len);
// 唤醒消费者对缓冲区中的数据进行处理
_consumer_cond.notify_one();
}
/* 线程的入口函数 -- 对消费缓冲区中的数据进行处理,处理完毕后,初始化缓冲区,交换缓冲区*/
void threadEntry() {
while (1) {
//互斥锁设置生命周期,交换完后解锁,不对数据过程加锁
{
// 1、 判断生产缓冲区有没有数据,有则交换,无则阻塞
std::unique_lock<std::mutex> lock(_mutex);
// 如果 _stop 标志为 true 且生产缓冲区为空,说明线程需要退出,此时跳出循环
if (_stop && _produce_buffer.bufferEmpty()) break;
//退出前被唤醒,或者有数据被唤醒,返回真,继续向下运行
_consumer_cond.wait(lock, [&](){ return ( _stop || !_produce_buffer.bufferEmpty()); });
//再次检查 _stop 标志,防止在等待期间 _stop 被设置为 true,如果是则跳出循环
if (_stop && _produce_buffer.bufferEmpty()) {
break;
}
_produce_buffer.bufferSwap(_consumer_buffer);
// 2、 唤醒生产者(只有安全状态生产者才会被阻塞)
_produce_cond.notify_all();
}
// 3、 被唤醒后,对消费缓冲区进行数据处理(处理过程无需加锁保护)
_callBack(_consumer_buffer);
// 4、 初始化消费缓冲区
_consumer_buffer.bufferReset();
}
}
private:
std::atomic<bool> _stop; // 工作器停止标志
std::mutex _mutex;
Buffer _produce_buffer; // 生产缓冲区
Buffer _consumer_buffer; // 消费缓冲区
std::condition_variable _produce_cond; // 生产条件变量
std::condition_variable _consumer_cond; // 消费条件变量
std::thread _thread;
std::function<void(Buffer &)> _callBack; //回调函数 具体对缓冲区数据进行处理的回调函数, 由异步工作器的使用者传入
};
}
成员变量
_stop
:控制生产消费模型的生产和停止,true
时,表示停止工作_thread
:消费者需要异步线程来处理缓冲区数据_produce_buffer
和_consumer_buffer
:缓冲区作为共享对象_mutex
:共享对象在访问时需要加锁互斥_produce_cond
和_consumer_cond
:条件变量用于线程通讯,当生产缓冲区满了生产者线程会调用,当生产缓冲区空了消费者线程会调用- 回调函数:
function
对象,它可以存储任意可调用对象,这里是存放一个参数是Buffer &
,返回值是void的函数指针,当消费缓冲区中有数据时,会调用这个回调函数来处理这些数据
成员函数
-
构造:初始化,注意线程对象初始化成员函数的方式
-
push
:用lambda 表达式,调用wait
时会先检查这个谓词,如果为false
则进入等待状态;被唤醒后会再次检查谓词,只有当谓词为true
(即生产缓冲区有足够空间)时才会继续执行后续代码,只有满足条件时才会继续执行。不能下面这么写,没有处理虚假唤醒-
if(_produce_buffer.writeAbleSize() < len){ _produce_cond.wait(lock); }
-
-
threadEntry
:同样wait
直到满足_stop
为true
或者生产缓冲区不为空的条件
logger.hpp-完整代码和测试
//looper.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <mutex>
#include <memory>
#include <atomic>
#include <cstdarg>
#include <condition_variable>
#include "buffer.hpp"
#include "format.hpp"
#include "level.hpp"
#include "message.hpp"
#include "sink.hpp"
#include "util.hpp"
namespace MySpace{
class AsynchLooper {
public:
AsynchLooper(const std::function<void(Buffer &)> &cb)
:_stop(false)
, _thread(std::thread(&AsynchLooper::threadEntry, this))
, _callBack(cb)
{}
~AsynchLooper(){
_stop = true; // 退出标志设置为true
_consumer_cond.notify_all(); // 唤醒所有工作线程
_thread.join(); // 等待工作线程退出
}
void push(const char *data, size_t len) {
std::unique_lock<std::mutex> lock(_mutex);
//缓冲区满了就阻塞
_produce_cond.wait(lock, [&](){ return _produce_buffer.writeAbleSize() >= len; });
//向缓冲区添加数据
_produce_buffer.push(data, len);
// 唤醒消费者对缓冲区中的数据进行处理
_consumer_cond.notify_one();
}
/* 线程的入口函数 -- 对消费缓冲区中的数据进行处理,处理完毕后,初始化缓冲区,交换缓冲区*/
void threadEntry() {
while (!_stop) {
//互斥锁设置生命周期,交换完后解锁,不对数据过程加锁
{
// 1、 判断生产缓冲区有没有数据,有则交换,无则阻塞
std::unique_lock<std::mutex> lock(_mutex);
// 如果 _stop 标志为 true 且生产缓冲区为空,说明线程需要退出,此时跳出循环
if (_stop && _produce_buffer.bufferEmpty()) break;
//退出前被唤醒,或者有数据被唤醒,返回真,继续向下运行
_consumer_cond.wait(lock, [&](){ return ( _stop || !_produce_buffer.bufferEmpty()); });
//再次检查 _stop 标志,防止在等待期间 _stop 被设置为 true,如果是则跳出循环
if (_stop) {
break;
}
_produce_buffer.bufferSwap(_consumer_buffer);
// 2、 唤醒生产者(只有安全状态生产者才会被阻塞)
_produce_cond.notify_all();
}
// 3、 被唤醒后,对消费缓冲区进行数据处理(处理过程无需加锁保护)
_callBack(_consumer_buffer);
// 4、 初始化消费缓冲区
_consumer_buffer.bufferReset();
}
}
private:
std::atomic<bool> _stop; // 工作器停止标志
std::mutex _mutex;
Buffer _produce_buffer; // 生产缓冲区
Buffer _consumer_buffer; // 消费缓冲区
std::condition_variable _produce_cond; // 生产条件变量
std::condition_variable _consumer_cond; // 消费条件变量
std::thread _thread;
std::function<void(Buffer &)> _callBack; //回调函数 具体对缓冲区数据进行处理的回调函数, 由异步工作器的使用者传入
};
}
测试
#include "looper.hpp"
#include "buffer.hpp"
#include <iostream>
#include <string>
#include <cstring>
#include <thread>
// 回调函数,用于处理缓冲区数据
void callback(MySpace::Buffer& buffer) {
std::cout << "Processing data: ";
const char* data = buffer.begin();
size_t size = buffer.readAbleSize();
for (size_t i = 0; i < size; ++i) {
std::cout << data[i];
}
std::cout << std::endl;
// 模拟处理后移动读指针
buffer.moveReader(size);
}
int main() {
// 创建 AsynchLooper 实例
std::shared_ptr<MySpace::AsynchLooper> looper = std::make_shared<MySpace::AsynchLooper>(callback);
// 向缓冲区推送数据
const char* testData = "Hello, AsynchLooper!";
size_t dataLen = strlen(testData);
looper->push(testData, dataLen);
// 等待一段时间,确保数据被处理
std::this_thread::sleep_for(std::chrono::seconds(1));
// 再次推送数据
const char* testData2 = "Another test message.";
size_t dataLen2 = strlen(testData2);
looper->push(testData2, dataLen2);
// 等待一段时间,确保数据被处理
std::this_thread::sleep_for(std::chrono::seconds(1));
// 销毁 AsynchLooper 实例
looper.reset();
return 0;
}
//输出
//Processing data: Hello, AsynchLooper!
//Processing data: Another test message.
condition_variable
类-条件变量
用于线程间通信,可任意控制线程等待和执行,一般和mutex
一起使用
#include <condition_variable>
//使用
std::condition_variable cv;
std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx);
cv.notify_one(); // 唤醒一个等待condition_variable 对象的线程
cv.notify_all(); // 唤醒所有等待condition_variable 对象的线程
cv.wait(lock); // 线程阻塞
cv.wait(lock, [] { return ready; });//等待直到 ready 为 true
- void wait( std::unique_lock< std::mutex >& lock, Predicate pred );
thread
类-线程
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );
//使用
std::thread t(printMessage);//创建线程并执行 printMessage 函数
t.join();// 等待线程执行完毕
//传递类成员函数,注意要有函数指针,类对象指针,若成员函数有参数,还需传递这些参数
MyClass obj;
std::thread t(&MyClass::memberFunction, &obj);
t.join();
-
join()
:阻塞当前线程,直到被调用线程执行完毕 -
detach()
:线程分离,允许线程独立执行,无法再对该线程进行join()
或detach()
操作
日志宏全局接口设计
为了简化用户管理,给接口在包装下,方便用户使用
//mylog.hpp
#pragma once
#include "logger.hpp"
namespace MySpace{
// 1、提供获取指定日志器的全局接口(避免用户自己操作单例对象)
std::shared_ptr<Logger> getLogger(const std::string& name) {
return LoggerManager::getInstance().getLogger(name);
}
std::shared_ptr<Logger> rootLogger() {
return LoggerManager::getInstance().rootLogger();
}
// 2、使用宏函数对日志器接口进行代理(代理模式)
#define debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define info(fmt, ...) info (__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define warn(fmt, ...) warn (__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define error(fmt, ...) error(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define fatal(fmt, ...) fatal(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
// 3、提供宏函数,直接通过默认日志器进行日志的标准输出打印(无需获取日志器)
#define DEBUG(fmt, ...) rootLogger()->debug(fmt, ##__VA_ARGS__)
#define INFO(fmt, ...) rootLogger()->info (fmt, ##__VA_ARGS__)
#define WARN(fmt, ...) rootLogger()->warn (fmt, ##__VA_ARGS__)
#define ERROR(fmt, ...) rootLogger()->error(fmt, ##__VA_ARGS__)
#define FATAL(fmt, ...) rootLogger()->fatal(fmt, ##__VA_ARGS__)
}
测试
#include <iostream>
#include "mylog.hpp"
int main() {
// 测试通过默认日志器的宏函数
MySpace::DEBUG("This is a DEBUG test");
MySpace::INFO("This is an INFO test");
MySpace::WARN("This is a WARN test");
MySpace::ERROR("This is an ERROR test");
MySpace::FATAL("This is a FATAL test");
return 0;
}
项目总结
- util.hpp:包含判断时间,寻找文件目录,判断文件是否存在等函数
- level.hpp:用枚举定义了不同的日志等级
- message.hpp:一条日志消息因该包括那些内容
- format.hpp:定义日志消息输出格式
- sink.hpp:决定日志消息的落地位置,控制台,文件,滚动文件
- logger.hpp:管理整个日志消息,最为重要
- buffer.hpp:异步日志器所需要的缓冲区
- looper.hpp:异步日志器对缓冲区内容的处理方法
- mylog.hpp:对上面内容封装,实际使用包含这个头文件就行了
具体流程用户传入消息构建message
日志消息对象,format
将消息格式化,sink
决定消息落地位置。加入日志器模块logger
将前面三个整合,一个日志器有多个落地方向,支持多个日志格式,可以控制输出等级,如果是串行日志器效率低,所以设计异步日志器提高效率,包括缓冲池存放日志消息,异步线程处理日志消息,局部和全局日志器更加灵活,方便用户使用用宏来简化接口。
结构
logs
util.hpp
level.hpp
message.hpp
format.hpp
sink.hpp
logger.hpp
buffer.hpp
looper.hpp
mylog.hpp
bench
bench.cpp
Makefile
运行:在bench 目录下运行Makefile ,控制台输出内容,在bench目录下生成logfile 目录
bench:bench.cpp
g++ -std=c++11 -o $@ $^ -pthread
性能测试
主要的测试方法:每秒能打印日志数 / 总的打印日志消耗时间
主要的测试要素 : 同步/异步 单线程/多线程
测试环境 :
CPU : 2核
内存 : 4G
速度: 4MB
OS : Linux =centos 7.2
测试代码
bench-测试主要代码
包括日志器名字,线程数量,日志数量,单条日志的大小
//bench.cpp
#include "../logs/mylog.hpp"
#include <chrono>
void bench(const std::string &logger_name, size_t thread_count, size_t msg_count, size_t msg_len) {
/* 1.获取日志器 */
std::shared_ptr<MySpace::Logger> logger = MySpace::getLogger(logger_name);
if (!logger.get()) {return ;}//需要注销测试下
std::cout << "测试日志:" << msg_count << " 条, 总大小:" << msg_count * msg_len / 1024 << "KB" << std::endl;
/* 2.组织指定长度的日志消息 */
std::string msg(msg_len - 1, 'A'); // 最后一个字节是换行符,便于换行打印
/* 3.创建指定数量的线程 */
std::vector<std::thread> threads;
std::vector<double> cost_array(thread_count);//总的数值统计
size_t msg_prt_thr = msg_count / thread_count; // 每个线程输出的日志条数
for (int i = 0; i < thread_count; i++) {
//引用捕获参数,传值i
threads.emplace_back([&, i](){
/* 4.记录开始时间 */
auto start = std::chrono::high_resolution_clock::now();
/* 5.开始循环写日志 */
for (int j = 0; j < msg_prt_thr; j++) {
logger->fatal("%s", msg.c_str());
}
/* 6.线程函数内部结束计时 */
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> cost = end - start;
cost_array[i] = cost.count();
std::cout << "线程[" << i << "]: " << " 输出日志数量:" << msg_prt_thr << ", 耗时:" << cost.count() << "s" << std::endl;
});
}
for (int i = 0; i < thread_count; i++) {
threads[i].join();
}
/* 7.计算总耗时 多线程中,每个线程都有自己运行的时间,但是线程是并发处理的,因此耗时最多的那个才是总时间 */
double max_cost = cost_array[0];
for (int i = 0; i < thread_count; i++) {
max_cost = max_cost > cost_array[i] ? max_cost : cost_array[i];
}
size_t msg_prt_sec = msg_count / max_cost;
size_t size_prt_sec = (msg_count * msg_len) / (max_cost * 1024);
/* 8.进行输出打印 */
std::cout << "总耗时: " << max_cost << "s" << std::endl;
std::cout << "每秒输出日志数量: " << msg_prt_sec << " 条" << std::endl;
std::cout << "每秒输出日志大小: " << size_prt_sec << " KB" << std::endl;
}
emplace_back
-末尾添加元素
emplace_back
:末尾直接构造一个新的元素,而不是先创建一个临时对象,lambda是一个对象push_back
:将一个已存在的对象添加到容器的末尾
std::vector<string> v1;
v1.emplace_back("Hello, World!");
std::vector<string> v2;
string s("Hello, World!");
v2.push_back(s);
chrono
-计时
auto start = std::chrono::high_resolution_clock::now();
:记录开始时间auto end = std::chrono::high_resolution_clock::now();
:记录结束时间std::chrono::duration<double> cost = end - start;
计算时间间隔std::cout << "代码块执行耗时: " << cost.count() << " 秒" << std::endl;
:输出耗时
测试结果
同步单线程测试
//同步日志测试
void sync_bench() {
std::shared_ptr<MySpace::LoggerBuilder> builder(new MySpace::GlobalLoggerBuilder());
builder->buildLoggerName("sync_logger");
builder->buildLoggerFormatter("%m%n");
builder->buildLoggerType(MySpace::LoggerType::LOGGER_SYNCH);//同步日志器
builder->buildSink<MySpace::FileSink>("./logfile/sync.log");
builder->buildSink<MySpace::RollBySizeSink>("./logfile/roll-sync-by-size", 1024 * 1024);
builder->build();
bench("sync_logger", 1, 1000000, 100);//单线程
//bench("sync_logger", 10, 1000000, 100);//10个线程
}
int main() {
sync_bench();
// async_bench();
return 0;
}
结果
[aaa@VM-12-6-centos bench]$ ./bench
测试日志:1000000 条, 总大小:97656KB
线程[0]: 输出日志数量:1000000, 耗时:2.71629s
总耗时: 2.71629s
每秒输出日志数量: 368149 条
每秒输出日志大小: 35952 KB
//生成logfile目录,里面有对应日志消息
同步多线程测试
//同步日志测试
void sync_bench() {
std::shared_ptr<MySpace::LoggerBuilder> builder(new MySpace::GlobalLoggerBuilder());
builder->buildLoggerName("sync_logger");
builder->buildLoggerFormatter("%m%n");
builder->buildLoggerType(MySpace::LoggerType::LOGGER_SYNCH);//同步日志器
builder->buildSink<MySpace::FileSink>("./logfile/sync.log");
builder->buildSink<MySpace::RollBySizeSink>("./logfile/roll-sync-by-size", 1024 * 1024);
builder->build();
//bench("sync_logger", 1, 1000000, 100);//单线程
bench("sync_logger", 10, 1000000, 100);//10个线程
}
int main() {
sync_bench();
// async_bench();
return 0;
}
结果
[aaa@VM-12-6-centos bench]$ ./bench
测试日志:1000000 条, 总大小:97656KB
线程[2]: 输出日志数量:100000, 耗时:2.34469s
线程[7]: 输出日志数量:100000, 耗时:2.54091s
线程[4]: 输出日志数量:100000, 耗时:2.56502s
线程[6]: 输出日志数量:100000, 耗时:2.56806s
线程[0]: 输出日志数量:100000, 耗时:2.57705s
线程[3]: 输出日志数量:100000, 耗时:2.60274s
线程[9]: 输出日志数量:100000, 耗时:2.5958s
线程[1]: 输出日志数量:100000, 耗时:2.63869s
线程[8]: 输出日志数量:100000, 耗时:2.70844s
线程[5]: 输出日志数量:100000, 耗时:2.72737s
总耗时: 2.72737s
每秒输出日志数量: 366653 条
每秒输出日志大小: 35805 KB
//生成logfile目录,里面有对应日志消息
异步单线程测试
//异步日志测试
void async_bench() {
std::shared_ptr<MySpace::LoggerBuilder> builder(new MySpace::GlobalLoggerBuilder());
builder->buildLoggerName("async_logger");
builder->buildLoggerFormatter("%m%n");
builder->buildLoggerType(MySpace::LoggerType::LOGGER_ASYNCH);//异步日志器
builder->buildSink<MySpace::FileSink>("./logfile/async.log");
builder->buildSink<MySpace::RollBySizeSink>("./logfile/roll-async-by-size", 1024 * 1024);
builder->build();
bench("async_logger", 1, 100000, 10);
// bench("async_logger", 10, 1000000, 100);//10个线程
}
int main() {
// sync_bench();
async_bench();
return 0;
}
结果
[aaa@VM-12-6-centos bench]$ ./bench
测试日志:100000 条, 总大小:976KB
线程[0]: 输出日志数量:100000, 耗时:0.235684s
总耗时: 0.235684s
每秒输出日志数量: 424297 条
每秒输出日志大小: 4143 KB
//生成logfile目录,里面有对应日志消息
异步多线程测试
//异步日志测试
void async_bench() {
std::shared_ptr<MySpace::LoggerBuilder> builder(new MySpace::GlobalLoggerBuilder());
builder->buildLoggerName("async_logger");
builder->buildLoggerFormatter("%m%n");
builder->buildLoggerType(MySpace::LoggerType::LOGGER_ASYNCH);//异步日志器
builder->buildSink<MySpace::FileSink>("./logfile/async.log");
builder->buildSink<MySpace::RollBySizeSink>("./logfile/roll-async-by-size", 1024 * 1024);
builder->build();
// bench("async_logger", 1, 100000, 10);
bench("async_logger", 10, 1000000, 100);//10个线程
}
int main() {
// sync_bench();
async_bench();
return 0;
}
结果
[aaa@VM-12-6-centos bench]$ ./bench
测试日志:100000 条, 总大小:976KB
线程[1]: 输出日志数量:10000, 耗时:0.0404783s
线程[0]: 输出日志数量:10000, 耗时:0.0438714s
线程[2]: 输出日志数量:10000, 耗时:0.0823503s
线程[3]: 输出日志数量:10000, 耗时:0.078529s
线程[4]: 输出日志数量:10000, 耗时:0.100931s
线程[7]: 输出日志数量:10000, 耗时:0.0608246s
线程[5]: 输出日志数量:10000, 耗时:0.0774649s
线程[6]: 输出日志数量:10000, 耗时:0.0773523s
线程[9]: 输出日志数量:10000, 耗时:0.0429028s
线程[8]: 输出日志数量:10000, 耗时:0.0775997s
总耗时: 0.100931s
每秒输出日志数量: 990779 条
每秒输出日志大小: 9675 KB
//生成logfile目录,里面有对应日志消息