当前位置: 首页 > article >正文

【项目】C++同步异步日志系统-包含运行教程


文章目录

  • 项目介绍
    • 地址:https://gitee.com/royal-never-give-up/c-log-system
  • 开发环境
  • 核心技术
  • 为什么需要日志系统
    • 同步日志
    • 异步日志
  • 知识补充
    • 不定参宏函数
      • `__FILE__`
      • `__LINE__`
      • `__VA_ARGS__`
    • C使用
    • C++使用
        • 左值右值
        • `sizeof...()` 运算符
        • 完美转发
        • 完整例子
        • `sizeof...()` 运算符获取可变参数包中参数的数量
    • 设计模式
      • 六大原则
      • 工厂模式
        • 简单工厂
        • 工厂方法
        • 抽象工厂
      • 建造者模式
        • `Computer` 类:提供_board`、 `_display` 和 `_os属性和设置属性的方法,成员变量设置为公有,派生类可以直接访问成员变量
        • `Winbook` 类:公有继承父类`Computer` 类,virtual 可以省略,同时父类成员变量是公有,所以可以直接修改父类成员,同时要重写父类抽象函数
        • `Builder` 类:抽象基类,不能实例化,提供访问接口
        • `WinbookBuilder` 类:构建 `Winbook` 电脑对象,重写访问接口需要构建Winbook 对象的智能指针,不能是computer 因为setos 无法实例化,之后用指针将computer 对象中的成员变量实例化
        • `Director` 类:指导 `Builder` 类构建电脑对象,不能实例化 `Builder` 类对象,因该在构造函数传入具体的 `build` 的子类对象
        • `main` 函数:创建 `Director` 对象并传入 `WinbookBuilder` 对象,调用 `build` 方法构建电脑,然后获取构建好的电脑对象并显示其信息
      • 代理模式
  • 日志系统框架设计
  • 代码设计
    • 实用类设计-util.hpp
      • 实现
      • 测试
      • access-检查文件是否存在
      • stat-检查文件是否存在,范围更广
      • `find_last_of`-查找指定字符或字符串最后一次出现的位置
      • `mkdir` -创建目录
      • #ifndef` 和 `#endif
    • 日志等级类设计-level.hpp
    • 日志消息类设计-message.hpp
    • 日志输出格式化设计-format.hpp
      • 整体框架
      • 格式化子项数组:
        • **基类 `FormatItem`**
        • 派生类 **`FormatItem` 的不同格式化子项**
        • `ostream` 和 `istream`
        • `localtime_r` 和 `strftime` 函数
      • 格式化字符串:
        • **`Formatter` **实现格式化字符串
        • `using ptr = std::shared_ptr<FormatItem>;` 和 `std::shared_ptr<FormatItem> ptr;`
      • 完整代码和测试
    • 日志落地类设计(工厂模式)-sink.hpp
      • 基类`LogSink `
      • 派生类`StdoutSink `-标准输出
      • 派生类`FileSink `-指定文件
        • `ofstream` 文件类
        • 关于string变量到char* 变量的传参-str.c_str()
      • 派生类`RollBySizeSink`-滚动文件
      • 工厂类
      • 完整代码和测试
    • 日志器类设计(建造者模式)-logger.hpp
      • 整体框架
      • Logger基类-logger.hpp
        • `name` 函数:获取日志器名称
        • 日志记录函数(`debug`、`info`、`warn`、`error`、`fatal`):记录不同级别的日志消息,`va_list` 类型的变量处理可变参数,具体实现交给`logMessage` 函数
        • `logMessage` 函数:实际处理日志消息的函数,可以简化日志记录函数,首先判断日志等级,之后将格式化字符串和可变参数列表组合,LogMsg对象包含所有信息,之后再格式化消息,在落地
        • `log` 纯虚函数
        • `atomic` 类-原子操作
        • `va_list` -处理可变数量的参数
        • `vasprintf` 函数-生成一个格式化的字符串
      • `SynchLogger` 派生类-同步日志器-logger.hpp
        • `unique_lock` 类-锁
      • `Logger`类和`SynchLogger`完整代码和测试
      • 异步日志器
        • `Buffer`类-缓冲区设计-在buffer.hpp
        • `push`:向缓冲区中写入长度为 `len` 的数据
        • `begin()`:返回可读数据的起始地址
        • `readAbleSize()`:返回可读数据的长度
        • `writeAbleSize()`:返回可写空间的长度
        • `moveWriter()`:将 `_write_idx` 向后移动 `len`
        • `bufferReset()`:重置
        • `bufferEmpty()`:判断缓冲区是否为空
        • `ensureEnoughSize()`:确保缓冲区有足够的空间来存储长度为 `len` 的数据,不够就扩容
          • `copy` 函数-把一个范围内的元素复制到另一个范围
        • `AsynchLogger`派生类-异步日志器-logger.hpp
          • `bind`函数
          • `placeholders`-命名空间
      • `LoggerBuilder`类-建造者-logger.hpp
          • 全局建造者单例模式-懒汉模式-logger.hpp
        • 完整代码和测试
      • `AsynchLooper`类-异步工作器-looper.hpp
        • logger.hpp-完整代码和测试
        • `condition_variable` 类-条件变量
        • `thread ` 类-线程
        • `join()`:阻塞当前线程,直到被调用线程执行完毕
        • `detach()`:线程分离,允许线程独立执行,无法再对该线程进行 `join()` 或 `detach()` 操作
    • 日志宏全局接口设计
  • 项目总结
  • 性能测试
    • 测试环境 :
    • 测试代码
      • bench-测试主要代码
      • ``emplace_back``-末尾添加元素
      • `chrono`-计时
    • 测试结果


项目介绍

  • 支持多级别的日志消息:不同的日志有不同的优先级
  • 支持同步日志和异步日志
  • 支持写入日志到控制台,文件和滚动文件中:当文件写到一定体积后,新的内容写到别的文件中
  • 支持多线程程序并发写日志
  • 支持扩展不同的日志存放位置

地址:https://gitee.com/royal-never-give-up/c-log-system

开发环境

  • centos7
  • vscode/vim

核心技术

  • 继承多态
  • C++11(多线程,auto,智能指针,右值引用)
  • 双缓冲区
  • 生产消费者模型
  • 多线程
  • 单例模式,工厂模式,代理模式

为什么需要日志系统

  • ⽣产环境的产品为了保证其稳定性及安全性是不允许开发⼈员附加调试器去排查问题,可以借助⽇志系统来打印⼀些⽇志帮助开发⼈员解决问题

  • 上线客⼾端的产品出现bug⽆法复现并解决,可以借助⽇志系统打印⽇志并上传到服务端帮助开发⼈员进⾏分析

  • 对于⼀些⾼频操作(如定时器、⼼跳包)在少量调试次数下可能⽆法触发我们想要的⾏为,通过断点的暂停⽅式,我们不得不重复操作⼏⼗次、上百次甚⾄更多,导致排查问题效率是⾮常低下,可以借助打印⽇志的⽅式查问题

  • 在分布式、多线程/多进程代码中,出现bug⽐较难以定位,可以借助⽇志系统打印log帮助定位bug

  • 帮助⾸次接触项⽬代码的新开发⼈员理解代码的运⾏流程

同步日志

顺序是:业务线程->调用磁盘IO->回到业务线程

异步日志

顺序是:业务线程->将日志放到缓冲区->回到业务线程,日志线程->将缓冲区中日志拿出来->回到日志线程。

业务线程是生产者,日志线程是消费者

知识补充

不定参宏函数

__FILE__

__FILE__ 是一个宏,用于获取当前源文件的文件名(包括路径),是字符串类型

__LINE__

__LINE__ 是一个宏,用于获取当前代码所在的行号

__VA_ARGS__

__VA_ARGS__ 是一个宏,用于在宏中处理可变数量的参数。

C使用

#define LOG(fmt, ...) printf("[%s:%d] " fmt, __FILE__, __LINE__, __VA_ARGS__);
  • LOG(fmt, ...)LOG 是宏的名称,fmt 是一个普通的宏参数;... 是 C 语言中的可变参数列表,来接受任意数量的参数
  • printf("[%s:%d] " fmt, __FILE__, __LINE__, __VA_ARGS__);:这是宏的替换体
    • [%s:%d]:这是日志信息的前缀,用于显示文件名和行号。
    • __FILE____LINE__:这是 C 语言的预定义宏,__FILE__ 会被替换为当前源文件的文件名,__LINE__ 会被替换为当前代码所在的行号。
    • fmt:是宏的第一个参数,用于指定输出的格式字符串。
    • __VA_ARGS__:这是一个可变参数宏,用于替换宏定义中 ... 所代表的可变参数。
LOG("%s-%d\n", "Hello World", 666);
  • fmt 被替换为 "%s-%d\n"
  • __VA_ARGS__ 被替换为 "Hello World", 666
  • 最终 LOG 宏会被替换为 printf("[%s:%d] %s-%d\n", __FILE__, __LINE__, "Hello World", 666);,该语句会输出包含文件名、行号、"Hello World" 和数字 666 的日志信息。
LOG("Hello World");
  • 会报错,fmt 被替换为 "Hello World"
  • 但是__VA_ARGS__就变成了NULL

解决方法:##____VA_ARGS__ __,##运算符将前一个标识符与可变参数(__VA_ARGS__)合并,只有当参数列表不为空时才插入前缀

#define LOG(fmt, ...) printf("[%s:%d] " fmt, __FILE__, __LINE__, ##__VA_ARGS__);

C++使用

左值右值

左值是持久内存地址的表达式,变量、数组元素、对象成员等

int& ref; 是错误的

类型 &引用名 = 左值;
int num = 10;  // num 是一个左值
int &ref = num;  // 声明一个左值引用 ref,引用 num

右值是临时对象、字面量、表达式的结果,通常只能出现在赋值语句的右边,实现移动语义和完美转发

int num = 10; int&& rref = num; 是错误的

类型 &&引用名 = 右值;
int&& rr1 = 10;
double&& rr2 = x + y;
int&& ret = Add(3, 4);// 函数的返回值是一个临时变量,是一个右值
sizeof...() 运算符

获取可变参数包中参数的数量

sizeof...(args)

获取参数包args中的参数数量,要用括号

完美转发

将参数以原始的左值或右值属性传递给另一个函数,避免不必要的对象拷贝,例如,如果传入的参数是右值,我们希望传递给下一个函数时它仍然是右值;如果传入的是左值,传递时也保持为左值

T&& 作为模板参数时,它既可以绑定到左值,也可以绑定到右值

  • 如果传入的参数是左值,T 会被推导为左值引用类型,T&& 最终是一个左值引用。
  • 如果传入的参数是右值,T 会被推导为非引用类型,T&& 就是一个右值引用。

forward 根据转发引用的推导结果,将参数以原始的左值或右值属性转发给其他函数

template <typename T>
void wrapper(T&& arg) {otherFunction(std::forward<T>(arg));
}

将参数 arg 以原始的左值或右值属性转发给 otherFunction

完整例子
#include <iostream>using namespace std;// 处理无参数的情况,输出换行符
void myprintf()
{cout << "\n";
}// 可变参数模板函数
template <class T, class... Args>
void myprintf(const T &val, Args &&...args)//传入字符串是常量要加const
{// 先打印 valcout << val;if (sizeof...(args) > 0){cout << " ";  // 为了让输出更美观,添加空格分隔参数// 使用 std::forward 进行完美转发myprintf(std::forward<Args>(args)...);}else{myprintf();  // 递归结束,输出换行符cout<<"没有了"<<endl;//也可以这样子}
}int main()
{myprintf();myprintf("hello");myprintf("hello ", "world");myprintf("hello ", "world", 66);return 0;
}
  • ...:参数包展开运算符。它表示 Args 是一个可变参数模板参数包,这个参数包可以包含零个或多个模板类型参数。

  • Args:是参数包的名称,可以命名别的,可包含参数例如 intdoublestring 等,和C语言不一样不能省略。

  • sizeof...() 运算符获取可变参数包中参数的数量
  • forward<Args>(args)...完美转发,使用时要记得展开

设计模式

六大原则

1、单一职责原则
定义:一个类应该只负责一项职责

2.开闭原则
定义:扩展新功能时不应该修改原有代码,用多态实现

3.里氏替换原则
定义:子类继承父类中所有方法

4.依赖倒置原则
定义:高层模块不应该依赖底层模块,两者都应该依赖其抽象;抽象不应该依赖具体,而具体应该依赖抽象

实现:每个类都应该派生于抽象类,而不应该派生于具体类

5.接口隔离原则
定义:类之间的依赖的关系应该建立在最少的接口上,不提供没有必要的接口

6.迪米特法则(最少知道原则)
定义:一个对象应该对其他对象保持最少程度了解,即尽量减少对象之间的耦合

工厂模式

将创建对象时封装起来,将创建-使用分离

简单工厂

三种水果,苹果,香蕉,他们分别包括价格和种类,常规设计二个类,现在设计父类水果,之后苹果,香蕉分别继承水果。

每次增加一个产品时,都需要增加一个具体类和对象实现工厂

