目录
前言:
一、项目介绍
二、为什么需要日志系统
三、日志系统技术实现
(一)同步写⽇志
(二)异步写⽇志
四、前置技术学习
(一)不定参宏函数
(二)C语言不定参数使用
(三)模拟实现printf
(四)C++风格不定参数使用
(五)设计模式
1. 六大原则
2. 单例模式
3. 工厂模式
4. 建造者模式
5. 代理模式
五、日志系统框架设计
(一)功能划分
(二)模块划分
(三)模块关系图
六、代码设计
(一)实用类设计
(二)日志等级类设计
(三)日志消息类设计
(四)日志输出格式化类设计
1. 格式化子项类的设计
2. 日志格式化类的设计
(五)日志落地类的设计
滚动文件的扩展
(六)日志器类的设计
1. 同步日志器类:
2. 构造日志器扩展
(七)异步日志器类:
1. 缓冲区设计思想:
单个缓冲区类的进一步设计
2. 异步工作器类设计
3. 异步日志器类设计
(八)单例日志器管理类设计
(九)日志全局接口设计
七、项目目录结构管理
(一)整体测试代码(功能样例代码)测试
(二)滚动文件扩展代码测试:
八、性能测试
前言:
日志:程序运行过程中所记录的程序运行状态信息。
作用:记录了程序运行状态信息,以便于程序员能够随时根据状态信息,对系统的运行状态进行分析。
功能:能够让用户非常简便的进行日志输出,以及控制。
一、项目介绍
- ⽀持多级别⽇志消息
- ⽀持同步⽇志和异步⽇志
- ⽀持可靠写⼊⽇志到控制台、⽂件以及滚动⽂件中
- ⽀持多线程程序并发写⽇志
- ⽀持扩展不同的⽇志落地⽬标地
二、为什么需要日志系统
- ⽣产环境的产品为了保证其稳定性及安全性是不允许开发⼈员附加调试器去排查问题, 可以借助日志系统来打印⼀些⽇志帮助开发⼈员解决问题。
- 上线客户端的产品出现bug⽆法复现并解决,可以借助⽇志系统打印⽇志并上传到服务端帮助开发⼈员进⾏分析。
- 对于⼀些高频操作(如定时器、⼼跳包)在少量调试次数下可能⽆法触发我们想要的⾏为,通过断点的暂停⽅式,我们不得不重复操作⼏⼗次、上百次甚⾄更多,导致排查问题效率是⾮常低下, 因此可以借助打印⽇志的⽅式查问题。
- 在分布式、多线程/多进程代码中,出现bug⽐较难以定位,可以借助⽇志系统打印log帮助定位 bug。
- 帮助⾸次接触项⽬代码的新开发⼈员理解代码的运⾏流程。
三、日志系统技术实现
⽇志系统的技术实现主要包括三种类型:
利⽤printf、std::cout等输出函数将⽇志信息打印到控制台。
对于⼤型商业化项⽬, 为了⽅便排查问题,我们⼀般会将⽇志输出到⽂件或者是数据库系统⽅便查 询和分析⽇志, 主要分为同步⽇志和异步⽇志⽅式:
(一)同步写⽇志
- ⼀⽅⾯,⼤量的⽇志打印陷⼊等量的write系统调⽤,有⼀定系统开销。
- 另⼀⽅⾯,使得打印⽇志的进程附带了⼤量同步的磁盘IO,影响程序性能。
(二)异步写⽇志
异步⽇志是指在进⾏⽇志输出时,⽇志输出语句与业务逻辑语句并不是在同⼀个线程中运⾏,⽽是有专⻔的线程⽤于进⾏⽇志输出操作。业务线程只需要将⽇志放到⼀个内存缓冲区中不⽤等待即可继续执⾏后续业务逻辑(作为⽇志的⽣产者),而日志的落地操作交给单独的⽇志线程去完成(作为⽇志 的消费者),这是⼀个典型的⽣产-消费模型。
- 主线程调用日志打印接⼝成为⾮阻塞操作。
- 同步的磁盘IO从主线程中剥离出来交给单独的线程完成。
四、前置技术学习
(一)不定参宏函数
上代码!
#include <stdio.h>
// 哪个文件的哪一行代码输出的日志
#define LOG(fmt, ...) printf("[%s:%d]" fmt, __FILE__, __LINE__, __VA_ARGS__);
int main()
{
LOG("%d good morning\n", 100); // [test.cc:10]100 good morning
return 0;
}
-
fmt
是格式字符串的占位符。 ...
表示这是一个可变参数宏,意味着你可以传递任意数量的参数给LOG
宏,这些参数会被传递给printf
函数(通过__VA_ARGS__
)。__FILE__
是一个预处理器宏,它在预处理时会被替换为当前源代码文件名(以字符串形式)。__LINE__
也是一个预处理器宏,它在预处理时会被替换为当前源代码行号(以整数形式)。__VA_ARGS__
在C和C++的宏定义中,__VA_ARGS__
是一个GCC(GNU Compiler Collection)和其他一些C/C++编译器所支持的预处理特性,它允许宏接受可变数量的参数。当你有一个宏,其参数数量在每次调用时都可能不同时,这个特性就非常有用了。
但调用时如果只有格式字符串,没有可变参数,即:
LOG("good morning\n");
这样子就会导致编译错误,这行代码在宏展开后会是:
printf("[%s:%d]" "good morning\n", ); // 编译错误,注意这里多了一个逗号
所以__VA_ARGS__
通常与 ##
操作符一起使用,以确保在宏调用中没有提供任何可变参数,那么宏展开后可能会在 printf
或其他函数的参数列表中留下一个额外的逗号,这会导致编译错误。
所以如果可变参数列表(...
)为空,并且你希望在宏展开时不留下逗号,你可以使用 ## 操作符来“吃掉”前面的逗号。
#define LOG(fmt, ...) printf("[%s:%d]" fmt, __FILE__, __LINE__, ##__VA_ARGS__);
LOG("good morning\n"); // [test.cc:10]good morning
// 展开后
printf("[%s:%d]" "good morning\n"); // 正常运行,不存在逗号
(二)C语言不定参数使用
上代码!
#include <stdio.h>
#include <stdarg.h>
/* C语言中不定参函数的使用,不定参数据的访问 */
void printNum(int count, ...)
{
va_list ap;
va_start(ap, count); // 获取指定参数的起始地址,这里就是获取count参数之后的第一个参数的起始地址
for(int i = 0; i < count; i++)
{
int num = va_arg(ap, int);
printf("pargm[%d]:%d\n", i, num);
}
va_end(ap); // 将ap指针置空
}
int main()
{
printNum(2, 100, 50);
printNum(5, 1, 2, 3, 4, 5);
return 0;
}
解释说明:
va_list
- va_list是一个类型,用于声明一个对象,该对象可以持有可变参数的信息。你可以将它看作是一个指针或者一个能够存储关于参数列表的元数据的特殊类型。
va_start
- va_start是一个宏,用于初始化
va_list
对象,并设置其指向可变参数列表中的第一个参数。它接受两个参数:一个是va_list
对象,另一个是在可变参数列表之前的最后一个固定参数。
va_arg
- va_arg是一个宏,用于从 va_list 对象中获取下一个参数。它接受两个参数:一个是
va_list
对象,另一个是你想要获取的参数的类型。va_arg 会更新 va_list 对象的内部状态,使其指向下一个参数。
va_end
- va_end是一个宏,用于清理
va_list
对象,并在使用完毕后释放与之关联的任何资源。在调用va_end
之后,你不应该再次使用同一个va_list
对象,除非你又重新调用了va_start
来初始化它。
(三)模拟实现printf
vasprintf 是一个 C 语言函数,它用于将格式化的字符串和可变参数列表组合成一个新的字符串,并返回这个新字符串的指针.
int vasprintf(char **strp, const char *fmt, va_list ap);
strp
是一个指向char *
的指针,函数成功后会将新字符串的指针赋值给它。fmt
是一个格式字符串,类似于printf
函数的格式字符串。ap
是一个va_list
类型的参数,表示可变参数列表。
vasprintf 函数将按照 fmt
指定的格式和 ap
提供的参数生成一个字符串,并动态分配内存来存储这个字符串。然后,它将新字符串的指针存储在 *strp
中,并返回非负值表示成功。如果内存分配失败,vasprintf
将返回 -1,并且 *strp
的值不会被改变。
使用 vasprintf 函数的典型方式是与 va_start
、va_arg
和 va_end
一起,来处理可变参数列表。但需要注意的是,调用 vasprintf
后,你需要负责释放分配的内存,通常使用 free
函数来完成。
// #define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
void myprintf(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt, ap);
if(ret != -1)
{
printf(res);
free(res);
}
va_end(ap); // 将ap指针置空
}
int main()
{
myprintf("[%s:%d]\n", __FILE__, __LINE__);
myprintf("good morning!\n");
return 0;
}
(四)C++风格不定参数使用
使用模版来扩展参数:
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
/* C++中不定参函数的使用 */
void xprintf() { std::cout << std::endl; }
template<typename T, typename ...Args>
void xprintf(const T &v, Args &&...args)
{
std::cout << v;
if((sizeof ...(args)) > 0)
{
xprintf(std::forward<Args>(args)...);
}
else
{
xprintf();
}
}
int main()
{
xprintf(__FILE__, " ", __LINE__);
xprintf("good morning!");
return 0;
}
(五)设计模式
1. 六大原则
- 类的职责应该单⼀,⼀个⽅法只做⼀件事。职责划分清晰了,每次改动到最⼩单位的⽅法或 类。
- 使⽤建议:两个完全不⼀样的功能不应该放⼀个类中,⼀个类中应该是⼀组相关性很⾼的函数、数据的封装。
- ⽤例:⽹络聊天:⽹络通信 & 聊天,应该分割成为⽹络通信类 & 聊天类。
- 对扩展开放,对修改封闭。
- 使⽤建议:对软件实体的改动,最好⽤扩展⽽⾮修改的⽅式。
- ⽤例:超时卖货:商品价格---不是修改商品的原来价格,⽽是新增促销价格。
- 通俗点讲,就是只要⽗类能出现的地⽅,⼦类就可以出现,⽽且替换为⼦类也不会产⽣任何错 误或异常。
- 在继承类时,务必重写⽗类中所有的⽅法,尤其需要注意⽗类的protected⽅法,⼦类尽量不要暴露⾃⼰的public⽅法供外界调⽤。
- 使⽤建议:⼦类必须完全实现⽗类的⽅法,孩⼦类可以有⾃⼰的个性。覆盖或实现⽗类的⽅法 时,输⼊参数可以被放⼤,输出可以缩⼩
- ⽤例:跑步运动员类-会跑步,⼦类⻓跑运动员-会跑步且擅⻓⻓跑, ⼦类短跑运动员-会跑步且 擅⻓短跑
- ⾼层模块不应该依赖低层模块,两者都应该依赖其抽象. 不可分割的原⼦逻辑就是低层模式,原⼦逻辑组装成的就是⾼层模块。
- 模块间依赖通过抽象(接⼝)发⽣,具体类之间不直接依赖。
- 使⽤建议:每个类都尽量有抽象类,任何类都不应该从具体类派⽣。尽量不要重写基类的⽅ 法。结合⾥⽒替换原则使⽤。
- ⽤例:奔驰⻋司机类--只能开奔驰; 司机类 -- 给什么⻋,就开什么⻋; 开⻋的⼈:司机--依 赖于抽象。
- 尽量减少对象之间的交互,从⽽减⼩类之间的耦合。⼀个对象应该对其他对象有最少的了解。 对类的低耦合提出了明确的要求:
· 只和直接的朋友交流, 朋友之间也是有距离的。⾃⼰的就是⾃⼰的(如果⼀个⽅法放在本类 中,既不增加类间关系,也对本类不产⽣负⾯影响,那就放置在本类中)。 - ⽤例:⽼师让班⻓点名--⽼师给班⻓⼀个名单,班⻓完成点名勾选,返回结果,⽽不是班⻓点 名,⽼师勾选。
- 客⼾端不应该依赖它不需要的接⼝,类间的依赖关系应该建⽴在最⼩的接⼝上。
- 使⽤建议:接⼝设计尽量精简单⼀,但是不要对外暴露没有实际意义的接⼝。
- ⽤例:修改密码,不应该提供修改⽤⼾信息接⼝,⽽就是单⼀的最⼩修改密码接⼝,更不要暴 露数据库操作。
- 单⼀职责原则告诉我们实现类要职责单⼀;
- ⾥⽒替换原则告诉我们不要破坏继承体系;
- 依赖倒置原则告诉我们要⾯向接⼝编程;
- 接⼝隔离原则告诉我们在设计接⼝的时候要精简单⼀;
- 迪⽶特法则告诉我们要降低耦合;
- 开闭原则是总纲,告诉我们要对扩展开放,对修改关闭。
2. 单例模式
// 单例模式
// 饿汉模式: 以空间换时间
class Singleton
{
private:
static Singleton _eton;
Singleton():_data(10) // 构造函数私有化
{
std::cout << "单例对象构造\n";
}
Singleton(const 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++》⼀书作者 Scott Meyers 提出的⼀种更加优雅简便的单例模式 Meyers' Singleton in C++。
- C++11 Static local variables 特性以确保C++11起,静态变量将能够在满⾜ thread-safe 的前提下唯⼀地被构造和析构。
// 懒汉模式: 懒加载 - 延迟加载的思想 -- 一个对象在需要用的时候在进行实例化
class Singleton
{
private:
Singleton():_data(10) // 构造函数私有化
{
std::cout << "单例对象构造\n";
}
Singleton(const 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;
}
3. 工厂模式
// 工厂模式
class Fruit
{
public:
Fruit() {}
virtual void show()
{
std::cout << "我是一个水果" << std::endl;
}
};
class Apple : public Fruit
{
public:
void show() override
{
std::cout << "我是一个苹果\n";
}
};
class Banana : public Fruit
{
public:
void show() override
{
std::cout << "我是一个香蕉\n";
}
};
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->show();
fruit = FruitFactory::create("香蕉");
fruit->show();
return 0;
}
- 优点:简单粗暴,直观易懂。使⽤⼀个工厂⽣产同⼀等级结构下的任意产品。
- 缺点:
1. 所有东西⽣产在⼀起,产品太多会导致代码量庞⼤。
2. 开闭原则遵循(开放拓展,关闭修改)的不是太好,要新增产品就必须修改⼯⼚⽅法。
这个模式的结构和管理产品对象的⽅式⼗分简单, 但是它的扩展性⾮常差,当我们需要新增产品的时 候,就需要去修改⼯⼚类新增⼀个类型的产品创建逻辑,违背了开闭原则。
⼯⼚⽅法模式:在简单⼯⼚模式下新增多个⼯⼚,多个产品,每个产品对应⼀个⼯⼚。假设现在有
// 工厂方法模式
class Fruit
{
public:
Fruit() {}
virtual void show()
{
std::cout << "我是一个水果" << std::endl;
}
};
class Apple : public Fruit
{
public:
void show() override
{
std::cout << "我是一个苹果\n";
}
};
class Banana : public Fruit
{
public:
void show() override
{
std::cout << "我是一个香蕉\n";
}
};
class FruitFactory
{
public:
virtual std::shared_ptr<Fruit> create() = 0;
};
class AppleFactory : public FruitFactory
{
public:
std::shared_ptr<Fruit> create() override
{
return std::make_shared<Apple>();
}
};
class BananaFactory : public FruitFactory
{
public:
std::shared_ptr<Fruit> create() override
{
return std::make_shared<Banana>();
}
};
int main()
{
std::shared_ptr<FruitFactory> ff(new AppleFactory());
std::shared_ptr<Fruit> fruit = ff->create();
fruit->show();
ff.reset(new BananaFactory());
fruit = ff->create();
fruit->show();
return 0;
}
- 优点:
1. 减轻了⼯⼚类的负担,将某类产品的⽣产交给指定的⼯⼚来进⾏。
2. 开闭原则遵循较好,添加新产品只需要新增产品的⼯⼚即可,不需要修改原先的⼯⼚类。 - 缺点:对于某种可以形成⼀组产品族的情况处理较为复杂,需要创建⼤量的⼯⼚类。
// 抽象⼯⼚:围绕⼀个超级⼯⼚创建其他⼯⼚。每个⽣成的⼯⼚按照⼯⼚模式提供对象。
// 思想:将⼯⼚抽象成两层,抽象⼯⼚ & 具体⼯⼚⼦类, 在⼯⼚⼦类种⽣产不同类型的⼦产品
class Fruit
{
public:
Fruit() {}
virtual void show() = 0;
};
class Apple : public Fruit
{
public:
void show() override
{
std::cout << "我是一个苹果\n";
}
};
class Banana : public Fruit
{
public:
void show() override
{
std::cout << "我是一个香蕉\n";
}
};
class Animal
{
public:
Animal() {}
virtual void show() = 0;
};
class Dog : public Animal
{
public:
void show() override
{
std::cout << "我是一个小狗\n";
}
};
class Lamp : public Animal
{
public:
void show() override
{
std::cout << "我是一个山羊\n";
}
};
// 抽象⼯⼚模式
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:
std::shared_ptr<Animal> getAnimal(const std::string &name)
{
return std::shared_ptr<Animal>();
}
std::shared_ptr<Fruit> getFruit(const std::string &name)
{
if(name == "苹果")
{
return std::make_shared<Apple>();
}
else
{
return std::make_shared<Banana>();
}
}
};
class AnimalFactory : public Factory
{
public:
std::shared_ptr<Fruit> getFruit(const std::string &name)
{
return std::shared_ptr<Fruit>();
}
std::shared_ptr<Animal> getAnimal(const std::string &name)
{
if(name == "小狗")
{
return std::make_shared<Dog>();
}
else
{
return std::make_shared<Lamp>();
}
}
};
class FactoryProducer
{
public:
static std::shared_ptr<Factory> create(const std::string &name)
{
if(name == "水果")
{
return std::make_shared<FruitFactory>();
}
else
{
return std::make_shared<AnimalFactory>();
}
}
};
int main()
{
std::shared_ptr<Factory> ff = FactoryProducer::create("水果");
std::shared_ptr<Fruit> fruit = ff->getFruit("苹果");
fruit->show();
fruit = ff->getFruit("香蕉");
fruit->show();
std::shared_ptr<Factory> factory = FactoryProducer::create("动物");
std::shared_ptr<Animal> animal = factory->getAnimal("小狗");
animal->show();
animal = factory->getAnimal("香蕉");
animal->show();
return 0;
}
4. 建造者模式
- 抽象产品类:
- 具体产品类:⼀个具体的产品对象类。
- 抽象Builder类:创建⼀个产品对象所需的各个部件的抽象接⼝。
- 具体产品的Builder类:实现抽象接⼝,构建各个部件。
- 指挥者Director类:统⼀组建过程,提供给调⽤者使⽤,通过指挥者来构造产品。
// 建造者模式
class Computer
{
public:
Computer() {}
void setBoard(const std::string &board)
{
_board = board;
}
void setDisplay(const std::string &display)
{
_display = display;
}
void showParamaters()
{
std::string param = "Computer Paramaters:\n";
param += "\tBoard: " + _board + "\n";
param += "\tDisolay: " + _display + "\n";
param += "\tOS: " + _os + "\n";
std::cout << param << std::endl;
}
virtual void setOS() = 0;
protected:
std::string _board;
std::string _display;
std::string _os;
};
/* 具体产品类 */
class MackBook : public Computer
{
public:
void setOS() override
{
_os = "Mac OS x12";
}
};
/* 抽象建造者类:包含创建⼀个产品对象的各个部件的抽象接⼝ */
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;
};
/* 具体产品的具体建造者类:实现抽象接⼝,构建和组装各个部件 */
class MacBookBuilder : public Builder
{
public:
MacBookBuilder() : _computer(new MackBook()) {}
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;
};
/* 指挥者类,提供给调⽤者使⽤,通过指挥者来构造复杂产品 */
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;
}
5. 代理模式
- 静态代理指的是,在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪个被代理类。
- 动态代理指的是,在运⾏时才动态⽣成代理类,并将其与被代理类绑定。这意味着,在运⾏时才能确定代理类要代理的是哪个被代理类。
以租房为例,房东将房⼦租出去,但是要租房⼦出去,需要发布招租启⽰, 带⼈看房,负责维修,这些⼯作中有些操作并⾮房东能完成,因此房东为了图省事,将房⼦委托给中介进⾏租赁。 代理模式实现:
// 代理模式
/* 房东要把⼀个房⼦通过中介租出去理解代理模式 */
class RentHouse
{
public:
virtual void rentHouse() = 0;
};
/* 房东类: 将房子租出去 */
class Landlord : public RentHouse
{
public:
void rentHouse()
{
std::cout << "将房子租出去\n";
}
};
/* 中介代理类: 将租房子进行功能加强,实现租房以外的其他功能 */
class Intermediary : public RentHouse
{
public:
void rentHouse()
{
std::cout << "发布招租启示\n";
std::cout << "带人看房\n";
_landloard.rentHouse();
std::cout << "负责租后维修\n";
}
private:
Landlord _landloard;
};
int main()
{
Intermediary intermediary;
intermediary.rentHouse();
return 0;
}
五、日志系统框架设计
(一)功能划分
1. 日志要写入指定位置(标准输出,指定文件,滚动文件,。。。)。日志系统需要支持将日志消息落地到不同的位置 -- 多落地方向
2. 日志要写入指定位置,支持不同的写入方式(同步,异步)。
- 同步:业务线程自己负责日志的写入(流程简单,但是有可能会因为阻塞导致效率降低)。
- 异步:业务线程将日志放入缓冲区内存,让其他异步线程负责将日志写入指定位置。
3. 日志输出以日志器为单位,支持多日志器(不同的项目组有不同的输出策略)
4. 日志器管理
(二)模块划分
日志等级模块: 对输出的日志的等级进行划分,以便于控制日志的输出,并提供等级枚举转字符串功能。
- OFF:关闭。
- DEBUG:调试,调试时的关键信息输出。
- INFO:提⽰,普通的提⽰型⽇志信息。
- WARN:警告,不影响运⾏,但是需要注意⼀下的⽇志。
- ERROR:错误,程序运⾏出现错误的⽇志。
- FATAL:致命,⼀般是代码异常导致程序⽆法继续推进运⾏的⽇志。
日志消息模块: 封装一条日志输出的所需的各项要素信息。
日志消息格式化模块 :设置指定的日志输出格式,并提供对日志消息进行格式化功能。
- %d{%H:%M:%S}:表⽰⽇期时间,花括号中的内容表⽰⽇期时间的格式。
- %T:表示制表符缩进。
- %t:表示线程ID
- %p:表示⽇志级别
- %c:表示⽇志器名称,不同的开发组可以创建⾃⼰的⽇志器进⾏⽇志输出,⼩组之间互不影
- 响。
- %f:表示⽇志输出时的源代码⽂件名。
- %l:表示⽇志输出时的源代码⾏号。
- %m:表示给与的⽇志有效载荷数据
- %n:表示换⾏
日志消息落地模块: 负责对日志消息进行指定方向的写入输出。( 可以是标准输出,也可以是⽇志⽂件,也可以滚动⽂ 件输出....)(工厂模式)
- 标准输出:表⽰将⽇志进⾏标准输出的打印。
- ⽇志⽂件输出:表⽰将⽇志写⼊指定的⽂件末尾。
- 滚动⽂件输出:当前以⽂件⼤⼩进⾏控制,当⼀个⽇志⽂件⼤⼩达到指定⼤⼩,则切换下⼀个 ⽂件进⾏输出。
后期,也可以扩展远程⽇志输出,创建客⼾端,将⽇志消息发送给远程的⽇志分析服务器。
日志器模块:对上面几个模块进行整合。(建造者模式)
- 同步日志器模块:完成日志的同步输出功能。
- 异步日志器模块:完成日志的异步输出功能。
⽇志器管理模块: 对创建的所有⽇志器进⾏统⼀管理,并提供⼀个默认⽇志器提供标准输出的⽇志输出。
异步线程模块:负责异步日志的实际落地输出。(代理模式)
单例的日志器管理模块:
(三)模块关系图
六、代码设计
(一)实用类设计
- 获取系统时间
- 判断⽂件是否存在
- 获取⽂件的所在⽬录路径
- 创建⽬录
Util.hpp
整体框架:
成员接口设置成静态的,就不需实例化对象而是通过 类名::接口 直接使用即可:
#ifndef __M_UTIL_H__
#define __M_UTIL_H__
/* 实用工具类的设计:
1. 获取系统时间
2. 判断文件是否存在
3. 获取文件所在路径
4. 创建目录
*/
#include <iostream>
#include <ctime>
#include <unistd.h>
#include <sys/stat.h>
namespace mylog
{
namespace util
{
class Date
{
public:
// 1. 获取系统时间
static size_t nowTime()
{
return (size_t)time(nullptr);
}
};
class File
{
public:
// 2. 判断文件是否存在
static bool exists(const std::string &pathname);
// 3. 获取文件所在路径
static std::string path(const std::string &pathname);
// 4. 创建目录
static void createDirectory(const std::string &pathname);
};
}
}
#endif
接口具体实现:
获取当前时间:
// 1. 获取系统时间
static size_t nowTime()
{
return (size_t)time(nullptr);
}
判断文件是否存在:
stat 用于获取文件或文件系统的状态信息。可以通过这个系统函数判断文件是否存在;
也可以通过access函数调用,F_OK为文件存在性检查,但这个函数只有在Linux操作系统下才具有:
// 2. 判断文件是否存在
static bool exists(const std::string &pathname)
{
// 此接口为POSIX(可移植操作系统接口)
struct stat st;
if (stat(pathname.c_str(), &st) < 0)
{
return false;
}
return true;
// 此接口只有Linux下有,不具有可移植性
// return (access(pathname.c_str(), F_OK) == 0);
}
获取文件路径:查找到最后一个 / 的位置,然后从起始位置截取到最后一个斜杠位置。
但需注意的是:Windows下文件路径使用 \ 分隔,Linux下为 / ,所以使用此接口查询所给字符串的任意一个字符:
// 3. 获取文件所在路径
static std::string path(const std::string &pathname)
{
// ./abc/a.txt
size_t pos = pathname.find_last_of("/\\");
if(pos == std::string::npos) return "."; // 没找到说明在当前目录下
return pathname.substr(0, pos+1); // +1包含最后一个斜杠
}
创建目录:传入需创建目录的路径,查找第一级目录,没有则创建,有则进行查找下一级目录,直到创建指定目录成功:
// 4. 创建目录
static void createDirectory(const std::string &pathname)
{
// ./abc/bcd/cde
size_t pos = 0, idx = 0;
while(idx < pathname.size())
{
pos = pathname.find_first_of("/\\", idx);
if(pos == std::string::npos)
{
mkdir(pathname.c_str(), 0777); // 赋予全部权限
}
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;
}
}
测试:
#include "Util.hpp"
int main()
{
std::cout << mylog::util::Date::nowTime() << std::endl;
std::string pathname = "./abc/bcd/cde/a.txt";
mylog::util::File::createDirectory(mylog::util::File::path(pathname));
return 0;
}
(二)日志等级类设计
- UNKNOW = 0,
- DEBUG:调试等级的日志,
- INFO: 提示等级的日志,
- WARNL:警告等级的日志,
- ERROR:错误等级的日志,
- FATAL: 致命错误等级的日志,
- OFF: 关闭等级
Level.hpp
#ifndef __M_LEVEL_H__
#define __M_LEVEL_H__
/*
1. 定义枚举类,枚举出日志等级
2. 提供转换接口: 将枚举转换为对应字符串
*/
namespace mylog
{
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 "UNKNOW";
}
};
}
#endif
测试:
#include "Util.hpp"
#include "Level.hpp"
int main()
{
std::cout << mylog::LogLevel::toString(mylog::LogLevel::value::DEBUG) << std::endl;
std::cout << mylog::LogLevel::toString(mylog::LogLevel::value::INFO) << std::endl;
std::cout << mylog::LogLevel::toString(mylog::LogLevel::value::WARN) << std::endl;
std::cout << mylog::LogLevel::toString(mylog::LogLevel::value::ERROR) << std::endl;
std::cout << mylog::LogLevel::toString(mylog::LogLevel::value::FATAL) << std::endl;
std::cout << mylog::LogLevel::toString(mylog::LogLevel::value::OFF) << std::endl;
return 0;
}
成功打印出各个日志等级:
(三)日志消息类设计
中间存储一条日志消息所需的各项要素:
- 日志的输出时间 用于过滤日志输出时间
- 日志等级 用于进行日志过滤分析
- 源文件名称
- 源代码行号 用于定位出现错误的代码位置
- 线程ID 用于过滤出错的线程
- 日志主体消息
- 日志器名称(当前支持多日志器同时使用)
Message.hpp
#ifndef __M__MSG_H__
#define __M__MSG_H__
/* 中间存储一条日志消息所需的各项要素: */
#include "Level.hpp"
#include "Util.hpp"
#include <iostream>
#include <string>
#include <thread>
namespace mylog
{
struct LogMsg
{
size_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::nowTime()),
_level(level),
_line(line),
_tid(std::this_thread::get_id()),
_file(file),
_logger(logger),
_payload(msg)
{}
};
}
#endif
这个类没啥测试的~~
(四)日志输出格式化类设计
对日志消息进行格式化,组织为指定格式的字符串。其主要包含以下内容:
- %d ⽇期
- %T 缩进
- %t 线程id
- %p ⽇志级别
- %c ⽇志器名称
- %f ⽂件名
- %l ⾏号
- %m ⽇志消息
- %n 换⾏
格式化字符串控制了日志的输出格式,定义格式化字符,是为了让日志系统进行日志格式化更加的灵活方便。
std::vector<FormatItem::ptr> items成员:用于按序保存格式化字符串对应的⼦格式化对象。
FormatItem类主要负责⽇志消息⼦项的获取及格式化。其包含以下⼦类:
- MsgFormatItem :表⽰要从LogMsg中取出有效⽇志数据。
- LevelFormatItem :表⽰要从LogMsg中取出⽇志等级。
- NameFormatItem :表⽰要从LogMsg中取出⽇志器名称。
- ThreadFormatItem :表⽰要从LogMsg中取出线程ID。
- TimeFormatItem :表⽰要从LogMsg中取出时间戳并按照指定格式进⾏格式化。
- CFileFormatItem :表⽰要从LogMsg中取出源码所在⽂件名。
- CLineFormatItem :表⽰要从LogMsg中取出源码所在⾏号。
- TabFormatItem :表⽰⼀个制表符缩进。
- NLineFormatItem :表⽰⼀个换⾏。
- OtherFormatItem :表⽰⾮格式化的原始字符串("["、"]"、":")。
格式化子项数组对格式化字符串进行解析,保存了日志消息要素的排序,不同的格式化子项,会从日志消息中取出指定的元素,转换为字符串。
⽰例:"[%d{%H:%M:%S}] %m%n"
pattern = "[%d{%H:%M:%S}] %m%n"
items = {
{OtherFormatItem(), "["},
{TimeFormatItem(), "%H:%M:%S"},
{OtherFormatItem(), "]"},
{MsgFormatItem (), ""},
{NLineFormatItem (), ""}
}
LogMsg msg = {
size_t _line = 22;
size_t _ctime = 12345678;
std::thread::id _tid = 0x12345678;
std::string _name = "logger";
std::string _file = "main.cpp";
std::string _payload = "创建套接字失败";
LogLevel::value _level = ERROR;
};
1. 格式化子项类的设计
对于格式化的日志消息,首先需先实现格式化子项类。
作用:从日志消息中取出指定的元素,追加到一块内存空间中。
设计思想:
- 抽象一个格式化子项基类。
- 基于基类,派生出不同的格式化子项子类(主体消息,日志等级,时间子项,文件名,行号,日志器名称,线程ID,制表符,换行,其他等待),这样就可以在父类中定义父类指针的数组,指向不同的格式化子项子类对象。
对于时间子类来说,需将时间戳转换成时间字符串,即分为两个步骤:
1. 时间戳 ——》 实际时间:
用到的函数为 localtime_r 。localtime 是一个在多种编程语言和环境中常见的函数,通常用于将时间戳(Unix 时间戳,即从1970年1月1日00:00:00 UTC到现在的秒数)转换为本地时间(即考虑了时区的时间)。
在C语言中,localtime 函数是一个非线程安全的函数,因为它返回指向静态数据的指针。在多线程环境中,这可能导致数据竞争和不可预测的结果。为了解决这个问题,一些系统提供了localtime_r(或localtime_s
在某些平台上)这样的线程安全版本。
localtime_r函数与localtime函数的功能相同,但它接受一个额外的参数,该参数是指向struct tm
的指针,用于存储结果,而不是使用静态内存。这使得多个线程可以同时调用localtime_r而不会相互干扰。
struct tm *localtime_r(const time_t *timer, struct tm *result);
timer
:指向时间戳(time_t
类型)的指针。result
:指向struct tm
的指针,用于存储转换后的本地时间。
2. 实际时间 ——》时间字符串:
用到的函数为 strftime 是一个在多种编程语言中常见的函数,用于将日期和时间格式化为字符串。
size_t strftime(char *str, size_t maxsize,
const char *format, const struct tm *timeptr);
str
:指向用于存储格式化时间的字符数组的指针。maxsize
:str
数组的大小。format
:指定日期和时间格式的 C 字符串。timeptr
:指向struct tm
结构体的指针,该结构体包含了要格式化的日期和时间信息。
Format.hpp
#ifndef __M__FMT_H__
#define __M__FMT_H__
#include "Level.hpp"
#include "Message.hpp"
#include <vector>
namespace mylog
{
// 抽象格式化子项基类
class FormatItem
{
public:
using ptr = std::shared_ptr<FormatItem>;
virtual void format(std::ostream &out, LogMsg &msg) = 0;
};
// 派生格式化子项子类 -- 有效消息、等级、时间、文件名、行号、线程ID、日志器名、制表符、换行、其他
// 有效消息子类
class MsgFormatItem : public FormatItem
{
public:
void format(std::ostream &out, LogMsg &msg) override
{
out << msg._payload;
}
};
// 等级子类
class LevelFormatItem : public FormatItem
{
public:
void format(std::ostream &out, 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, LogMsg &msg) override
{
struct tm t;
localtime_r(&msg._ctime, &t);
char tmp[32] = {0};
strftime(tmp, 31, _time_fmt.c_str(), &t);
out << tmp;
}
private:
std::string _time_fmt; // %H:%M:%S
};
// 文件名子类
class LevelFormatItem : public FormatItem
{
public:
void format(std::ostream &out, LogMsg &msg) override
{
out << msg._file;
}
};
// 行号子类
class LineFormatItem : public FormatItem
{
public:
void format(std::ostream &out, LogMsg &msg) override
{
out << msg._line;
}
};
// 线程ID子类
class ThreadFormatItem : public FormatItem
{
public:
void format(std::ostream &out, LogMsg &msg) override
{
out << msg._tid;
}
};
// 日志器子类
class LoggerFormatItem : public FormatItem
{
public:
void format(std::ostream &out, LogMsg &msg) override
{
out << msg._logger;
}
};
// 制表符子类
class TabFormatItem : public FormatItem
{
public:
void format(std::ostream &out, LogMsg &msg) override
{
out << "\t";
}
};
// 换行子类
class NLineFormatItem : public FormatItem
{
public:
void format(std::ostream &out, LogMsg &msg) override
{
out << "\n";
}
};
// 其他子类 如: abcdefg[%d{%H}]
class OtherFormatItem : public FormatItem
{
public:
OtherFormatItem(const std::string &str) : _str(str) {}
void format(std::ostream &out, LogMsg &msg) override
{
out << _str;
}
private:
std::string _str;
};
}
#endif
设计格式化完子项类就可以设计日志格式化类了。
2. 日志格式化类的设计
Format.hpp
整体框架:
#ifndef __M__FMT_H__
#define __M__FMT_H__
#include "Level.hpp"
#include "Message.hpp"
#include <vector>
namespace mylog
{
/*
%d 表示日期,包含子格式{%H:%M:%S}
%t 表示线程ID
%c 表示日志器名称
%f 表示源码文件名
%l 表示源码行号
%p 表示日志级别
%T 表示制表符缩进
%m 表示主体消息
%n 表示换行
*/
class Formatter
{
public:
using ptr = std::shared_ptr<Formatter>;
Formatter(const std::string &pattern = "[%D{%H:%H:%S}][%t][%c][%f:%l][%p]%T%m%n") :
_pattern(pattern) {}
// 对msg进行格式化
void format(std::ostream &out, LogMsg &msg);
std::string format(LogMsg &msg);
// 对格式话规则字符串进行解析
bool parsePattern();
private:
// 根据不同的格式化字符创建不同的格式化子项对象
FormatItem::ptr createItem(const std::string &key, const std::string &val);
private:
std::string _pattern; // 格式化规则字符串
std::vector<FormatItem::ptr> _items;
};
}
#endif
各接口具体实现:
构造函数:
Formatter(const std::string &pattern = "[%D{%H:%H:%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::stringstream ss;
format(ss, msg);
return ss.str();
}
根据不同的格式化字符创建不同的格式化子项对象:
// 根据不同的格式化字符创建不同的格式化子项对象
FormatItem::ptr createItem(const std::string &key, const std::string &val)
{
if(key == "d") return std::make_shared<TimeFormatItem>(val);
if(key == "t") return std::make_shared<ThreadFormatItem>();
if(key == "c") return std::make_shared<LoggerFormatItem>();
if(key == "f") return std::make_shared<FileFromatItem>();
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>();
return std::make_shared<OtherFormatItem>(val);
}
对格式化规则字符串进行解析:
举个例子:
规则字符串的处理过程就是一个循环的过程:
while()
{
1. 处理原始字符串
2. 原始字符串结束后,遇到%。则处理下一个格式胡字符
}
在处理过程中,需将处理得到的信息保存下来,例如图中的字符串:
- key = nullptr, val = abcde[
- key = d, val = %H:%M:%S
- key = nullptr, val = ][
- key = p, val = nullptr
- key = nullptr, val = ]
- key = T, val = nullptr
- key = m, val = nullptr
- key = n, val = nullptr
拿到这个数组之后,根据这个数组内容创建对应格式化子项对象,添加到 _items成员数组中:
// 对格式化规则字符串进行解析
bool parsePattern()
{
// 1. 对格式化规则字符串进行解析
// ab%c%cde[%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())
{
// 1. 处理原始字符串 - 判断是否是 % ,不是就为原始字符串
if(_pattern[pos] != '%')
{
val.push_back(_pattern[pos++]);
continue;
}
// 能走下来就代表pos位置就是 % 字符,%% 称为一个原始 % 字符(转义字符)
if(pos+1 < _pattern.size() && _pattern[pos+1] == '%')
{
val.push_back('%');
pos += 2;
continue;
}
// 能走下来就代表 % 后一个字符为格式化字符,代表原始字符串处理完毕,
// 并将原始字符串添加到格式化数组内
if(val.empty() == false)
{
fmt_order.push_back(std::make_pair("", val));
val.clear();
}
// 这时候为格式化字符的处理
// pos++指向的是 % 后一个字符位置,即格式化字符位置
pos++;
if(pos == _pattern.size())
{
std::cout << "%之后没有对应的格式化字符!\n";
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 << "子规则{}匹配出错!\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;
}
功能测试:
#include "Util.hpp"
#include "Level.hpp"
#include "Message.hpp"
#include "Format.hpp"
int main()
{
mylog::LogMsg msg(mylog::LogLevel::value::INFO, 100, "test.c", "root", "格式化功能测试...");
mylog::Formatter fmt;
std::string str = fmt.format(msg);
std::cout << str << std::endl;
return 0;
}
特殊情况测试:
双百分号:
mylog::Formatter fmt("abc%%acb[%d{%H:%M:%S}] %m%n");
缺少 } 花括号:
mylog::Formatter fmt("abc%%acb[%d{%H:%M:%S] %m%n");
%后缺少字符:
mylog::Formatter fmt("abc%%acb[%d{%H:%M:%S}] %m%");
特殊字符处理:%g
mylog::Formatter fmt("abc%%acb[%d{%H:%M:%S}] %m%n[%g%g%g]");
(五)日志落地类的设计
- 抽象出落地模块基类。
- 不同落地方向从基类进行派生。
- 使用工厂模式进行创建与表示的分离。
Sink.hpp
#ifndef __M_SINK_H__
#define __M_SINK_H__
/*
抽象出落地模块基类。
不同落地方向从基类进行派生。
使用工厂模式进行创建与表示的分离。
*/
#include "Util.hpp"
#include <memory>
#include <iostream>
#include <fstream>
namespace mylog
{
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:
// 构造是传入文件名,并打开文件,将操作句柄管理起来
FileSink(const std::string &pathname);
// 将日志消息写入到标准输出
void log(const char *data, size_t len);
private:
std::string _pathname;
std::ofstream _ofs;
};
// 落地方向: 滚动文件 (以大小进行滚动)
class RollBySizeSink : public LogSink
{
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- ——> ./long/base-525623462346.log
std::ofstream _ofs;
size_t _max_fsize; // 记录最大大小,当前文件超过了这个大小就要切换文件
size_t _cur_fsize; // 记录当前文件已经写入的数据大小
};
}
#endif
落地标准输出方向类成员接口具体实现:
// 落地方向: 标准输出
class StdoutSink : public LogSink
{
public:
// 将日志消息写入到标准输出
void log(const char *data, size_t len)
{
std::cout.write(data, len);
}
};
落地指定文件方向类成员接口具体实现:
// 落地方向: 指定文件
class FileSink : public LogSink
{
public:
// 构造是传入文件名,并打开文件,将操作句柄管理起来
FileSink(const std::string &pathname) : _pathname(pathname)
{
// 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;
};
落地滚动文件方向类成员接口具体实现:
滚动⽇志⽂件输出的必要性:
- 由于机器磁盘空间有限, 我们不可能⼀直⽆限地向⼀个⽂件中增加数据。
- 如果⼀个⽇志⽂件体积太⼤,⼀⽅⾯是不好打开,另⼀⽅⾯是即时打开了由于包含数据巨 ⼤,也不利于查找我们需要的信息。
- 所以实际开发中会对单个⽇志⽂件的⼤⼩也会做⼀些控制,即当⼤⼩超过某个⼤⼩时(如 1GB),我们就重新创建⼀个新的⽇志⽂件来滚动写⽇志。 对于那些过期的⽇志, ⼤部分企业内部都有专⻔的运维⼈员去定时清理过期的⽇志,或者设置系统定时任务,定时清理过 期⽇志。
⽇志⽂件滚动的条件有两个:文件大小 和 时间。我们可以选择:
- ⽇志⽂件在⼤于 1GB 的时候会更换新的⽂件。
- 每天定点滚动⼀个⽇志⽂件。
本项⽬基于文件大小 的判断滚动⽣成新的⽂件。
注意:如果我们设置的最大大小太小(下面程序中设置成了1MB),导致程序可能会在一秒钟之内生成多个同名文件,所以我们可以添加一个计数器区分文件名。
// 落地方向: 滚动文件 (以大小进行滚动)
class RollBySizeSink : public LogSink
{
public:
// 构造时传入文件名,并打开文件,将操作句柄管理起来
RollBySizeSink(const std::string &basename, size_t max_size) :
_basename(basename), _max_fsize(max_size), _cur_fsize(0), _name_coumt(0)
{
std::string pathname = createNewFile();
// 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)
{
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::nowTime();
struct tm lt;
localtime_r(&t, <);
std::stringstream filename;
filename << _basename;
filename << lt.tm_year + 1900; // 时间戳从1900/01/01 00:00:00开始的
filename << "-";
filename << lt.tm_mon + 1; // 系统月份默认为从 0 开始,所以需+1
filename << "-";
filename << lt.tm_mday;
filename << " ";
filename << lt.tm_hour;
filename << ":";
filename << lt.tm_min;
filename << ":";
filename << lt.tm_sec;
filename << "-";
filename << _name_coumt++;
filename << ".log";
return filename.str();
}
private:
// 通过基础文件名 + 扩展文件名(以时间生成)组成一个实际的当前输出文件名
size_t _name_coumt; // 如果我们设置的最大大小太小,导致程序可能会在一秒钟之内生成多个同名文件,所以我们可以添加一个计数器区分文件名
std::string _basename; // ./log/base- ——> ./long/base-525623462346.log
std::ofstream _ofs;
size_t _max_fsize; // 记录最大大小,当前文件超过了这个大小就要切换文件
size_t _cur_fsize; // 记录当前文件已经写入的数据大小
};
实现了以上功能类,还差一个工厂类来创建不同的功能类:
使用模版跟不定参来解决创建不同的功能类与其成员接口参数不一致的问题:
#ifndef __M_SINK_H__
#define __M_SINK_H__
/*
抽象出落地模块基类。
不同落地方向从基类进行派生。
使用工厂模式进行创建与表示的分离。
*/
#include "Util.hpp"
#include <memory>
#include <iostream>
#include <fstream>
#include <sstream>
#include <cassert>
namespace mylog
{
// ...
// 落地方向工厂
class SinkFactory
{
public:
template <typename SinkType, typename ...Args>
static LogSink::ptr create(Args &&...args)
{
return std::make_shared<SinkType>(std::forward<Args>(args)...);
}
};
}
#endif
#include "Util.hpp"
#include "Level.hpp"
#include "Message.hpp"
#include "Format.hpp"
#include "Sink.hpp"
int main()
{
mylog::LogMsg msg(mylog::LogLevel::value::INFO, 100, "test.c", "root", "格式化功能测试...");
mylog::Formatter fmt;
std::string str = fmt.format(msg);
mylog::LogSink::ptr stdout_lsp = mylog::SinkFactory::create<mylog::StdoutSink>();
mylog::LogSink::ptr file_lsp = mylog::SinkFactory::create<mylog::FileSink>("./logfile/test.log");
mylog::LogSink::ptr roll_lsp = mylog::SinkFactory::create<mylog::RollBySizeSink>("./logfile/roll-", 1024*1024);
stdout_lsp->log(str.c_str(), str.size());
file_lsp->log(str.c_str(), str.size());
size_t cursize = 0;
size_t count = 0;
while(cursize < 1024 * 1024 * 10)
{
std::string tmp = str + std::to_string(count++);
roll_lsp->log(tmp.c_str(), tmp.size());
cursize += tmp.size();
}
return 0;
}
可以看到:
标准输出打印成功
落地文件创建成功,再查看里面的日志:
滚动文件创建了10个,每个都超过了1MB,再选择一个来查看里面的日志:
滚动文件的扩展
以时间进行文件滚动,实际上是以时间段进行滚动。
#include "Util.hpp"
#include "Level.hpp"
#include "Message.hpp"
#include "Format.hpp"
#include "Sink.hpp"
#include <unistd.h>
/*
扩展一个以时间作为入职文件滚动切换类型的日志落地模块
以时间进行文件滚动,实际上是以时间段进行滚动
实现思想: 以当前系统时间,取模时间段大小,可以得到当前时间是第几个时间段,
每次以当前系统时间取模,判断与当前文件的时间是否一致,不一致代表不是同一个时间段。
*/
enum class TimeGap
{
GAP_SECOND,
GAP_MINUTE,
GAP_HOUR,
GAP_DAY
};
// 落地方向: 滚动文件 (以时间进行滚动)
class RollByTimeSink : public mylog::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 ? mylog::util::Date::nowTime() : mylog::util::Date::nowTime() % _gap_size; // 获取当前是第几个时间段
std::string filename = createNewFile();
// 1. 创建日志文件所在目录
mylog::util::File::createDirectory(mylog::util::File::path(filename));
// 2. 创建并打开日志文件,以二进制格式和追加形式打开
_ofs.open(filename, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
// 将日志消息写入到文件 - 写入前判断文件时间段
void log(const char *data, size_t len)
{
time_t cur = mylog::util::Date::nowTime();
if((cur % _gap_size) != _cur_gap)
{
_ofs.close();
std::string filename = createNewFile();
_ofs.open(filename, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
_ofs.write(data, len);
assert(_ofs.good());
}
private:
// 进行大小判断,超过指定大小就创建新文件
std::string createNewFile()
{
// 获取系统时间,以时间来构造文件名扩展名
time_t t = mylog::util::Date::nowTime();
struct tm lt;
localtime_r(&t, <);
std::stringstream filename;
filename << _basename;
filename << lt.tm_year + 1900; // 时间戳从1900/01/01 00:00:00开始的
filename << "-";
filename << lt.tm_mon + 1; // 系统月份默认为从 0 开始,所以需+1
filename << "-";
filename << lt.tm_mday;
filename << " ";
filename << lt.tm_hour;
filename << ":";
filename << lt.tm_min;
filename << ":";
filename << lt.tm_sec;
filename << "-";
filename << ".log";
return filename.str();
}
private:
std::string _basename;
std::ofstream _ofs;
size_t _cur_gap; // 当前是第几个时间段
size_t _gap_size; // 时间段的大小
};
int main()
{
mylog::Formatter fmt;
std::string str = fmt.format(msg);
// 以一秒作为时间间隔
mylog::LogSink::ptr time_lsp = mylog::SinkFactory::create<RollByTimeSink>("./logfile/roll-", TimeGap::GAP_SECOND);
time_t old = mylog::util::Date::nowTime();
while(mylog::util::Date::nowTime() < old + 5)
{
time_lsp->log(str.c_str(), str.size());
usleep(1000);
}
return 0;
}
这里我们测试以一秒作为时间间隔,预期生成五个日志消息文件,查看运行结果:
这里43秒时的文件大小与其他时间段的文件大小差了很多,因为此时的43秒不是一个完整的时间段,即不是完整的一秒:
(六)日志器类的设计
日志器主要是⽤来和前端交互, 当我们需要使⽤⽇志系统打印log的时候, 只需要创建Logger对象,调⽤该对象debug、info、warn、error、fatal等⽅法输出⾃⼰想打印的⽇志即可,⽀持解析可变参数列表和输出格式, 即可以做到像使⽤printf函数⼀样打印⽇志。
功能:对前边所以模块进行整合,向外提供接口完成不同等级日志的输出。
管理的成员:
- 格式化模块对象
- 落地模块对象数组(一个日志器可能会向多个位置进行日志输出)
- 默认的日志输出限制等级(大于等于限制等级的日志才能输出)
- 互斥锁(保证日志输出过程是线程安全的,不会出现交叉日志)
- 日志器名称(日志器的唯一标识,以便于查找)
提供的操作:
- 获取日志器名称
- debug等级日志的输出操作(分别会封装日志消息LogMsg--各个接口日志等级不同)
- info等级日志的输出操作
- warn等级日志的输出操作
- error等级日志的输出操作
- fatal等级日志的输出操作
实现:
- 抽象Logger基类(派生出同步日志器类 & 异步日志器类)。
- 因为两种不同的日志器,只有落地方式不同,因此将落地操作给抽象出来,不同的日志器调用各自的落地操作进行日志落地。
- 模块关联中使用基类指针对子类日志器对象进行日志管理和操作。
Logger.hpp
基类整体框架:
#ifndef __M_LOGG_H__
#define __M_LOGG_H__
/*
1. 抽象Logger基类(派生出同步日志器类 & 异步日志器类)
2. 因为两种不同的日志器,只有落地方式不同,因此将落地操作给抽象出来,
不同的日志器调用各自的落地操作进行日志落地。
*/
#include "Util.hpp"
#include "Level.hpp"
#include "Format.hpp"
#include "Sink.hpp"
#include <mutex>
#include <atomic>
namespace mylog
{
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),
_sinks(sinks.begin(), sinks.end())
{}
// 获取日志器名称
const std::string &getName() { 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:
// 抽象接口完成实际的落地输出 - 不同的日志器会有不同的实际落地方式
virtual void log(const char *data, size_t len) = 0;
protected:
std::mutex _mutex;
std::string _logger_name;
// 输出大量日志消息时对于日志等级会进行大量并发访问,所以将其设置成为原子性的
std::atomic<LogLevel::value> _limit_level;
Formatter::ptr _formatter;
std::vector<LogSink::ptr> _sinks;
};
}
#endif
各个接口具体实现:
// 完成构造日志消息对象过程并进行格式化,得到格式化后的日志消息字符串,然后进行落地输出
void debug(const std::string &file, size_t line, const std::string &fmt, ...)
{
// 通过传入的参数构造出一个日志消息对象,进行日志的格式化,最终落地
// 1. 判断当前的日志是否到了输出等级
if(LogLevel::value::DEBUG < _limit_level) { return ; }
// 2. 对fmt格式化字符串和不定参进行字符串组织,得到的日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if(ret == -1)
{
std::cout << "vasprintf failed!\n";
return;
}
va_end(ap); // 将ap指针置空
// 3. 构造LogMsg对象
LogMsg msg(LogLevel::value::DEBUG, line, file, _logger_name, res);
// 4. 通过格式化工具对LogMsg进行格式化,得到格式化后的日志字符串
std::stringstream ss;
_formatter->format(ss, msg);
// 5. 对日志落地
log(ss.str().c_str(), ss.str().size());
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());
}
各个等级消息格式化的实现:
// 完成构造日志消息对象过程并进行格式化,得到格式化后的日志消息字符串,然后进行落地输出
void debug(const std::string &file, size_t line, const std::string &fmt, ...)
{
// 通过传入的参数构造出一个日志消息对象,进行日志的格式化,最终落地
// 1. 判断当前的日志是否到了输出等级
if(LogLevel::value::DEBUG < _limit_level) { return ; }
// 2. 对fmt格式化字符串和不定参进行字符串组织,得到的日志消息的字符串
va_list ap;
va_start(ap, fmt);
char *res;
int ret = vasprintf(&res, fmt.c_str(), ap);
if(ret == -1)
{
std::cout << "vasprintf failed!\n";
return;
}
va_end(ap); // 将ap指针置空
serialize(LogLevel::value::DEBUG, file, line, res);
free(res);
}
void info(const std::string &file, size_t line, const std::string &fmt, ...)
{
if(LogLevel::value::INFO < _limit_level) { return ; }
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, ...)
{
if(LogLevel::value::WARN < _limit_level) { return ; }
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, ...)
{
if(LogLevel::value::ERROR < _limit_level) { return ; }
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, ...)
{
if(LogLevel::value::FATAL < _limit_level) { return ; }
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);
}
有了基类的抽象实现,接下来就可以派生出同步日志器类跟异步日志器类了:
1. 同步日志器类:
同步日志器是直接将日志消息进行格式化写入文件
namespace mylog
{
// 基类code...
// 同步日志器 - 将日志直接通过落地模块句柄进行日志落地
class SyncLogger : public Logger
{
public:
SyncLogger(const std::string &logger_name,
LogLevel::value level,
Formatter::ptr &formatter,
std::vector<LogSink::ptr> &sinks) :
Logger(logger_name, level, formatter, sinks)
{}
protected:
void log(const char *data, size_t len)
{
std::unique_lock<std::mutex> lock(_mutex);
if(_sinks.empty()) return;
for(auto &sink : _sinks)
{
sink->log(data, len);
}
}
};
}
编写测试代码,运行并查看结果:
#include "Util.hpp"
#include "Level.hpp"
#include "Message.hpp"
#include "Format.hpp"
#include "Sink.hpp"
#include "Logger.hpp"
#include <unistd.h>
#include <memory>
int main()
{
std::string logger_name = "sync_logger";
mylog::LogLevel::value limit = mylog::LogLevel::value::WARN;
mylog::Formatter::ptr fmt(new mylog::Formatter("[%d{%H:%M:%S}][%c][%f:%l][%p]%T%m%n"));
mylog::LogSink::ptr stdout_lsp = mylog::SinkFactory::create<mylog::StdoutSink>();
mylog::LogSink::ptr file_lsp = mylog::SinkFactory::create<mylog::FileSink>("./logfile/test.log");
mylog::LogSink::ptr roll_lsp = mylog::SinkFactory::create<mylog::RollBySizeSink>("./logfile/roll-", 1024*1024);
std::vector<mylog::LogSink::ptr> sinks = {stdout_lsp, file_lsp, roll_lsp};
mylog::Logger::ptr logger(new mylog::SyncLogger(logger_name, limit, fmt, sinks));
logger->debug(__FILE__, __LINE__, "%s", ":测试日志");
logger->info(__FILE__, __LINE__, "%s", "测试日志");
logger->warn(__FILE__, __LINE__, "%s", "测试日志");
logger->error(__FILE__, __LINE__, "%s", "测试日志");
logger->fatal(__FILE__, __LINE__, "%s", "测试日志");
size_t cursize = 0, count = 0;
while(cursize < 1024 * 1024 * 10)
{
logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
cursize += 20;
}
return 0;
}
查看生成的目录:
查看输出到的指定文件test.log,可以看到就写入了WARN,ERRO,FATAL三个等级的日志消息,因为我们设置的最小限制等级为WARN:
2. 构造日志器扩展
使用建造者模式来建造日志器,而不是让用户直接去构造日志器,简化用户的使用复杂度:
- 抽象一个日志器建造者类(派生出同步日志器类 & 异步日志器类)
1. 设置日志器类型
2. 将不同类型日志器的创建放到同一个日志器建造者类中完成 - 派生出具体的建造者类 -- 局部日志器的建造者 & 全局的日志器建造者(后边添加了全局单例管理器之后,将日志器添加全局管理)
namespace mylog
{
// ...
/* 使用建造者模式来建造日志器,而不是让用户直接去构造日志器,简化用户的使用复杂度 */
// 1. 抽象一个日志器建造者类(派生出同步日志器类 & 异步日志器类)
// 1. 设置日志器类型
// 2. 将不同类型日志器的创建放到同一个日志器建造者类中完成
enum LoggerType
{
LOGGER_SYNC,
LOGGER_ASYNC
};
class LoggerBuilder
{
public:
LoggerBuilder() :
_logger_type(LoggerType::LOGGER_SYNC),
_limit_level(LogLevel::value::DEBUG)
{}
void buildLoggerType(LoggerType type);
void buildLoggerName(const std::string &name);
void buildLoggerLevel(LogLevel::value level);
void buildFormatter(const std::string &pattern);
template<typename SinkType, typename ...Args>
void buildSing(Args &&...args);
virtual void 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:
void build() override;
};
}
整体实现:
/* 使用建造者模式来建造日志器,而不是让用户直接去构造日志器,简化用户的使用复杂度 */
// 1. 抽象一个日志器建造者类(派生出同步日志器类 & 异步日志器类)
// 1. 设置日志器类型
// 2. 将不同类型日志器的创建放到同一个日志器建造者类中完成
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(const std::string &name) { _logger_name = name; }
void buildLoggerLevel(LogLevel::value level) { _limit_level = level; }
void buildFormatter(const std::string &pattern)
{
_formatter = std::make_shared<Formatter>(pattern);
}
template<typename SinkType, typename ...Args>
void buildSink(Args &&...args)
{
LogSink::ptr psink = SinkFactory::create<SinkType>(std::forward<Args>(args)...);
_sinks.push_back(psink);
}
virtual Logger::ptr build() = 0;
protected:
LoggerType _logger_type;
std::string _logger_name;
LogLevel::value _limit_level;
Formatter::ptr _formatter;
std::vector<LogSink::ptr> _sinks;
};
// 2. 派生出具体的建造者类 -- 局部日志器的建造者 & 全局的日志器建造者(后边添加了全局单例管理器之后,将日志器添加全局管理)
class LocalLoggerBuilder : public LoggerBuilder
{
public:
Logger::ptr build() override
{
assert(_logger_name.empty() == 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<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);
}
};
编写测试代码,运行并查看结果:
#include "Util.hpp"
#include "Level.hpp"
#include "Message.hpp"
#include "Format.hpp"
#include "Sink.hpp"
#include "Logger.hpp"
#include <unistd.h>
#include <memory>
int main()
{
std::unique_ptr<mylog::LoggerBuilder> builder(new mylog::LocalLoggerBuilder());
builder->buildLoggerName("sync_logger");
builder->buildLoggerLevel(mylog::LogLevel::value::WARN);
builder->buildFormatter("%m%n");
builder->buildLoggerType(mylog::LoggerType::LOGGER_SYNC);
builder->buildSink<mylog::FileSink>("./logfile/test.log"); // 落地到指定文件
builder->buildSink<mylog::StdoutSink>(); // 落地到标准输出
mylog::Logger::ptr logger = builder->build();
logger->debug(__FILE__, __LINE__, "%s", ":测试日志");
logger->info(__FILE__, __LINE__, "%s", "测试日志");
logger->warn(__FILE__, __LINE__, "%s", "测试日志");
logger->error(__FILE__, __LINE__, "%s", "测试日志");
logger->fatal(__FILE__, __LINE__, "%s", "测试日志");
size_t cursize = 0, count = 0;
while(cursize < 1024 * 1024 * 10)
{
logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
cursize += 20;
}
return 0;
}
成功落地到标准输出:
成功落地到指定文件 test.log:
(七)异步日志器类:
异步日志器:将日志消息放⼊缓冲区,由异步线程进⾏落地输出。
思想:为了避免因为写日志的过程阻塞,导致业务线程在写日志的时候影响效率,因此异步的思想就是不让业务线程进行日志的实际落地操作,而是将日志消息放到缓冲区中(一块指定1的内存),会有一个专门的异步线程去针对缓冲区的数据进行处理(实际的落地操作)。
同步日志器:
异步日志器:
1. 缓冲区设计思想:
要求:不能涉及到空间的频繁申请和释放,否则会降低效率。
使用的数据结构:使用环形队列(提前申请空间,然后对空间循环利用),缓存日志消息,逐条处理。
一个头指针,一个尾指针开始时都指向第一个节点,若写入了一个日志,则尾指针往后走一步,日志数据拿走时,头指针往后走一步;尾指针走到尾时再走一步只需指到环形队列头部就行,头指针同理。
引出问题:缓冲区的读写操作会涉及到多线程,因此缓冲区的操作必须保证线程安全。
线程安全实现:对于缓冲区的读写操作进行加锁
因为写日志操作,在实际开发中,并不会分配太多资源,所以工作线程只需要一个日志器有一个写日志线程就行。
涉及到的冲突:生产者与生产者的互斥 & 生产者与消费者的互斥。
引出问题:锁冲突较为严重,因为所有线程之间都存在互斥关系。
单个缓冲区类的进一步设计
设计一个缓冲区:直接存放格式化后的日志消息字符串。
好处:
- 减少了LogMsg对象频繁的构造的消耗,提高效率。
- 可以针对缓冲区中的日志消息,一次性进行IO操作,减少IO次数,提高效率。
缓冲区类的设计:
- 管理一个存放字符串数据的缓冲区(使用vector进行空间管理)。
- 当前的写入数据位置的指针(指向可写区域的起始位置,避免数据的写入覆盖)。
- 当前的读取数据位置的指针(指向可读数据区域的起始位置,当读取指针与写入指针指向相同位置则表示数据读取完了)。
提供的操作:
- 向缓冲区写入数据
- 获取可读数据起始地址的接口
- 获取可读写数据长度的接口
- 移动读写位置的接口
- 初始化缓冲区的操作(将读写位置初始化 -- 将一个缓冲区所有数据处理完毕之后)
- 提供缓冲区扩容的接口
- 提供交换缓冲区的操作(交换空间地址,并不交换空间数据)
Buffer.hpp
整体框架:
#ifndef __M_BUFFER_H__
#define __M__BUFFER_H__
/* 异步日志缓冲区 */
#include "Util.hpp"
#include <vector>
namespace mylog
{
#define DEFAULT_BUFFER_SIZE (1024*1024*100)
class Buffer
{
public:
Buffer() : _buffer(DEFAULT_BUFFER_SIZE), _reader_idx(0), _writer_idx(0) {}
// 向缓冲区写入数据
void push(const char *data, size_t len);
// 返回可读数据的起始地址
const char *begin();
// 返回可读写数据的长度
size_t readAbleSize();
size_t writeAbleSize();
//对读指针进行向后偏移操作
void moveReader(size_t len);
// 重置读写位置,初始化缓冲区
void reset();
//对buffer实现交换操作
void swap(Buffer &buffer);
// 判断缓冲区是否为空
bool empty();
private:
// 对空间进行扩容
void ensureEnoughSize(size_t len);
//对写指针进行向后偏移操作
void moveWriter(size_t len);
private:
std::vector<char> _buffer;
size_t _reader_idx; // 当前可读数据的指针
size_t _writer_idx; // 当前可写数据的指针
};
}
#endif
总体实现:
#ifndef __M_BUFFER_H__
#define __M__BUFFER_H__
/* 异步日志缓冲区 */
#include "Util.hpp"
#include <vector>
#include <cassert>
namespace mylog
{
#define DEFAULT_BUFFER_SIZE (1024*1024*100) // 默认大小
#define THRESHOLD_BUFFER_SIZE (80*1024*1024) // 阈值大小
#define INCREMENT_BUFFER_SIZE (10*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; 不考虑 - 在lopper类已经考虑
// (2) 动态扩容,用于极限性能测试
ensureEnoughSize(len);
// 1.将数据拷贝进缓冲区
std::copy(data, data+len, &_buffer[_writer_idx]);
// 2. 将当前写入位置向后偏移
moveWriter(len);
}
// 返回可读数据的起始地址
const char *begin()
{
return &_buffer[_reader_idx];
}
// 返回可读写数据的长度
size_t readAbleSize()
{
// 因为当前实现的缓冲区设计思想是双缓冲区,处理完就交换,所以不存在空间循环使用的
return (_writer_idx - _reader_idx);
}
size_t writeAbleSize()
{
// 对于扩容思路来说,不存在可写空间大小,因为总是可写
// 因此这个接口仅仅针对固定大小缓冲区提供
return (_buffer.size() - _writer_idx);
}
//对读指针进行向后偏移操作
void moveReader(size_t len)
{
assert(len <= readAbleSize());
_reader_idx += len;
}
// 重置读写位置,初始化缓冲区
void reset()
{
_writer_idx = 0; // 缓冲区所有空间都是空闲的
_reader_idx = 0; // 与_write_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 + len; // 小于阈值则翻倍增长
}
else
{
new_size = _buffer.size() + INCREMENT_BUFFER_SIZE + len; // 否则线性增长
}
_buffer.resize(new_size);
}
//对写指针进行向后偏移操作
void moveWriter(size_t len)
{
assert((len+_writer_idx) <= _buffer.size());
_writer_idx += len;
}
private:
std::vector<char> _buffer;
size_t _reader_idx; // 当前可读数据的指针
size_t _writer_idx; // 当前可写数据的指针
};
}
#endif
编写测试代码,运行并查看结果:
测试思想:读取文件数据,一点一点写入缓冲区,最终将缓冲区数据写入文件,判断生成的新文件与源文件是否一致:
#include "Util.hpp"
#include "Level.hpp"
#include "Message.hpp"
#include "Format.hpp"
#include "Sink.hpp"
#include "Logger.hpp"
#include "Buffer.hpp"
#include <unistd.h>
#include <memory>
int main()
{
// 读取文件数据,一点一点写入缓冲区,最终将缓冲区数据写入文件,判断生成的新文件与源文件是否一致
std::ifstream ifs("./logfile/test.log", std::ios::binary);
if (ifs.is_open() == false) { std::cout << "open faild!\n"; return -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;
mylog::Buffer buffer;
for(int i = 0; i < body.size(); i++)
{
buffer.push(&body[i], 1);
}
std::cout << "缓冲区大小: " << buffer.readAbleSize() << std::endl;
std::ofstream ofs("./logfile/tmp.log", std::ios::binary);
// ofs.write(buffer.begin(), buffer.readAbleSize()); // 一次性写入
size_t rsize = buffer.readAbleSize();
for(int i = 0; i < rsize; i++)
{
ofs.write(buffer.begin(), 1);
if(ofs.good() == false) { std::cout << "write error!\n"; return -1;}
buffer.moveReader(1);
}
if(ofs.good() == false) {std::cout << "write error!\n"; return -1;}
ofs.close();
return 0;
}
可以看到logfile目录下生成了一个 tmp.log 文件,经过MD5值计算与源文件值一致,说明两个文件内容一致,从而说明文件写入成功:
2. 异步工作器类设计
异步工作使用双缓冲区思想:外界将任务数据添加到输入缓冲区中,异步线程对处理缓冲区中的数据进行处理,若处理缓冲区中没有数据了则交换缓冲区。
实现:
管理的成员:
- 双缓冲区(生产,消费)
- 互斥锁(保证线程安全)
- 条件变量 - 生产&消费(生产缓冲区没有数据,处理完消费缓冲区数据后就休眠)
- 回调函数(针对缓冲区中数据的处理接口 - 外界传入一个函数,告诉异步工作器数据该如何处理)
提供的操作:
- 停止异步工作器
- 添加数据到缓冲区
私有操作:
- 创建线程,线程入口函数中,交换缓冲区,对消费缓冲区数据使用回调函数进行处理,处理完后再次交换。
Lopper.hpp
#ifndef __M_LOPPER_H__
#define __M_LOPPER_H__
/* 异步工作器 */
#include "Buffer.hpp"
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <memory>
namespace mylog
{
using Functor = std::function<void(Buffer &)>;
enum class AsyncType
{
ASYNC_SAFE, // 安全状态,表示缓冲区满了则阻塞,避免资源耗尽的风险
ASYNC_UNSAFE // 不安全考虑资源耗尽的问题,无限扩容,常用于测试
};
class AsyncLooper
{
public:
using ptr = std::shared_ptr<AsyncLooper>;
AsyncLooper(const Functor &cb, AsyncType loop_type = AsyncType::ASYNC_SAFE) :
_looper_type(loop_type),
_stop(false),
_thread(std::thread(&AsyncLooper::threadEntry, this)),
_callBack(cb)
{}
~AsyncLooper() { stop(); }
void stop()
{
_stop = true; // 将退出标志设置为true
_cond_con.notify_all(); // 唤醒所有的工作线程
_thread.join(); // 等待工作线程的退出
}
void push(const char *data, size_t len)
{
// 1. 无线扩容 - 非安全; 2. 固定大小 - 生产缓冲区中数据满了就阻塞
std::unique_lock<std::mutex> lock(_mutex);
// 条件变量空置,若缓冲区剩余空间大小大于数据长度,则可以添加数据
if(_looper_type == AsyncType::ASYNC_SAFE)
_cond_pro.wait(lock, [&](){ return _pro_buf.writeAbleSize() >= len; });
// 能够走下来代表满足了条件,可以向缓冲区添加数据
_pro_buf.push(data, len);
// 唤醒消费者对缓冲区中的数据进行处理
_cond_con.notify_one();
}
private:
// 线程入口函数 - 对消费缓冲区中的数据进行处理,处理完毕后,初始化缓冲区,交换缓冲区
void threadEntry()
{
while(1)
{
// 为互斥锁设置一个生命周期,当缓冲区交换完毕后就解锁(并不对数据的处理过程加锁保护)
{
// 1. 判断生产缓冲区有没有数据,有则交换,无则阻塞
std::unique_lock<std::mutex> lock(_mutex);
// 退出标志被设置。且生成缓冲区已无数据,这时候再退出,否则有可能会造成生产缓冲区中有数据,但是没有被完全处理
if(_stop && _pro_buf.empty()) break;
// 若当前是退出前被唤醒,或者有数据被唤醒,则返回真,继续向下运行,否则重新陷入休眠
_cond_con.wait(lock, [&](){ return _stop || !_pro_buf.empty(); });
_con_buf.swap(_pro_buf);
// 2. 唤醒生产者
if(_looper_type == AsyncType::ASYNC_SAFE)
_cond_pro.notify_all();
}
// 3. 被唤醒后,对消费缓冲区进行数据处理
_callBack(_con_buf);
// 4. 初始化消费缓冲区
_con_buf.reset();
}
}
private:
Functor _callBack; // 具体对缓冲区数据进行处理的回调函数,由异步工作器使用者传入
private:
AsyncType _looper_type;
bool _stop; // 工作器停止标志
Buffer _pro_buf; // 生产缓冲区
Buffer _con_buf; // 消费缓冲区
std::mutex _mutex;
std::condition_variable _cond_pro;
std::condition_variable _cond_con;
std::thread _thread; // 异步工作器对应的工作线程
};
}
#endif
3. 异步日志器类设计
思想:
- 继承与Logger日志器类:对于写日志操作进行函数重写(不再将数据直接写入文件,而是通过异步消息处理器,放到缓冲区中)。
-
realLog函数主要由异步线程进⾏调⽤(是为异步消息处理器设置的回调函数),进行日志数据的实际落地。
管理的成员:
- 异步工作器(异步消息处理器)
Logger.hpp
namespace mylog
{
// 异步落地器
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),
_looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::realLog, this, std::placeholders::_1), looper_type))
{}
// 将数据写入缓冲区
void log(const char *data, size_t len)
{
_looper->push(data, len);
}
// 设计一个实际落地函数(将缓冲区中的数据落地)
void realLog(Buffer &buf)
{
if(_sinks.empty()) return;
for(auto &sink : _sinks)
{
sink->log(buf.begin(), buf.readAbleSize());
}
}
private:
AsyncLooper::ptr _looper;
};
}
完成后,完善日志器建造者,进行异步日志器安全模式的选择,提供异步日志器的创建:
namespace mylog
{
/* 使用建造者模式来建造日志器,而不是让用户直接去构造日志器,简化用户的使用复杂度 */
// 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 buildEnableUnSafeAsync() { _looper_type = AsyncType::ASYNC_UNSAFE; }
void buildLoggerName(const std::string &name) { _logger_name = name; }
void buildLoggerLevel(LogLevel::value level) { _limit_level = level; }
void buildFormatter(const std::string &pattern)
{
_formatter = std::make_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);
}
};
}
编写测试代码,测试异步日志器总体模块:
#include "Util.hpp"
#include "Level.hpp"
#include "Message.hpp"
#include "Format.hpp"
#include "Sink.hpp"
#include "Logger.hpp"
#include "Buffer.hpp"
#include "Looper.hpp"
#include <unistd.h>
#include <memory>
int main()
{
std::unique_ptr<mylog::LoggerBuilder> builder(new mylog::LocalLoggerBuilder());
builder->buildLoggerName("async_logger");
builder->buildLoggerLevel(mylog::LogLevel::value::WARN);
builder->buildFormatter("[%c]%m%n");
builder->buildLoggerType(mylog::LoggerType::LOGGER_ASYNC);
builder->buildEnableUnSafeAsync();
builder->buildSink<mylog::FileSink>("./asynclogfile/async.log");
builder->buildSink<mylog::StdoutSink>();
mylog::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 < 500000)
{
logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
}
return 0;
}
(八)单例日志器管理类设计
- 对所有创建的日志器进行管理。
- 可以在程序的任意位置,获取相同的单例对象,获取其中的日志器进行日志输出。
- 管理的成员:
- 默认日志器
- 所管理的日志器数组
- 互斥锁
- 添加日志器管理
- 判断是否管理了指定名称的日志器
- 获取指定名称的日志器
- 获取默认日志器
namespace mylog
{
// 日志器管理类
class LoggerManager
{
public:
static LoggerManager& getInstance()
{
// 在C++11之后,针对静态局部变量,编译器在编译的层面实现了线程安全
// 当静态局部变量在没有构造完成之前,其他的线程进入就会阻塞
static LoggerManager eton;
return eton;
}
void addLogger(Logger::ptr &logger)
{
if(hasLogger(logger->getName())) return;
std::unique_lock<std::mutex> lock(_mutex);
_loggers.insert(std::make_pair(logger->getName(), logger));
}
bool hasLogger(const std::string &name)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _loggers.find(name);
if(it == _loggers.end())
return false;
return true;
}
Logger::ptr getLogger(const std::string &name)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _loggers.find(name);
if(it == _loggers.end())
return Logger::ptr();
return it->second;
}
Logger::ptr rootLogger() { return _root_logger; }
private:
LoggerManager()
{
std::unique_ptr<mylog::LoggerBuilder> builder(new mylog::LocalLoggerBuilder());
builder->buildLoggerName("root");
_root_logger = builder->build();
_loggers.insert(std::make_pair("root", _root_logger));
}
private:
std::mutex _mutex;
Logger::ptr _root_logger; // 默认日志器
std::unordered_map<std::string, Logger::ptr> _loggers;
};
}
实现了管理类后,就可以派生出具体的建造者类 -- 全局的日志器建造者(添加了全局单例管理器之后,将日志器添加全局管理):
namespace mylog
{
// 2. 派生出具体的建造者类 -- 全局的日志器建造者(添加了全局单例管理器之后,将日志器添加全局管理)
class GlobalLoggerBuilder : 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>();
}
Logger::ptr logger;
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;
}
};
}
编写测试代码,运行并查看结果测试全局管理日志器:
#include "Util.hpp"
#include "Level.hpp"
#include "Message.hpp"
#include "Format.hpp"
#include "Sink.hpp"
#include "Logger.hpp"
#include "Buffer.hpp"
#include "Looper.hpp"
#include <unistd.h>
#include <memory>
// 测试全局管理日志器
void test_log()
{
mylog::Logger::ptr logger = mylog::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 < 500000)
{
logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
}
}
int main()
{
std::unique_ptr<mylog::LoggerBuilder> builder(new mylog::GlobalLoggerBuilder());
builder->buildLoggerName("async_logger");
builder->buildLoggerLevel(mylog::LogLevel::value::WARN);
builder->buildFormatter("[%c]%m%n");
builder->buildLoggerType(mylog::LoggerType::LOGGER_ASYNC);
builder->buildEnableUnSafeAsync();
builder->buildSink<mylog::FileSink>("./asynclogfile/async.log");
builder->buildSink<mylog::StdoutSink>();
builder->build();
test_log();
return 0;
}
运行结果查看日志消息,可以看到标准输出和指定文件都输出了50003条(1条warn、1条error、1条fatal、500000条fatal)数据,都符合预期:
(九)日志全局接口设计
- 提供获取指定日志器的全局接口(避免用户自己操作单例对象)。
- 使用宏函数对日志器的接口进行代理(代理模式)。
- 提供宏函数,直接通过默认日志器进行日志的标准输出打印(不用获取日志器了)。
#ifndef __M_MYLOG_H__
#define __M_MYLOG_H__
#include "Logger.hpp"
namespace mylog
{
// 1. 提供获取指定日志器的全局接口(避免用户自己操作单例对象)
Logger::ptr getLogger(const std::string &name)
{
return mylog::LoggerManager::getInstance().getLogger(name);
}
Logger::ptr rootLogger()
{
return mylog::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, ...) mylog::rootLogger()->debug(fmt, ##__VA_ARGS__)
#define INFO(fmt, ...) mylog::rootLogger()->info(fmt, ##__VA_ARGS__)
#define WARN(fmt, ...) mylog::rootLogger()->warn(fmt, ##__VA_ARGS__)
#define ERROR(fmt, ...) mylog::rootLogger()->error(fmt, ##__VA_ARGS__)
#define FATAL(fmt, ...) mylog::rootLogger()->fatal(fmt, ##__VA_ARGS__)
}
#endif
编写测试代码,测试通过宏函数能否落地到指定文件:
#include "mylog.h"
#include <unistd.h>
// 测试日志器
void test_log()
{
mylog::Logger::ptr logger = mylog::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 < 500000)
{
logger->fatal("测试日志-%d", count++);
}
}
int main()
{
std::unique_ptr<mylog::LoggerBuilder> builder(new mylog::GlobalLoggerBuilder());
builder->buildLoggerName("async_logger");
builder->buildLoggerLevel(mylog::LogLevel::value::WARN);
builder->buildFormatter("[%c][%f:%l]%m%n");
builder->buildLoggerType(mylog::LoggerType::LOGGER_ASYNC);
builder->buildEnableUnSafeAsync();
builder->buildSink<mylog::FileSink>("./asynclogfile/async.log");
builder->buildSink<mylog::StdoutSink>();
builder->build();
test_log();
return 0;
}
测试通过默认日志器宏函数,能否进行标准输出打印:
// 测试日志器
void test_log()
{
DEBUG("%s", "测试日志");
INFO("%s", "测试日志");
WARN("%s", "测试日志");
ERROR("%s", "测试日志");
FATAL("%s", "测试日志");
size_t count = 0;
while(count < 500000)
{
FATAL("测试日志-%d", count++);
}
}
七、项目目录结构管理
(一)整体测试代码(功能样例代码)测试
各个组件功能打包成一个目录,进行测试:
/example/test.cc
#include "../logsys/mylog.h"
#include <unistd.h>
// 测试日志器
void test_log(const std::string &name)
{
INFO("%s", "测试开始");
mylog::Logger::ptr logger = mylog::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<mylog::LoggerBuilder> builder(new mylog::GlobalLoggerBuilder());
builder->buildLoggerName("async_logger");
builder->buildLoggerLevel(mylog::LogLevel::value::DEBUG);
builder->buildFormatter("[%c][%f:%l][%p]%m%n");
builder->buildLoggerType(mylog::LoggerType::LOGGER_SYNC);
builder->buildSink<mylog::FileSink>("./logfile/sync.log");
builder->buildSink<mylog::StdoutSink>();
builder->buildSink<mylog::RollBySizeSink>("./logfile/roll-sync-by-size.log", 1024*1024);
builder->build();
test_log("async_logger");
return 0;
}
运行程序可以看到标准输出、指定文件、滚动文件都落地到位:
(二)滚动文件扩展代码测试:
#include "../logsys/mylog.h"
#include <unistd.h>
/*
扩展一个以时间作为入职文件滚动切换类型的日志落地模块
以时间进行文件滚动,实际上是以时间段进行滚动
实现思想: 以当前系统时间,取模时间段大小,可以得到当前时间是第几个时间段,
每次以当前系统时间取模,判断与当前文件的时间是否一致,不一致代表不是同一个时间段。
*/
enum class TimeGap
{
GAP_SECOND,
GAP_MINUTE,
GAP_HOUR,
GAP_DAY
};
// 落地方向: 滚动文件 (以时间进行滚动)
class RollByTimeSink : public mylog::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 ? mylog::util::Date::nowTime() : mylog::util::Date::nowTime() % _gap_size; // 获取当前是第几个时间段
std::string filename = createNewFile();
// 1. 创建日志文件所在目录
mylog::util::File::createDirectory(mylog::util::File::path(filename));
// 2. 创建并打开日志文件,以二进制格式和追加形式打开
_ofs.open(filename, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
// 将日志消息写入到文件 - 写入前判断文件时间段
void log(const char *data, size_t len)
{
time_t cur = mylog::util::Date::nowTime();
if((cur % _gap_size) != _cur_gap)
{
_ofs.close();
std::string filename = createNewFile();
_ofs.open(filename, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
_ofs.write(data, len);
assert(_ofs.good());
}
private:
// 进行大小判断,超过指定大小就创建新文件
std::string createNewFile()
{
// 获取系统时间,以时间来构造文件名扩展名
time_t t = mylog::util::Date::nowTime();
struct tm lt;
localtime_r(&t, <);
std::stringstream filename;
filename << _basename;
filename << lt.tm_year + 1900; // 时间戳从1900/01/01 00:00:00开始的
filename << "-";
filename << lt.tm_mon + 1; // 系统月份默认为从 0 开始,所以需+1
filename << "-";
filename << lt.tm_mday;
filename << " ";
filename << lt.tm_hour;
filename << ":";
filename << lt.tm_min;
filename << ":";
filename << lt.tm_sec;
filename << "-";
filename << ".log";
return filename.str();
}
private:
std::string _basename;
std::ofstream _ofs;
size_t _cur_gap; // 当前是第几个时间段
size_t _gap_size; // 时间段的大小
};
int main()
{
std::unique_ptr<mylog::LoggerBuilder> builder(new mylog::GlobalLoggerBuilder());
builder->buildLoggerName("async_logger");
builder->buildLoggerLevel(mylog::LogLevel::value::WARN);
builder->buildFormatter("[%c][%f:%l]%m%n");
builder->buildLoggerType(mylog::LoggerType::LOGGER_ASYNC);
builder->buildSink<RollByTimeSink>("./logfile/roll-async-by-size.log", TimeGap::GAP_SECOND);
mylog::Logger::ptr logger = builder->build();
size_t cur = mylog::util::Date::nowTime();
while(mylog::util::Date::nowTime() < cur + 5)
{
logger->fatal("这是一条测试日志");
usleep(1000);
}
return 0;
}
八、性能测试
测试三要素:
- 测试环境:
- 测试方法:
- 测试结果:
测试环境:
测试工具的编写:
- 可以控制写日志线程数量
- 可以控制写日志的总数量
方法:分别对于同步日志器 & 异步日志器在单多线程下生成指定数量的日志消息总耗时的性能测试:
- 需要测试单写日志线程的性能
- 需要测试多写日志线程的性能
实现:
- 封装一个接口,传入日志器名称,线程数量,日志数量,单条日志大小。
- 在接口内,创建指定数量的线程,各自负责一部分日志的输出。
- 在输出之前计时开始,在输出完毕后即使结束,所耗时间 = 结束时间 - 起始时间。
- 每秒输出量 = 日志数量 / 总耗时。
- 每秒输出大小 = 日志数量 * 单条日志大小 / 总耗时。
注意:异步日志输出启动非安全模式,纯内存写入(不去考虑实际落地的时间)。
#include "../logsys/mylog.h"
#include <chrono>
void bench(const std::string &logger_name, size_t thr_count, size_t msg_count, size_t msg_len)
{
// 1. 获取日志器
mylog::Logger::ptr logger = mylog::getLogger(logger_name);
if(logger.get() == nullptr)
{
return ;
}
std::cout << "测试日志: " << msg_count << "条,总大小: " << (msg_count * msg_len) / 1024 << "KB\n";
// 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; // 总日志数量 / 线程数量就是每个线程要输出的日志数量
for(int i = 0; i < thr_count; i++)
{
threads.emplace_back([&, i]()
{
// 4. 线程函数内部开始计时
auto start = std::chrono::high_resolution_clock::now();
// 5. 开始循环写日志
for(int j = 0; j < msg_per_thr; j++)
{
logger->fatal("%s", msg.c_str());
}
// 6. 线程函数内部结束计时
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> cost = end - start;
cost_arry[i] = cost.count();
std::cout << "\t线程" << i << ": " << "\t输出数量: " << msg_per_thr << ", 耗时: " << cost.count() << "s" << std::endl;
});
}
for(int i = 0; i < thr_count; i++)
{
threads[i].join();
}
// 7. 计算总耗时: 在多线程中,每个线程都会耗费时间,但是线程时并发处理的,因此耗时最高的那个就是总时间
double max_cost = cost_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";
}
void sync_bench()
{
std::unique_ptr<mylog::LoggerBuilder> builder(new mylog::GlobalLoggerBuilder());
builder->buildLoggerName("sync_logger");
builder->buildFormatter("%m%n");
builder->buildLoggerType(mylog::LoggerType::LOGGER_SYNC);
builder->buildSink<mylog::FileSink>("./logfile/sync.log");
builder->build();
// bench("sync_logger", 1, 1000000, 100);
bench("sync_logger", 3, 1000000, 100);
}
void async_bench()
{
std::unique_ptr<mylog::LoggerBuilder> builder(new mylog::GlobalLoggerBuilder());
builder->buildLoggerName("async_logger");
builder->buildFormatter("%m%n");
builder->buildLoggerType(mylog::LoggerType::LOGGER_ASYNC);
builder->buildEnableUnSafeAsync(); // 开启非安全模式 - 主要是为了将落地时间排除在外
builder->buildSink<mylog::FileSink>("./logfile/async.log");
builder->build();
// bench("async_logger", 1, 1000000, 100);
bench("async_logger", 3, 1000000, 100);
}
int main()
{
// sync_bench();
async_bench();
}
测试结果:同步下多线程比单线程慢:单线程不涉及到锁冲突,每次都加锁、写日志,加锁、写日志...,是一个串行化的过程;但多线程就涉及到锁冲突,另外磁盘性能也上限了 ,导致多线程性能反而降低了:
测试结果:异步下可以看到安全与非安全模式二者消耗时间差不了多少,在非安全模式下也就少了一丢丢时间,也就是少了实际落地的时间:
测试结果:异步多线程下只考虑线程只往内存去写数据,不考虑磁盘的性能,这时考验的就是CPU和内存的性能了: