基于多设计模式下的同步异步日志系统
目录
一、项目介绍
1.1 项目功能
1.2 开发环境
1.3 核心技术
1.4 环境搭建
二、日志系统介绍
2.1 日志系统存在的必要性
2.2 日志系统的技术实现
三、相关技术知识的补充
3.1 不定参函数的使用
3.2 设计模式
四、日志系统框架设计
4.1 模块划分
4.2 模块关系图
五、代码设计
5.1 实用类设计
5.2 日志等级类的设计
5.3 日志消息类的设计
5.4 日志输出格式化类的设计
5.5 日志落地类的设计(简单工厂模式)
5.6 日志器类的设计(建造者模式)
5.7 双缓冲区异步任务处理器的设计
5.8 异步日志器类的设计
5.9 单例日志器管理器类的设计
5.10 日志宏&&全局接口的设计(代理模式)
六、功能样例
七、扩展样例
八、性能测试
一、项目介绍
1.1 项目功能
- 本项目主要实现⼀个日志系统, 其主要支持以下功能:
- ⽀持多级别日志消息
- ⽀持同步日志和异步日志
- ⽀持可靠写⼊日志到控制台、文件以及滚动文件中
- ⽀持多线程程序并发写日志
- ⽀持扩展不同的日志落地目标地
1.2 开发环境
- Ubuntu 22.04
- vscode/vim
- g++/gdb
- makefile
1.3 核心技术
- 类层次设计(继承和多态的应用)
- C++11(多线程、auto、智能指针、右值引用等)
- 双缓冲区
- ⽣产消费模型
- 多线程
- 设计模式(单例、工厂、代理、建造者等)
1.4 环境搭建
二、日志系统介绍
2.1 日志系统存在的必要性
- 生产环境的产品为了保证其稳定性及安全性是不允许开发⼈员附加调试器去排查问题, 可以借助日志系统来打印⼀些日志帮助开发⼈员解决问题
- 上线客户端的产品出现bug无法复现并解决, 可以借助日志系统打印日志并上传到服务端帮助开发人员进行分析
- 对于⼀些高频操作(如定时器、心跳包)在少量调试次数下可能无法触发我们想要的行为,通过断 点的暂停方式,我们不得不重复操作几十次、上百次甚至更多,导致排查问题效率非常低下, 可以借助打印日志的方式查问题
-
在分布式、多线程/多进程代码中, 出现bug比较难以定位, 可以借助⽇志系统打印log帮助定位 bug
-
帮助首次接触项目代码的新开发⼈员理解代码的运行流程
2.2 日志系统的技术实现
-
利⽤printf、std::cout等输出函数将日志信息打印到控制台
-
对于大型商业化项目, 为了方便排查问题,我们⼀般会将日志输出到⽂件或者是数据库系统方便查询和分析日志, 主要分为同步日志和异步日志方式
-
同步写日志
-
异步写日志
三、相关技术知识的补充
3.1 不定参函数的使用
-
不定参宏函数的使用
#define LOG(fmt,...) printf("[%s:%d]"fmt,__FILE__,__LINE__,##__VA_ARGS__);
LOG("日志系统\n");
LOG("%s - %d\n","日志系统",222);
__VA_ARGS__是C/C++中的一个宏,用于在宏定义中表示可变数量的参数。它通常与宏函数(函数式宏)一起使用,以便可以在宏中处理不定数量的参数。
在宏定义中,__VA_ARGS__表示所有传递给宏的参数,可以是零个或多个参数。
__VA_ARGS__ 必须位于宏定义的参数列表中的最后。在 __VA_ARGS__ 前后可以有其他参数,但至少要有一个参数,以便在没有传递额外参数时编译不会出错。使用逗号 , 分隔参数。
- C风格不定参函数
void printNumber(int count,...)
{
va_list al;
va_start(al,count); ///让al指向n参数之后的第⼀个可变参数
int sum=0;
for(int i=0;i<count;i++)
{
sum+=va_arg(al,int); //从可变参数中取出一个整型参数
}
va_end(al); 清空可变参数列表--其实是将al置空
printf("%d\n",sum);
}
C 语言中的 va_list 类型允许函数接受可变数量的参数,这在编写需要处理不定数量参数的函数时非常有用。va_list 类型是在 stdarg.h 头文件中定义的,它允许函数处理可变数量的参数。
va_list 是一个指向参数列表的指针,它允许函数处理不定数量的参数。
va_start
:初始化 va_list 类型的变量,使其指向参数列表的起始位置。va_arg
:获取参数列表中的下一个参数,并将指针移动到下一个参数。va_end
:清理 va_list 类型的变量。
va_list还可以和vasprintf( )配合使用:
void myprintf(const char *fmt, ...)
{
//int vasprintf(char **strp, const char *fmt, va_list ap);
char *res;
va_list al;
va_start(al, fmt);
int len = vasprintf(&res, fmt, al);
va_end(al);
std::cout << res << std::endl;
free(res);
}
- C++风格不定参函数
#include <iostream>
#include <cstdarg>
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("日志系统");
xprintf("这是一个", "日志系统");
xprintf("这是一个", "日志系统", 222);
return 0;
}
C++中使用可变参数包(模板参数、函数参数)来接收可变数量的参数
对参数包的展开实际上是一种递归调用,在调用时使用右值引用和完美转发机制来保持实参的原本属性,使其不变地传递下去。当可变参数包的数量为0时,不能继续递归下,必须将递归函数进行函数重载成一个无参的函数调用,调用重载后的函数结束递归
3.2 设计模式
- 六大原则
- 单⼀职责原则告诉我们实现类要职责单⼀
- 里氏替换原则告诉我们不要破坏继承体系
- 依赖倒置原则告诉我们要面向接口编程
- 接口隔离原则告诉我们在设计接口的时候要精简单⼀
- 迪米特法则告诉我们要降低耦合
- 开闭原则是总纲,告诉我们要对扩展开放,对修改关闭
- 单例模式
- ⼀个类只能创建⼀个对象,即单例模式,该设计模式可以保证系统中该类只有⼀个实例,并提供⼀个 访问它的全局访问点,该实例被所有程序模块共享。⽐如在某个服务器程序中,该服务器的配置信息 存放在⼀个⽂件中,这些配置数据由⼀个单例对象统⼀读取,然后服务进程中的其他对象再通过这个 单例对象获取这些配置信息,这种⽅式简化了在复杂环境下的配置管理。
- 单例模式有两种实现模式:饿汉模式和懒汉模式
- 饿汉模式:程序启动时就会创建⼀个唯⼀的实例对象。 因为单例对象已经确定, 所以比较适用于多线程环境中, 多线程获取单例对象不需要加锁, 可以有效的避免资源竞争, 提高性能。这是一种以时间换空间的策略,对象在程序一启动的时候就已经被创建好了,这可能会导致程序启动的时间变长。
- 懒汉模式:在第一次使用的时候才去创建单例对象,如果单例对象构造特别耗时或者耗费济
源(加载插件、加载往络资源等), 可以选择懒汉模式。这是一种以懒加载,可以有效减少程序加载的时间。
-
代码实现:
//饿汉模式--以时间换空间,在程序一启动就被初始化好了
class Singleton
{
private:
Singleton() : _data(99)
{
std::cout << "单例对象创建" << std::endl;
}
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;
~Singleton() {}
private:
int _data;
static Singleton _eton;
public:
static Singleton &getInstance()
{
return _eton;
}
int getData()
{
return _data;
}
};
Singleton Singleton::_eton;
定义一个私有的静态单例对象,在类外进行初始化,并且将允许对象创建的接口都私有化或者不允许编译器默认提供,对外提供一个获取单例对象的接口,这样就保证了这个类只能实例化出一个单例对象
懒汉模式:
class Singleton
{
private:
Singleton() : _data(99)
{
std::cout << "单例对象创建" << std::endl;
}
Singleton(const Singleton &) = delete;
Singleton &operator=(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;
}
同样将将允许对象创建的接口都私有化或者不允许编译器默认提供,对外提供一个获取单例对象的接口,不同的是,用户在调用这个获取单例对象接口的时候才会创建单例对象,并不会在程序一启动的时候就创建
- 工厂模式
工厂模式是⼀种创建型设计模式, 它提供了⼀种创建对象的最佳方式。在工厂模式中,我们创建对象时不会对上层暴露创建逻辑,而是通过使用⼀个共同结构来指向新创建的对象,以此实现创建-使用的分离。
也就是说抽象出一个基类,对基类进行派生出不同的子类,在定义的工厂类中,使用基类的指针或者引用来获得创建好的子类对象
- 简单工厂模式
class Fruit
{
public:
virtual void name() = 0;
};
class Apple : public Fruit
{
public:
void name() override
{
std::cout << "我是一个苹果!" << std::endl;
}
};
class Banana : public Fruit
{
public:
void name() override
{
std::cout << "我是一个香蕉!" << std::endl;
}
};
// 简单工厂模式
class FruitFactory
{
public:
static std::shared_ptr<Fruit> creat(const std::string &name)
{
if (name == "苹果")
{
return std::make_shared<Apple>();
}
else
{
return std::make_shared<Banana>();
}
}
};
这种工厂模式在使用的时候非常直观易懂,但是当我们想要去修改工厂所生产的种类的个数时,就必须对工厂类进行修改,违背了开闭原则
- 工厂方法模式
// 工厂方法模式
class FruitFactory
{
public:
virtual std::shared_ptr<Fruit> creat() = 0;
};
class AppleFactory : public FruitFactory
{
public:
std::shared_ptr<Fruit> creat() override
{
return std::make_shared<Apple>();
}
};
class BananaFactory : public FruitFactory
{
public:
std::shared_ptr<Fruit> creat() override
{
return std::make_shared<Banana>();
}
};
在生产产品时,直接创建一个生产该产品的工厂对象,工厂对象中再对具体的产品进行生产
当我们需要修改产品的种类时,不再需要对工产类进行修改,只需要新增或者删除对应的产品的工厂子类
- 抽象工厂模式
class Animal
{
public:
virtual void name() = 0;
};
class Lamp : public Animal
{
public:
void name() override
{
std::cout << "我是一只山羊!" << std::endl;
}
};
class Dog : public Animal
{
public:
void name() override
{
std::cout << "我是一只土狗!" << std::endl;
}
};
// 抽象工厂模式
class Factory
{
public:
virtual std::shared_ptr<Fruit> creatFruit(const std::string &name) = 0;
virtual std::shared_ptr<Animal> creatAnimal(const std::string &name) = 0;
};
class FruitFactory : public Factory
{
public:
std::shared_ptr<Fruit> creatFruit(const std::string &name) override
{
if (name == "苹果")
{
return std::make_shared<Apple>();
}
else
{
return std::make_shared<Banana>();
}
}
std::shared_ptr<Animal> creatAnimal(const std::string &name)
{
return std::shared_ptr<Animal>();
}
};
class AnimalFactory : public Factory
{
public:
std::shared_ptr<Fruit> creatFruit(const std::string &name) override
{
return std::shared_ptr<Fruit>();
}
std::shared_ptr<Animal> creatAnimal(const std::string &name) override
{
if (name == "山羊")
{
return std::make_shared<Lamp>();
}
else
{
return std::make_shared<Dog>();
}
}
};
class FactoryProducer
{
public:
static std::shared_ptr<Factory> creat(const std::string &name)
{
if (name == "水果工厂")
{
return std::make_shared<FruitFactory>();
}
else
{
return std::make_shared<AnimalFactory>();
}
}
};
- 建造者模式
- 抽象产品类:
- 具体产品类:⼀个具体的产品对象类
- 抽象Builder类:创建⼀个产品对象所需的各个部件的抽象接口
- 具体产品的Builder类:实现抽象接口,构建各个部件
- 指挥者Director类:统⼀组建过程,提供给调⽤者使使用,通过指挥者来构造产品
#include <iostream>
#include <string>
#include <memory>
class Computer
{
public:
void setBoard(const std::string &board)
{
_board = board;
}
void setDisplay(const std::string &display)
{
_display = display;
}
virtual void setOs() = 0;
void showParamaters()
{
std::string param = "Computr Paramaters: \n";
param += "\tBoard: " + _board + "\n";
param += "\tDisplay: " + _display + "\n";
param += "\tOs: " + _os + "\n";
std::cout<<param<<std::endl;
}
protected:
std::string _board;
std::string _display;
std::string _os;
};
class MacComputer : public Computer
{
public:
void setOs() override
{
_os = "Max 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 ComputerBuilder : public Builder
{
public:
ComputerBuilder():_computer(new MacComputer())
{}
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 ComputerBuilder();
std::unique_ptr<Director> director(new Director(builder));
director->construct("英特尔主板","VOC显示器");
std::shared_ptr<Computer> computer=builder->build();
computer->showParamaters();
return 0;
}
- 代理模式
- 静态代理指的是,在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪个被代理类。
- 动态代理指的是,在运行时才动态生成代理类,并将其与被代理类绑定。这意味着,在运行时才能确定代理类要代理的是哪个被代理类。
#include <iostream>
#include <string>
class RentHouse
{
public:
virtual void renthouse() = 0;
};
class Landlord : public RentHouse
{
public:
void renthouse() override
{
std::cout << "将房子租出去" << std::endl;
}
};
class Intermediary : public RentHouse
{
public:
void renthouse() override
{
std::cout << "发布招租信息" << std::endl;
std::cout << "带人看房" << std::endl;
_landlord.renthouse();
std::cout << "负责租后维修工作" << std::endl;
}
private:
Landlord _landlord;
};
int main()
{
Intermediary intermediary;
intermediary.renthouse();
return 0;
}
四、日志系统框架设计
本项目实现的是一个多日志器的日志系统,并且支持将日志消息落地到不同的方向,支持不同的落地方式(同步&&异步)
4.1 模块划分
- 日志等级模块:对日志的输出等级进行划分,以便后续控制日志消息等级的输出打印,提供枚举转字符串的功能
- UNKNOW 0
- DEBUG 进行debug时候打印日志的等级
- INFO 打印⼀些用户提示信息
- WARN 打印警告信息
- ERROR 打印错误信息
- FATAL 打印致命信息- 导致程序崩溃的信息
- OFF 关闭所有日志输出
- 日志消息模块:存储的是日志消息的各个组成要素
- 日志时间戳信息:用来过滤日志输出时间
- 日志等级:用来进行日志过滤分析
- 源文件文件名:
- 源文件的行号:用来定位出现错误的代码位置
- 线程ID:用来过滤出错的线程
- 日志主体消息
- 日志器名称:当前支持多日志器的同时使用
- 日志消息格式化模块:提供两个类的设计,一个是日志消息格式化子项类的设计,一个是日志消息实际格式化类的设计,子项类负责把按照格式化规则分割好的不同数据进行打印输出,日志消息实际格式化类负责将传入的格式化字符串进行分割,并根据分割好的格式化字符创建不同的格式化子项对象,再循环调用子项对象的输出接口
- %d 日期
- %T 缩进
- %t 线程id
- %p 日志级别
- %c 日志器名称
- %f 文件名
- %l 行号
- %m 日志消息
- %n 换行
- 日志消息落地模块:决定了日志消息的落地方向,可以是标准输出,普通文件,滚动文件,支持落地方向的扩展,设计不同的子类,不同的子类控制不同的落地方向
- 日志器模块:为了降低用户的使用难度,设计一个日志器模块,用户通过日志器就可以进行日志的打印输出,不需要再显示调用某些接口,该模块包含:日志消息落地模块对象,日志消息格式化模块对象,日志输出等级
- 日志器管理模块:为了降低项目开发的日志耦合,不同的项目组可以有自己的日志器来控制输出格式以及落地方向,因此本项⽬是⼀个多日志器的日志系统。管理模块就是对创建的所有日志器进行统⼀管理。并提供⼀个默认日志器提供标准输出的日志输出。
- 异步线程模块:该模块需要包含缓冲区类的设计和异步工作器的设计,实现对日志消息的异步输出,业务线程只需要将日志消息放入缓冲区中即可,具体的落地操作由异步工作线程来完成,这样可以避免因为业务线程的阻塞而导致日志消息输出的效率降低,提供更加高效的非阻塞日志输出
4.2 模块关系图
五、代码设计
5.1 实用类设计
-
获取系统时间
-
判断文件是否存在
-
获取文件的所在目录路径
-
创建目录
/*
通⽤功能类,与业务⽆关的功能实现
1. 获取系统时间
2. 获取⽂件⼤⼩
3. 创建⽬录
4. 获取⽂件所在⽬录
*/
#ifndef __M_UTIL_H__
#define __M_UTIL_H__
#include <iostream>
#include <ctime>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
namespace clog
{
namespace util
{
class Date
{
public:
static size_t now()
{
return (size_t)time(nullptr);
}
};
class File
{
public:
static bool exists(const std::string &pathname)
{
struct stat st;
return (stat(pathname.c_str(), &st) == 0);
}
static std::string path(const std::string &pathname)
{
size_t pos = pathname.find_last_of("/\\");
if (pos == std::string::npos)
return ".";
return pathname.substr(0, pos + 1);
}
static void creatDirectory(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 (parent_dir == "." || parent_dir == "..")
// {
// idx = pos + 1;
// continue;
// }
if (exists(parent_dir))
{
idx = pos + 1;
continue;
}
mkdir(parent_dir.c_str(), 0777);
idx = pos + 1;
}
}
};
}
}
#endif
5.2 日志等级类的设计
/*
日志等级类的设计
UNKNOW 0
DEBUG 进⾏debug时候打印⽇志的等级
INFO 打印⼀些⽤⼾提⽰信息
WARN 打印警告信息
ERROR 打印错误信息
FATAL 打印致命信息- 导致程序崩溃的信息
OFF 关闭所有⽇志输出
*/
#ifndef __M_LEVEL_H__
#define __M_LEVEL_H__
#include <iostream>
namespace clog
{
class LogLevel
{
public:
enum class value
{
UNKNOW = 0,
DEBUG,
INFO,
WARM,
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::WARM:
return "WARM";
case LogLevel::value::ERROR:
return "ERROR";
case LogLevel::value::FATAL:
return "FATAL";
case LogLevel::value::OFF:
return "OFF";
default:
return "UNKNOW";
}
return "UNKNOW";
}
};
}
#endif
5.3 日志消息类的设计
/*
日志消息类设计
1.日志时间戳信息 用来过滤日志输出时间
2.日志等级 用来进行日志过滤分析
3.源文件文件名
4.源文件的行号 用来定位出现错误的代码位置
5.线程ID 用来过滤出错的线程
6.日志主体消息
7.日志器名称 (当前支持多日志器的同时使用)
*/
#ifndef __M_MSG_H__
#define __M_MSG_H__
#include "util.hpp"
#include "level.hpp"
#include <string>
#include <thread>
namespace clog
{
struct LogMsg
{
time_t _ctime; // 时间
LogLevel::value _level; // 等级
size_t _line; // 行号
std::string _file; // 文件名
std::thread::id _tid; // 线程ID
std::string _logger; // 日志器
std::string _payload; // 主体消息
LogMsg(LogLevel::value level, size_t line, const std::string file, const std::string logger, const std::string payload)
: _ctime(clog::util::Date::now())
, _level(level)
, _line(line)
, _file(file)
, _tid(std::this_thread::get_id())
, _logger(logger)
, _payload(payload)
{
}
};
}
#endif
5.4 日志输出格式化类的设计
- 日志格式化子项类的设计:主要是对按照格式化字符串取出的各个元素进行打印输出
#ifndef __M_FMT_H__
#define __M_FMT_H__
#include "message.hpp"
#include "level.hpp"
#include "util.hpp"
#include <vector>
#include <cassert>
#include <memory>
#include <sstream>
namespace clog
{
class FormatItem
{
public:
using ptr = std::shared_ptr<FormatItem>;
virtual void format(std::ostream &out, const LogMsg &msg) = 0;
};
// 日志消息 时间 等级 线程ID 源文件名称 行号 日志器 换行 制表符 其他
class MesFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._payload;
}
};
class TimeFormatItem : public FormatItem
{
public:
TimeFormatItem(const std::string &format = "%H:%M:%S")
: _format(format)
{
if (format.empty())
{
_format = "%H:%M:%S";
}
}
void format(std::ostream &out, const LogMsg &msg) override
{
struct tm t;
localtime_r(&msg._ctime, &t);
char time[128];
strftime(time, 127, _format.c_str(), &t);
out << time;
}
private:
std::string _format;
};
class LevelFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << LogLevel::toString(msg._level);
}
};
class ThreadFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._tid;
}
};
class FileFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._file;
}
};
class LineFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._line;
}
};
class LoggerFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << msg._logger;
}
};
class NlineFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << "\n";
}
};
class TabFormatItem : public FormatItem
{
public:
void format(std::ostream &out, const LogMsg &msg) override
{
out << "\t";
}
};
class OtherFormatItem : public FormatItem
{
public:
OtherFormatItem(const std::string &str = "")
: _str(str)
{
}
void format(std::ostream &out, const LogMsg &msg) override
{
out << _str;
}
private:
std::string _str;
};
- 日志格式化类:对传入的格式化字符串进行分割,并按顺序创建各个元素的子项对象,其实就是按照格式化字符串依次从Msg中取出对应的要素进行连接的过程,最终组织出来格式化消息
class Formatter
{
public:
using ptr = std::shared_ptr<Formatter>;
Formatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n")
: _pattern(pattern)
{
assert(parsePattern());
}
// 对msg进行格式化
std::ostream &format(std::ostream &out, const LogMsg &msg)
{
for (auto &it : _items)
{
it->format(out, msg);
}
return out;
}
std::string format(const LogMsg &msg)
{
std::stringstream ss;
for (auto &it : _items)
{
it->format(ss, msg);
}
return ss.str();
}
// 根据不同的格式化字符创建不同的格式化子项对象
/*
%d ⽇期
%T 缩进
%t 线程id
%p ⽇志级别
%c ⽇志器名称
%f ⽂件名
%l ⾏号
%m ⽇志消息
%n 换⾏
*/
private:
FormatItem::ptr createItem(const std::string &key, const std::string &val)
{
if (key == "d")
return std::make_shared<TimeFormatItem>(val);
if (key == "T")
return std::make_shared<TabFormatItem>();
if (key == "t")
return std::make_shared<ThreadFormatItem>();
if (key == "p")
return std::make_shared<LevelFormatItem>();
if (key == "c")
return std::make_shared<LoggerFormatItem>();
if (key == "f")
return std::make_shared<FileFormatItem>();
if (key == "l")
return std::make_shared<LineFormatItem>();
if (key == "m")
return std::make_shared<MesFormatItem>();
if (key == "n")
return std::make_shared<NlineFormatItem>();
if (key == "")
return std::make_shared<OtherFormatItem>(val);
std::cout << "不存在该格式化字符:%" << key << std::endl;
abort();
return FormatItem::ptr();
}
// 对格式化字符串进行解析
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())
{
// 判断是否为原始字符
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())
{
fmt_order.push_back(std::make_pair("", val));
val.clear();
}
// 处理格式化字符
pos++;
if (pos == _pattern.size())
{
std::cout << "%之后没有格式化字符!\n";
return false;
}
key = _pattern[pos];
pos++;
// 格式化字符的下一个位置,判断是否存在子规则
if (pos < _pattern.size() && _pattern[pos] == '{')
{
pos++; // 指向子规则的起始位置
while (pos < _pattern.size() && _pattern[pos] != '}')
{
val.push_back(_pattern[pos++]);
}
// 跳出循环 如果走到了格式字符串的末尾 说明不存在},格式出错
if (pos == _pattern.size())
{
std::cout << "不存在},格式化字符串格式错误\n";
return false;
}
// 此时pos指向}的位置,向后走一步,走到下一轮新处理的位置
pos++;
}
fmt_order.push_back(std::make_pair(key, val));
val.clear();
key.clear();
}
// 创建格式化子项对象,初始化数组_items
for (auto &it : fmt_order)
{
_items.push_back(createItem(it.first, it.second));
}
return true;
}
private:
std::string _pattern; // 格式化规则字符串
std::vector<FormatItem::ptr> _items;
};
}
#endif
5.5 日志落地类的设计(简单工厂模式)
日志落地类主要负责落地日志消息到目的地。 它主要包括以下内容:
- 使用了实用工具类的相关接口,便于获取系统时间,以及创建目录
- 这个类支持可扩展,其成员函数log设置为纯虚函数,当我们需要增加⼀个log输出目标, 可以增加⼀ 个类继承自该类并重写log方法实现具体的落地日志逻辑。
- 目前实现了三个不同⽅向上的日志落地:
- 标准输出:StdoutSink
- 固定⽂件:FileSink
- 滚动⽂件:RollSink
- 滚动日志文件输出的必要性:
- 由于机器磁盘空间有限, 我们不可能⼀直无限地向⼀个文件中增加数据
- 如果⼀个日志文件体积太⼤,一方面是不好打开,另一方面是即时打开了由于包含数据巨 大,也不利于查找我们需要的信息
- 所以实际开发中会对单个日志文件的大小也会做⼀些控制,即当大小超过某个大小时(如 1GB),我们就重新创建⼀个新的日志文件来滚动写日志。 对于那些过期的日志, ⼤部分企业内部都有专门的运维⼈员去定时清理过期的日志,或者设置系统定时任务,定时清理过期日志。
日志文件的滚动思想:
- 日志文件滚动的条件有两个:文件大小和时间。我们可以选择:
- 日志文件在大于 1GB 的时候会更换新的文件
- 每天定点滚动⼀个日志文件
- 本项目基于文件大小的判断滚动生成新的文件,后续会做扩展
/*
日志落地模块设计
1.抽象出不同落地子类的基类
2.派生出不同的落地子类
3.使用工厂模式进行创建与表示的分离
*/
#ifndef __M_SINK_H__
#define __M_SINK_H__
#include "util.hpp"
#include <fstream>
#include <sstream>
#include <cassert>
#include <memory>
namespace clog
{
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:
using ptr = std::shared_ptr<StdoutSink>;
StdoutSink() = default;
void log(const char *data, size_t len) override
{
std::cout.write(data, len);
}
};
// 落地方向:普通文件
class FileSink : public LogSink
{
public:
using ptr = std::shared_ptr<FileSink>;
// 传入文件名,打开文件,将操作句柄管理起来
FileSink(const std::string &pathname)
: _pathname(pathname)
{
// 1.创建文件所在的目录
util::File::creatDirectory(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) override
{
_ofs.write(data, len);
if (_ofs.good() == false)
{
std::cout << "日志文件输出失败!\n";
}
}
private:
std::string _pathname;
std::ofstream _ofs; // 为了避免每次对文件写入都要打开文件,对操作句柄进行管理
};
// 落地方向:滚动文件
class RollBySizeSink : public LogSink
{
public:
using ptr = std::shared_ptr<FileSink>;
RollBySizeSink(const std::string &basename, size_t max_fsize)
: _basename(basename), _max_fsize(max_fsize), _cur_fsize(0), _name_count(0)
{
std::string filename = creatNewFile();
// 1.创建文件所在的目录
util::File::creatDirectory(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) override
{
if (_cur_fsize >= _max_fsize)
{
_ofs.close(); // 关闭旧文件,避免内存泄露
std::string filename = creatNewFile();
_ofs.open(filename);
assert(_ofs.is_open());
_cur_fsize = 0;
}
_ofs.write(data, len);
if (_ofs.good() == false)
{
std::cout << "日志文件输出失败!\n";
}
_cur_fsize += len;
}
private:
// 进行文件大小判断,超过指定大小创建新文件
std::string creatNewFile()
{
// 获取系统时间
time_t t = util::Date::now();
struct tm tl;
localtime_r(&t, &tl);
std::stringstream ss;
ss << _basename;
ss << tl.tm_year + 1900;
ss << tl.tm_mon + 1;
ss << tl.tm_mday;
ss << tl.tm_hour;
ss << tl.tm_min;
ss << tl.tm_sec;
ss << "-";
ss << _name_count++;
ss << ".log";
return ss.str();
}
private:
// 文件名=基础文件名+以文件大小进行切换的不同扩展文件名
std::string _basename;
std::ofstream _ofs;
size_t _max_fsize;
size_t _cur_fsize;
size_t _name_count;
};
class SinkFactory
{
public:
template <typename sinktype, typename... Args>
static LogSink::ptr creat(Args &&...args)
{
return std::make_shared<sinktype>(std::forward<Args>(args)...);
}
};
}
#endif
- 使用简单工厂模式来表示创建与使用的分离,通过对外提供统一的接口来获取不同的落地方式对象,因为获取不同的落地方式对象类型不同且需要传入的参数个数不定,所以使用模版参数和可变参数包来控制以保证每个对象的创建都能适应该接口
5.6 日志器类的设计(建造者模式)
- 日志器主要是用来和前端交互, 当我们需要使用日志系统打印log的时候, 只需要创建Logger对象, 调用该对象debug、info、warn、error、fatal等方法输出自己想打印的日志即可,⽀持解析可变参数 列表和输出格式, 即可以做到像使用printf函数⼀样打印日志。
- 当前日志系统⽀持同步日志& 异步日志两种模式,两个不同的日志器唯⼀不同的地方在于他们在日的落地方式上有所不同:
- 同步日志器:直接对日志消息进行输出。
- 异步日志器:将日志消息放⼊缓冲区,由异步线程进行输出。
- 因此日志器类在设计的时候先设计出⼀个Logger基类,在Logger基类的基础上,继承出SyncLogger同步日志器和AsyncLogger异步日志器。
- 因为日志器模块是对前边多个模块的整合,想要创建⼀个日志器,需要设置日志器名称,设置日志输出等级,设置日志器类型,设置日志输出格式,设置落地方向,且在一个日志器中落地方向有可能存在多个,整个日志器的创建过程较为复杂,需要的要素过多,为了保持良好的代码风格,编写出优雅简洁的代码,因此日志器的创建这里采用了建造者模式来进行代码的设计
*
日志器模块
1.抽象出日志器基类
2.对基类进行派生出两个不同落地方式的子类(同步日志器&&异步日志器
*/
#ifndef __M_LOGGER_H__
#define __M_LOGGER_H__
#include "util.hpp"
#include "message.hpp"
#include "format.hpp"
#include "sink.hpp"
#include "looper.hpp"
#include <mutex>
#include <atomic>
#include <cstdarg>
#include <cassert>
#include <unordered_map>
namespace clog
{
enum class LoggerType
{
LOG_SYNC = 0,
LOG_ASYNC
};
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), _level(level), _formatter(formatter), _sinks(sinks.begin(), sinks.end())
{
}
std::string GetLoggerName()
{
return _logger_name;
}
// 构造出日志消息对象,并进行格式化,格式化成消息字符串后再落地输出
void debug(const std::string &file, size_t line, const std::string &fmt, ...)
{
// 通过传入的参数构造出一个日志消息对象,并格式化后落地输出
// 1.判断当前输出等级是否达到限制的输出等级
if (LogLevel::value::DEBUG < _level)
{
return;
}
// 构造出一个日志消息对象
va_list ap;
va_start(ap, fmt);
char *str;
int ret = vasprintf(&str, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf faild!\n";
return;
}
va_end(ap);
serialize(LogLevel::value::DEBUG, file, line, str);
free(str); // 避免内存泄露
}
void info(const std::string &file, size_t line, const std::string &fmt, ...)
{
// 通过传入的参数构造出一个日志消息对象,并格式化后落地输出
// 1.判断当前输出等级是否达到限制的输出等级
if (LogLevel::value::INFO < _level)
{
return;
}
// 构造出一个日志消息对象
va_list ap;
va_start(ap, fmt);
char *str;
int ret = vasprintf(&str, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf faild!\n";
return;
}
va_end(ap);
serialize(LogLevel::value::INFO, file, line, str);
free(str); // 避免内存泄露
}
void warn(const std::string &file, size_t line, const std::string &fmt, ...)
{
// 通过传入的参数构造出一个日志消息对象,并格式化后落地输出
// 1.判断当前输出等级是否达到限制的输出等级
if (LogLevel::value::WARM < _level)
{
return;
}
// 构造出一个日志消息对象
va_list ap;
va_start(ap, fmt);
char *str;
int ret = vasprintf(&str, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf faild!\n";
return;
}
va_end(ap);
serialize(LogLevel::value::WARM, file, line, str);
free(str); // 避免内存泄露
}
void error(const std::string &file, size_t line, const std::string &fmt, ...)
{
// 通过传入的参数构造出一个日志消息对象,并格式化后落地输出
// 1.判断当前输出等级是否达到限制的输出等级
if (LogLevel::value::ERROR < _level)
{
return;
}
// 构造出一个日志消息对象
va_list ap;
va_start(ap, fmt);
char *str;
int ret = vasprintf(&str, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf faild!\n";
return;
}
va_end(ap);
serialize(LogLevel::value::ERROR, file, line, str);
free(str); // 避免内存泄露
}
void fatal(const std::string &file, size_t line, const std::string &fmt, ...)
{
// 通过传入的参数构造出一个日志消息对象,并格式化后落地输出
// 1.判断当前输出等级是否达到限制的输出等级
if (LogLevel::value::FATAL < _level)
{
return;
}
// 构造出一个日志消息对象
va_list ap;
va_start(ap, fmt);
char *str;
int ret = vasprintf(&str, fmt.c_str(), ap);
if (ret == -1)
{
std::cout << "vasprintf faild!\n";
return;
}
va_end(ap);
serialize(LogLevel::value::FATAL, file, line, str);
free(str); // 避免内存泄露
}
protected:
void serialize(LogLevel::value level, const std::string &file, size_t line, const std::string &str)
{
LogMsg msg(level, line, file, _logger_name, str);
// 格式化
std::stringstream ss;
_formatter->format(ss, msg);
// 落地输出
log(ss.str().c_str(), ss.str().size());
}
// 根据不同的日志器类型 派生出不同落地方式的子类
virtual void log(const char *data, size_t len) = 0;
std::mutex _mutex;
std::string _logger_name;
std::atomic<LogLevel::value> _level;
Formatter::ptr _formatter;
std::vector<LogSink::ptr> _sinks;
};
// 同步日志器 将格式化后的日志消息直接通过日志落地模块句柄进行落地
class SyncLogger : public Logger
{
public:
SyncLogger(const std::string &logger_name,
LogLevel::value level,
Formatter::ptr formatter,
std::vector<LogSink::ptr> &sinks)
: Logger(logger_name, level, formatter, sinks)
{
}
protected:
void log(const char *data, size_t len) override
{
std::unique_lock<std::mutex> lock(_mutex);
if (_sinks.empty())
return;
for (auto &sink : _sinks)
{
sink->log(data, len);
}
}
};
- 为了实现让日志器能过滤对应等级的日志消息,需要给出指定的日志等级
- 由于一个日志器中可能存在多种不同的日志落地方向,需要给出一个存储日志落地方向的vector容器
- 为了支持传入实际日志消息的多样化,使用可变参数包
- 外部调用debug、info等接口直接实现对日志消息的打印输出,真正的输出接口由log实现。由于落地方式(同步&&异步)不同,所以必须对log函数进行重写,log到磁盘还是缓冲区,由对应的子类对虚函数进行重写
- 如果是多线程下使用同步日志器,可能会出现线程安全问题,所以必须对落地的过程(log的过程)进行加锁,保证任意时刻,只有一个线程在向指定落地方向输出日志消息
// 建造者模式
/*
1.抽象出一个日志器建造者基类
1.设置日志器类型(同步&&异步)
2.实现各个接口,完成一个日志器各个零部件的构建
3.将不同类型的日志器放在同一个建造者类中实现
*/
class LoggerBuilder
{
public:
LoggerBuilder()
: _logger_type(LoggerType::LOG_SYNC), _level(LogLevel::value::DEBUG), _looper_type(AsyncType::ASYNC_SAVE)
{
}
void buildEnableUnsaveAsync()
{
_looper_type = AsyncType::ASYNC_UNSAVE;
}
void buildloggertype(LoggerType type)
{
_logger_type = type;
}
void buildloggername(const std::string &name)
{
_logger_name = name;
}
void buildloggerlevel(LogLevel::value level)
{
_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 = std::make_shared<sinktype>(std::forward<Args>(args)...);
_sinks.push_back(psink);
}
virtual Logger::ptr build() = 0;
protected:
LoggerType _logger_type;
std::string _logger_name;
LogLevel::value _level;
Formatter::ptr _formatter;
std::vector<LogSink::ptr> _sinks;
AsyncType _looper_type;
};
/*
2.派生出具体的建造者类--局部日志器建造者&&全局日志器建造者(添加全局单例管理器之后,将日志器添加到全局管理)
*/
class LocalLoggerBuilder : public LoggerBuilder
{
public:
Logger::ptr build() override
{
assert(!_logger_name.empty()); // 必须要有日志器名称
if (_formatter.get() == nullptr)
{
_formatter = std::make_shared<Formatter>();
}
if (_sinks.empty())
{
buildsink<StdoutSink>();
}
if (_logger_type == LoggerType::LOG_ASYNC)
{
return std::make_shared<AsyncLogger>(_logger_name, _level, _formatter, _sinks, _looper_type);
}
return std::make_shared<SyncLogger>(_logger_name, _level, _formatter, _sinks);
}
};
- 由于在创建日志器的时候,需要在用户使用的层面先提前创建好构建一个日志器所需要的要素对象,这提高了用户的使用难度,因此采用建造者的设计方式实现一个建造者类,用户只需要传入对应的要素参数,减少了操作难度。
- 由于我们考虑到,如果用户创建的是一个局部的日志器,那么他只能在该作用域内有效,函数结束后就会被销毁;如果用户创建的是一个全局的日志器,那么他在整个程序运行的范围内都是有效的,在整个程序的运行期间一直存在
- 所以我们先设计一个建造者的基类,提供一个真正生产日志器(返回同步&&异步日志器)的虚函数build,再对基类进行派生出两个产生不同作用域日志器的建造者子类,他们分别对build进行重写,同时我们将同步&&异步日志器的生成放在同一个建造者中实现
- 为了在build中区分我们要生产的是同步还是异步日志器,定义一个枚举类LoggerType来帮助我们进行判断
- 因为日志器的名称是唯一的,用户必须提供;如果用户不提供格式化字符串,就采用默认的格式化器;如果用户不提供落地方向,就采用默认的标准输出
5.7 双缓冲区异步任务处理器的设计
- 设计思想:异步处理线程 + 数据池
- 使用者将需要完成的任务添加到任务池中,由异步线程来完成任务的实际落地操作
- 在实现数据池的时候,如果使用链表来存储,会涉及到资源的不断申请和释放,不合适;如果使用循环队列来存储,可以减少资源的不断申请和释放,但是由于业务线程在写入的时候,工作线程不能读取数据,并且一个业务线程在写入其他的业务线程不能写入,一个工作线程在读取其他的工作线程不能进行读取,这就意味着我们必须使用互斥锁来保证线程之间的安全,既然使用互斥锁,那么任何两个角色之前都会存在大量的锁冲突,会降低日志器的效率
- 因此本项目采用双缓冲区的思想,也就是说,存在一个输入缓冲区和一个处理缓冲区,当输入缓冲区中有数据的时候,就可交换两个缓冲区,此时工作线程对缓冲区中的数据进行读取,业务线程也可以在此时对另外一个空的缓冲区进行写入,这样子就可以实现业务线程和工作线程的同步,两者唯一存在的锁冲突就是交换缓冲区的那一刻,提高了写入和读取的效率,两者不需要进行大量的锁竞争,且双缓冲区不涉及资源的频繁申请和释放
- 任务池的设计思想:双缓冲区阻塞数据池
- 优势:避免了空间的频繁申请释放,且尽可能的减少了⽣产者与消费者之间锁冲突的概率,提高了任务处理效率。
- 在任务池的设计中,有很多备选方案,比如循环队列等等,但是不管是哪⼀种都会涉及到锁冲突的情况,因为在生产者与消费者模型中,任何两个角色之间都具有互斥关系,因此每⼀次的任务添加与取出都有可能涉及锁的冲突,而双缓冲区不同,双缓冲区是处理器将⼀个缓冲区中的任务全部处理完毕后,然后交换两个缓冲区,重新对新的缓冲区中的任务进⾏处理,虽然同时多线程写⼊也会冲突,但是冲突并不会像每次只处理⼀条的时候频繁(减少了生产者与消费者之间的锁冲突),且不涉及到空间的频繁申请释放所带来的消耗。
/*
异步缓冲区类的设计
*/
#ifndef __M_BUFFER_H__
#define __M_BUFFER_H__
#include <iostream>
#include <vector>
#include <cassert>
#define DEFAULT_BUFFER_SIZE (1 * 1024 * 1024)
#define THREHOLD_BUFFER_SIZE (8 * 1024 * 1024)
#define LINEARGROWTH_BUFFER_SIZE (1 * 1024 * 1024)
namespace clog
{
class Buffer
{
public:
Buffer()
: _buffer(DEFAULT_BUFFER_SIZE), _reader_idx(0), _writer_idx(0)
{
}
// 写入数据
void push(const char *data, size_t len)
{
// 缓冲区满了:1.返回阻塞 2.扩容
// if (len > writeAbleSize())s
// return;
// 扩容--用于极限测试
ensureEnoughSize(len);
// 将数据拷贝到缓冲区
std::copy(data, data + len, &_buffer[_writer_idx]);
// 当前写入位置向后偏移
moveWriterIndx(len);
}
// 当前可写入的空间大小
size_t writeAbleSize()
{
// 对于扩容来说,不存在可写入空间的大小
// 该接口只针对缓冲区大小固定的场景提供
return (_buffer.size() - _writer_idx); // 因为是双缓冲区机制,写满就交换缓冲区,所以在单缓冲区中不存在空间循环利用的情况
}
// 读取数据的起始位置
const char *begin()
{
return &_buffer[_reader_idx];
}
// 可读数据的起始长度
size_t readAbleSize()
{
return (_writer_idx - _reader_idx);
}
// 当前读取位置向后偏移
void movReaderIndx(size_t len)
{
// 判断是否有足够的数据被读取
assert(len <= readAbleSize());
_reader_idx += len;
}
// 重置读写位置,初始化缓冲区
void reset()
{
_reader_idx = 0;
_writer_idx = 0;
}
// 两个缓冲区的交换
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 moveWriterIndx(size_t len)
{
assert((_writer_idx + len) <= _buffer.size());
_writer_idx += len;
}
void ensureEnoughSize(size_t len)
{
// 不需要扩容
if (len <= writeAbleSize())
return;
size_t newsize = 0;
if (_buffer.size() < THREHOLD_BUFFER_SIZE)
{
// 小于阈值 二倍扩容
newsize = _buffer.size() * 2 + len;
}
else
{
// 大于阈值 线性增长
newsize = _buffer.size() + LINEARGROWTH_BUFFER_SIZE + len;
}
_buffer.resize(newsize);
}
std::vector<char> _buffer;
size_t _reader_idx; // 可读位置的指针
size_t _writer_idx; // 可写位置的指针
};
}
#endif
- 缓冲区的设计中包含了空间的扩容,当此时的空间大小小于规定的阈值,进行二倍扩容,如果高于阈值,则进行线性扩容
- 因为扩容不可能无限进行,因此我们的双缓冲区就可以很好的避免无限扩容的问题。当一个缓冲区中的写入空间不足的时候,业务线程会被阻塞;当一个缓冲区中无数据可读取的时候,工作线程会被阻塞
/*
异步工作器的设计
*/
#ifndef __M_LOOPER_H__
#define __M_LOOPER_H__
#include "buffer.hpp"
#include <iostream>
#include <condition_variable>
#include <functional>
#include <thread>
#include <memory>
#include <atomic>
#include <mutex>
#include <condition_variable>
namespace clog
{
enum class AsyncType
{
ASYNC_SAVE = 0,
ASYNC_UNSAVE
};
class AsyncLooper
{
using Functor = std::function<void(Buffer &)>;
public:
using ptr = std::shared_ptr<AsyncLooper>;
AsyncLooper(const Functor &cb, AsyncType looper_type = AsyncType::ASYNC_SAVE)
: _stop(false), _thread(std::thread(&AsyncLooper::threadEntry, this)), _cb(cb), _looper_type(looper_type)
{
}
void push(const char *data, size_t len)
{
// 加锁
std::unique_lock<std::mutex> lock(_mutex);
// 判断是否有足够的空间进行写入,是否需要阻塞,缓冲区满了就不能再写入
if (_looper_type == AsyncType::ASYNC_SAVE)
{
// 阻塞
_cond_proc.wait(lock, [&]()
{ return len <= _pro_buf.writeAbleSize(); });
}
// 写入条件满足
_pro_buf.push(data, len);
// 唤醒异步工作线程
_cond_con.notify_all();
}
void stop()
{
_stop = true;
// 唤醒所有异步工作线程
_cond_con.notify_all();
_thread.join();
}
~AsyncLooper()
{
stop();
}
private:
void threadEntry()
{
while (1)
{
// 限定加锁的生命周期,因为在读取完数据后,处理数据的过程和放入数据的过程是可以并行的!
{
// 加锁
std::unique_lock<std::mutex> lock(_mutex);
// 异步工作器已经退出并且输入缓冲区数据读完了,就退出结束交换读取操作
if (_stop && _pro_buf.empty())
break;
// 如果输入缓冲区中没有数据,就无法交换缓冲区,就会被阻塞
_cond_con.wait(lock, [&]()
{ return _stop || !_pro_buf.empty(); });
// 走到这里说明输入缓冲区中有数据,可以进行交换读取
_pro_buf.swap(_con_buf);
}
// 交换完后就可以继续向输入缓冲区中push数据了
if (_looper_type == AsyncType::ASYNC_SAVE)
{
// 唤醒业务线程
_cond_proc.notify_all();
}
// 处理数据--调用回调函数
_cb(_con_buf);
// 初始化处理缓冲区
_con_buf.reset();
}
}
private:
AsyncType _looper_type;
Buffer _con_buf; // 处理缓冲区
Buffer _pro_buf; // 输入缓冲区
std::atomic<bool> _stop; // 异步工作器停止标志
std::mutex _mutex; // 互斥锁
std::condition_variable _cond_con;
std::condition_variable _cond_proc;
std::thread _thread; // 处理数据的异步工作线程
private:
Functor _cb; // 处理数据的回调函数
};
}
#endif
- 异步工作器中需要包含两个缓冲区,一个是输入缓冲区,一个是处理缓冲区,已经一把互斥锁和两个条件变量,因为可能存在多线程写入或者多线程读取,不满足写入或者读取条件就要在相应的条件变量下等待
- 还需要有一个处理数据的异步工作线程,负责数据的实际落地,所以必须有对应的处理数据的回调函数,由外部传入,让该线程在创建好的时候调用回调函数的入口函数,因为回调函数的入口函数属于类内成员函数,所以在传参数时,还需要传入一个this指针
- 该类提供一个安全写入和非安全写入两种方式,非安全写入不受写入空间的约束而阻塞,安全写入受写入空间的约束,一旦写入空间不足,就会被阻塞,需要等待工作线程的交换完缓冲区后唤醒业务线程继续写入
5.8 异步日志器类的设计
- 异步日志器类继承自日志器类, 并在同步日志器类上拓展了异步工作器。当我们需要异步输出日志的时候, 需要创建异步日志器和异步工作器, 调用异步日志器的debug、error、info、fatal等函数输 出不同级别日志。
- log函数为重写Logger类的函数, 主要实现将日志消息加⼊异步队列缓冲区中 ,realLog函数主要由异步线程进行调用(是为异步消息处理器设置的回调函数),完成日志的实际落地⼯作。
// 异步日志器--业务线程将数据写入缓冲区后,异步工作线程从缓冲区中拿取数据写入磁盘(双缓冲区--减少锁冲突--主要是生产者和消费者之间的冲突)
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))
{
}
protected:
// 首先落地到输入缓冲区
void log(const char *data, size_t len) override
{
_looper->push(data, len);
}
// 实际的落地方向--对从处理缓冲区中拿到的数据进行处理
void reallog(Buffer &buffer)
{
if (_sinks.empty())
return;
for (auto &sink : _sinks)
{
sink->log(buffer.begin(), buffer.readAbleSize());
}
}
private:
AsyncLooper::ptr _looper;
};
- 初始化列表中初始化异步工作器的时候,需要传入异步日志器类中的定义好的回调调函reallog,由于调用类中的成员函数还需要传入this指针,但是异步工作器中调用的回调函数的参数只有一个Buffer类型的变量,但是传过去的却有this指针和buffer,这显然会出错。因此使用绑定器和function机制来绑定回调函数第一个参数的位置
- 异步工作器有安全和非安全两种模式,默认采用安全模式,但是可以在建造者基类中设计一个转换为非安全模式异步工作器的接口
void buildEnableUnsaveAsync()
{
_looper_type = AsyncType::ASYNC_UNSAVE;
}
5.9 单例日志器管理器类的设计
- 日志的输出,我们希望能够在任意位置都可以进行,但是当我们创建了⼀个日志器之后,就会受到日志器所在作用域的访问属性限制。
- 因此,为了突破访问区域的限制,我们创建⼀个日志器管理类,且这个类是⼀个单例类,这样的话, 我们就可以在任意位置来通过管理器单例获取到指定的日志器来进行日志输出了。
- 基于单例日志器管理器的设计思想,我们对于日志器建造者类进行继承,继承出⼀个全局日志器建造者类,实现⼀个日志器在创建完毕后,直接将其添加到单例的日志器管理器中,以便于能够在任何位 置通过日志器名称能够获取到指定的日志器进行日志输出。
// 日志器管理器--单例对象
class LoggerManager
{
private:
LoggerManager()
{
std::unique_ptr<LoggerBuilder> lb(new LocalLoggerBuilder()); // 不能使用全局建造者 会陷入循环阻塞
lb->buildloggername("root"); // 默认日志器只需要构建一个日志器名称
lb->buildloggertype(LoggerType::LOG_SYNC); // 同步日志器
_root_logger = lb->build();
_loggers.insert(std::make_pair("root", _root_logger));
}
public:
LoggerManager(const LoggerManager &lm) = delete;
const LoggerManager &operator=(const LoggerManager &lm) = delete;
static LoggerManager &GetInstance()
{
static LoggerManager lm;
return lm;
}
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;
}
bool hashLogger(const std::string &name)
{
// 加锁
std::unique_lock<std::mutex> lock(_mutex);
auto it = _loggers.find(name);
if (it == _loggers.end())
{
return false;
}
return true;
}
void addLogger(const std::string &name, const Logger::ptr logger)
{
std::unique_lock<std::mutex> lock(_mutex);
_loggers.insert(std::make_pair(name, logger));
}
Logger::ptr getRootLogger()
{
std::unique_lock<std::mutex> lock(_mutex);
return _root_logger;
}
private:
std::mutex _mutex;
// 默认日志器
Logger::ptr _root_logger;
// 用于存储日志器和日志器名称映射关系的哈希表
std::unordered_map<std::string, Logger::ptr> _loggers;
};
- 单例日志管理器提供一个默认日志器,和一个用于存储日志器和日志器名称映射关系的哈希表,便于使用者通过名称获取相应的日志器
- 为了避免在多线程的场景下,多个线程同时使用该管理器获取相同的日志器,需要在获取日志器的时候进行加锁
- 构造函数中在初始化默认日志器的时候,不能使用全局建造者来创建,因为全局建造者中需要把创建好的日志器添加到单例日志管理器中,添加的时候单例日志管理器才被创建,才开始调用构造函数,这时候就会陷入循环阻塞
// 全局日志器建造者--将日志器添加到单例对象中去
class GlobalLoggerBuilder : public LoggerBuilder
{
public:
using ptr = std::shared_ptr<GlobalLoggerBuilder>;
Logger::ptr build() override
{
assert(!_logger_name.empty()); // 必须要有日志器名称
if (_formatter.get() == nullptr)
{
_formatter = std::make_shared<Formatter>();
}
if (_sinks.empty())
{
buildsink<StdoutSink>();
}
Logger::ptr logger;
if (_logger_type == LoggerType::LOG_ASYNC)
{
logger = std::make_shared<AsyncLogger>(_logger_name, _level, _formatter, _sinks, _looper_type);
}
else
{
logger = std::make_shared<SyncLogger>(_logger_name, _level, _formatter, _sinks);
}
// 添加到单例对象中
LoggerManager::GetInstance().addLogger(_logger_name, logger);
return logger;
}
};
- 全局建造者与局部建造者唯一的差别就是,全局建造者会将创建好的日志器添加到单例日志管理器中管理
5.10 日志宏&&全局接口的设计(代理模式)
- 提供全局的日志器获取接口
- 使用代理模式通过全局函数或宏函数来代理Logger类的log、debug、info、warn、error、fatal等接 接口,以便于控制源码文件名称和行号的输出控制,用户不需要再自己传入文件名和行号,简化用户操作
- 当仅需标准输出日志的时候可以通过默认日志器来打印日志。 且操作时只需要通过宏函数直接进行输出即可
- 至此,用户在使用该日志系统的时候,只需要包含一个clog.h的头文件
#ifndef __M_CLOG_H__
#define __M_CLOG_H__
#include "logger.hpp"
/*
全局接口的设置
1.提供获取指定日志器的全局接口(避免用户自己创建单例对象)
2.使用宏函数对日志器的接口进行代理(代理模式)
3.提供宏函数,直接通过默认日志器进行标准输出的打印(不需要获取日志器)
*/
namespace clog
{
// 提供获取日志器的全局接口
Logger::ptr GetLogger(const std::string &name)
{
return clog::LoggerManager::GetInstance().getLogger(name);
}
Logger::ptr GetRootLogger()
{
return clog::LoggerManager::GetInstance().getRootLogger();
}
// 使用宏函数对日志器的接口进行代理
#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__)
// 使用宏函数,直接通过默认日志器进行标准输出的打印
#define DEBUG(fmt, ...) clog::GetRootLogger()->debug(fmt, ##__VA_ARGS__)
#define INFO(fmt, ...) clog::GetRootLogger()->info(fmt, ##__VA_ARGS__)
#define WARN(fmt, ...) clog::GetRootLogger()->warn(fmt, ##__VA_ARGS__)
#define ERROR(fmt, ...) clog::GetRootLogger()->error(fmt, ##__VA_ARGS__)
#define FATAL(fmt, ...) clog::GetRootLogger()->fatal(fmt, ##__VA_ARGS__)
}
#endif
六、功能样例
#include <unistd.h>
#include "../logs/clog.hpp"
void test_log(const std::string &name)
{
INFO("%s", "测试开始");
clog::Logger::ptr logger = clog::LoggerManager::GetInstance().getLogger(name);
logger->debug( "%s", "测试日志");
logger->info("%s", "测试日志");
logger->warn( "%s", "测试日志");
logger->error( "%s", "测试日志");
logger->fatal( "%s", "测试日志");
INFO("%s", "测试结束");
}
int main()
{
const char *logger_name = "sync_logger";
std::unique_ptr<clog::LoggerBuilder> bd(new clog::GlobalLoggerBuilder());
bd->buildloggername(logger_name);
bd->buildformatter("[%c][%p][%f:%l][%m]%n");
bd->buildloggerlevel(clog::LogLevel::value::DEBUG);
bd->buildloggertype(clog::LoggerType::LOG_SYNC);
bd->buildsink<clog::StdoutSink>();
bd->buildsink<clog::FileSink>("./logfile/sync.log");
bd->buildsink<clog::RollBySizeSink>("./logfile/roll-by-size-sync.log-", 1024 * 1024);
bd->build();
test_log(logger_name);
return 0;
}
同步日志器测试结果:
异步日志器测试结果:
七、扩展样例
日志落地模块扩展:以时间为间隔进行文件的滚动的日志落地模块,实际上以时间为间隔进行文件滚动,就是以时间段进行滚动
实现思想:
- 用当前系统时间取模规定好的时间段大小,判断当前系统时间处于哪个时间段
- 判断与当前文件所在的时间段是否一致,不一致进行文件切换
#include <unistd.h>
#include "../logs/clog.hpp"
/*
日志落地模块扩展:以时间为间隔进行文件的滚动的日志落地模块
实际上以时间为间隔进行文件滚动,就是以时间段进行滚动
实现思想:
1.用当前系统时间取模规定好的时间段大小,判断当前系统时间处于哪个时间段
2.判断与当前文件所在的时间段是否一致,不一致进行文件切换
*/
enum timeGap
{
GAP_SECOND,
GAP_MINUTE,
GAP_HOUR,
GAP_DAY
};
class RollByTimeSink : public clog::LogSink
{
public:
using ptr = std::shared_ptr<RollByTimeSink>;
// 传入文件名,打开文件,将操作句柄管理起来
RollByTimeSink(const std::string &basename, timeGap gaptype)
: _basename(basename)
{
switch (gaptype)
{
case GAP_SECOND:
_gap_size = 1;
case GAP_MINUTE:
_gap_size = 60;
case GAP_HOUR:
_gap_size = 3600;
case GAP_DAY:
_gap_size = 3600 * 24;
}
std::string filename = creatNewFile();
// 当时间段大小为1时要注意取模的结果
_cur_gap = _gap_size == 1 ? clog::util::Date::now() : (clog::util::Date::now() % _gap_size); // 获得当前系统所在的时间段
// 创建目录
clog::util::File::creatDirectory(clog::util::File::path(filename));
// 创建文件
_ofs.open(filename, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
void log(const char *data, size_t len) override
{
time_t cur_time = clog::util::Date::now();
if ((cur_time % _gap_size) != _cur_gap)
{
_ofs.close();
// 创建新文件
_ofs.open(creatNewFile(), std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
_ofs.write(data, len);
if (_ofs.good() == false)
{
std::cout << "日志文件输出失败!\n";
}
}
private:
std::string creatNewFile()
{
// 获取系统时间
time_t t = clog::util::Date::now();
struct tm tl;
localtime_r(&t, &tl);
std::stringstream ss;
ss << _basename;
ss << tl.tm_year + 1900;
ss << tl.tm_mon + 1;
ss << tl.tm_mday;
ss << tl.tm_hour;
ss << tl.tm_min;
ss << tl.tm_sec;
ss << ".log";
return ss.str();
}
std::string _basename;
std::ofstream _ofs; // 为了避免每次对文件写入都要打开文件,对操作句柄进行管理
size_t _cur_gap; // 当前系统所在的时间段
size_t _gap_size; // 时间段大小
};
int main()
{
const char *logger_name = "async_logger";
std::unique_ptr<clog::LoggerBuilder> bd(new clog::GlobalLoggerBuilder());
bd->buildloggername(logger_name);
bd->buildformatter("[%c][%p][%f:%l][%m]%n");
bd->buildloggerlevel(clog::LogLevel::value::WARM);
bd->buildloggertype(clog::LoggerType::LOG_ASYNC);
bd->buildsink<clog::StdoutSink>();
bd->buildsink<clog::FileSink>("./logfile/async.log");
bd->buildsink<RollByTimeSink>("./logfile/roll-by-time-async.log-", timeGap::GAP_SECOND);
clog::Logger::ptr logger = bd->build();
size_t cur = clog::util::Date::now();
while (clog::util::Date::now() < cur + 5)
{
logger->fatal("这是一条日志消息");
usleep(1000);
}
return 0;
}
测试结果:
八、性能测试
- 下⾯对日志系统做⼀个性能测试,测试⼀下平均每秒能打印多少条日志消息到文件,以及每秒输出的大小
- 主要的测试方法法是:
- 每秒能打印日志数 = 打印日志条数 / 总的打印日志消耗时间
- 每秒能输出的大小=打印日志条数*日志的总条数 / (总的打印日志消耗时间*1024)
- 主要测试要素:
- 同步/异步 & 单线程/多线程不同场景下:
- 100w+条指定长度(100)的日志输出所耗时间
- 每秒可以输出多少条日志
- 每秒可以输出多少MB日志
- 测试环境:
-
OS ubuntu-22.04TLS虚拟机(2CPU核心/4G内存)
-
CPU 12th Gen Intel(R) Core(TM) i7-12700H (20 CPUs), ~2.3GHz
-
RAM 16G DDR4 4267
-
ROM 512G-SSD
#include "../logs/clog.hpp"
#include <vector>
#include <thread>
#include <chrono>
void bench(const std::string &logger_name,size_t thr_count,size_t msg_count,size_t msg_len)
{
// 获取指定名称的日志器
clog::Logger::ptr logger = clog::GetLogger(logger_name);
if (logger.get() == nullptr)
{
return;
}
std::cout << "测试日志 : " << msg_count << "条, 总大小 : " << msg_count * msg_len / 1024 << "KB\n";
// 组织指定长度的日志消息
std::string msg(msg_len - 1, 'A'); // 留一个位置方便换行显示
// 创建指定数量的线程
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++)
{
//i传值传参保证每个线程各有一份i
threads.emplace_back([&,i](){
//线程函数内部开始计时
auto start = std::chrono::high_resolution_clock::now();
//开始循环写日志
for(int j = 0; j < msg_per_thr; j++)
{
logger->fatal("%s",msg.c_str());
}
//线程函数内部结束计时
auto end = std::chrono::high_resolution_clock::now();
auto cost = std::chrono::duration_cast<std::chrono::duration<double>>(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();
}
// 计算总耗时:因为在多线程中,是并发执行的,因此总耗时就是耗费最高的时间的那个线程所消耗的时间
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);
// 输出结果
std::cout << "\t总耗时 : " << max_cost << "s\n";
std::cout << "\t每秒输出日志数量 : " << msg_per_sec << "条\n";
std::cout << "\t每秒输出日志大小 : " << size_per_sec << "KB\n";
}
void bench_sync()
{
const char *logger_name = "sync_logger";
// std::unique_ptr<clog::LoggerBuilder> bd(new clog::GlobalLoggerBuilder());
// bd->buildloggername(logger_name);
// bd->buildformatter("[%m]%n");
// bd->buildloggertype(clog::LoggerType::LOG_SYNC);
// bd->buildsink<clog::FileSink>("./logfile/sync.log");
// bd->build();
// bench(logger_name,1,1000000,100);
// //bench(logger_name,3,1000000,100);
clog::GlobalLoggerBuilder::ptr lbp(new clog::GlobalLoggerBuilder);
lbp->buildloggername(logger_name);
lbp->buildformatter("%m%n");
lbp->buildsink<clog::FileSink>("./logs/sync.log");
lbp->buildloggertype(clog::LoggerType::LOG_SYNC);
lbp->build();
//bench(logger_name, 1, 1000000, 100);
bench(logger_name, 5, 1000000, 100);
}
void bench_async()
{
const char *logger_name = "async_logger";
std::unique_ptr<clog::LoggerBuilder> bd(new clog::GlobalLoggerBuilder());
bd->buildloggername(logger_name);
bd->buildformatter("%m%n");
bd->buildEnableUnsaveAsync(); //启动非安全模式,忽略实际落地的时间
bd->buildloggertype(clog::LoggerType::LOG_ASYNC);
bd->buildsink<clog::FileSink>("./logfile/async.log");
bd->build();
bench(logger_name,1,1000000,100);
//bench(logger_name,5,1000000,100);
}
int main()
{
bench_sync();
//bench_async();
return 0;
}
测试结果:
- 同步单线程:
- 同步多线程:
- 异步单线程:
- 异步多线程:
- 综合以上结果分析,可以看出,异步多线程日志器下的输出效率最高,异步单线程日志器下的输出效率最低,同步日志器位于效率位于两者之前,但是同步单线程日志器的效率要比同步多线程日志器的效率略高一点,但差别不大
- 在同步日志器中,为什么多线程反而比单线程效率更低一点呢?这是因为限制同步日志器效率的最⼤原因是磁盘性能,输出日志消息的效率的线程多少并无明显区别,线程多了反而会降低,IO操作在用户态都会有缓冲区进行缓冲区,因此我们当前测试用例看起来的同步其实⼤多时候也是在操作内存,只有在缓冲区满了才会涉及到阻塞写磁盘操作,同步多线程下因为增加了磁盘的读写争抢,锁冲突提高,所以效率反而低一点
- 在异步日志器中,相反,限制日志器效率的并不是磁盘的性能,而是CPU处理的效率,日志消息的输出并不存在落地的阻塞,并且我们在测试中开启了非安全模式来间接忽略实际落地的时间,当同时有多个线程存在时,多线程写入的效率变高,异步线程不断从缓冲区中交换读取数据,并且在大多数时间下,写入和读取的动作是同时进行的,因此在异步多线程的场景下,效率有了显著提高
- 为什么异步单线程要比同步单线程效率来得低?因为在同步单线程中,并不涉及锁冲突,而异步单线程中存在锁冲突,因此性能会有所降低
项目代码:阿赭/日志系统https://gitee.com/cyqqwe/log-system.git