#include <iostream>
#include <memory>
#include <string>
using namespace std;
class Fruit
{
public:// 父类指针指向new出来的子类对象,父类的析构函数必须要加virtual// 否则delete时就会调用父类的析构函数,极大可能会导致 内存泄漏或其他潜在风险virtual ~Fruit() {}virtual void name() = 0;
};class Apple : public Fruit
{
public:virtual void name() override{cout << "我是苹果\n";}
};class Banana : public Fruit
{
public:virtual void name() override {cout << "我是香蕉\n";}
};class FruitFactory 
{
public:static std::shared_ptr<Fruit> create(const std::string& str){if (str == "APPLE")return std::make_shared<Apple>();else if (str == "BANANA")return std::make_shared<Banana>();elsereturn std::shared_ptr<Fruit>(); // 空的智能指针//不是创建一个新的 Fruit 对象,不能用make_shared代替}
};int main()
{std::shared_ptr<Fruit> sp = FruitFactory::create("APPLE");sp->name();sp = FruitFactory::create("BANANA");sp->name();return 0;
}
  • virtual ~Fruit() {}:类指针指向派生类对象,并且通过基类指针删除派生类对象时,如果基类析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数,这可能导致派生类中动态分配的资源无法释放,从而造成内存泄漏,所以多态通常会把基类析构换成虚函数
  • virtual void name() = 0;:纯虚函数,基类virtual 不能省,后面 = 0 让抽象类不能实例化,必须要被继承
  • virtual void name() override:virtual 派生类virtual 可以省略,override 用于检查派生类是否重写基类虚函数
  • static std::shared_ptr<Fruit> create(const std::string& str):static 让函数可以直接调用,返回类型是指向Fruit 的智能指针,指向派生类对象的指针可以隐式转换为指向基类对象的指针
    • 如果是"APPLE" 创建Apple 对象返回指向Apple 的智能指针,std::shared_ptr<Apple> 会隐式转换为 std::shared_ptr<Fruit> 类型返回
    • 如果不是上面字符,返回空的 std::shared_ptr<Fruit> 智能指针,而不是创建一个 Fruit 对象并返回指向它的智能指针
工厂方法

还是水果,每一个水果都对应一个工厂

#include <iostream>
#include <memory>
#include <string>// 抽象水果类
class Fruit {
public:virtual ~Fruit() {}virtual void name() = 0;
};// 苹果类
class Apple : public Fruit {
public:void name() override {std::cout << "我是苹果\n";}
};// 香蕉类
class Banana : public Fruit {
public:void name() override {std::cout << "我是香蕉\n";}
};// 抽象工厂类
class FruitFactory {
public:virtual std::shared_ptr<Fruit> create() = 0;virtual ~FruitFactory() {}
};// 苹果工厂类
class AppleFactory : public FruitFactory {
public:std::shared_ptr<Fruit> create() override {return std::make_shared<Apple>();}
};// 香蕉工厂类
class BananaFactory : public FruitFactory {
public:std::shared_ptr<Fruit> create() override {return std::make_shared<Banana>();}
};int main() {// 创建苹果工厂std::shared_ptr<FruitFactory> appleFactory = std::make_shared<AppleFactory>();// 通过苹果工厂创建苹果对象std::shared_ptr<Fruit> apple = appleFactory->create();apple->name();// 创建香蕉工厂std::shared_ptr<FruitFactory> bananaFactory = std::make_shared<BananaFactory>();// 通过香蕉工厂创建香蕉对象std::shared_ptr<Fruit> banana = bananaFactory->create();banana->name();return 0;
}
  • FruitFactory 类定义纯虚函数,因为后面有具体的工厂,纯虚函数的声明格式是在函数声明后加上 = 0
  • std::shared_ptr<Fruit> create() overridecreate 函数重写了基类 FruitFactory 中的纯虚函数 create()
  • return std::make_shared<Apple>();:由于 AppleFruit 的派生类,std::shared_ptr<Apple> 可以隐式转换为 std::shared_ptr<Fruit>
抽象工厂

将工厂再封装下,和工厂方法差不多

#include <iostream>
#include <memory>
#include <string>// 抽象水果类
class Fruit {
public:virtual ~Fruit() {}virtual void name() = 0;
};// 中国苹果类
class ChineseApple : public Fruit {
public:void name() override {std::cout << "我是中国苹果\n";}
};// 中国香蕉类
class ChineseBanana : public Fruit {
public:void name() override {std::cout << "我是中国香蕉\n";}
};// 美国苹果类
class AmericanApple : public Fruit {
public:void name() override {std::cout << "我是美国苹果\n";}
};// 美国香蕉类
class AmericanBanana : public Fruit {
public:void name() override {std::cout << "我是美国香蕉\n";}
};// 抽象水果工厂类
class FruitFactory {
public:virtual std::shared_ptr<Fruit> createApple() = 0;virtual std::shared_ptr<Fruit> createBanana() = 0;virtual ~FruitFactory() {}
};// 中国水果工厂类
class ChineseFruitFactory : public FruitFactory {
public:std::shared_ptr<Fruit> createApple() override {return std::make_shared<ChineseApple>();}std::shared_ptr<Fruit> createBanana() override {return std::make_shared<ChineseBanana>();}
};// 美国水果工厂类
class AmericanFruitFactory : public FruitFactory {
public:std::shared_ptr<Fruit> createApple() override {return std::make_shared<AmericanApple>();}std::shared_ptr<Fruit> createBanana() override {return std::make_shared<AmericanBanana>();}
};int main() {// 创建中国水果工厂std::shared_ptr<FruitFactory> chineseFactory = std::make_shared<ChineseFruitFactory>();// 通过中国水果工厂创建中国苹果对象std::shared_ptr<Fruit> chineseApple = chineseFactory->createApple();chineseApple->name();// 通过中国水果工厂创建中国香蕉对象std::shared_ptr<Fruit> chineseBanana = chineseFactory->createBanana();chineseBanana->name();// 创建美国水果工厂std::shared_ptr<FruitFactory> americanFactory = std::make_shared<AmericanFruitFactory>();// 通过美国水果工厂创建美国苹果对象std::shared_ptr<Fruit> americanApple = americanFactory->createApple();americanApple->name();// 通过美国水果工厂创建美国香蕉对象std::shared_ptr<Fruit> americanBanana = americanFactory->createBanana();americanBanana->name();return 0;
}

建造者模式

将对象的创建和表示分离,通过多个步骤创建对象。

  • **产品:Computer 类及其子类 Winbook
  • 抽象建造者:定义创建产品各个部分的抽象方法
  • 具体建造者:实现了抽象建造者接口,负责具体的步骤,WinbookBuilder 是具体建造者
  • 指挥者:安排产品的构建步骤,Director 类是指挥者
#include <iostream>
#include <memory>
#include <string>
using namespace std;
// 每个电脑都必须要有一个OS,因此我们把OS定为纯虚函数,强制子类重写
class Computer
{
public:virtual ~Computer() {}void setBoard(const string &board){_board = board;}void setDisplay(const string &display){_display = display;}virtual void setOS() = 0;void show(){cout << "board:\t" << _board << "\n";cout << "display:\t" << _display << "\n";cout << "os:\t" << _os << "\n";}string _board;string _display;string _os;
};class Winbook : public Computer
{
public:virtual void setOS() override{_os = "Win 10";}
};class Builder
{
public:virtual ~Builder() {}virtual void BuilderBoard(const string &board) = 0;virtual void BuilderDisplay(const string &display) = 0;virtual void BuilderOS() = 0;virtual std::shared_ptr<Computer> get() = 0;
};class WinbookBuilder : public Builder
{
public:WinbookBuilder() : _computer(std::make_shared<Winbook>()) {}virtual void BuilderBoard(const string &board){_computer->setBoard(board);}virtual void BuilderDisplay(const string &display){_computer->setDisplay(display);}virtual void BuilderOS(){_computer->setOS();}virtual shared_ptr<Computer> get(){return _computer;}private:shared_ptr<Winbook> _computer;
};class Director
{
public:Director(const shared_ptr<Builder> &builder): _builder(builder) {}void build(const string &board, const string &display){_builder->BuilderBoard(board);_builder->BuilderDisplay(display);_builder->BuilderOS();}shared_ptr<Builder> get(){return _builder;}private:shared_ptr<Builder> _builder;
};int main()
{// 步骤1:创建具体的建造者对象auto winBuilder = std::make_shared<WinbookBuilder>();// 步骤2:创建指挥者对象,并将建造者对象传递给指挥者Director director(winBuilder);// 步骤3:指挥者调用建造者的方法来构建电脑director.build("Z主板", "小米显示器");// 步骤4:通过建造者获取构建好的电脑对象auto builder = director.get();auto sp = builder->get();// 步骤5:调用电脑对象的 show 方法,显示电脑的信息sp->show();return 0;
}

作为基类将析构函数变为虚函数,防止内存泄漏

接口测试

void testWinbook() {Winbook winbook;winbook.setBoard("Winbook Board");winbook.setDisplay("Winbook Display");winbook.setOS();winbook.show();
}void testWinbookBuilder() {WinbookBuilder builder;builder.BuilderBoard("Builder Board");builder.BuilderDisplay("Builder Display");builder.BuilderOS();auto computer = builder.get();computer->show();
}void testDirector() {auto builder = std::make_shared<WinbookBuilder>();Director director(builder);director.build("Director Board", "Director Display");auto computer = builder->get();computer->show();
}

代理模式

允许一个对象(代理对象)替代另一个对象(目标对象)来提供服务

#include <iostream>using namespace std;class RentHouse
{
public:virtual void rent()=0;
};class Landlord:public RentHouse//房东
{
public:virtual void rent() override{cout<<"把房子租出去\n";}
};class Intermediary:public RentHouse//中介
{
public:virtual void rent(){cout<<"发布招租启示\n";cout<<"带人看房\n";_landlord.rent();cout<<"负责维修\n";}
private:Landlord _landlord;
};int main()
{Intermediary intermediary;intermediary.rent();return 0;
}
  • RentHouse:是抽象类,不能被实例化,纯虚函数 rent
  • Landlord:实现了 rent 方法
  • IntermediaryIntermediary 类充当了 Landlord 类的代理

日志系统框架设计

代码设计

实用类设计-util.hpp

  • 获取系统时间
  • 传入文件绝对路径,获取文件所在目录的绝对路径
  • 判断文件是否存在
  • 传入一个绝对路径,根据路径依次创建目录

实现

//util.hpp
#ifndef UTIL_H
#define UTIL_H
#include <iostream>
#include <ctime>
#include <string>
#include <unistd.h>
#include <sys/stat.h>// 这个文件中包含一些通用工具
namespace MySpace
{class util{public:// 1、获取当前时间戳static size_t getCurTime(){return (size_t)time(nullptr);}// 2、获取文件目录static std::string getDirectory(const std::string& pathname){int pos = pathname.find_last_of("/\\");//查找斜杠或者反斜杠if (pos == std::string::npos)//没找到return std::string("./");return pathname.substr(0, pos + 1);}// 3、判断文件是否存在static bool isExist(const std::string& pathname){struct stat st;return (stat(pathname.c_str(), &st) == 0);//return (access(pathname.c_str(), F_OK) == 0);//上面接口更广泛}// 4、创建一个目录static void createDirectory(const std::string& pathname){// ./abc/asize_t pos = 0, idx = 0;//pos:用于记录路径中分隔符(/ 或 \)位置,idx:作为循环的索引while (idx < pathname.size()) {pos = pathname.find_first_of("/\\", idx);//从索引 idx 开始,在路径中查找第一个出现的分隔符if (pos == std::string::npos) {mkdir(pathname.c_str(), 0777);break;  // 找到末尾,退出循环}//截取从路径开头到分隔符位置(包含分隔符)的子字符串std::string parent_dir = pathname.substr(0 , pos + 1);if (!isExist(parent_dir.c_str())) {mkdir(parent_dir.c_str() , 0777);} idx = pos + 1;}}};
}
#endif
  • getCurTime 函数time 函数返回系统时间秒数,返回值强转下
  • getDirectory 函数:路径 pathname 中提取出目录部分,用 find_last_of 找到最后一个斜杠或反斜杠,然后截取从开头到最后一个斜杠或反斜杠
  • isExist 函数:判断指定路径 pathname 的文件或目录是否存在
  • createDirectory 函数
    • pos = pathname.find_first_of("/\\", idx) 查找从索引 idx 开始的第一个分隔符位置。
    • 当找不到分隔符(pos 等于 std::string::npos)时,说明已经到达路径的末尾,使用 mkdir 函数创建最后的目录,并通过 break 语句退出循环。
    • 如果找到了分隔符,则截取从路径开头到分隔符位置(包含分隔符)的子字符串作为父目录 parent_dir
    • 使用 isExist 函数检查父目录是否存在,如果不存在,则使用 mkdir 函数创建该父目录。
    • 最后更新 idxpos + 1,继续下一轮循环查找下一个分隔符。

测试

// 测试 getCurTime 函数
size_t timeStamp = MySpace::util::getCurTime();
// 测试 getDirectory 函数
string path1 = "/home/user/documents/file.txt";
string result1 = MySpace::util::getDirectory(path1);
// 测试 isExist 函数
string existingFile = "/etc/passwd";
bool existsResult1 = MySpace::util::isExist(existingFile);
// 测试 createDirectory 函数
string testDirPath = "./abc/def/ert";
MySpace::util::createDirectory(testDirPath);

access-检查文件是否存在

#include <unistd.h>
int access(const char *pathname, int mode);
//使用
string pathname;
if(access(pathname.c_str(), F_OK) == 0){...}
  • pathname 需要要检查的文件或目录的路径名,字符串
  • mode :指定检查模式,填写F_OK:检查文件或目录是否存在
  • 返回0表示存在,返回-1表示不存在

stat-检查文件是否存在,范围更广

#include <sys/stat.h>
int stat(const char *pathname, struct stat *buf);
//使用
string pathname;
struct stat st;
if(stat(pathname.c_str(), &st) == 0){...}
  • pathname参数需要获取目录的路径名
  • buf是一个指向struct stat结构体的指针,用于存储获取到的文件信息
  • 成功返回0,失败返回-1

find_last_of-查找指定字符或字符串最后一次出现的位置

size_t find_last_of(charT c, size_t pos = npos) const;
size_t find_last_of(const basic_string& str, size_t pos = npos) const;
//使用
string str = "hello, world"
// 查找字符串 \"lo\" 中任意字符最后一次出现在位置
size_t pos1 = str.find_last_of("lo");
  • 查找字符c最后一次出现的位置,或者字符串 str 中包含的任意字符最后出现位置
  • 不指定pos,则默认从后往前搜索
  • 找到返回最后一次出现的位置,没找到返回npos

mkdir -创建目录

#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
  • pathname:要创建的目录的路径名,可以填绝对路径和相对路径(在当前目录下创建目录)
  • mode:指定新创建目录的权限模式,一般写 0777
  • 成功返回0,失败返回-1

#ifndef#endif

//名为 example.h 的头文件
#ifndef EXAMPLE_H
#define EXAMPLE_H// 代码块
#endif
  • #ifndef:即 “if not defined” 的缩写,检查宏名字(EXAMPLE_H)是否未被定义,如果未被定义则,编译 #ifndef#endif 之间的代码块;如果已定义,则跳过该代码块
  • 上面代码都可以用 #pragma once代替

日志等级类设计-level.hpp

枚举实现不同的等级,只有输出的日志等级大于日志器的默认限制等级才可以进行日志输出

  • OFF : 最高等级,可用于禁止所有日志输出

  • DEBUG : 调试等级日志

  • INFO : 提示等级日志

  • WARN : 警告等级日志

  • ERROR : 错误等级日志

  • FATAL : 致命错误等级日志

  • 定义枚举类,枚举出日志等级

  • 提供转换接口:将美剧转换为对应字符串

实现

//level.hpp
#pragma once
#include <string>
namespace MySpace
{class LogLevel{public:enum value{DEBUG,INFO,WARN,ERROR,FATAL,OFF};static const std::string toString(value level){switch (level){case DEBUG: return "DEBUG";case INFO : return "INFO";case WARN : return "WARN";case ERROR: return "ERROR";case FATAL: return "FATAL";case OFF  : return "OFF";}return "UNKNOW";}};
}

测试

MySpace::LogLevel::value debugLevel = MySpace::LogLevel::DEBUG;
std::string debugStr = MySpace::LogLevel::toString(debugLevel);

日志消息类设计-message.hpp

存储日志的各个属性信息

  • 日志的输出时间
  • 日志的等级
  • 源文件名称
  • 源代码行号
  • 线程ID
  • 日志主体消息
  • 日志器名称 ,允许多日志器同时使用

实现

//message.hpp#pragma once#include "level.hpp"
#include "util.hpp"
#include <ctime>
#include <iostream>
#include <string>
#include <thread>namespace MySpace {class LogMsg {public:time_t _ctime;                   // 日志产生的时间戳LogLevel::value _level;          // 日志等级std::string _file;               // 源文件名称size_t _line;                    // 源文件行号std::thread::id _tid;            // 线程IDstd::string _payload;            // 有效载荷,日志主题消息std::string _logger;             // 日志器LogMsg(LogLevel::value level, size_t line, const std::string file, const std::string logger, const std::string msg) : _level(level), _ctime(util::getCurTime()), _line(line), _file(file), _logger(logger), _payload(msg), _tid(std::this_thread::get_id()) {}};
}
  • 将上面内容设置,注意std::thread::id _tid; 中id不能省略,我们存储的是线程id不是线程对象
  • std::this_thread::get_id():返回的标识符类型是std::thread::id

测试

// 创建一个 LogMsg 对象MySpace::LogMsg logMsg(MySpace::LogLevel::INFO, 10, "test.cpp", "test_logger", "This is a test log message.");
// 输出 LogMsg 对象的各个成员信息
std::cout << "Log Time: " << logMsg._ctime << std::endl;

日志输出格式化设计-format.hpp

控制日志的输出格式

整体框架

通过格式化字符串定义日志的输出格式为下面方法,

 [%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n //默认格式[时间][线程ID][日志器名称][文件名:行号][日志级别]    日志主体消息

如果用户不希望显示线程id 也可以修改为 [%d{%H:%M:%S}][%c][%f:%l][%p]%T%m%n

将上面每个部分拆分交给格式化子项数组完成。

格式化子项数组:

将不同的格式化规则封装在不同的 FormatItem 子类中,并通过 Formatter 类进行统一管理和调用

基类 FormatItem

基类作为抽象类,后面的格式化子项类都有相同的操作方式,方便在 Formatter 类中统一调用

//format.hpp
//namespace MySpace
class FormatItem {
public:virtual void format(std::ostream& out, LogMsg& msg) = 0;
};
  • 纯虚函数 format:将日志消息按照特定的格式输出到流中,具体让子类实现,也是为了统一接口
派生类 FormatItem 的不同格式化子项

各个子类按照特定格式输出的任务

//format.hpp
//namespace MySpace  
class payloadFormatItem : public FormatItem{public:virtual void format(std::ostream& out, LogMsg& msg) override{out<<msg._payload;}};class levelFormatItem : public FormatItem{public:virtual void format(std::ostream& out, LogMsg& msg) override{//日志等级在LogLevel域中定义//不能用to_string 没有定义这个枚举类型out<<LogLevel::toString(msg._level);}};class ctimeFormatItem : public FormatItem{public:ctimeFormatItem(const std::string &fmt) : fmt_time(fmt) {}virtual void format(std::ostream& out, LogMsg& msg) override {struct tm t;localtime_r(&msg._ctime, &t);char buff[32] = {0};strftime(buff, sizeof(buff), fmt_time.c_str(), &t);out << buff;}private: std::string fmt_time; // %H:%M:%S};class fileFormatItem : public FormatItem{public:virtual void format(std::ostream& out, LogMsg& msg) override{out<<msg._file;}};class lineFormatItem : public FormatItem{public:virtual void format(std::ostream& out, LogMsg& msg) override{out<<std::to_string(msg._line);}};class tidFormatItem : public FormatItem{public:virtual void format(std::ostream& out, LogMsg& msg) override{out<<msg._tid;}};class loggerFormatItem : public FormatItem{public:virtual void format(std::ostream& out, LogMsg& msg) override{out<<msg._logger;}};class TabFormatItem  : public FormatItem{public:virtual void format(std::ostream& out, LogMsg& msg) override{out<<'\t';}};class NewLineFormatItem  : public FormatItem{public:virtual void format(std::ostream& out, LogMsg& msg) override{out<<'\n';}};//[]class OtherFormatItem  : public FormatItem{public:OtherFormatItem(const std::string& str):_str(str){}virtual void format(std::ostream& out, LogMsg& msg) override{out<<_str;}private:std::string _str;};
  • payloadFormatItem:输出主体消息。
  • levelFormatItem:输出日志级别,日志级别从枚举类型转化字符串类型。
  • ctimeFormatItem:把 时间戳转化为指定格式字符串
  • fileFormatItem:输出 文件名。
  • lineFormatItem:输出 行号。
  • tidFormatItem:输出 线程 ID。
  • loggerFormatItem:输出 日志器名称。
  • TabFormatItem:输出缩进。
  • NewLineFormatItem:输出换行。
  • OtherFormatItem:输出字符串。
ostreamistream
  • std::coutstd::ostream 类的一个实例对象,同理cin 是istream 的实例对象
  • 当需要自定义类型输出时,用ostream
localtime_rstrftime 函数

localtime_r 是一个用于将 time_t 类型的时间戳转换为本地时间的函数

struct tm *localtime_r(const time_t *timep, struct tm *result);
//使用
struct tm t;
time_t timeValue = time(nullptr);
localtime_r(&timeValue, &t);
  • timep:指向 time_t 类型的指针

  • result:指向 tm 结构体的指针

    •   struct tm {int tm_sec;   // 秒,范围从0到60(包含60,用于表示闰秒)int tm_min;   // 分钟,范围从0到59int tm_hour;  // 小时,范围从0到23int tm_mday;  // 一个月中的第几天,范围从1到31int tm_mon;   // 月份,从0开始计数,0表示1月,11表示12月int tm_year;  // 从1900年开始计数的年份,例如2023年表示为123int tm_wday;  // 一周中的第几天,从0开始计数,0表示星期日,6表示星期六int tm_yday;  // 一年中的第几天,从0开始计数,0表示1月1日int tm_isdst; // 夏令时标志,非零表示夏令时,零表示非夏令时,-1表示不确定};
      
  • 成功返回指向 result 的指针,失败返回空

strftime 函数用于将 tm 结构体表示的时间信息按照指定的格式转换为字符串

size_t strftime(char *buff, size_t max, const char *format, const struct tm *tm);
//使用
char buffer[80];
struct tm timeinfo;
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &timeinfo);
  • buff:指向用于存储格式化后时间字符串的字符数组的指针。
  • max:字符数组 s 的最大长度,防止缓冲区溢出。
  • format:指向格式化字符串的指针,包含了格式化指令,用于指定输出字符串的格式。
    • %Y:四位数的年份。
    • %m:两位数的月份(01 - 12)。
    • %d:两位数的日期(01 - 31)。
    • %H:24 小时制的小时数(00 - 23)。
    • %M:分钟数(00 - 59)。
    • %S:秒数(00 - 59)。
  • tm:指向 tm 结构体的指针,包含了要格式化的时间信息。
  • 格式化后没超出长度就返回写入buff 的字符数,否则返回0

格式化字符串:

**Formatter **实现格式化字符串

解析用户提供的字符串,根据该字符串创建相应的格式化子项,然后将日志消息按照指定格式输出

//format.hpp
/* %d  表示日期,    子格式 {%H:%M:%S} %t  表示鲜橙ID %c  表示日志器名称%f  表示源码文件名%l  表示源码行号%p  表示日志级别%T  表示制表符缩进%m  表示主体消息%n  表示换行*/
//namespace MySpace  class Formatter{public:Formatter(const std::string& pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"):_pattern(pattern){assert(parsePattern());}//对msg格式化void format(std::ostream& out, LogMsg& msg){for (auto &item : _items) {item->format(out, msg);}}std::string format(LogMsg &msg) {std::ostringstream out;format(out, msg);return out.str();}//对格式化字符串进行解析bool parsePattern(){//1.对格式化字符串解析std::vector<std::pair<std::string, std::string>> fmt_order;size_t pos = 0;std::string key, val;while(pos<_pattern.size()){//处理原始字符abc[]if (_pattern[pos] != '%') {val.push_back(_pattern[pos++]); continue;}//处理%%if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%') {val.push_back('%'); pos += 2; continue;}//原始字符串处理完毕fmt_order.push_back(std::make_pair("", val));val.clear();//处理格式化字符{}pos += 1;if (pos == _pattern.size()) { std::cout << "%之后没有对应的格式化字符" << std::endl;return false; }key = _pattern[pos];pos += 1;if (pos < _pattern.size() && _pattern[pos] == '{') {pos += 1;   // 这时pos指向子规则的起始位置while (pos < _pattern.size() && _pattern[pos] != '}') {val.push_back(_pattern[pos++]);}// 若走到了末尾,还没有找到},则说明格式是错误的,跳出循环if (pos == _pattern.size()) {   std::cout << "子规则{}匹配出错" << std::endl;return false;}pos += 1; // 因为pos指向的是 } 位置,向后走一步就到了下一次处理的新位置}fmt_order.push_back(std::make_pair(key, val));key.clear(); val.clear();}//2.根据解析后的数据初始化格式化子项数组成员for (auto &it : fmt_order) {_items.push_back(createItem(it.first, it.second));} return true;}private://根据不同的格式化字符创建不同的格式化子项对象std::shared_ptr<FormatItem> createItem(const std::string &key, const std::string &val){if (key == "d")  return std::make_shared<ctimeFormatItem>(val);if (key == "t")  return std::make_shared<tidFormatItem>();if (key == "c")  return std::make_shared<loggerFormatItem>();if (key == "f")  return std::make_shared<fileFormatItem>();if (key == "l")  return std::make_shared<lineFormatItem>();if (key == "p")  return std::make_shared<levelFormatItem>();if (key == "T")  return std::make_shared<TabFormatItem>();if (key == "m")  return std::make_shared<payloadFormatItem>();if (key == "n")  return std::make_shared<NewLineFormatItem>();return std::make_shared<OtherFormatItem>(val);}private:std::string _pattern;//格式化规则字符串std::vector<std::shared_ptr<FormatItem>> _items;// 格式化子项数组};
  • 构造:用字符串 _pattern接收,定义了一个默认的,可以改,初始化的时候就解析字符串,如果字符串不规范就停止初始化
  • format 将日志消息按照特定的格式输出到流中,默认有输出流和日志消息两个参数
  • parsePattern 用于解析格式化模式字符串,结果存储在 fmt_order 中,包含格式化指令和对应的参数
  • createItem 根据格式化指令的键值对创建相应的 FormatItem 对象
  • _pattern:存储用户传入的格式化字符串,默认值为 [%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n。这个字符串定义了日志消息的输出格式
  • _items:是一个存储 FormatItem 智能指针的向量。每个 FormatItem 对象代表一个格式化子项,负责将日志消息的特定部分按照指定格式输出。
using ptr = std::shared_ptr<FormatItem>;std::shared_ptr<FormatItem> ptr;
  • using ptr = std::shared_ptr<FormatItem>;:类型别名声明,ptr 就等价于 std::shared_ptr<FormatItem>也可以用 typedef
  • std::shared_ptr<FormatItem> ptr;:变量声明

完整代码和测试

//format.hpp
#pragma once
#include "level.hpp"
#include "util.hpp"
#include "message.hpp"
#include <vector>
#include <iostream>
#include <unordered_map>
#include <sstream>
#include <assert.h>namespace MySpace{class FormatItem {public:virtual void format(std::ostream& out, LogMsg& msg) = 0;};class payloadFormatItem : public FormatItem{public:virtual void format(std::ostream& out, LogMsg& msg) override{out<<msg._payload;}};class levelFormatItem : public FormatItem{public:virtual void format(std::ostream& out, LogMsg& msg) override{//日志等级在LogLevel域中定义//不能用to_string 没有定义这个枚举类型out<<LogLevel::toString(msg._level);}};class ctimeFormatItem : public FormatItem{public:ctimeFormatItem(const std::string &fmt) : fmt_time(fmt) {}virtual void format(std::ostream& out, LogMsg& msg) override {struct tm t;localtime_r(&msg._ctime, &t);char buff[32] = {0};strftime(buff, sizeof(buff), fmt_time.c_str(), &t);out << buff;}private: std::string fmt_time; // %H:%M:%S};class fileFormatItem : public FormatItem{public:virtual void format(std::ostream& out, LogMsg& msg) override{out<<msg._file;}};class lineFormatItem : public FormatItem{public:virtual void format(std::ostream& out, LogMsg& msg) override{out<<std::to_string(msg._line);}};class tidFormatItem : public FormatItem{public:virtual void format(std::ostream& out, LogMsg& msg) override{out<<msg._tid;}};class loggerFormatItem : public FormatItem{public:virtual void format(std::ostream& out, LogMsg& msg) override{out<<msg._logger;}};class TabFormatItem  : public FormatItem{public:virtual void format(std::ostream& out, LogMsg& msg) override{out<<'\t';}};class NewLineFormatItem  : public FormatItem{public:virtual void format(std::ostream& out, LogMsg& msg) override{out<<'\n';}};//[]class OtherFormatItem  : public FormatItem{public:OtherFormatItem(const std::string& str):_str(str){}virtual void format(std::ostream& out, LogMsg& msg) override{out<<_str;}private:std::string _str;};/* %d  表示日期,    子格式 {%H:%M:%S} %t  表示鲜橙ID %c  表示日志器名称%f  表示源码文件名%l  表示源码行号%p  表示日志级别%T  表示制表符缩进%m  表示主体消息%n  表示换行*/class Formatter{public:Formatter(const std::string& pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"):_pattern(pattern){assert(parsePattern());}//对msg格式化void format(std::ostream& out, LogMsg& msg){for (auto &item : _items) {item->format(out, msg);}}std::string format(LogMsg &msg) {std::ostringstream out;format(out, msg);return out.str();}//对格式化字符串进行解析bool parsePattern(){//1.对格式化字符串解析std::vector<std::pair<std::string, std::string>> fmt_order;size_t pos = 0;std::string key, val;while(pos<_pattern.size()){//处理原始字符abc[]if (_pattern[pos] != '%') {val.push_back(_pattern[pos++]); continue;}//处理%%if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%') {val.push_back('%'); pos += 2; continue;}//原始字符串处理完毕fmt_order.push_back(std::make_pair("", val));val.clear();//处理格式化字符{}pos += 1;if (pos == _pattern.size()) { std::cout << "%之后没有对应的格式化字符" << std::endl;return false; }key = _pattern[pos];pos += 1;if (pos < _pattern.size() && _pattern[pos] == '{') {pos += 1;   // 这时pos指向子规则的起始位置while (pos < _pattern.size() && _pattern[pos] != '}') {val.push_back(_pattern[pos++]);}// 若走到了末尾,还没有找到},则说明格式是错误的,跳出循环if (pos == _pattern.size()) {   std::cout << "子规则{}匹配出错" << std::endl;return false;}pos += 1; // 因为pos指向的是 } 位置,向后走一步就到了下一次处理的新位置}fmt_order.push_back(std::make_pair(key, val));key.clear(); val.clear();}//2.根据解析后的数据初始化格式化子项数组成员for (auto &it : fmt_order) {_items.push_back(createItem(it.first, it.second));} return true;}private://根据不同的格式化字符创建不同的格式化子项对象std::shared_ptr<FormatItem> createItem(const std::string &key, const std::string &val){if (key == "d")  return std::make_shared<ctimeFormatItem>(val);if (key == "t")  return std::make_shared<tidFormatItem>();if (key == "c")  return std::make_shared<loggerFormatItem>();if (key == "f")  return std::make_shared<fileFormatItem>();if (key == "l")  return std::make_shared<lineFormatItem>();if (key == "p")  return std::make_shared<levelFormatItem>();if (key == "T")  return std::make_shared<TabFormatItem>();if (key == "m")  return std::make_shared<payloadFormatItem>();if (key == "n")  return std::make_shared<NewLineFormatItem>();return std::make_shared<OtherFormatItem>(val);}private:std::string _pattern;//格式化规则字符串std::vector<std::shared_ptr<FormatItem>> _items;// 格式化子项数组};}

测试

#include <iostream>
#include <sstream>
#include "format.hpp"int main() {// 创建一个 LogMsg 对象MySpace::LogMsg logMsg(MySpace::LogLevel::INFO, 11, "test.cpp", "root", "This is a test message");// 创建一个 Formatter 对象MySpace::Formatter formatter;//MySpace::Formatter formatter("abc[%d]{%H%M%S}");// 解析格式化字符串std::ostringstream oss;// 对 LogMsg 进行格式化formatter.format(oss, logMsg);// 输出格式化后的日志std::cout << oss.str() << std::endl;return 0;
}
//输出
//[16:43:30][1][root][test.cpp:11][INFO]  This is a test message

日志落地类设计(工厂模式)-sink.hpp

将格式化完成后的日志消息,输出到指定位置,标准输出,指定文件,滚动文件(大小,时间),数据库,服务器

抽象出落地模块作为基类,从不同的落地方向从基类进行派生,用工厂模式将创建和表示分离

基类LogSink

统一日志输出接口

//sink.hpp
//namespace MySpace
class LogSink {public:virtual void log(const std::string& data, size_t len) = 0;
};
  • 派生类都必须实现 log 函数,可以通过基类指针或引用调用 log 函数

派生类StdoutSink -标准输出

把日志信息输出到控制台

//sink.hpp
//namespace MySpace
// 落地方向: 标准输出
class StdoutSink : public LogSink {public:// 将日志消息写到标准输出,定长输出void log(const std::string& data, size_t len) override {//从0开始截取长度为lenstd::string str(data, 0,len);std::cout << str.c_str()<< std::endl;}
};
  • data 的起始位置(索引为 0)开始截取长度为 len 的子字符串,然后输出到控制台

派生类FileSink -指定文件

将日志消息写入指定的文件

//sink.hpp
//namespace MySpace
// 落地方向: 指定文件
class FileSink : public LogSink {public: // 构造时传入文件名FileSink(const std::string &pathname) {// 1、 创建日志文件所在的目录util::createDirectory(util::getDirectory(pathname));// 2、 创建并打开日志文件_ofs.open(pathname, std::ios::binary | std::ios::app);}void log(const std::string& data, size_t len) override {_ofs.write(data.c_str(), len);if (_ofs.fail()) {std::cerr << "Failed to write to file." << std::endl;}}private:std::string _pathname;std::ofstream _ofs;
};
  • 创建FileSink对象,用户可以指定文件名字,然后找到文件目录,打开文件
  • 之后将日志消息写入文件
ofstream 文件类
#include <fstream>
ofstream::ofstream(const char* filename, ios_base::openmode mode = ios_base::out);
//使用,在当前目录下创建文件example.txt
std::ofstream file("example.txt");

上面实例化对象,下面是一些接口使用

ofs.open(file_name, mode)  // 打开文件
ofs.is_open()        // 判断文件是否打开成功,成功返回true
ofs.write(data, len) // 将指定长度 len 的数据 data 写入到与 ofs 关联的文件,data 通常是一个字符数组或指向字符数据的指针  
ofs.good()         // 若文件读或写失败,某些字段会被设置,调用good()返回false
  • file_name :要打开的文件名,可以是相对路径或绝对路径
  • mode :打开文件的模式,二进制用按位或操作(|)
    • std::ios_base::out 打开文件,没有就创建
    • std::ios_base::app追加文件
    • std::ios_base::binary二进制模式
关于string变量到char* 变量的传参-str.c_str()
string str = "abc";
const char* cstr = str.c_str();

str.c_str() 直接将str 从string 类型变成了 const char*类型

派生类RollBySizeSink-滚动文件

日志文件写入量不断增大,RollBySizeSink 类的主要作用就是当日志文件达到指定的最大大小时,自动创建新的日志文件来继续记录日志,从而实现日志文件的滚动存储

//sink.hpp
//namespace MySpace
// 落地方向: 滚动文件,按大小
class RollBySizeSink : public LogSink {public://用户决定文件基本名字和文件大小RollBySizeSink(const std::string &basename, size_t max_size): _basename(basename), _max_fsize(max_size), _cur_fsize(0), _name_count(0){std::string pathname = createNewFile();util::createDirectory(util::getDirectory(pathname));_ofs.open(pathname, std::ios::binary | std::ios::app);}void log(const std::string& data, size_t len) override{if (_cur_fsize + len >= _max_fsize) {_ofs.close();                         // 关闭原来已经打开的文件std::string pathname = createNewFile();_cur_fsize = 0;util::createDirectory(util::getDirectory(pathname));_ofs.open(pathname, std::ios::binary | std::ios::app);}_ofs.write(data.c_str(), len);_cur_fsize += len;}private://根据时间创建新的滚动文件std::string createNewFile(){_name_count += 1;// 获取系统时间,以时间来构建文件扩展名time_t t = util::getCurTime();struct tm lt;localtime_r(&t, &lt);std::string filename;filename += _basename;filename += std::to_string(lt.tm_year + 1900);filename += "-";filename += std::to_string(lt.tm_mon + 1);filename += "-";filename += std::to_string(lt.tm_mday);filename += " ";filename += std::to_string(lt.tm_hour);filename += ":";filename += std::to_string(lt.tm_min);filename += ":";filename += std::to_string(lt.tm_sec);filename += "-";filename += std::to_string(_name_count);filename += ".log";return filename;}private:std::string _basename;   // 基础文件名   std::ofstream _ofs;      // 操作句柄        size_t _max_fsize;       // 记录文件允许存储最大数据量,超过大小就要切换文件size_t _cur_fsize;       // 记录当前文件已经写入数据大小size_t _name_count;      // 滚动文件数量
};
  • 构造时,需要用户确定文件名和文件大小,createNewFile 函数创建新的日志文件,同时创建该文件所在的目录,最后以二进制追加模式打开文件
  • 写入日志前,检查当前文件的已写入数据大小 _cur_fsize 加上要写入的数据大小 len 是否超过最大文件大小 _max_fsize,如果超过最大文件大小,关闭当前文件,调用 createNewFile 函数创建新的日志文件,重置当前文件大小为 0,创建新文件所在的目录并打开新文件,将日志数据写入文件,并更新当前文件的已写入数据大小
  • 根据当前时间和文件计数生成新的文件名

工厂类

将不同 LogSink 派生类的创建逻辑封装,外部代码只需调用工厂类的 create 方法并传入相应的参数,就能得到所需的 LogSink 对象

//sink.hpp
//namespace MySpace
class SinkFactory {public:template<class T, class ...Args>static std::shared_ptr<LogSink> create(Args&&... args) {return std::make_shared<T>(std::forward<Args>(args)...);}
};

完整代码和测试

//sink.hpp
#pragma once
#include "level.hpp"
#include "util.hpp"
#include "message.hpp"
#include <string>
#include <iostream>
#include <assert.h>
#include <fstream>namespace MySpace{class LogSink {public:virtual void log(const std::string& data, size_t len) = 0;};// 落地方向: 标准输出class StdoutSink : public LogSink {public:// 将日志消息写到标准输出,定长输出void log(const std::string& data, size_t len) override {//从0开始截取长度为lenstd::string str(data, 0,len);std::cout << str.c_str()<< std::endl;}};// 落地方向: 指定文件class FileSink : public LogSink {public: // 构造时传入文件名FileSink(const std::string &pathname) {// 1、 创建日志文件所在的目录util::createDirectory(util::getDirectory(pathname));// 2、 创建并打开日志文件_ofs.open(pathname, std::ios::binary | std::ios::app);}void log(const std::string& data, size_t len) override {_ofs.write(data.c_str(), len);if (_ofs.fail()) {std::cerr << "Failed to write to file." << std::endl;}}private:std::string _pathname;std::ofstream _ofs;};// 落地方向: 滚动文件,按大小class RollBySizeSink : public LogSink {public://用户决定文件基本名字和文件大小RollBySizeSink(const std::string &basename, size_t max_size): _basename(basename), _max_fsize(max_size), _cur_fsize(0), _name_count(0){std::string pathname = createNewFile();util::createDirectory(util::getDirectory(pathname));_ofs.open(pathname, std::ios::binary | std::ios::app);}void log(const std::string& data, size_t len) override{if (_cur_fsize + len >= _max_fsize) {_ofs.close();                         // 关闭原来已经打开的文件std::string pathname = createNewFile();_cur_fsize = 0;util::createDirectory(util::getDirectory(pathname));_ofs.open(pathname, std::ios::binary | std::ios::app);}_ofs.write(data.c_str(), len);_cur_fsize += len;}private://根据时间创建新的滚动文件std::string createNewFile(){_name_count += 1;// 获取系统时间,以时间来构建文件扩展名time_t t = util::getCurTime();struct tm lt;localtime_r(&t, &lt);std::string filename;filename += _basename;filename += std::to_string(lt.tm_year + 1900);filename += "-";filename += std::to_string(lt.tm_mon + 1);filename += "-";filename += std::to_string(lt.tm_mday);filename += " ";filename += std::to_string(lt.tm_hour);filename += ":";filename += std::to_string(lt.tm_min);filename += ":";filename += std::to_string(lt.tm_sec);filename += "-";filename += std::to_string(_name_count);filename += ".log";return filename;}private:std::string _basename;   // 基础文件名   std::ofstream _ofs;      // 操作句柄        size_t _max_fsize;       // 记录文件允许存储最大数据量,超过大小就要切换文件size_t _cur_fsize;       // 记录当前文件已经写入数据大小size_t _name_count;      // 滚动文件数量};class SinkFactory {public:template<class T, class ...Args>static std::shared_ptr<LogSink> create(Args&&... args) {return std::make_shared<T>(std::forward<Args>(args)...);}};}

测试

#include <iostream>
#include <sstream>
#include "format.hpp"
#include "sink.hpp"int main() {// 创建一个 LogMsg 对象MySpace::LogMsg logMsg(MySpace::LogLevel::INFO, 11, "test.cpp", "root", "This is a test message");// 创建一个 Formatter 对象MySpace::Formatter formatter;// 对 LogMsg 进行格式化std::string str = formatter.format(logMsg);//三种输出方式std::shared_ptr<MySpace::LogSink> stdout_ptr = MySpace::SinkFactory::create<MySpace::StdoutSink>();std::shared_ptr<MySpace::LogSink> file_ptr = MySpace::SinkFactory::create<MySpace::FileSink>("./logfile/test.log");std::shared_ptr<MySpace::LogSink> roll_ptr = MySpace::SinkFactory::create<MySpace::RollBySizeSink>("./logfile/roll-", 1024*1024);stdout_ptr->log(str.c_str(), str.size());file_ptr->log(str.c_str(), str.size());roll_ptr->log(str.c_str(), str.size());return 0;
}
//输出
//屏幕输出信息,当前路径下创建logfile目录,在该目录下创建指定文件和滚动文件

日志器类设计(建造者模式)-logger.hpp

从上面的测试可以看出,如果不对资源整合用户使用就是这么麻烦,所以对前面模块整合,向外提供接口完成不同等级的日志输出,支持多个落地方向,

  • 同步日志器:直接对日志消息进行输出
  • 异步日志器,将日志消息放入缓冲区中,由异步线程进行日志输出

整体框架

Logger基类-logger.hpp

抽象Logger基类,派生出同步日志器和异步日志器,两种日志器的落地方式不同所以将落地方式抽象

//logger.hpp    
class Logger {public:Logger(const std::string &logger_name, MySpace::LogLevel::value limit_level, std::shared_ptr<MySpace::Formatter > formatter, std::vector<std::shared_ptr<MySpace::LogSink >> sinks){}//获取日志器名称const std::string &name(){ return _logger_name; }/* 构造日志消息对象过程, 并得到格式化后的日志消息字符串-- 然后进行落地输出*/void debug(const std::string& file, size_t line, const std::string &fmt, ...){}void info (const std::string& file, size_t line, const std::string &fmt, ...){}void warn (const std::string& file, size_t line, const std::string &fmt, ...){}void error(const std::string& file, size_t line, const std::string &fmt, ...){}void fatal(const std::string& file, size_t line, const std::string &fmt, ...){}protected:void logMessage(MySpace::LogLevel::value level, const std::string& file, size_t line, const std::string &fmt, va_list ap) {}/* 抽象接口完成实际的落地输出 -- 不同的日志器会有不同的实际落地方式 */virtual void log(const std::string& data, size_t len) = 0;protected:std::mutex _mutex;std::string _logger_name;std::atomic<MySpace::LogLevel::value> _limit_level;    std::shared_ptr<MySpace::Formatter> _formatter;std::vector<std::shared_ptr<MySpace::LogSink>> _sinks;};

成员变量

  • _mutex :一个日志器可能会被多个线程同时访问,确保同一时间日志落地的时候不会有其他线程干扰
  • _logger_name :日志器名称
  • _limit_level :日志输出等级,原子操作,比锁消耗性能低
  • _fomatter :将LogMsg对象格式化成指定字符串
  • _sinks :确定落地位置

下面是成员函数具体实现

    class Logger {public:Logger(const std::string &logger_name, MySpace::LogLevel::value limit_level, std::shared_ptr<MySpace::Formatter > formatter, std::vector<std::shared_ptr<MySpace::LogSink >> sinks):_logger_name(logger_name), _limit_level(limit_level), _formatter(formatter), _sinks(sinks.begin(), sinks.end()){}//获取日志器名称const std::string &name(){ return _logger_name; }/* 构造日志消息对象过程, 并得到格式化后的日志消息字符串-- 然后进行落地输出*/void debug(const std::string& file, size_t line, const std::string &fmt, ...){va_list ap;va_start(ap, fmt);logMessage(LogLevel::value::DEBUG, file, line, fmt, ap);va_end(ap);}void info (const std::string& file, size_t line, const std::string &fmt, ...){va_list ap;va_start(ap, fmt);logMessage(LogLevel::value::INFO, file, line, fmt, ap);va_end(ap);}void warn (const std::string& file, size_t line, const std::string &fmt, ...){va_list ap;va_start(ap, fmt);logMessage(LogLevel::value::WARN, file, line, fmt, ap);va_end(ap);}void error(const std::string& file, size_t line, const std::string &fmt, ...){va_list ap;va_start(ap, fmt);logMessage(LogLevel::value::ERROR, file, line, fmt, ap);va_end(ap);}void fatal(const std::string& file, size_t line, const std::string &fmt, ...){// 2、 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串va_list ap;va_start(ap, fmt);logMessage(LogLevel::value::FATAL, file, line, fmt, ap);va_end(ap);}protected:void logMessage(MySpace::LogLevel::value level, const std::string& file, size_t line, const std::string &fmt, va_list ap) {/* 通过传入的参数构造出一个日志消息对象,进行日志格式化,最终落地*/// 1、 判断当前日志等级是否达到输出标准if (level < _limit_level)  return;// 2、 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串char *res = nullptr;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1) { std::cout << "vasprintf failed! " << std::endl; return;}// 3、 构造LogMsg对象LogMsg msg(level, line, file, _logger_name, res);// 4、 通过格式化工具对LogMsg进行格式化,获得格式化后的日志字符串std::string real_message = _formatter->format(msg);// 5、 进行日志落地log(real_message.c_str(), real_message.size());free(res);// vasprintf() 内部开辟空间了,是动态申请的,需要我们手动释放}/* 抽象接口完成实际的落地输出 -- 不同的日志器会有不同的实际落地方式 */virtual void log(const std::string& data, size_t len) = 0;protected:std::mutex _mutex;std::string _logger_name;std::atomic<MySpace::LogLevel::value> _limit_level;    std::shared_ptr<MySpace::Formatter> _formatter;std::vector<std::shared_ptr<MySpace::LogSink>> _sinks;};
atomic 类-原子操作
 #include <atomic> 
std::atomic<int> atomicInt;
创建了一个 std::atomic 对象 atomicInt
va_list -处理可变数量的参数
#include <cstdarg> 
void xxx(int count, ...)
va_list ap;
va_start(ap, count);
va_end(ap);
  • va_start(ap, count):这个宏用于初始化 va_list 类型的变量 apcount 是函数中...前一个变量
  • va_end(ap):用于清理 va_list 相关的资源
vasprintf 函数-生成一个格式化的字符串

根据格式化字符串和可变参数列表生成一个格式化的字符串,并将结果存储在动态分配的内存中

int vasprintf(char **strp, const char *fmt, va_list ap);
//使用
void print_formatted_string(const char *fmt, ...){va_list ap;va_start(ap, fmt);char *result = nullptr;int ret = vasprintf(&result, fmt, ap);if (ret == -1) {perror("vasprintf");return;}//其他代码free(result);va_end(ap);}

SynchLogger 派生类-同步日志器-logger.hpp

日志消息应如何落地

    class SynchLogger : public Logger {public:SynchLogger(const std::string &logger_name, MySpace::LogLevel::value limit_level, std::shared_ptr<MySpace::Formatter> formatter, std::vector<std::shared_ptr<MySpace::LogSink>> sinks): Logger(logger_name, limit_level, formatter, sinks){}protected:/* 同步日志器,是将日志直接通过落地模块 句柄进行日志落地 */void log(const std::string& data, size_t len) override{std::unique_lock<std::mutex> lock(_mutex);if (_sinks.empty()) return;for (auto &sink : _sinks) {sink->log(data, len);}}};
  • 构造:全部传给基类
  • log :互斥锁可以保证同一时间只有一个线程能够执行日志落地操作,把日志消息通过 _sinks 里的各个日志接收器输出到指定位置
unique_lock 类-锁
#include <mutex>
std::unique_lock<std::mutex> lock;std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx);
  • 锁在离开作用域时自动释放

Logger类和SynchLogger完整代码和测试

//logger.hpp
#pragma once
#include <iostream>
#include <unordered_map>
#include <string>
#include <vector>
#include <mutex>
#include <memory>
#include <atomic>
#include <cstdarg>
#include <condition_variable> 
#include "buffer.hpp"
#include "format.hpp"
#include "level.hpp"
#include "looper.hpp"
#include "message.hpp"
#include "sink.hpp"
#include "util.hpp"namespace MySpace{class Logger {public:Logger(const std::string &logger_name, MySpace::LogLevel::value limit_level, std::shared_ptr<MySpace::Formatter > formatter, std::vector<std::shared_ptr<MySpace::LogSink >> sinks):_logger_name(logger_name), _limit_level(limit_level), _formatter(formatter), _sinks(sinks.begin(), sinks.end()){}//获取日志器名称const std::string &name(){ return _logger_name; }/* 构造日志消息对象过程, 并得到格式化后的日志消息字符串-- 然后进行落地输出*/void debug(const std::string& file, size_t line, const std::string &fmt, ...){va_list ap;va_start(ap, fmt);logMessage(LogLevel::value::DEBUG, file, line, fmt, ap);va_end(ap);}void info (const std::string& file, size_t line, const std::string &fmt, ...){va_list ap;va_start(ap, fmt);logMessage(LogLevel::value::INFO, file, line, fmt, ap);va_end(ap);}void warn (const std::string& file, size_t line, const std::string &fmt, ...){va_list ap;va_start(ap, fmt);logMessage(LogLevel::value::WARN, file, line, fmt, ap);va_end(ap);}void error(const std::string& file, size_t line, const std::string &fmt, ...){va_list ap;va_start(ap, fmt);logMessage(LogLevel::value::ERROR, file, line, fmt, ap);va_end(ap);}void fatal(const std::string& file, size_t line, const std::string &fmt, ...){// 2、 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串va_list ap;va_start(ap, fmt);logMessage(LogLevel::value::FATAL, file, line, fmt, ap);va_end(ap);}protected:void logMessage(MySpace::LogLevel::value level, const std::string& file, size_t line, const std::string &fmt, va_list ap) {/* 通过传入的参数构造出一个日志消息对象,进行日志格式化,最终落地*/// 1、 判断当前日志等级是否达到输出标准if (level < _limit_level)  return;// 2、 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串char *res = nullptr;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1) { std::cout << "vasprintf failed! " << std::endl; return;}// 3、 构造LogMsg对象LogMsg msg(level, line, file, _logger_name, res);// 4、 通过格式化工具对LogMsg进行格式化,获得格式化后的日志字符串std::string real_message = _formatter->format(msg);// 5、 进行日志落地log(real_message.c_str(), real_message.size());free(res);// vasprintf() 内部开辟空间了,是动态申请的,需要我们手动释放}/* 抽象接口完成实际的落地输出 -- 不同的日志器会有不同的实际落地方式 */virtual void log(const std::string& data, size_t len) = 0;protected:std::mutex _mutex;std::string _logger_name;std::atomic<MySpace::LogLevel::value> _limit_level;    std::shared_ptr<MySpace::Formatter> _formatter;std::vector<std::shared_ptr<MySpace::LogSink>> _sinks;};enum LoggerType {LOGGER_SYNCH,   //同步日志器LOGGER_ASYNCH   //异步日志器};class SynchLogger : public Logger {public:SynchLogger(const std::string &logger_name, MySpace::LogLevel::value limit_level, std::shared_ptr<MySpace::Formatter> formatter, std::vector<std::shared_ptr<MySpace::LogSink>> sinks): Logger(logger_name, limit_level, formatter, sinks){}protected:/* 同步日志器,是将日志直接通过落地模块 句柄进行日志落地 */void log(const std::string& data, size_t len) override{std::unique_lock<std::mutex> lock(_mutex);if (_sinks.empty()) return;for (auto &sink : _sinks) {sink->log(data, len);}}};  
}

测试

#include "logger.hpp"
#include "format.hpp"
#include "sink.hpp"
#include <iostream>
#include <memory>
#include <vector>// 主测试函数
int main() {// 创建 Formatterauto formatter = std::make_shared<MySpace::Formatter >("[%d{%H:%M:%S}][%c][%f:%l][%p]%T%m%n");//三种输出方式std::shared_ptr<MySpace::LogSink> stdout_ptr = MySpace::SinkFactory::create<MySpace::StdoutSink>();std::shared_ptr<MySpace::LogSink> file_ptr = MySpace::SinkFactory::create<MySpace::FileSink>("./logfile/test.log");std::shared_ptr<MySpace::LogSink> roll_ptr = MySpace::SinkFactory::create<MySpace::RollBySizeSink>("./logfile/roll-", 1024*1024);std::vector<std::shared_ptr<MySpace::LogSink>> sinks = {stdout_ptr,file_ptr,roll_ptr};// 创建 logger,日志等级是WARN,所以等级小于WARN的不会输出MySpace::SynchLogger logger("TestLogger", MySpace::LogLevel::value::WARN, formatter, sinks);// 测试不同级别的日志记录logger.debug(__FILE__, __LINE__, "This is a debug message");logger.info(__FILE__, __LINE__, "This is an info message");logger.warn(__FILE__, __LINE__, "This is a warn message");logger.error(__FILE__, __LINE__, "This is an error message");logger.fatal(__FILE__, __LINE__, "This is a fatal message");return 0;
}
//终端输出,因为设置等级为WARN,所以等级小于WARN的不会输出
[00:31:42][TestLogger][test.cpp:27][WARN]       This is a warn message
[00:31:42][TestLogger][test.cpp:28][ERROR]      This is an error message
[00:31:42][TestLogger][test.cpp:29][FATAL]      This is a fatal message
//当前路径下创建logfile目录,在该目录下创建指定文件和滚动文件
//然后在文件当中输出和终端一样的内容

异步日志器

为了避免写日志过程阻塞,使用异步日志器,首先将消息放到缓冲区,之后会有异步线程处理日志落地。所以首先设计缓冲区,之后设计异步类,最后用建造者负责具体的建造步骤。

Buffer类-缓冲区设计-在buffer.hpp

使用双缓冲区,避免空间的频繁申请和释放,分为任务写入缓冲区和任务处理缓冲区,业务线程将日志写入缓冲区,异步工作线程处理任务缓冲区,当写入缓冲区满了后和任务处理缓冲区交换。

//buffer.hpp
#pragma once
#include "level.hpp"
#include <vector>
#include <iostream>
#include <assert.h>#define DEFAULT_BUFFER_SIZE (1 * 1024 * 1024)//1M大小
#define THRESHOLD_BUFFER_SIZE (8 * 1024 * 1024)//8M大小
#define INCREMENT_BUFFER_SIZE (1 * 1024 * 1024)//1M大小namespace MySpace{class Buffer{public:Buffer() : _buffer(DEFAULT_BUFFER_SIZE), _write_idx(0), _read_idx(0) {}// 向缓冲区写入数据void push(const char* data, size_t len){// 缓冲区剩余空间不够的情况: // if (len > writeAbleSize()) return; //情况一满了返回ensureEnoughSize(len); //情况二扩容// 1、将数据拷贝进缓冲区std::copy(data, data + len, &_buffer[_write_idx]);// 2、将当前写入数据向后偏移moveWriter(len);}// 返回可读数据的起始地址const char* begin() { return &_buffer[_read_idx]; }// 返回可读数据的长度size_t readAbleSize() { return _write_idx-_read_idx; }// 返回可写空间的长度size_t writeAbleSize() { return _buffer.size()-_write_idx; }// 对读写指针进行向后偏移操作void moveWriter(size_t len) { assert(len+_write_idx <= writeAbleSize()); _write_idx += len; }// 对读写指针进行向后偏移操作void moveReader(size_t len) { assert(len <= readAbleSize()); _read_idx += len; }// 重制读写位置,初始化缓冲区void bufferReset() { _read_idx = 0; _write_idx = 0; }// 对buffer实现交换的操作void bufferSwap(Buffer &buffer){_buffer.swap(buffer._buffer);std::swap(_read_idx, buffer._read_idx);std::swap(_write_idx, buffer._write_idx);}// 判断缓冲区是否为空bool bufferEmpty() { return _read_idx == _write_idx; }// 对空间进行扩容操作void ensureEnoughSize(size_t len){if (len <= writeAbleSize()) return;size_t new_size = 0;while (writeAbleSize() < len) {if (_buffer.size() < THRESHOLD_BUFFER_SIZE) {new_size = _buffer.size() * 2; // 小于阈值翻倍增长} else {new_size = _buffer.size() + INCREMENT_BUFFER_SIZE; // 大于阈值线性增长}_buffer.resize(new_size);}}private:std::vector<char> _buffer;  // 存放字符串数据缓冲区size_t _read_idx;           // 当前可读数据的指针size_t _write_idx;          // 当前可写数据的指针};
}
copy 函数-把一个范围内的元素复制到另一个范围
#include <algorithm> 
template< class InputIt, class OutputIt >
OutputIt copy( InputIt first, InputIt last, OutputIt d_first );
//使用
std::vector<int, 5> source = {1, 2, 3, 4, 5};
std::vector<int, 5> destination;
// 复制 source 数组的元素到 destination 数组
std::copy(source.begin(), source.end(), destination.begin());
  • firstlast:是源元素的范围
  • d_first:指定了复制元素的目标范围起始位置
AsynchLogger派生类-异步日志器-logger.hpp
//logger.hpp
#pragma once
#include <iostream>
#include <unordered_map>
#include <string>
#include <vector>
#include <mutex>
#include <memory>
#include <atomic>
#include <cstdarg>
#include <condition_variable> 
#include "buffer.hpp"
#include "format.hpp"
#include "level.hpp"
#include "looper.hpp"
#include "message.hpp"
#include "sink.hpp"
#include "util.hpp"namespace MySpace{class Logger {public:Logger(const std::string &logger_name, MySpace::LogLevel::value limit_level, std::shared_ptr<MySpace::Formatter > formatter, std::vector<std::shared_ptr<MySpace::LogSink >> sinks):_logger_name(logger_name), _limit_level(limit_level), _formatter(formatter), _sinks(sinks.begin(), sinks.end()){}//获取日志器名称const std::string &name(){ return _logger_name; }/* 构造日志消息对象过程, 并得到格式化后的日志消息字符串-- 然后进行落地输出*/void debug(const std::string& file, size_t line, const std::string &fmt, ...){va_list ap;va_start(ap, fmt);logMessage(LogLevel::value::DEBUG, file, line, fmt, ap);va_end(ap);}void info (const std::string& file, size_t line, const std::string &fmt, ...){va_list ap;va_start(ap, fmt);logMessage(LogLevel::value::INFO, file, line, fmt, ap);va_end(ap);}void warn (const std::string& file, size_t line, const std::string &fmt, ...){va_list ap;va_start(ap, fmt);logMessage(LogLevel::value::WARN, file, line, fmt, ap);va_end(ap);}void error(const std::string& file, size_t line, const std::string &fmt, ...){va_list ap;va_start(ap, fmt);logMessage(LogLevel::value::ERROR, file, line, fmt, ap);va_end(ap);}void fatal(const std::string& file, size_t line, const std::string &fmt, ...){// 2、 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串va_list ap;va_start(ap, fmt);logMessage(LogLevel::value::FATAL, file, line, fmt, ap);va_end(ap);}protected:void logMessage(MySpace::LogLevel::value level, const std::string& file, size_t line, const std::string &fmt, va_list ap) {/* 通过传入的参数构造出一个日志消息对象,进行日志格式化,最终落地*/// 1、 判断当前日志等级是否达到输出标准if (level < _limit_level)  return;// 2、 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串char *res = nullptr;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1) { std::cout << "vasprintf failed! " << std::endl; return;}// 3、 构造LogMsg对象LogMsg msg(level, line, file, _logger_name, res);// 4、 通过格式化工具对LogMsg进行格式化,获得格式化后的日志字符串std::string real_message = _formatter->format(msg);// 5、 进行日志落地log(real_message.c_str(), real_message.size());free(res);// vasprintf() 内部开辟空间了,是动态申请的,需要我们手动释放}/* 抽象接口完成实际的落地输出 -- 不同的日志器会有不同的实际落地方式 */virtual void log(const std::string& data, size_t len) = 0;protected:std::mutex _mutex;std::string _logger_name;std::atomic<MySpace::LogLevel::value> _limit_level;    std::shared_ptr<MySpace::Formatter> _formatter;std::vector<std::shared_ptr<MySpace::LogSink>> _sinks;};enum LoggerType {LOGGER_SYNCH,   //同步日志器LOGGER_ASYNCH   //异步日志器};class AsynchLogger : public Logger {public:AsynchLogger(const std::string &logger_name, LogLevel::value level, std::shared_ptr<Formatter> &formatter, std::vector<std::shared_ptr<LogSink>> &sinks): Logger(logger_name, level, formatter, sinks), _looper(std::make_shared<AsynchLooper>(std::bind(&AsynchLogger::realLog, this, std::placeholders::_1))){}/* 将数据写入缓冲区*/virtual void log(const std::string& data, size_t len) override{_looper->push(data.c_str(), len);}/* 设计一个实际落地函数(将缓冲区中的数据落地) */void realLog(Buffer &buf) {if (_sinks.empty()) return;for (auto &sink : _sinks) {sink->log(buf.begin(), buf.readAbleSize());}}private: std::shared_ptr<AsynchLooper> _looper;};}
bind函数

注意区分套接字中的bind,C++中bind把一个可调用对象(像函数、成员函数、函数对象等)和它的部分参数绑定在一起,生成一个新的可调用对象

#include <functional>
template< class F, class... Args >
bind( F&& f, Args&&... args );
//使用
// 绑定 add 函数,固定第一个参数为 3,且还需要一个参数
int add(int a, int b){}
auto addThree = std::bind(add, 3, std::placeholders::_1);

需要注意的是,普通函数可以直接调用,成员函数由于和类紧密相关,所以调用时,传函数变成传函数指针,同时还要提供一个对象的指针

placeholders-命名空间

一个命名空间,指定新可调用对象传入的参数在原可调用对象中的位置

#include <functional>
void test(int a, int b, int c){}
auto boundFunc = std::bind(test, std::placeholders::_2, std::placeholders::_3, std::placeholders::_1);
boundFunc(3, 1, 2);
  • 创建了一个新的可调用对象 boundFunc
  • boundFunc(3, 1, 2)test 函数的第一个参数绑定的是 std::placeholders::_2,所以调用 boundFunc 时传入的第二个参数 1 会作为 test 函数的第一个参数

LoggerBuilder类-建造者-logger.hpp

建造者分为局部建造者(LocalLoggerBuilder)和全局建造者(GlobalLoggerBuilder),目的是为了传入相关参数就能会返回对应的日志器,和局部相比,全局日志器会自动添加构建出来的日志器到日志管理器单例对象当中

全局建造者单例模式-懒汉模式-logger.hpp

对于全局建造者我们新增懒汉模式,当全局需要实例化一个全局建造者类时,我们新增一个实例

完整代码和测试
//logger.hpp
#pragma once
#include <iostream>
#include <unordered_map>
#include <string>
#include <vector>
#include <mutex>
#include <memory>
#include <atomic>
#include <cstdarg>
#include <condition_variable> 
#include "buffer.hpp"
#include "format.hpp"
#include "level.hpp"
#include "looper.hpp"
#include "message.hpp"
#include "sink.hpp"
#include "util.hpp"namespace MySpace{class Logger {public:Logger(const std::string &logger_name, MySpace::LogLevel::value limit_level, std::shared_ptr<MySpace::Formatter > formatter, std::vector<std::shared_ptr<MySpace::LogSink >> sinks):_logger_name(logger_name), _limit_level(limit_level), _formatter(formatter), _sinks(sinks.begin(), sinks.end()){}//获取日志器名称const std::string &name(){ return _logger_name; }/* 构造日志消息对象过程, 并得到格式化后的日志消息字符串-- 然后进行落地输出*/void debug(const std::string& file, size_t line, const std::string &fmt, ...){va_list ap;va_start(ap, fmt);logMessage(LogLevel::value::DEBUG, file, line, fmt, ap);va_end(ap);}void info (const std::string& file, size_t line, const std::string &fmt, ...){va_list ap;va_start(ap, fmt);logMessage(LogLevel::value::INFO, file, line, fmt, ap);va_end(ap);}void warn (const std::string& file, size_t line, const std::string &fmt, ...){va_list ap;va_start(ap, fmt);logMessage(LogLevel::value::WARN, file, line, fmt, ap);va_end(ap);}void error(const std::string& file, size_t line, const std::string &fmt, ...){va_list ap;va_start(ap, fmt);logMessage(LogLevel::value::ERROR, file, line, fmt, ap);va_end(ap);}void fatal(const std::string& file, size_t line, const std::string &fmt, ...){// 2、 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串va_list ap;va_start(ap, fmt);logMessage(LogLevel::value::FATAL, file, line, fmt, ap);va_end(ap);}protected:void logMessage(MySpace::LogLevel::value level, const std::string& file, size_t line, const std::string &fmt, va_list ap) {/* 通过传入的参数构造出一个日志消息对象,进行日志格式化,最终落地*/// 1、 判断当前日志等级是否达到输出标准if (level < _limit_level)  return;// 2、 对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串char *res = nullptr;int ret = vasprintf(&res, fmt.c_str(), ap);if (ret == -1) { std::cout << "vasprintf failed! " << std::endl; return;}// 3、 构造LogMsg对象LogMsg msg(level, line, file, _logger_name, res);// 4、 通过格式化工具对LogMsg进行格式化,获得格式化后的日志字符串std::string real_message = _formatter->format(msg);// 5、 进行日志落地log(real_message.c_str(), real_message.size());free(res);// vasprintf() 内部开辟空间了,是动态申请的,需要我们手动释放}/* 抽象接口完成实际的落地输出 -- 不同的日志器会有不同的实际落地方式 */virtual void log(const std::string& data, size_t len) = 0;protected:std::mutex _mutex;std::string _logger_name;std::atomic<MySpace::LogLevel::value> _limit_level;    std::shared_ptr<MySpace::Formatter> _formatter;std::vector<std::shared_ptr<MySpace::LogSink>> _sinks;};enum LoggerType {LOGGER_SYNCH,   //同步日志器LOGGER_ASYNCH   //异步日志器};class SynchLogger : public Logger {public:SynchLogger(const std::string &logger_name, MySpace::LogLevel::value limit_level, std::shared_ptr<MySpace::Formatter> formatter, std::vector<std::shared_ptr<MySpace::LogSink>> sinks): Logger(logger_name, limit_level, formatter, sinks){}protected:/* 同步日志器,是将日志直接通过落地模块 句柄进行日志落地 */void log(const std::string& data, size_t len) override{std::unique_lock<std::mutex> lock(_mutex);if (_sinks.empty()) return;for (auto &sink : _sinks) {sink->log(data, len);}}};class AsynchLogger : public Logger {public:AsynchLogger(const std::string &logger_name, LogLevel::value level, std::shared_ptr<Formatter> &formatter, std::vector<std::shared_ptr<LogSink>> &sinks): Logger(logger_name, level, formatter, sinks), _looper(std::make_shared<AsynchLooper>(std::bind(&AsynchLogger::realLog, this, std::placeholders::_1))){}/* 将数据写入缓冲区*/virtual void log(const std::string& data, size_t len) override{_looper->push(data.c_str(), len);}/* 设计一个实际落地函数(将缓冲区中的数据落地) */void realLog(Buffer &buf) {if (_sinks.empty()) return;for (auto &sink : _sinks) {sink->log(buf.begin(), buf.readAbleSize());}}private: std::shared_ptr<AsynchLooper> _looper;};class LoggerBuilder {public:LoggerBuilder(): _logger_type(LoggerType::LOGGER_SYNCH), _limit_level(LogLevel::value::DEBUG){}void buildLoggerType(LoggerType type)                 { _logger_type = type; }void buildLoggerName(const std::string &name)         { _logger_name = name; }void buildLoggerLevel(LogLevel::value level)          { _limit_level = level;  }void buildLoggerFormatter(const std::string &pattern) {  _formatter.reset(new Formatter(pattern)); }template<typename SinkType, typename ...Args>void buildSink(Args&&... args) { _sinks.push_back(SinkFactory::create<SinkType>(std::forward<Args>(args)...)); }virtual std::shared_ptr<Logger> build() = 0; //  建造日志器protected:LoggerType       _logger_type;std::string      _logger_name;LogLevel::value  _limit_level;    // 需要频繁访问std::shared_ptr<Formatter>   _formatter;std::vector<std::shared_ptr<LogSink>>  _sinks;};class LocalLoggerBuilder : public LoggerBuilder {public:virtual std::shared_ptr<Logger> build() override {assert(!_logger_name.empty());      // 必须有日志器名称if (_formatter.get() == nullptr) { _formatter = std::make_shared<Formatter>(); }if (_sinks.empty()) { buildSink<StdoutSink>(); }if (_logger_type == LoggerType::LOGGER_ASYNCH) {//异步日志器return std::make_shared<AsynchLogger>(_logger_name, _limit_level, _formatter, _sinks);}//同步日志器return std::make_shared<SynchLogger>(_logger_name, _limit_level, _formatter, _sinks);}};//日志器建造者-懒汉模式class LoggerManager {public:static LoggerManager& getInstance(){//声明,静态局部变量没有构造完成之前,其他线程就会阻塞static LoggerManager eton;return eton;}//添加void addLogger(std::shared_ptr<Logger> &logger){//防止重复添加if(findLogger(logger->name()))return;std::unique_lock<std::mutex>(_mutex);_loggers.insert(std::make_pair(logger->name(), logger));}//查找bool findLogger(const std::string &name){std::unique_lock<std::mutex>(_mutex);//没找到if(_loggers.find(name) == _loggers.end()){return false;}return true;}//获取std::shared_ptr<Logger> getLogger(const std::string &name){std::unique_lock<std::mutex>(_mutex);//没找到if(_loggers.find(name) == _loggers.end()){return std::shared_ptr<Logger>();}return _loggers.find(name)->second;}std::shared_ptr<Logger> rootLogger() { return _root_logger; }private://构造函数私有LoggerManager() {std::shared_ptr<LoggerBuilder> LoggerBuilder(new MySpace::LocalLoggerBuilder());LoggerBuilder->buildLoggerName("root");_root_logger = LoggerBuilder->build();_loggers.insert(std::make_pair("root", _root_logger));}                              LoggerManager(const LoggerManager&) = delete;   //删除拷贝构造std::mutex _mutex;std::shared_ptr<Logger> _root_logger;          // 默认日志器std::unordered_map<std::string, std::shared_ptr<Logger>> _loggers;};/* 全局日志器建造者 -- 在局部的基础上新增:自动添加日志器到单例对象中 */class GlobalLoggerBuilder : public LoggerBuilder {public:virtual std::shared_ptr<Logger> build() override {assert(!_logger_name.empty());      // 必须有日志器名称if (_formatter.get() == nullptr) { _formatter = std::make_shared<Formatter>(); }if (_sinks.empty()) { buildSink<StdoutSink>(); }std::shared_ptr<Logger> logger;if (_logger_type == LoggerType::LOGGER_ASYNCH) {logger = std::make_shared<AsynchLogger>(_logger_name, _limit_level, _formatter, _sinks);} else {logger = std::make_shared<SynchLogger>(_logger_name, _limit_level, _formatter, _sinks);}LoggerManager::getInstance().addLogger(logger);   // 新增return logger;}};}

测试

#include "logger.hpp"
#include "format.hpp"
#include "sink.hpp"
#include <iostream>
#include <memory>
#include <vector>// 主测试函数
int main() {// 创建 LocalLoggerBuilder 对象MySpace::LocalLoggerBuilder localBuilder;// 设置日志器名称localBuilder.buildLoggerName("TestLogger");// 设置日志级别localBuilder.buildLoggerLevel(MySpace::LogLevel::value::DEBUG);// 设置格式化器模式localBuilder.buildLoggerFormatter("%m%n");// 添加一个 StdoutSinklocalBuilder.buildSink<MySpace::StdoutSink>();// 构建日志器std::shared_ptr<MySpace::Logger> logger = localBuilder.build();// 测试日志记录函数logger->info(__FILE__, __LINE__, "This is a test info message");logger->debug(__FILE__, __LINE__, "This is a test debug message");return 0;
}
//输出This is a test info message This is a test debug message

AsynchLooper类-异步工作器-looper.hpp

外界将任务数据放到缓冲区中,异步线程对缓冲区中数据进行处理,使用的是生产消费模型

  • push :是生产者不断将数据写到 _produce_buffer 生产缓冲区
  • threadEntry :是消费者不断检查 _produce_buffer 生产缓冲区是否有数据。如果有数据,将其交换到 _consumer_buffer 消费缓冲区,并调用回调函数 _callBack 进行处理
//looper.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <mutex>
#include <memory>
#include <atomic>
#include <cstdarg>
#include <condition_variable> 
#include "buffer.hpp"
#include "format.hpp"
#include "level.hpp"
#include "message.hpp"
#include "sink.hpp"
#include "util.hpp"namespace MySpace{class AsynchLooper {public:AsynchLooper(const std::function<void(Buffer &)> &cb) :_stop(false), _thread(std::thread(&AsynchLooper::threadEntry, this)), _callBack(cb){}~AsynchLooper(){_stop = true;                    // 退出标志设置为true _consumer_cond.notify_all();     // 唤醒所有工作线程_thread.join();                  // 等待工作线程退出}void push(const char *data, size_t len) {std::unique_lock<std::mutex> lock(_mutex);//缓冲区满了就阻塞_produce_cond.wait(lock, [&](){ return _produce_buffer.writeAbleSize() >= len; });//向缓冲区添加数据_produce_buffer.push(data, len);//  唤醒消费者对缓冲区中的数据进行处理_consumer_cond.notify_one();   }/* 线程的入口函数 -- 对消费缓冲区中的数据进行处理,处理完毕后,初始化缓冲区,交换缓冲区*/void threadEntry() {while (1) {//互斥锁设置生命周期,交换完后解锁,不对数据过程加锁{// 1、 判断生产缓冲区有没有数据,有则交换,无则阻塞std::unique_lock<std::mutex> lock(_mutex);// 如果 _stop 标志为 true 且生产缓冲区为空,说明线程需要退出,此时跳出循环if (_stop && _produce_buffer.bufferEmpty()) break;//退出前被唤醒,或者有数据被唤醒,返回真,继续向下运行_consumer_cond.wait(lock, [&](){ return ( _stop || !_produce_buffer.bufferEmpty()); });//再次检查 _stop 标志,防止在等待期间 _stop 被设置为 true,如果是则跳出循环if (_stop && _produce_buffer.bufferEmpty()) {break;}_produce_buffer.bufferSwap(_consumer_buffer);// 2、 唤醒生产者(只有安全状态生产者才会被阻塞)_produce_cond.notify_all();}// 3、 被唤醒后,对消费缓冲区进行数据处理(处理过程无需加锁保护)_callBack(_consumer_buffer);// 4、 初始化消费缓冲区_consumer_buffer.bufferReset();}}  private:std::atomic<bool> _stop;                  // 工作器停止标志std::mutex _mutex;          Buffer _produce_buffer;                   // 生产缓冲区Buffer _consumer_buffer;                  // 消费缓冲区std::condition_variable _produce_cond;    // 生产条件变量std::condition_variable _consumer_cond;   // 消费条件变量std::thread _thread;        std::function<void(Buffer &)> _callBack;  //回调函数 具体对缓冲区数据进行处理的回调函数, 由异步工作器的使用者传入};
}

成员变量

  • _stop :控制生产消费模型的生产和停止, true 时,表示停止工作
  • _thread:消费者需要异步线程来处理缓冲区数据
  • _produce_buffer_consumer_buffer:缓冲区作为共享对象
  • _mutex:共享对象在访问时需要加锁互斥
  • _produce_cond_consumer_cond:条件变量用于线程通讯,当生产缓冲区满了生产者线程会调用,当生产缓冲区空了消费者线程会调用
  • 回调函数:function 对象,它可以存储任意可调用对象,这里是存放一个参数是 Buffer & ,返回值是void的函数指针,当消费缓冲区中有数据时,会调用这个回调函数来处理这些数据

成员函数

  • 构造:初始化,注意线程对象初始化成员函数的方式

  • push:用lambda 表达式,调用 wait 时会先检查这个谓词,如果为 false 则进入等待状态;被唤醒后会再次检查谓词,只有当谓词为 true(即生产缓冲区有足够空间)时才会继续执行后续代码,只有满足条件时才会继续执行。不能下面这么写,没有处理虚假唤醒

    •   if(_produce_buffer.writeAbleSize() < len){_produce_cond.wait(lock);}
      
  • threadEntry:同样wait直到满足 _stoptrue 或者生产缓冲区不为空的条件

logger.hpp-完整代码和测试
//looper.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <mutex>
#include <memory>
#include <atomic>
#include <cstdarg>
#include <condition_variable> 
#include "buffer.hpp"
#include "format.hpp"
#include "level.hpp"
#include "message.hpp"
#include "sink.hpp"
#include "util.hpp"namespace MySpace{class AsynchLooper {public:AsynchLooper(const std::function<void(Buffer &)> &cb) :_stop(false), _thread(std::thread(&AsynchLooper::threadEntry, this)), _callBack(cb){}~AsynchLooper(){_stop = true;                    // 退出标志设置为true _consumer_cond.notify_all();     // 唤醒所有工作线程_thread.join();                  // 等待工作线程退出}void push(const char *data, size_t len) {std::unique_lock<std::mutex> lock(_mutex);//缓冲区满了就阻塞_produce_cond.wait(lock, [&](){ return _produce_buffer.writeAbleSize() >= len; });//向缓冲区添加数据_produce_buffer.push(data, len);//  唤醒消费者对缓冲区中的数据进行处理_consumer_cond.notify_one();   }/* 线程的入口函数 -- 对消费缓冲区中的数据进行处理,处理完毕后,初始化缓冲区,交换缓冲区*/void threadEntry() {while (!_stop) {//互斥锁设置生命周期,交换完后解锁,不对数据过程加锁{// 1、 判断生产缓冲区有没有数据,有则交换,无则阻塞std::unique_lock<std::mutex> lock(_mutex);// 如果 _stop 标志为 true 且生产缓冲区为空,说明线程需要退出,此时跳出循环if (_stop && _produce_buffer.bufferEmpty()) break;//退出前被唤醒,或者有数据被唤醒,返回真,继续向下运行_consumer_cond.wait(lock, [&](){ return ( _stop || !_produce_buffer.bufferEmpty()); });//再次检查 _stop 标志,防止在等待期间 _stop 被设置为 true,如果是则跳出循环if (_stop) {break;}_produce_buffer.bufferSwap(_consumer_buffer);// 2、 唤醒生产者(只有安全状态生产者才会被阻塞)_produce_cond.notify_all();}// 3、 被唤醒后,对消费缓冲区进行数据处理(处理过程无需加锁保护)_callBack(_consumer_buffer);// 4、 初始化消费缓冲区_consumer_buffer.bufferReset();}}  private:std::atomic<bool> _stop;                  // 工作器停止标志std::mutex _mutex;          Buffer _produce_buffer;                   // 生产缓冲区Buffer _consumer_buffer;                  // 消费缓冲区std::condition_variable _produce_cond;    // 生产条件变量std::condition_variable _consumer_cond;   // 消费条件变量std::thread _thread;        std::function<void(Buffer &)> _callBack;  //回调函数 具体对缓冲区数据进行处理的回调函数, 由异步工作器的使用者传入};
}

测试

#include "looper.hpp"
#include "buffer.hpp"
#include <iostream>
#include <string>
#include <cstring>
#include <thread>// 回调函数,用于处理缓冲区数据
void callback(MySpace::Buffer& buffer) {std::cout << "Processing data: ";const char* data = buffer.begin();size_t size = buffer.readAbleSize();for (size_t i = 0; i < size; ++i) {std::cout << data[i];}std::cout << std::endl;// 模拟处理后移动读指针buffer.moveReader(size);
}int main() {// 创建 AsynchLooper 实例std::shared_ptr<MySpace::AsynchLooper> looper = std::make_shared<MySpace::AsynchLooper>(callback);// 向缓冲区推送数据const char* testData = "Hello, AsynchLooper!";size_t dataLen = strlen(testData);looper->push(testData, dataLen);// 等待一段时间,确保数据被处理std::this_thread::sleep_for(std::chrono::seconds(1));// 再次推送数据const char* testData2 = "Another test message.";size_t dataLen2 = strlen(testData2);looper->push(testData2, dataLen2);// 等待一段时间,确保数据被处理std::this_thread::sleep_for(std::chrono::seconds(1));// 销毁 AsynchLooper 实例looper.reset();return 0;
}    
//输出
//Processing data: Hello, AsynchLooper!
//Processing data: Another test message.
condition_variable 类-条件变量

用于线程间通信,可任意控制线程等待和执行,一般和mutex一起使用

#include <condition_variable> 
//使用
std::condition_variable cv;
std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx);
cv.notify_one(); // 唤醒一个等待condition_variable 对象的线程
cv.notify_all(); // 唤醒所有等待condition_variable 对象的线程
cv.wait(lock); // 线程阻塞
cv.wait(lock, [] { return ready; });//等待直到 ready 为 true
  • void wait( std::unique_lock< std::mutex >& lock, Predicate pred );
thread 类-线程
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );
//使用
std::thread t(printMessage);//创建线程并执行 printMessage 函数
t.join();// 等待线程执行完毕
//传递类成员函数,注意要有函数指针,类对象指针,若成员函数有参数,还需传递这些参数
MyClass obj;
std::thread t(&MyClass::memberFunction, &obj);
t.join();

日志宏全局接口设计

为了简化用户管理,给接口在包装下,方便用户使用

//mylog.hpp
#pragma once
#include "logger.hpp"namespace MySpace{// 1、提供获取指定日志器的全局接口(避免用户自己操作单例对象)std::shared_ptr<Logger> getLogger(const std::string& name) {return LoggerManager::getInstance().getLogger(name);}std::shared_ptr<Logger> rootLogger() {return LoggerManager::getInstance().rootLogger();}// 2、使用宏函数对日志器接口进行代理(代理模式)#define debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)#define info(fmt, ...)  info (__FILE__, __LINE__, fmt, ##__VA_ARGS__)#define warn(fmt, ...)  warn (__FILE__, __LINE__, fmt, ##__VA_ARGS__)#define error(fmt, ...) error(__FILE__, __LINE__, fmt, ##__VA_ARGS__)#define fatal(fmt, ...) fatal(__FILE__, __LINE__, fmt, ##__VA_ARGS__)// 3、提供宏函数,直接通过默认日志器进行日志的标准输出打印(无需获取日志器)#define DEBUG(fmt, ...) rootLogger()->debug(fmt, ##__VA_ARGS__)#define INFO(fmt, ...)  rootLogger()->info (fmt, ##__VA_ARGS__)#define WARN(fmt, ...)  rootLogger()->warn (fmt, ##__VA_ARGS__)#define ERROR(fmt, ...) rootLogger()->error(fmt, ##__VA_ARGS__)#define FATAL(fmt, ...) rootLogger()->fatal(fmt, ##__VA_ARGS__)
}

测试

#include <iostream>
#include "mylog.hpp"int main() {// 测试通过默认日志器的宏函数MySpace::DEBUG("This is a DEBUG test");MySpace::INFO("This is an INFO test");MySpace::WARN("This is a WARN test");MySpace::ERROR("This is an ERROR test");MySpace::FATAL("This is a FATAL test");return 0;
}

项目总结

  • util.hpp:包含判断时间,寻找文件目录,判断文件是否存在等函数
  • level.hpp:用枚举定义了不同的日志等级
  • message.hpp:一条日志消息因该包括那些内容
  • format.hpp:定义日志消息输出格式
  • sink.hpp:决定日志消息的落地位置,控制台,文件,滚动文件
  • logger.hpp:管理整个日志消息,最为重要
  • buffer.hpp:异步日志器所需要的缓冲区
  • looper.hpp:异步日志器对缓冲区内容的处理方法
  • mylog.hpp:对上面内容封装,实际使用包含这个头文件就行了

具体流程用户传入消息构建message日志消息对象,format将消息格式化,sink决定消息落地位置。加入日志器模块logger将前面三个整合,一个日志器有多个落地方向,支持多个日志格式,可以控制输出等级,如果是串行日志器效率低,所以设计异步日志器提高效率,包括缓冲池存放日志消息,异步线程处理日志消息,局部和全局日志器更加灵活,方便用户使用用宏来简化接口。

结构

logsutil.hpplevel.hppmessage.hppformat.hppsink.hpplogger.hppbuffer.hpplooper.hppmylog.hpp
benchbench.cppMakefile

运行:在bench 目录下运行Makefile ,控制台输出内容,在bench目录下生成logfile 目录

bench:bench.cppg++ -std=c++11 -o $@ $^ -pthread

性能测试

主要的测试方法:每秒能打印日志数 / 总的打印日志消耗时间

主要的测试要素 : 同步/异步 单线程/多线程

测试环境 :

CPU : 2核
内存 : 4G 
速度: 4MB
OS : Linux =centos 7.2

测试代码

bench-测试主要代码

包括日志器名字,线程数量,日志数量,单条日志的大小

//bench.cpp
#include "../logs/mylog.hpp"
#include <chrono>void bench(const std::string &logger_name, size_t thread_count, size_t msg_count, size_t msg_len) {/* 1.获取日志器           */std::shared_ptr<MySpace::Logger> logger = MySpace::getLogger(logger_name);if (!logger.get()) {return ;}//需要注销测试下std::cout << "测试日志:" << msg_count << " 条, 总大小:" << msg_count * msg_len / 1024 << "KB" << std::endl;/* 2.组织指定长度的日志消息 */std::string msg(msg_len - 1, 'A'); // 最后一个字节是换行符,便于换行打印 /* 3.创建指定数量的线程    */std::vector<std::thread> threads;std::vector<double> cost_array(thread_count);//总的数值统计size_t msg_prt_thr = msg_count / thread_count;   // 每个线程输出的日志条数for (int i = 0; i < thread_count; i++) {//引用捕获参数,传值ithreads.emplace_back([&, i](){/* 4.记录开始时间  */auto start = std::chrono::high_resolution_clock::now();/* 5.开始循环写日志       */for (int j = 0; j < msg_prt_thr; j++) {logger->fatal("%s", msg.c_str());}/* 6.线程函数内部结束计时  */auto end = std::chrono::high_resolution_clock::now();std::chrono::duration<double> cost = end - start;cost_array[i] = cost.count();std::cout << "线程[" << i << "]: " << "  输出日志数量:" << msg_prt_thr << ", 耗时:" << cost.count()  << "s" << std::endl;});}for (int i = 0; i < thread_count; i++) {threads[i].join();}/* 7.计算总耗时  多线程中,每个线程都有自己运行的时间,但是线程是并发处理的,因此耗时最多的那个才是总时间 */double max_cost = cost_array[0];for (int i = 0; i < thread_count; i++) {max_cost = max_cost > cost_array[i] ? max_cost : cost_array[i];}size_t msg_prt_sec = msg_count / max_cost;size_t size_prt_sec = (msg_count * msg_len) / (max_cost * 1024);/* 8.进行输出打印 */std::cout << "总耗时: " << max_cost << "s" << std::endl;std::cout << "每秒输出日志数量: " << msg_prt_sec  << " 条"  << std::endl;std::cout << "每秒输出日志大小: " << size_prt_sec << " KB" << std::endl; 
}

emplace_back-末尾添加元素

  • emplace_back :末尾直接构造一个新的元素,而不是先创建一个临时对象,lambda是一个对象
  • push_back:将一个已存在的对象添加到容器的末尾
std::vector<string> v1;
v1.emplace_back("Hello, World!");
std::vector<string> v2;
string s("Hello, World!");
v2.push_back(s);

chrono-计时

  • auto start = std::chrono::high_resolution_clock::now();:记录开始时间
  • auto end = std::chrono::high_resolution_clock::now();:记录结束时间
  • std::chrono::duration<double> cost = end - start; 计算时间间隔
  • std::cout << "代码块执行耗时: " << cost.count() << " 秒" << std::endl;:输出耗时

测试结果

同步单线程测试

//同步日志测试
void sync_bench() {std::shared_ptr<MySpace::LoggerBuilder> builder(new MySpace::GlobalLoggerBuilder());builder->buildLoggerName("sync_logger");builder->buildLoggerFormatter("%m%n");builder->buildLoggerType(MySpace::LoggerType::LOGGER_SYNCH);//同步日志器builder->buildSink<MySpace::FileSink>("./logfile/sync.log");builder->buildSink<MySpace::RollBySizeSink>("./logfile/roll-sync-by-size", 1024 * 1024);builder->build();bench("sync_logger", 1, 1000000, 100);//单线程//bench("sync_logger", 10, 1000000, 100);//10个线程
}
int main() {sync_bench();// async_bench();return 0;
}

结果

[aaa@VM-12-6-centos bench]$ ./bench
测试日志:1000000 条, 总大小:97656KB
线程[0]:   输出日志数量:1000000, 耗时:2.71629s
总耗时: 2.71629s
每秒输出日志数量: 368149 条
每秒输出日志大小: 35952 KB
//生成logfile目录,里面有对应日志消息

同步多线程测试

//同步日志测试
void sync_bench() {std::shared_ptr<MySpace::LoggerBuilder> builder(new MySpace::GlobalLoggerBuilder());builder->buildLoggerName("sync_logger");builder->buildLoggerFormatter("%m%n");builder->buildLoggerType(MySpace::LoggerType::LOGGER_SYNCH);//同步日志器builder->buildSink<MySpace::FileSink>("./logfile/sync.log");builder->buildSink<MySpace::RollBySizeSink>("./logfile/roll-sync-by-size", 1024 * 1024);builder->build();//bench("sync_logger", 1, 1000000, 100);//单线程bench("sync_logger", 10, 1000000, 100);//10个线程
}
int main() {sync_bench();// async_bench();return 0;
}

结果

[aaa@VM-12-6-centos bench]$ ./bench
测试日志:1000000 条, 总大小:97656KB
线程[2]:   输出日志数量:100000, 耗时:2.34469s
线程[7]:   输出日志数量:100000, 耗时:2.54091s
线程[4]:   输出日志数量:100000, 耗时:2.56502s
线程[6]:   输出日志数量:100000, 耗时:2.56806s
线程[0]:   输出日志数量:100000, 耗时:2.57705s
线程[3]:   输出日志数量:100000, 耗时:2.60274s
线程[9]:   输出日志数量:100000, 耗时:2.5958s
线程[1]:   输出日志数量:100000, 耗时:2.63869s
线程[8]:   输出日志数量:100000, 耗时:2.70844s
线程[5]:   输出日志数量:100000, 耗时:2.72737s
总耗时: 2.72737s
每秒输出日志数量: 366653 条
每秒输出日志大小: 35805 KB
//生成logfile目录,里面有对应日志消息

异步单线程测试

//异步日志测试
void async_bench() {std::shared_ptr<MySpace::LoggerBuilder> builder(new MySpace::GlobalLoggerBuilder());builder->buildLoggerName("async_logger");builder->buildLoggerFormatter("%m%n");builder->buildLoggerType(MySpace::LoggerType::LOGGER_ASYNCH);//异步日志器builder->buildSink<MySpace::FileSink>("./logfile/async.log");builder->buildSink<MySpace::RollBySizeSink>("./logfile/roll-async-by-size", 1024 * 1024);builder->build();bench("async_logger", 1, 100000, 10);// bench("async_logger", 10, 1000000, 100);//10个线程
}int main() {// sync_bench();async_bench();return 0;
}

结果

[aaa@VM-12-6-centos bench]$ ./bench
测试日志:100000 条, 总大小:976KB
线程[0]:   输出日志数量:100000, 耗时:0.235684s
总耗时: 0.235684s
每秒输出日志数量: 424297 条
每秒输出日志大小: 4143 KB
//生成logfile目录,里面有对应日志消息

异步多线程测试

//异步日志测试
void async_bench() {std::shared_ptr<MySpace::LoggerBuilder> builder(new MySpace::GlobalLoggerBuilder());builder->buildLoggerName("async_logger");builder->buildLoggerFormatter("%m%n");builder->buildLoggerType(MySpace::LoggerType::LOGGER_ASYNCH);//异步日志器builder->buildSink<MySpace::FileSink>("./logfile/async.log");builder->buildSink<MySpace::RollBySizeSink>("./logfile/roll-async-by-size", 1024 * 1024);builder->build();// bench("async_logger", 1, 100000, 10);bench("async_logger", 10, 1000000, 100);//10个线程
}int main() {// sync_bench();async_bench();return 0;
}

结果

[aaa@VM-12-6-centos bench]$ ./bench
测试日志:100000 条, 总大小:976KB
线程[1]:   输出日志数量:10000, 耗时:0.0404783s
线程[0]:   输出日志数量:10000, 耗时:0.0438714s
线程[2]:   输出日志数量:10000, 耗时:0.0823503s
线程[3]:   输出日志数量:10000, 耗时:0.078529s
线程[4]:   输出日志数量:10000, 耗时:0.100931s
线程[7]:   输出日志数量:10000, 耗时:0.0608246s
线程[5]:   输出日志数量:10000, 耗时:0.0774649s
线程[6]:   输出日志数量:10000, 耗时:0.0773523s
线程[9]:   输出日志数量:10000, 耗时:0.0429028s
线程[8]:   输出日志数量:10000, 耗时:0.0775997s
总耗时: 0.100931s
每秒输出日志数量: 990779 条
每秒输出日志大小: 9675 KB
//生成logfile目录,里面有对应日志消息

相关文章:

【项目】C++同步异步日志系统-包含运行教程

文章目录 项目介绍地址&#xff1a;https://gitee.com/royal-never-give-up/c-log-system 开发环境核心技术为什么需要日志系统同步日志异步日志 知识补充不定参宏函数__FILE____LINE____VA_ARGS__ C使用C使用左值右值sizeof...() 运算符完美转发完整例子sizeof...() 运算符获取…...

Yolo_v8的安装测试

前言 如何安装Python版本的Yolo&#xff0c;有一段时间不用了&#xff0c;Yolo的版本也在不断地发展&#xff0c;所以重新安装了运行了一下&#xff0c;记录了下来&#xff0c;供参考。 一、搭建环境 1.1、创建Pycharm工程 首先创建好一个空白的工程&#xff0c;如下图&…...

Success is the sum of small efforts repeated day in and day out.

&#xff08;翻译&#xff1a;"成功是日复一日微小努力的总和。"&#xff09; 文章内容&#xff1a; Title: The Silent Power of Consistency &#xff08;标题翻译&#xff1a;《持续坚持的无声力量》&#xff09; Consistency is the quiet force that turns asp…...

软件兼容性测试的矩阵爆炸问题有哪些解决方案

解决软件兼容性测试中的矩阵爆炸问题主要有优先级划分、组合测试方法、自动化测试技术等方案。其中&#xff0c;组合测试方法尤其有效。组合测试通过科学的组合算法&#xff0c;能够显著降低测试用例的数量&#xff0c;同时保持较高的测试覆盖率&#xff0c;例如正交实验设计&a…...

嵌入式学习(32)-TTS语音模块SYN6288

一、概述 SYN6288 中文语音合成芯片是北京宇音天下科技有限公司于 2010年初推出的一款性/价比更高,效果更自然的一款中高端语音合成芯片。SYN6288 通过异步串口(UART)通讯方式&#xff0c;接收待合成的文本数据&#xff0c;实现文本到语音(或 TTS 语音)的转换。宇音天下于 2002…...

霸王茶姬小程序(2025年1月版)任务脚本

脚本用于自动执行微信小程序霸王茶姬的日常签到和积分管理任务。 脚本概述 脚本设置了定时任务(cron),每天运行两次,主要用于自动签到以获取积分,积分可以用来换取优惠券。 核心方法 constructor:构造函数,用于初始化网络请求的配置,设置了基础的 HTTP 请求头等。 logi…...

从零到一:打造顶尖生成式AI应用的全流程实战

简介 生成式AI正以前所未有的速度改变我们的世界&#xff0c;从内容创作到智能客服&#xff0c;再到医疗诊断&#xff0c;它正在成为各行各业的核心驱动力。然而&#xff0c;构建一个高效、安全且负责任的生成式AI系统并非易事。本文将带你从零开始&#xff0c;逐步完成一个完整…...

Windows 10更新失败解决方法

在我们使用 Windows 时的时候&#xff0c;很多时候遇到系统更新 重启之后却一直提示“我们无法完成更新&#xff0c;正在撤销更改” 这种情况非常烦人&#xff0c;但其实可以通过修改文件的方法解决&#xff0c;并且正常更新到最新版操作系统 01修改注册表 管理员身份运行注…...

Windows下在IntelliJ IDEA 使用 Git 拉取、提交脚本出现换行符问题

文章目录 背景问题拉取代码时提交代码时 问题原因解决方案1.全局配置 Git 的换行符处理策略2.在 IntelliJ IDEA 中配置换行符3.使用 .gitattributes 文件 背景 在 Windows 系统下使用 IntelliJ IDEA 进行 Git 操作&#xff08;如拉取和提交脚本&#xff09;时&#xff0c;经常…...

ubuntu24.04.2 NVIDIA GeForce RTX 4060笔记本安装驱动

https://www.nvidia.cn/drivers/details/242281/ 上面是下载地址 sudo chmod x NVIDIA-Linux-x86_64-570.133.07.run # 赋予执行权限把下载的驱动复制到家目录下&#xff0c;基本工具准备&#xff0c;如下 sudo apt update sudo apt install build-essential libglvnd-dev …...

一种监控录像视频恢复的高效解决方案,从每一帧中寻找可能性

该软件旨在恢复从监控设备中删除或丢失的视频。该程序经过调整以处理大多数流行供应商的闭路电视系统中使用的专有格式&#xff0c;并通过智能重建引擎进行了增强&#xff0c;能够为监控记录提供任何通用解决方案都无法实现的恢复结果。如果不需要持续使用该软件&#xff0c;则…...

如何快速下载并安装 Postman?

从下载、安装、启动 Postman 这三个方面为大家详细讲解下载安装 Postman 每一步操作&#xff0c;帮助初学者快速上手。 Postman 下载及安装教程(2025最新)...

Unity Shader 学习18:Shader书写基本功整理

1. Drawer [HideInInspector]&#xff1a;面板上隐藏[NoScaleOffset]&#xff1a;隐藏该纹理贴图的TillingOffset[Normal]&#xff1a;检查该纹理是否设为法线贴图[HDR]&#xff1a;将颜色类型设为高动态范围颜色&#xff08;摄像机也要开启HDR才有效果&#xff09;[PowerSlid…...

1.1 计算机网络的概念

首先来看什么是计算机网络&#xff0c;关于计算机网络的定义并没有一个统一的标准&#xff0c;不同的教材有 不同的说法&#xff08;这是王道书对于计算机网络的定义&#xff09;&#xff0c;我们可以结合自己的生活经验去体会这个 定义。 可以用不同类型的设备去连接计算机网络…...

Blender绘图——旋转曲线(以LCP与RCP为例)

最近在做左旋圆偏振光&#xff08;LCP&#xff09;与右旋圆偏振光&#xff08;RCP&#xff09;的研究&#xff0c;因此需要画出他们的图&#xff0c;接下来我就介绍一下用Blender怎么去画LCP与RCP。 首先你需要下载Blender软件&#xff0c;网上直接能搜到&#xff0c;图标如下…...

Spring与Mybatis整合

持久层整合 1.Spring框架为什么要与持久层技术进行整合 JavaEE开发需要持久层进行数据库的访问操作 JDBC Hibernate Mybatis进行持久层开发存在大量的代码冗余 Spring基于模板设计模式对于上述的持久层技术进行了封装 2.Mybatis整合 SqlSessionFactoryBean MapperScannerConfi…...

JDBC FetchSize不生效,批量变全量致OOM问题分析

背景 一个简单的基于 JDBC 采集数据库表的功能&#xff0c;当采集 Postgre SQL 某表&#xff0c;其数据量达到 500万左右的时候&#xff0c;程序一启动就将 JVM 堆内存「6G」干满了。 问题是程序中使用了游标的只前进配置&#xff0c;且设置了 fetchSize 属性&#xff1a; q…...

docker - compose up - d`命令解释,重复运行会覆盖原有容器吗

docker - compose up - d`命令解释,重复运行会覆盖原有容器吗 docker - compose up - d 是一个用于管理 Docker 容器的命令,具体含义如下: 命令含义: up:用于创建、启动并运行容器,会根据 docker - compose.yml 文件中定义的服务配置来操作。-d:表示以“分离模式”(det…...

Python 装饰器(Decorators)

什么是装饰器&#xff1f; 装饰器&#xff08;Decorator&#xff09;本质上是一个 修改其他函数功能的函数。它的核心思想是&#xff1a;不修改原函数代码&#xff0c;动态添加新功能。比如&#xff1a; 记录函数执行时间 检查用户权限 缓存计算结果 自动重试失败操作 理解…...

A2 最佳学习方法

记录自己想法的最好理由是发现自己的想法&#xff0c;并将其组织成可传播的形式 (The best reason for recording what one thinks is to discover what one thinks and to organize it in transmittable form.) Prof Ackoff 经验之谈&#xff1a; 做培训或者写文章&#xff…...

蓝桥杯省模拟赛 阶乘求值

问题描述 给定 n&#xff0c;求 n! 除以 1000000007的余数。 其中 n! 表示 n 的阶乘&#xff0c;值为从 1 连乘到 n 的积&#xff0c;即 n!123…n。 输入格式 输入一行包含一个整数 n。 输出格式 输出一行&#xff0c;包含一个整数&#xff0c;表示答案。 样例输入 3样…...

MYTOOL-记事本

一、前言 目录 1.原型设计 2.程序实现 3.最终界面说明 二、环境 windows10 每个软件工具前期会设计大概的原型&#xff0c;我设计的原型工具使用Axure RP9&#xff0c;很不错的一个设计工具 三、正文 1.原型设计 2.程序实现 3.最终界面说明 四、结语...

Golang使用 ip2region 查询IP的地区信息

利用 ip2region 进行 IP 地址定位 import ("fmt""log""github.com/lionsoul2014/ip2region/binding/golang/xdb" )func main() {ip : "213.118.179.98"dbPath : ".\\cmd\\ip\\ip2region.xdb"// 1、初始化查询器//searcher,…...

StarRocks 中 CURRENT_TIMESTAMP 和 CURRENT_TIME 分区过滤问题

背景 本文基于Starrocks 3.3.5 最近在进行Starrocks 跑数据的时候&#xff0c;发现了一个SQL 扫描了所有分区的数据&#xff0c;简化后的SQL如下&#xff1a; select date_created from tableA where date_createddate_format(current_time(), %Y-%m-%d %H:%i:%S) limit 20其…...

OMI(operating mode indication)

OMI(operating mode indication,操作模式指示)是11ax引入的用以交互形式分配兼容性以及信道带宽的协商。可以降终端活跃时间的耗电量. 802.11ax终端使用802.11数据使用OM控制字段(OM Control Subfield,其通常位于数据或者管理帧中),其用来指示改变AP的发送或者接收模式。8…...

4、网工软考—VLAN配置—hybird配置

1、实验环境搭建&#xff1a; 2、实验过程 SW1&#xff1a; 先创建vlan2和vlan3 [Huawei-Ethernet0/0/2]port link-type hybrid //hybird端口 [Huawei-Ethernet0/0/2]port hybrid pvid vlan 2 [Huawei-Ethernet0/0/2]port hybrid untagged vlan 10 //撕掉vlan10的标签 …...

Chrome 开发环境快速屏蔽 CORS 跨域限制!

Chrome 开发环境快速屏蔽 CORS 跨域限制【详细教程】 ❓ 为什么需要临时屏蔽 CORS&#xff1f; 在前后端开发过程中&#xff0c;我们经常会遇到 跨域请求被浏览器拦截 的问题。例如&#xff0c;你在 http://localhost:3000 调用 https://api.example.com 时&#xff0c;可能会…...

第 8 章:使用更好的库_《C++性能优化指南》_notes

使用更好的库 第八章核心知识点解析编译与测试建议总结优化原则重点内容&#xff1a;第一部分&#xff1a;多选题&#xff08;10题&#xff09;第二部分&#xff1a;设计题答案与解析多选题答案&#xff1a;设计题答案示例&#xff08;部分&#xff09;&#xff1a; 测试用例设…...

基于深度学习的图像超分辨率技术研究与实现

一、引言 在数字图像处理领域&#xff0c;图像超分辨率技术一直是一个备受关注的热点话题。随着人们对图像质量要求的不断提高&#xff0c;如何将低分辨率图像提升到高分辨率&#xff0c;同时保持图像的细节和清晰度&#xff0c;成为了一个极具挑战性的问题。传统的图像超分辨率…...

ubuntu22.04 ROS2humble 路径文件

ROS2humble 路径文件 /opt/ros/humble/include/opt/ros/humble/lib/opt/ros/humble/share 下载ros2之后会有下面的文件&#xff0c;在/opt/ros/humble下 /opt/ros/humble/include C/C 头文件&#xff08;.h, .hpp&#xff09; /opt/ros/humble/lib 作用: 存放 编译生成的二…...