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

【C++项目实战】日志系统

目录

日志系统

1、项目介绍

2、开发环境

3、核心技术

4、日志系统介绍

4.1 日志系统的价值

4.2 日志系统技术实现

5、相关技术知识

5.1 不定参函数

5.2 设计模式

6、日志系统框架设计

6.1 模块划分

7、代码设计

7.1 实用类设计

7.2 日志等级类设计

7.3 日志消息类设计

7.4 日志输出格式化类设计

7.5 日志落地类设计(工厂模式)

7.6 日志器类设计(建造者模式)

7.7 双缓冲区异步任务处理器设计

7.8 异步工作器设计

7.9 单例日志器管理类设计和全局日志器建造者

7.10 日志宏&全局接口设计

8、单元测试

9、 性能测试


日志系统

日志:程序运行过程中记录的程序运行状态信息

作用:记录了程序运行状态信息,以便程序猿能够随时根据状态信息,对系统程序的运行状态进行分析。能让用户非常简便的进行详细的日志输出以及控制

1、项目介绍

本项目主要实现的是一个日志系统,其支持以下功能:

  • 支持多级别日志信息

  • 支持同步日志和异步日志

  • 支持可靠写入日志到控制台、文件、滚动文件、数据库中

  • 支持多线程程序并发写日志

  • 支持扩展不同的日志落地

2、开发环境

  • 操作系统 :CentOS 7

  • 编辑器: vscode + vim

  • 编译器/调试器:g++/ gdb

  • 项目自动化构建工具:Makefile

3、核心技术

  • 类层次化设计(继承、多态的实际应用)

  • C++11语法(多线程库,智能指针,右值引用等)

  • 双缓冲区设计思想

  • 生产消费模型

  • 设计模式(单例、工厂、代理、建造者等)

4、日志系统介绍

4.1 日志系统的价值
  • 在生产环境中的产品,为了保证其稳定性以及安全性,是不允许开发人员附加调试器去排查问题的,可以借助日志系统来打印一些日志帮助开发人员解决问题

  • 上线客户端的产品出现的Bug无法复现并解决,可以借助日志系统打印日志并且上传到服务端帮助开发人员进行分析

  • 对于一些高频操作(如定时器,心跳包等)在少量调试次数下可能无法出发我们想要的行为,通过断点暂停的方式,我们需要重复几十次甚至上百次,导致排查问题的效率非常低下,可以借助打印日志的方式排查问题

  • 在分布式、多线程/多进程的代码中,出现bug非常难定位,可以借助日志系统打印日志帮助定位bug

  • 可以帮助刚接触项目不久的开发人员理解代码的运行流程

4.2 日志系统技术实现

日志的技术实现主要包括三种类型:

  • 利用printf、std::cout等输出函数将日志信息打印到控制台

  • 对于大型商业化项目,为了方便排查问题,我们一般会将日志输出到文件或者说是数据库方便查询和分析日志,主要分为同步日志和异步日志

4.2.1 同步写日志

同步日志指的是当输出日志时,必须等待日志输出语句执行完毕后,才能执行后面的业务逻辑语句,日志输出语句与程序的业务逻辑语句将在同一个线程中运行。每调用一次打印日志API就对应一次系统调用write写日志文件

在高并发的场景下,随着日志数量多不断增加,同步日志系统容易产生系统瓶颈:

  • 一方面,大量的日志打印陷入等量的write系统调用,具有一定的系统开销

  • 另一方面,使得打印日志的进程附带了大量同步的磁盘IO,影响程序性能

4.2.2 异步写日志

异步日志是指在进行日志输出时,日志输出语句与业务逻辑语句并不是在同一个线程中运行,而是有专门的线程用于进行日志输出操作,业务线程只需要将日志放在放到一个内存缓冲区,不需要等待即可继续执行后续业务逻辑(作为日志的生产者),而日志的落地操作交给单独的日志线程完成(作为日志的消费者)

这样的好处是即使日志没有真正的完成输出也不会影响住业务,以提高程序的性能

  • 主线程调用日志打印接口成为非阻塞操作

  • 同步的磁盘IO操作从主线程剥离出来交给单独的线程完成

5、相关技术知识

5.1 不定参函数

在学C语言的时候,我们就已经接触过不定参函数了。例如printf就是一个典型的可以根据格式化字符串解析,对传上来的数据进行格式化的函数

这种不定参函数在实际的使用中非常多见,这里简单的做一下介绍

5.1.1 不定参宏函数

__FILE____LINE__是C语言的宏函数,可以用于获取文件名,和代码当前行数,我们可以使用printf打印一条包含当前文件信息和行数信息的日志

#include <cstdio>
int main() {printf("[%s : %d] %s - %d\n", __FILE__, __LINE__, "clx", 666);   //输出: [test.cpp : 5] clx - 666return 0;
}

但是我们每次打印日志都要写printf,__FILE__,、__LINE__实在是太麻烦了,我们可以使用不定参的宏函数对其进行替换

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

解释一下:

  • fmt(format):就是我们的格式化字符串,编译器就是以它为依据进行不定参数解析的,例:"%s,%c"或"hellow world"这种格式化字符传给fmt

  • ...: 就是我们的不定参数

  • "[%s : %d] " fmt :因为这两个都是格式化字符串,C语言是支持直接连接的

  • __VA_ARGS__也是C语言给我们提供的宏函数,用于给我们的fmt传参

  • ##__VA_ARGS__ 加了##是为了告诉编译器,若我们只想传一个不定参数,可以省略前面的fmt参数的传递

fmt就相当于接收字符串,然而fmt后面有一个逗号,如果我们不传参数则会报错,加了##后,如果我们不传参数,他会自动将逗号去除

5.1.2 C 风格不定参数使用

#include <stdarg.h>
void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);
void va_copy(va_list dest, va_list src);

我们可以用一段代码来理解这一系列借口的使用

#include <cstdio>
#include <cstdarg>#define LOG(fmt, ...) printf("[%s : %d] " fmt, __FILE__, __LINE__, ##__VA_ARGS__);void printNum(int count, ...) {									// count 不定参数的个数va_list ap;                                 // va_list实际就是一个char*类型的指针va_start(ap, count);                        // 将char*类型指针指向不定参数的起始位置for (int i = 0; i < count; i++) {            int num = va_arg(ap, int);              // 从ap位置取一个整形大小空间数据拷贝给num,并将ap向后移动一个整形大小空间printf("param[%d], %d\n", i, num);       }va_end(ap);                                 // 将ap指针置空
}int main() {printNum(2, 666, 222);return 0;
}
  • va_list ap: 就是定义一个char* 类型的指针

  • va_start : 让指针指向不定参数的起始位置,第二个参数传的是不定参数的前一个参数,因为函数调用过程中是会将实参一个个压入函数栈帧中的,所以参数之间都是紧挨着的。我们找到了前一个参数count的地址,也就等于找到了不定参数的起始地址

  • va_arg : 用于从不定参数中解析参数,第一个参数数据的起始位置,第二个参数指定参数类型,根据类型我们可以推导出参数的大小,从而将参数数据解析出来

  • va_end : 将ap指针置空

这里我们解释传入类型只能是int类型,我们如何使用上述接口将不定参数分离的原理,那么printf这类函数是如何将不定参数分离的呢??**这是因为我们在使用printf函数开始传递了format参数,其中包含了%s, %d这类的信息,printf内部通过对format 参数进行解析就知道了后面的参数依次都是什么类型的,然后将类型依次放入va_arg函数,就可以将参数全部提取出来了

void myprintf(const char *fmt, ...) {va_list ap;va_start(ap, fmt);char* res;int ret = vasprintf(&res, fmt, ap);if (ret != -1) {printf(res);free(res);}va_end(ap);
}
  • vasprintf 函数会帮助提取不定参数并且将其拼接到格式化字符串中,并开辟空间将处理好的字符串数据放入空间,并将我们传入的指针指向这块空间

  • 成功返回打印的字节数,失败返回-1

5.1.3 C++风格不定参数使用

void xprintf() {std::cout << std::endl;
}
/*C++风格的不定参数*/
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() {printNum(2, 666, 222);myprintf("%s - %d\n", "clx", 666);xprintf("hello");xprintf("hello", "world");xprintf("hello", "I", " am" , "clx");return 0;
}
5.2 设计模式

项目中用到了很多种设计模式,设计模式是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它是一套提高代码复用性,可维护性,可读性,稳健性以及安全性的解决方案

5.2.1六大原则

1. 单一责任原则(Single Responsibility Principle)

  • 类的职责应该单一,一个方法只做一件事,职责划分清晰,每次改动到最小单位的方法或类

  • 使用建议:两个完全不一样的功能不应该放在一个类中,一个类中应该是一组相关性很高的函数、数据的封装

  • 用例:网络聊天类(❌)应该分割成网络通信类 + 聊天类

2. 开闭原则(Open Closed Principle)

  • 对扩展开放,对修改封闭(只添加新功能,不修改原有内容)

  • 使用建议:对软件实体的改动,最好用扩展而非修改的方式

  • 用例:超时卖货:商品价格—不是修改商品原来的价格,而是新增促销的价格

3. 里氏替换原则(Liskov Substitution Principle)

  • 凡事父类能够出现的地方,子类就可以出现,而且替换为子类也不会出现任何的错误或者异常

  • 在继承类时,务必重写父类中的所有方法,尤其注意父类的protected方法,子类尽量不要暴露自己的public方法供外界调用

  • 使用建议:子类无比完全实现父类的方法,还子类可以有自己的个性,覆盖或者实现父类的方法时,输入的参数可以被放大,输出也可以缩小

  • 用例:跑步运动员类:会跑步, 子类长跑运动员-会跑步且擅长长跑,子类短跑运动员:会跑步且擅长短跑

4. 依赖倒置原则(Dependence Inversion Principle)

  • 高层模块不应该依赖底层模块,两者都应该依赖其抽象,不可分隔的原子逻辑就是低层的模式,原子逻辑组装成的就是高层模块

  • 模块间依赖通过抽象(接口)发生,具体类之间不能直接依赖

  • 使用建议:每一个类都尽量有抽象类,任何类都不应该从具体类派生。尽量不要重写基类的方法。结合里氏替换原则使用

  • 用例:奔驰车司机 – 只能开奔驰,司机类:给什么车开什么车 : 开车的人 : 司机 – 依赖抽象

5. 迪米特法则(Law of Demeter) 最少知道法则

  • 尽量减少对象之间的交互,从而减少类之间的耦合。一个对象应该对其他对象有最少的了解,对类的低耦合提出了明确的要求:

  • 只喝直接的朋友交流,朋友间也是有剧烈的。自己的就是自己的(如果一个方法放在本类中,既不增加类间关系,也不对本类造成负面影响,那就放置在本类中)

  • 用例:老师让班长点名,老师给班长名单,班长点名勾选,返回结果。老师只和班长交互,同学们只和班长交互

6. 接口隔离原则

  • 客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上

  • 使用建议:接口设计尽量精简单一,但是不要对外暴露没有啥意义的接口

  • 用例:修改密码,不应该提供用户信息接口,而是单一使用修改密码接口

从整体上理解六大设计原则,可以简要概括为一句话,用抽象构建框架,用实现扩展细节,具体到每一条设计原则,则对应一条注意事项

5.2.2 单例模式

饿汉单例模式是单例模式的一种实现方式,与懒汉单例模式不同,饿汉单例模式在程序启动时就创建单例实例,而不是在首次使用时才创建。 

/* 饿汉单例模式 以空间换时间 */
class Singleton{
public:static Singleton& getInstance() { return _eton; }int getData() { return _data; }
private:Singleton(int data = 99) : _data(data){}~Singleton(){};Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;private:static Singleton _eton;int _data;
};
Singleton Singleton::_eton;int main() {std::cout << Singleton::getInstance().getData() << std::endl;return 0;
}

懒汉单例模式指的是单例实例在首次被使用时才会创建,只实例化一次,再次调用时只会返回之前实例化的对象

/* 懒汉单例模式 懒加载 -- 延时加载思想 -- 一个对象用的时候再实例化 */
// 这里介绍<Effective C++> 作者提出的一种更加优雅简便的单例模式 Meyers Singleton int C++
// C++11后是线程安全的class Singleton{
public:static Singleton& getInstance() {static Singleton _eton;return _eton;}int getData() { return _data; }
private:Singleton(int data = 99) : _data(data){}~Singleton() {};Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;int _data;
};int main() {std::cout << Singleton::getInstance().getData() << std::endl;return 0;
}

5.2.3 工厂模式

工厂模式是一种创建型的设计模式,它提供了一种创建对象的最佳方式。在工厂模式中,我们创建对象不会对上层暴露创建逻辑,而是通过使用一个共同结构来指向新创建的对象,因此实现创建-使用的分离

工厂模式分为:

  • 简单工厂模式:简单工厂模式实现需要由一个工厂对象通过类型决定创建出来的制定产品类的实例。假设有个工厂可以生产水果,当客户需要产品时明确告知工厂生产哪种水果,工厂需要接收用户提供的类别信息,当新增产品的时候,工厂内部取添加新产品的生产方式

class Fruit{
public:virtual void name() = 0;
private:   
};class Apple : public Fruit{
public:void name() override{std::cout << "I'm a apple" << std::endl;}
};class Banana : public Fruit{
public:void name() override {std::cout << "I'm a banana" << std::endl;}
};class FruitFactory {
public:static std::shared_ptr<Fruit> create(const std::string &name) {if (name == "苹果") {return std::make_shared<Apple>();} else {return std::make_shared<Banana>();}}
};int main() {std::shared_ptr<Fruit> fruit = FruitFactory::create("苹果");fruit->name();fruit = FruitFactory::create("香蕉");fruit->name();return 0;
}

这个模式的结构和管理产品对象的方式非常简单,但是它的扩展性非常差,当我们需要新增产品的时候,就需要去修改工厂类新增一个类型的产品创造逻辑,违背了开闭原则

  • 工厂方法模式:在简单的工厂模式下新增了多个工厂,多个产品,每个产品对应一个工厂。假设现在有A、B两种产品,则开两个工厂,工厂A主要负责生产产品A,工厂B主要生产产品B,用户只要知道产品的工厂名,而不需要知道具体的产品信息,工厂不需要接收客户的产品类别,只负责生产产品

/* 工厂方法模式 */
class Fruit{
public:virtual void name() = 0;
private:   
};class Apple : public Fruit{
public:void name() override{std::cout << "I'm a apple" << std::endl;}
};class Banana : public Fruit{
public:void name() override {std::cout << "I'm a banana" << std::endl;}
};
class FruitFactory {
public:virtual std::shared_ptr<Fruit> createFruit() = 0;
};class AppleFactory : public FruitFactory {
public:virtual std::shared_ptr<Fruit> createFruit() override {return std::make_shared<Apple>();}
};class BananaFactory : public FruitFactory {
public:virtual std::shared_ptr<Fruit> createFruit() override {return std::make_shared<Banana>();}
};int main() {std::shared_ptr<FruitFactory> ff(new AppleFactory());std::shared_ptr<Fruit> fruit1 = ff->createFruit();fruit1->name();ff.reset(new BananaFactory());std::shared_ptr<Fruit> fruit2 = ff->createFruit();fruit2->name();return 0;
}
  • 抽象工厂模式:工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必增加系统的开销,此时我们可以考虑将一些相关的产品组成一个产品族(位于不同产品等级结构中功能相互关联的产品组成的家族),由于一个工厂统一生产,这就是抽象工厂模式的基本思想

#include <iostream>
#include <memory>/* 简单工厂模式 */
class Fruit{
public:virtual void name() = 0;
private:   
};class Apple : public Fruit{
public:void name() override{std::cout << "I'm a apple" << std::endl;}
};class Banana : public Fruit{
public:void name() override {std::cout << "I'm a banana" << std::endl;}
};class Animal {public:virtual void name() = 0;
};class Lamp : public Animal {public:virtual void name() override { std::cout << "I'm a Lamp" << std::endl;}
};class Dog : public Animal {public:virtual void name() override {std::cout << "I'm  a dog" << std::endl;}
};class Factory {public: virtual std::shared_ptr<Fruit> getFruit(const std::string& name) = 0;virtual std::shared_ptr<Animal> getAnimal(const std::string& name) = 0;
};class FruitFactory : public Factory {public:virtual std::shared_ptr<Fruit> getFruit(const std::string& name) override{if (name == "苹果") {return std::make_shared<Apple>();} else {return std::make_shared<Banana>();}}virtual std::shared_ptr<Animal> getAnimal(const std::string& name) override{return std::shared_ptr<Animal>();}
};class AnimalFactory : public Factory {public:virtual std::shared_ptr<Fruit> getFruit(const std::string& name) override {return std::shared_ptr<Fruit>();}virtual std::shared_ptr<Animal> getAnimal(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> create(const std::string &name) {if (name == "水果") {return std::make_shared<FruitFactory>();} else {return std::make_shared<AnimalFactory>();}}
};int main() {std::shared_ptr<Factory> ff = FactoryProducer::create("水果");std::shared_ptr<Fruit> fruit = ff->getFruit("苹果");fruit->name();fruit = ff->getFruit("香蕉");fruit->name();ff = FactoryProducer::create("动物");std::shared_ptr<Animal> animal = ff->getAnimal("山羊");animal->name();animal = ff->getAnimal("小狗");animal->name();return 0;
}

5.2.4 建造者模式

建造者模式是一种创建型的设计模式,使用多个简单对象一步一步构建成一个复杂的对象,能够将一个复杂的对象的构建与它的表示分离,提供一种创建对象的最佳方式。主要用于解决对象的构建过于复杂的问题

建造者模式主要基于四个核心实现:

  • 抽象产品类

  • 具体产品类:一个具体的产品对象类

  • 抽象Builder类:创建一个产品对象所需要的各个零部件的抽象接口

  • 具体产品的Builder类:实现抽象接口,构建各个部件

  • 指挥者Director类:统一组建过程,提供给调用者使用,通过指挥者来获取产品

#include <iostream>
#include <string>
#include <memory>/* 通过MacBook的构造理解建造者模式*/class Computer{public:Computer(){};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 = "Computer Paramaters: \n";param += "\tBoard: " + _board + "\n";param += "\tDispaly: " + _display + "\n";param += "\tOs: " + _os + "\n";std::cout << param << std::endl;}protected:std::string _board;std::string _display;std::string _os;
};
class MacBook : public Computer{public:virtual void setOs() override {_os = "Mac OS x12";}
};class Builder {public:virtual void buildBoard(const std::string &board) = 0;virtual void buildDisplay(const std::string &display) = 0;virtual void buildOs() = 0;virtual std::shared_ptr<Computer> build() = 0;
};class MacBookBuilder : public Builder{public:MacBookBuilder() : _computer(new MacBook()) {}void buildBoard(const std::string& board) {_computer->setBoard(board);}void buildDisplay(const std::string& display) {_computer->setDisplay(display);}void buildOs() {_computer->setOs();}std::shared_ptr<Computer> build() {return _computer;}private:std::shared_ptr<Computer> _computer;
};class Director {public:Director(Builder* builder) : _builder(builder) {}void construct(const std::string& board, const std::string& display) {_builder->buildBoard(board);_builder->buildDisplay(display);_builder->buildOs();}private:std::shared_ptr<Builder> _builder;
};int main() {Builder * builder = new MacBookBuilder();std::unique_ptr<Director> director(new Director(builder));director->construct("华硕主板", "三星显示器");std::shared_ptr<Computer> computer = builder->build();computer->showParamaters();return 0;
}

5.2.5 代理模式

代理模式指的是代理控制对其他对象的访问,也就是代理对象控制对原对象的引用。在某些情况下,一个对象不适合或者不能直接被引用访问,而代理对象可以在客户端和目标对象之间起到中介作用

代理模式的结构包括一个是真正的你要访问的目标对象(目标类)、一个是代理对象。目标对象与代理对象实现同一个接口,先访问代理类再通过代理类访问目标对象。代理模式一般分为静态代理、动态代理

  • 静态代理指的是,在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时已经确定了代理要代理的是哪一个被代理类

  • 动态代理指的是,在运行时才动态生成代理类,并将其与被代理类绑定。这意味着,在运行时才能确定代理类要代理的是哪个被代理类

#include <iostream>/* 代理模式 */
class RentHouse {public:virtual void rentHouse() = 0;
};class Landlord : public RentHouse {public:void rentHouse() {std::cout << "房子租出去了" << std::endl;}
};class Intermediary : public RentHouse {public:void rentHouse() {std::cout << "发布招租启事" << std::endl;std::cout << "带人看房" << std::endl;_landload.rentHouse();std::cout << "负责租后维修" << std::endl;}private:Landlord _landload;
};int main() {Intermediary intermediary;intermediary.rentHouse();return 0;
}

6、日志系统框架设计

日志系统的作用

本项目实现的是一个多日志器日志系统,主要实现的功能是让程序员能够轻松的将程序运行的日志信息落地到指定的位置,且支持同步与异步两种方式的日志落地

1. 日志要写入指定位置(标准输出,指定文件,滚动文件…)

  • 日志系统需要支持将日志消息落地到不同位置—多落地方向

2. 日志写入指定位置,支持不同的写入方式(同步,异步)

  • 同步:业务线程自己负责日志写入(流程简单,但是可能因为阻塞导致效率降低)

  • 异步:业务线程将日志放入缓冲区内存,让其他异步线程负责将日志写入到指定位置(业务线程不会阻塞)

3. 日志输出以日志器为单位,支持多日志器(不同的项目组有不同的输出策略)

4. 日志器的管理

6.1 模块划分

日志等级模块:对输出日志的等级进行划分,以便控制日志输出,并提供等级枚举转字符串功能

  • OFF : 关闭

  • DEBUG : 调试,调试时的关键信息输出

  • INFO : 提示,普通的提示型日志信息

  • WARN : 警告,不影响运行,但是需要注意一下的小错误

  • ERROR : 错误,程序运行时出现错误的日志

7、代码设计

7.1 实用类设计

实用类也叫做工具类,其中编写的是我们项目中需要经常使用的几个函数

实用类整体框架

/*  实用工具类实现 :1、 获取系统时间2、 判断文件是否存在3、 获得文件所在路径4、 创建目录
*/
#include <ctime>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <iostream>namespace CH
{namespace Util{class date{public://返回现在的时间,设置为静态函数,不用实例化,也可以调用函数static time_t now(){return (size_t)time(nullptr);}};class file{public://判断文件是否存在static bool is_file_exit(const string& name){}//返回文件的目录static string file_path(const string& name){}// 创建指定文件夹,如果文件夹已经存在,则不需要创建,不存在就要创建static void create_directory(const string& path){}};}
}

Data 类实现

class date{
public://返回现在的时间,设置为静态函数,不用实例化,也可以调用函数static time_t now(){return (size_t)time(nullptr);}
};
time_t time(time_t *t);

now()函数:用于获取当前时间戳

注意:time_t 实际就是一个long int 类型,实际占用八个字节

File 类实现

  • is_file_exit()函数: 用于获取指定文件是否存在

//判断文件是否存在
static bool is_file_exit(const string& name){//存储文件的属性struct stat st;//将该文件的属性,存储在st中,如果存储成功,则返回0,即代表文件存在//如果存储失败,则返回-0,即代表文件不存在return stat(name.c_str(),&st)==0;
}

这里使用的是stat()函数来帮助我们进行判断,

stat()函数返回有关文件的信息,文件本身不需要任何权限,但是在stat()和lstat()的情况下,需要对指向该文件的路径中的所有目录具有execute(搜索)权限。 stat()函数统计路径指向的文件并填充buf

成功返回0,失败返回-1

 int stat(const char *path, struct stat *buf);struct stat {dev_t     st_dev;     /* ID of device containing file */ino_t     st_ino;     /* inode number */mode_t    st_mode;    /* protection */nlink_t   st_nlink;   /* number of hard links */uid_t     st_uid;     /* user ID of owner */gid_t     st_gid;     /* group ID of owner */dev_t     st_rdev;    /* device ID (if special file) */off_t     st_size;    /* total size, in bytes */blksize_t st_blksize; /* blocksize for file system I/O */blkcnt_t  st_blocks;  /* number of 512B blocks allocated */time_t    st_atime;   /* time of last access */time_t    st_mtime;   /* time of last modification */time_t    st_ctime;   /* time of last status change */};
  • file_path()函数:用于获取文件所在目录路径

//返回文件的目录
static string file_path(const string& name)
{//为空,则表示该路径下没有文件if(name.empty()) return ".";int pos=name.rfind("/");//从最后开始找if(pos==string::npos) return ".";return name.substr(0,pos+1);//返回路径,不包括最后一个文件
}

获取一个文件所在文件夹只需要找到最后一个/即可,前面的路径就是目录路径,如果不存在则说明该文件就在当前目录下返回.就好了

  • create_directory()函数:用于创建指定目录,我们想要在指定目录下创建指定文件,首先要保证该目录存在

我们只需要将输入的文件路径根据/进行逐级分层,若文件夹存在则跳过,若不存在则创建即可 

// 创建指定文件夹,如果文件夹已经存在,则不需要创建,不存在就要创建
static void create_directory(const string& path)
{//如果为空或文件不存在,则直接返回if(path.empty()) return;if(is_file_exit(path)) return;size_t pos=0,idx=0;while(idx<path.size()){//./path/pos=path.find("/",idx);if(pos==string::npos){//如果为".","/",则表示文件夹已经存在,即创建时会创建失败,返回//如果为".abc","adc",则会在该路径下创建abc文件夹mkdir(path.c_str(),0777);return;}// ./abc/a/cstring str=path.substr(0,pos+1);if(!is_file_exit(str)){mkdir(str.c_str(),0777);}idx=pos+1;}
}
int mkdir(const char *pathname, mode_t mode);
  • mkdir 函数接收两个参数,第一个是要创建的目录名,第二个是目录的权限

  • 若目录创建成功,mkdir 函数返回 0;若失败,则返回 -1。

实用类完整代码(util.hpp)


#pragma once
#include <ctime>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <iostream>using namespace std;/*  实用工具类实现 :1、 获取系统时间2、 判断文件是否存在3、 获得文件所在路径4、 创建目录
*/
namespace CH
{namespace Util{class date{public://返回现在的时间,设置为静态函数,不用实例化,也可以调用函数static time_t now(){return (size_t)time(nullptr);}};class file{public://判断文件是否存在static bool is_file_exit(const string& name){//存储文件的属性struct stat st;//将该文件的属性,存储在st中,如果存储成功,则返回0,即代表文件存在//如果存储失败,则返回-0,即代表文件不存在return stat(name.c_str(),&st)==0;}//返回文件的目录static string file_path(const string& name){//为空,则表示该路径下没有文件if(name.empty()) return ".";int pos=name.rfind("/");//从最后开始找if(pos==string::npos) return ".";return name.substr(0,pos+1);//返回路径,不包括最后一个文件}// 创建指定文件夹,如果文件夹已经存在,则不需要创建,不存在就要创建static void create_directory(const string& path){//如果为空或文件不存在,则直接返回if(path.empty()) return;if(is_file_exit(path)) return;size_t pos=0,idx=0;while(idx<path.size()){//./path/pos=path.find("/",idx);if(pos==string::npos){//如果为".","/",则表示文件夹已经存在,即创建时会创建失败,返回//如果为".abc","adc",则会在该路径下创建abc文件夹mkdir(path.c_str(),0777);return;}// ./abc/a/cstring str=path.substr(0,pos+1);if(!is_file_exit(str)){mkdir(str.c_str(),0777);}idx=pos+1;}}};}
}
7.2 日志等级类设计

设计思路:

1、定义出日志系统锁包含的所有日志等级(使用枚举类实现)

  • UNKNOW : 最低等级日志

  • DEBUG : 调试等级日志

  • INFO : 提示等级日志

  • WARN : 警告等级日志

  • ERROR : 错误等级日志

  • FATAL : 致命错误等级日志

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

只有输出的日志等级大于日志器的默认限制等级才可以进行日志输出,规定日志等级可以起到日志过滤的作用

2、提供一个接口,将枚举类型转换成一个对应的字符串,方便我们打印

日志等级类完整代码(level.hpp)

#pragma oncenamespace CH
{//日志等级class LogLevel{public:enum class value{UNKNOW = 0,      // 最低等级日志DEBUG,           // 调试等级日志INFO,            // 提示等级日志WARN,            // 警告等级日志ERROR,           // 错误等级日志FATAL,           // 致命错误等级日志OFF              // 最高等级,可用于禁止所有日志输出};static const char* tostring(value level){switch(level){case value::DEBUG: return "DEBUG";case value::ERROR: return "ERROR";case value::FATAL: return "FATAL";case value::INFO : return "INFO";case value::OFF  : return "OFF";case value::WARN : return "WARN";default:return "UNKNOW";}}};
}
7.3 日志消息类设计

日志消息类主要是为了封装一条完整的日志内容,其中各个字段用于存储日志的各个属性信息,只需简单提供构造函数即可(将日志的各个属性全部存储在这里)

1、 日志的输出时间 可用于过滤日志
2、 日志的等级 用于进行日志的过滤分析
3、 源文件名称
4、 源代码行号 用于定位出现错误的代码位置
5、 线程ID 用于过滤出错的线程
6、 日志主题消息
7、 日志器名称 项目允许多日志器同时使用

日志消息类完整代码(message.hpp)

#pragma once
#include<ctime>
#include "level.hpp"
#include<string>
#include<thread>
#include"util.hpp"
/* 日志消息类, 用于进行日志中间信息的存储:1、 日志的输出时间         可用于过滤日志2、 日志的等级            用于进行日志的过滤分析3、 源文件名称  4、 源代码行号            用于定位出现错误的代码位置5、 线程ID               用于过滤出错的线程6、 日志主题消息7、 日志器名称            项目允许多日志器同时使用
*/ namespace CH
{class LogMsg{public:time_t _ctime;                   //日志产生的时间戳LogLevel::value _level;          //日志等级const std::string _filename;     //文件名size_t _line;                    //文件行号std::thread::id _tid;            //线程idconst std::string _payload;      //日志消息const std::string _logname;      //日志器名称LogMsg(const std::string& filename,const std::string& logname,size_t line,LogLevel::value level,const std::string& payload):_filename(filename),_logname(logname),_payload(payload),_level(level),_line(line),_ctime(Util::date::now()),_tid(std::this_thread::get_id()){}};
}
7.4 日志输出格式化类设计

日志格式化(Formatter)类主要负责对日志消息对象内各个字段进行格式化,组织成为指定格式的字符串。

日志输出格式化类整体框架

	//格式化字符串class Formater{public:using prt=std::shared_ptr<Formater>;// 时间{年-月-日 时:分:秒}缩进 线程ID 缩进 [日志级别] 缩进 [日志名称] 缩进 文件名:行号 缩进 消息换行Formater(const std::string &pattern = "[%d{%H:%M:%S}][%t][%p][%c][%f:%l] %m%n"): _pattern(pattern){assert(parsePattern()); // 判断输进来的字符串格式是否正确,如果错误,则没有必要进行了}//这里的参数,是由我们自己输入的,是一个给用户使用的接口void format(std::ostream& out,const LogMsg& msg){}/*  对msg进行格式化*/std::string format(const LogMsg &msg) {}private:// 将子类实例化,并且返回一个智能指针,后面存储在vector中FormatItem::prt createItem(const std::string &key, const std::string &val){}//对格式化字符串进行解析bool parsePattern(){}private:const std::string _pattern;			 /// 用于存储字符串的格式std::vector<FormatItem::prt> _items; // 格式化子项数组};

其主要包含以下两个成员变量

1、格式化字符串

2、格式化子项数组 (用于按序保存格式化字符串对应的字格式化对象)

格式化字符串

定义格式化字符成员是为了让日志系统进行日志格式化时更加灵活方便,我们可以通过解析格式化字符串,取出格式化字符够劲啊格式化子项数组对Msg各个字段数据进行组织拼接成指定格式输出

1、格式化字符:

  • %d 日期

  • %T 缩进

  • %t 线程ID

  • %p 日志级别

  • %c 日志器名称

  • %f 文件名

  • %l 行号

  • %m 日志消息

  • %n 换行

定义格式化字符成员是为了让日志系统进行日志格式化时更加灵活方便

格式化子项(数组)

实现思想:从日志消息中取出指定元素,追加到一块内存空间中

设计思想: 抽象一个格式化子项基类,基于基类派生出不同的格式化子项子类(主体消息,日志等级,时间子项,文件名,行号,日志器名称,线程ID,制表符,换行,其他),这样就可以让父类中定义父类指针的数组,指向不同的格式化子项子类对象

比如这是一串用户输入的格式化字符串[%d{%H:%M:%S}][%f:%l]%m%n我们将其解析可以获得以下顺序的格式化子项

1、[ 其他信息子项 调用 OtherFormatItem 进行处理 输出 [字符到指定位置

2、%d 日期子项 调用 TimeFormatItem 进行处理 输出 00:00:00字符到指定字符串

3、%f 文件子项 调用 FileNameFormatItem 进行处理 输出 文件名到指定位置字符串

4、%l 行号子项 调用 LineFormatItem 进行处理 输出 行号到指定位置字符串

5、%m 用户输入子项 调用 LoggerFormatItem 进行处理 输出 用户日志信息到指定字符串

6、%n 换行子项 调用 NewLineFormatItem 进行处理 输出 \n字符到指定字符串

注意:%d日期子项是特殊的,日期的输出格式可以有很多种,比如带年份的和不带年份的。为了满足多种情况,日期子项后带有{}字段,其代表日期子项的输出格式,在后续解析过程中,我们会将该部分交给TimeFormatItem的子项处理器,进行日期信息格式化

// 抽象格式化子项基类
class FormatItem
{
public:// 重命名using prt = std::shared_ptr<FormatItem>;					   // 等于typedef          std::shared_ptr<FormatItem>  previrtual ~FormatItem(){};virtual void format(std::ostream &out, const LogMsg &msg) = 0; // 用于将LogMsg各个字段格式化到指定out流中
};

以下即是LogMsg各个字段的处理子项,每个子项器会将对应的LogMsg对象的对应字段放入指定out流中

	// 派生格式化子项子类 -- 消息class PayloadFormatItem : public FormatItem{public:virtual void format(std::ostream &out, const LogMsg &msg) override{out << msg._payload;}};// 派生格式化子项子类 -- 等级class LevelFormatItem : public FormatItem{public:virtual void format(std::ostream &out, const LogMsg &msg) override{out << LogLevel::tostring(msg._level);}};// 派生格式化子项子类 -- 文件名class FileNameFormatItem : public FormatItem{public:virtual void format(std::ostream &out, const LogMsg &msg){out << msg._filename;}};// 派生格式化子项子类 -- 行号class LineFormatItem : public FormatItem{public:virtual void format(std::ostream &out, const LogMsg &msg){out << msg._line;}};// 派生格式化子项子类 -- 日志器名字class LognameFormatItem : public FormatItem{public:virtual void format(std::ostream &out, const LogMsg &msg){out << msg._logname;}};// 派生格式化子项子类 -- 线程idclass TidFormatItem : public FormatItem{public:virtual void format(std::ostream &out, const LogMsg &msg){out << msg._tid;}};// 派生格式化子项子类 -- 时间class TimeFormatItem : public FormatItem{public:TimeFormatItem(const std::string &format = "%H:%M:%S"): _format(format){}virtual void format(std::ostream &out, const LogMsg &msg){struct tm t;// 将时间戳,转换为各种时间存储在t结构体中localtime_r(&msg._ctime, &t);char ch[32];// 按照_format格式,从结构体t中,取时间,并将时间转化为字符串,放入ch中strftime(ch, 32, _format.c_str(), &t);out << ch;}private:std::string _format;};// 派生格式化子项子类 -- 其他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;};// 派生格式化子项子类 -- "\t"class TabFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg) override{out << '\t';}};// 派生格式化子项子类 -- "\n"class NewLineFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg) override{out << '\n';}};

Formatter 类成员函数实现

Formatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"); : 构造函数

用于接受用户输入的格式化字符串,设置缺省值方便用户使用。然后就是对格式化字符串进行解析,如果解析都不成功那么我们的日志器肯定就无法工作,所以写一个断言,保证我们的日志器可以成功解析格式化字符串

Formater(const std::string &pattern = "[%d{%H:%M:%S}][%t][%p][%c][%f:%l] %m%n"): _pattern(pattern)
{assert(parsePattern()); // 判断输进来的字符串格式是否正确,如果错误,则没有必要进行了
}

std::string format(const LogMsg &msg) : 将LogMsg对象格式化成字符串

void format(std::ostream &out, const LogMsg &msg) :将LogMsg对象格式化到指定输入流中,可以是文件也可以是字符流

前者是后者的字函数,我们将LogMsg进行解析放入C++提供的字符流中stringstream将其变为字符串后返回

void format(std::ostream& out,const LogMsg& msg)
{//_items里面存储的是各个子类的实例化,该实例化已经在parsePattern函数中完成了for(auto item:_items){item->format(out,msg);}
}
/*  对msg进行格式化*/
std::string format(const LogMsg &msg) {std::stringstream ss;format(ss, msg);return ss.str();
}

注意:ss.str()要从stringstream对象中获取其保存的字符串需要调用str()函数

  • bool parsePattern() 函数:解析格式化字符串,填充格式化子项数组

设计思路:

规则字符串的处理过程是一个循环的过程:

while(){1. 处理原始字符串2. 原始字符串处理结束后,遇到%,则处理一个格式化字符
}

并且在处理过程中我们还需要将处理得到的信息都保存下来,下面以abc[%d{%H:%M:%S}][%p]%%为例进行说明

key = nullptr |  val = abc[
key = d		  |  val = %H%M%S
key = nullptr |  val = ][
key = p       |  val = nullptr
key = %%	  |  val = %

通过解析出来的key value 将其构建成数组,将其按照顺序传递给createItem()函数可以获取每一项的格式化处理器并构建处理器数组,得到数组后根据数组内容创建对应的格式化子对象,添加到items成员数组中

//对格式化字符串进行解析
bool parsePattern()
{//  默认格式 "%d{%H:%M:%S}%T%t%T[%p]%T[%c]%T%f:%l%T%m%n"std::vector<pair<std::string,std::string>> fmt_order;//对格式化字符,进行解析,为调用子类实例化做准备size_t pos=0;std::string key,val;while(pos<_pattern.size()){//首先找到%d,不是就是原始字符串,后将其放入val中if(_pattern[pos]!='%'){val.push_back(_pattern[pos]);pos+=1;continue;}//"asd%%ddddddac%d{%H:%M:%S}%T%t%T[%p]%T[%c]%T%f:%l%T%m%n//防止将%作为字符串if(pos+1<_pattern.size()&&_pattern[pos+1]=='%'){val.push_back(_pattern[pos+1]);pos+=2;continue;}//走到这里,已经1找到了第一个格式化字符的%,所有将存储在val中的原始字符串,存入数组中//后调用其他子类进行实例化输出// 万一出现第一个自符就是格式化字符串,那么处理原始字符串的操作就会向数组插入{"",""}// 虽然不会产生错误但是便于逻辑理解,最好还是判断处理一下if (!val.empty()) {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;// 此时pos指向格式化字符串后面的位置,判断是否有格式化子串if(pos<_pattern.size()&&_pattern[pos]=='{'){// 这时pos指向子规则的起始位置pos+=1;while(pos<_pattern[pos]&&_pattern[pos]!='}'){val.push_back(_pattern[pos]);pos+=1;}// 若走到了末尾,还没有找到},则说明格式是错误的,跳出循环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;
}

注意事项: 注意事项在代码中都有所标注,可以跟代码结合食用

  • FormatItem::ptr createItem(const std::string &key, const::std::string &val) : 格式化字符,以及子项数据获取指定格式化子项处理器

// 将子类实例化,并且返回一个智能指针,后面存储在vector中
FormatItem::prt 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<TidFormatItem>();if (key == "c")return std::make_shared<LognameFormatItem>();if (key == "f")return std::make_shared<FileNameFormatItem>();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>();if (key.empty())return std::make_shared<OtherFormatItem>(val);//这里传来的是原始字符串std::cout<<"没有对应的格式化字符%"<<key<<endl;assert(false);   return FormatItem::prt();
}

日志输出格式化类完整代码(formatter.hpp)

#pragma once
#include <memory>
#include <iostream>
#include <time.h>
#include <vector>
#include <assert.h>
#include <sstream>
#include "message.hpp"/*日志消息类, 用于进行日志中间信息的存储:1、 日志的输出时间         可用于过滤日志2、 日志的等级            用于进行日志的过滤分析3、 源文件名称4、 源代码行号            用于定位出现错误的代码位置5、 线程ID               用于过滤出错的线程6、 日志主题消息7、 日志器名称            项目允许多日志器同时使用
*/namespace CH
{// 抽象格式化子项基类class FormatItem{public:// 重命名using prt = std::shared_ptr<FormatItem>;					   // 等于typedef  std::shared_ptr<FormatItem>  previrtual ~FormatItem(){};virtual void format(std::ostream &out, const LogMsg &msg) = 0; // 用于将LogMsg各个字段格式化到指定out流中};// 派生格式化子项子类 -- 消息class PayloadFormatItem : public FormatItem{public:virtual void format(std::ostream &out, const LogMsg &msg) override{out << msg._payload;}};// 派生格式化子项子类 -- 等级class LevelFormatItem : public FormatItem{public:virtual void format(std::ostream &out, const LogMsg &msg) override{out << LogLevel::tostring(msg._level);}};// 派生格式化子项子类 -- 文件名class FileNameFormatItem : public FormatItem{public:virtual void format(std::ostream &out, const LogMsg &msg){out << msg._filename;}};// 派生格式化子项子类 -- 行号class LineFormatItem : public FormatItem{public:virtual void format(std::ostream &out, const LogMsg &msg){out << msg._line;}};// 派生格式化子项子类 -- 日志器名字class LognameFormatItem : public FormatItem{public:virtual void format(std::ostream &out, const LogMsg &msg){out << msg._logname;}};// 派生格式化子项子类 -- 线程idclass TidFormatItem : public FormatItem{public:virtual void format(std::ostream &out, const LogMsg &msg){out << msg._tid;}};// 派生格式化子项子类 -- 时间class TimeFormatItem : public FormatItem{public:TimeFormatItem(const std::string &format = "%H:%M:%S"): _format(format){}virtual void format(std::ostream &out, const LogMsg &msg){struct tm t;// 将时间戳,转换为各种时间存储在t结构体中localtime_r(&msg._ctime, &t);char ch[32];// 按照_format格式,从结构体t中,取时间,并将时间转化为字符串,放入ch中strftime(ch, 32, _format.c_str(), &t);out << ch;}private:std::string _format;};// 派生格式化子项子类 -- 其他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;};// 派生格式化子项子类 -- "\t"class TabFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg) override{out << '\t';}};// 派生格式化子项子类 -- "\n"class NewLineFormatItem : public FormatItem{public:void format(std::ostream &out, const LogMsg &msg) override{out << '\n';}};//格式化字符串class Formater{public:using prt=std::shared_ptr<Formater>;// 时间{年-月-日 时:分:秒}缩进 线程ID 缩进 [日志级别] 缩进 [日志名称] 缩进 文件名:行号 缩进 消息换行Formater(const std::string &pattern = "[%d{%H:%M:%S}][%t][%p][%c][%f:%l] %m%n"): _pattern(pattern){assert(parsePattern()); // 判断输进来的字符串格式是否正确,如果错误,则没有必要进行了}//这里的参数,是由我们自己输入的,是一个给用户使用的接口void format(std::ostream& out,const LogMsg& msg){//_items里面存储的是各个子类的实例化,该实例化已经在parsePattern函数中完成了for(auto item:_items){item->format(out,msg);}}/*  对msg进行格式化*/std::string format(const LogMsg &msg) {std::stringstream ss;format(ss, msg);return ss.str();}private:// 将子类实例化,并且返回一个智能指针,后面存储在vector中FormatItem::prt 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<TidFormatItem>();if (key == "c")return std::make_shared<LognameFormatItem>();if (key == "f")return std::make_shared<FileNameFormatItem>();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>();if (key.empty())return std::make_shared<OtherFormatItem>(val);//这里传来的是原始字符串std::cout<<"没有对应的格式化字符%"<<key<<endl;assert(false);   return FormatItem::prt();}//对格式化字符串进行解析bool parsePattern(){//  默认格式 "%d{%H:%M:%S}%T%t%T[%p]%T[%c]%T%f:%l%T%m%n"std::vector<pair<std::string,std::string>> fmt_order;//对格式化字符,进行解析,为调用子类实例化做准备size_t pos=0;std::string key,val;while(pos<_pattern.size()){//首先找到%d,不是就是原始字符串,后将其放入val中if(_pattern[pos]!='%'){val.push_back(_pattern[pos]);pos+=1;continue;}//"asd%%ddddddac%d{%H:%M:%S}%T%t%T[%p]%T[%c]%T%f:%l%T%m%n//防止将%作为字符串if(pos+1<_pattern.size()&&_pattern[pos+1]=='%'){val.push_back(_pattern[pos+1]);pos+=2;continue;}//走到这里,已经1找到了第一个格式化字符的%,所有将存储在val中的原始字符串,存入数组中//后调用其他子类进行实例化输出// 万一出现第一个自符就是格式化字符串,那么处理原始字符串的操作就会向数组插入{"",""}// 虽然不会产生错误但是便于逻辑理解,最好还是判断处理一下if (!val.empty()) {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;// 此时pos指向格式化字符串后面的位置,判断是否有格式化子串if(pos<_pattern.size()&&_pattern[pos]=='{'){// 这时pos指向子规则的起始位置pos+=1;while(pos<_pattern[pos]&&_pattern[pos]!='}'){val.push_back(_pattern[pos]);pos+=1;}// 若走到了末尾,还没有找到},则说明格式是错误的,跳出循环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:const std::string _pattern;			 /// 用于存储字符串的格式std::vector<FormatItem::prt> _items; // 格式化子项数组};}
7.5 日志落地类设计(工厂模式)

功能:将格式化完成后的日志消息字符串,输出到指定位置

目前实现了三个不同方向的日志落地,并且包含一个扩展示例,用户可以根据示例添加自己的扩展实现更多的日志落地方式

  • 标准输出:StdoutSink

  • 固定文件:FileSink

  • 滚动文件:RollSizeSink(根据大小滚动)

滚动日志文件的必要性:

由于机器的磁盘空间是有限的,我们不可能一直无限的向某一个文件中增加数据。如果一个文件的体积很大,一方面是不好打开,另一方面数据量过大也不利于我们查找需要的信息

实际开发中也会对单个日志文件的大小进行一些控制,当某个文件的大小超过限制大小时(比如1GB),我们就会重新创建一个新的日志文件来滚动写日志,对于那些过期的日志,大部分企业内部都会有专门的韵味人员清理过期的日志,或者在系统内部设置定时任务,定时清理过期日志

日志滚动的方式:这里实现了根据大小滚动(比如超过1GB就更换新文件),时间滚动的方式(每一天写一个文件)

日志落地类的实现思想:

1、抽象出落地模块类

2、不同落地方向从基类进行派生

3、使用工厂模式进行创建和表示分离

抽象落地基类

基类是一个纯虚函数,其就包含一个又用的成员函数log()其作用就是将内存中data开始的len字节的数据输出到指定位置

// 基类
class LogSink
{
public:using prt = std::shared_ptr<LogSink>;virtual ~LogSink() {}// 将从data开始len字节的数据输出到指定位置virtual void log(const char *data, size_t size) = 0;
};

StdoutSink 派生类

该类用于将内存数据输出到标准输出,输出函数的编写十分简单使用std::cout对象的write()函数,因为我们不能确定日志消息是否是以’\0’结尾,使用std::cout<<输出可能会超出len个字节

// 落地方向: 标准输出
class StdoutSink : public LogSink
{
public:// 将日志消息写到标准输出void log(const char *data, size_t size) override{// 不要直接使用std::cout << 进行输出,因为不一定是字符串需要按照长度输出std::cout.write(data, size);}
};

FileSink 派生类

生类包含两个成员变量:_pathname(用于标记日志输出文件路径),ofs(用于标记打开的文件)

// 落地方向: 指定文件
class FileSink : public LogSink
{
public:构造时传入文件名,并打开文件,将操作句柄管理起来FileSink(const std::string &pathname): _pathname(pathname){// 创建目录,如果该文件的路径存在,则不会创建CH::Util::file::create_directory(Util::file::file_path(_pathname));// std::ios::binary:以二进制模式打开文件,用于处理二进制数据。// std::ios::app:以追加模式打开文件,新写入的数据会被添加到文件末尾。_ofs.open(_pathname, std::ios::binary | std::ios::app);//如果没有该文件,则会直接创建文件assert(_ofs.is_open());}// 将数据写入流中void log(const char *data, size_t size) override{_ofs.write(data, size);// 检查流的状态是否良好assert(_ofs.good());}private:std::ofstream _ofs;std::string _pathname; // 路径名
};

使用文件落地方式的话我们需要将日志文件的路径告诉落地器,落地器在构造的时候会创建并打开对应的文件目录,log()函数只要把内存中的数据写入到文件当中就可以了

ofstream 使用 : 代码中的ofs代表ofstream类型的对象

ofs.open(file_name, mode)  // 打开文件.  
ofs.is_open()        // 判断文件是否打开成功
ofs.write(data, len) // 写文件    
ofs.good()         // 若文件读或写失败,某些字段会被设置,调用good()返回false

RollBySizeSink

整体框架:

	// 落地方向: 翻滚文件class RollSizeSink : public LogSink{public:RollSizeSink(const std::string &basename, size_t max_filesize): _basename(basename), _max_filesize(max_filesize), _cur_filesize(0), _file_count(0){}// 将数据写入文件void log(const char *data, size_t size) override{}private:// 创建新文件名std::string CreateNewFile(){}private:std::string _basename; // 基础文件名 + 扩展文件名(以时间来生成) = 实际输出文件名/std::ofstream _ofs;	   // 操作句柄size_t _max_filesize;  // 记录文件允许存储最大数据量size_t _cur_filesize;  // 记录当前文件已经写入数据大小size_t _file_count;	   // 记录当前文件的数量};

成员变量:

  • _basename : 基础文件名

  • _ofs : 操作句柄,内存输出到的文件流

  • _max_filesize : 单文件的最大储存数据量

  • _cur_filesize : 当前文件储存数据量

  • _file_count. : 滚动文件数量

成员函数

  • RollBySizeSink(const std::string &basename, size_t _max_filesize) : 构造函数

需要由日志器传入基础的文件名称,单文件的最大存储数据量,在初始化日志落地器的时候需要构建日志文件,并打开。构建日志文件的操作被封装成了一个函数,稍后讲解 

RollSizeSink(const std::string &basename, size_t max_filesize): _basename(basename), _max_filesize(max_filesize), _cur_filesize(0), _file_count(0)
{std::string pathname = CreateNewFile();// 返回文件的目录,并且判断,如果目录不存在则创建Util::file::create_directory(Util::file::file_path(pathname));// 以追加的方式打开文件_ofs.open(pathname, std::ios::binary | std::ios::app);assert(_ofs.is_open());
}
  • void log(const char *data, size_t size) : 将数据写入到指定文件

 实现流程:

  • 首先需要判断文件是否还有足够空间,若空间足够就直接写入,若空间不足需要创建一个新的文件进行写入

  • 创建新文件需要先将原文件关闭,再打开新文件

  • 向文件中写入size字节数据后需要更新_cur_filesize字段

// 将数据写入文件
void log(const char *data, size_t size) override
{// 如果文件满了,则重新创建文件if (_cur_filesize + size > _max_filesize){// 关闭原来的文件_ofs.close();// 得到新文件的名字std::string pathname = CreateNewFile();_cur_filesize = 0;Util::file::create_directory(Util::file::file_path(pathname));_ofs.open(pathname, std::ios::binary | std::ios::app);assert(_ofs.is_open());}// 如果文件没有满,则直接写入_ofs.write(data, size);_cur_filesize+=size;assert(_ofs.good());
}
  • std::string createNewFile() : 根据时间创建新的滚动文件

实现流程:

  • 获取当前时间,并使用localtime_r()函数对时间进行格式化

  • 使用字符流stringstream来构建文件名称

  • basename + 格式化时间 + 滚动文件个数 + .log(后缀) 构建文件名

// 创建新文件名
std::string CreateNewFile()
{_file_count += 1;// 获取系统时间time_t now = Util::date::now();struct tm t;localtime_r(&now, &t);std::stringstream filename;filename << _basename;filename << (t.tm_year + 1900);filename << "-";filename << (t.tm_mon + 1);filename << "-";filename << t.tm_mday;filename << " ";filename << t.tm_hour;filename << ":";filename << t.tm_min;filename << ":";filename << t.tm_sec;filename << "-";filename << _file_count;filename << ".log";return filename.str();
}

落地器简单工厂

由于传统的简单工厂是通过if else语句根据产品的类型,进行生产。导致我们想要增加产品就需要修改工厂源代码,不符合开闭原则。所以我们这里使用了 模版 + 可变参数的工厂模式来替换if else语句

实现思路:

  • 工厂的静态成员函数与类型进行绑定,在编译阶段根据代码生成响应落地器类型的create()静态成员函数

  • 由于各个落地器的构建参数不同,我们使用可变参数进行替换

  • 将参数包传递给make_shared函数进行构建对应的落地器对象,并使用std::forward()函数保持变量属性,实现完美转发提高性能

//落地工厂
class SinkFactory
{
public:template <typename SinkType, typename... Args>static LogSink::prt create(Args &&...args){return std::make_shared<SinkType>(std::forward<Args>(args)...);}
};

这样如果再增加落地器的类型只需要增加对应的落地器实例代码即可,不需要修改我们原有的工厂代码,符合开闭原则

日志落地器完整代码:

#pragma once
#include <memory>
#include <iostream>
#include <fstream>
#include <string>
#include <cassert>
#include <ctime>
#include <sstream>
#include <error.h>
#include "util.hpp"namespace CH
{// 基类class LogSink{public:using prt = std::shared_ptr<LogSink>;virtual ~LogSink() {}// 将从data开始len字节的数据输出到指定位置virtual void log(const char *data, size_t size) = 0;};// 落地方向: 标准输出class StdoutSink : public LogSink{public:// 将日志消息写到标准输出void log(const char *data, size_t size) override{// 不要直接使用std::cout << 进行输出,因为不一定是字符串需要按照长度输出std::cout.write(data, size);}};// 落地方向: 指定文件class FileSink : public LogSink{public:构造时传入文件名,并打开文件,将操作句柄管理起来FileSink(const std::string &pathname): _pathname(pathname){// 创建目录,如果该文件的路径存在,则不会创建CH::Util::file::create_directory(Util::file::file_path(_pathname));// std::ios::binary:以二进制模式打开文件,用于处理二进制数据。// std::ios::app:以追加模式打开文件,新写入的数据会被添加到文件末尾。_ofs.open(_pathname, std::ios::binary | std::ios::app);//如果没有该文件,则会直接创建文件assert(_ofs.is_open());}// 将数据写入流中void log(const char *data, size_t size) override{_ofs.write(data, size);// 检查流的状态是否良好assert(_ofs.good());}private:std::ofstream _ofs;std::string _pathname; // 路径名};// 落地方向: 翻滚文件class RollSizeSink : public LogSink{public:RollSizeSink(const std::string &basename, size_t max_filesize): _basename(basename), _max_filesize(max_filesize), _cur_filesize(0), _file_count(0){std::string pathname = CreateNewFile();// 返回文件的目录,并且判断,如果目录不存在则创建Util::file::create_directory(Util::file::file_path(pathname));// 以追加的方式打开文件_ofs.open(pathname, std::ios::binary | std::ios::app);assert(_ofs.is_open());}// 将数据写入文件void log(const char *data, size_t size) override{// 如果文件满了,则重新创建文件if (_cur_filesize + size > _max_filesize){// 关闭原来的文件_ofs.close();// 得到新文件的名字std::string pathname = CreateNewFile();_cur_filesize = 0;Util::file::create_directory(Util::file::file_path(pathname));_ofs.open(pathname, std::ios::binary | std::ios::app);assert(_ofs.is_open());}// 如果文件没有满,则直接写入_ofs.write(data, size);_cur_filesize+=size;assert(_ofs.good());}private:// 创建新文件名std::string CreateNewFile(){_file_count += 1;// 获取系统时间time_t now = Util::date::now();struct tm t;localtime_r(&now, &t);std::stringstream filename;filename << _basename;filename << (t.tm_year + 1900);filename << "-";filename << (t.tm_mon + 1);filename << "-";filename << t.tm_mday;filename << " ";filename << t.tm_hour;filename << ":";filename << t.tm_min;filename << ":";filename << t.tm_sec;filename << "-";filename << _file_count;filename << ".log";return filename.str();}private:std::string _basename; // 基础文件名 + 扩展文件名(以时间来生成) = 实际输出文件名/std::ofstream _ofs;	   // 操作句柄size_t _max_filesize;  // 记录文件允许存储最大数据量size_t _cur_filesize;  // 记录当前文件已经写入数据大小size_t _file_count;	   // 记录当前文件的数量};//落地工厂class SinkFactory{public:template <typename SinkType, typename... Args>static LogSink::prt create(Args &&...args){return std::make_shared<SinkType>(std::forward<Args>(args)...);}};}
7.6 日志器类设计(建造者模式)

日志器是我们日志系统的核心,其负责和前端交互,当我们需要打印日志的时候,只需要获取对应的日志器对象(Logger),调用该对象的debug,info, warm,,error, fatal 方法 就可以打印日志,日志器支持解析可变参数列表和输出格式,即可以像printf函数一样打印日志

当前日志系统支持 同步日志,异步日志两种模式,两种日志器唯一不同的地方在于日志的落地方式有所不同

  • 同步日志器:直接对日志消息进行输出

  • 异步日志器,将日志消息放入缓冲区中,由异步线程进行日志落地

因为两种日志器在接口的设计,功能的实现上都非常类似,我们在设计时先设计出一个Logger基类,在基类的基础上派生出SynchLogger 同步日志器 和 AynchLogger 异步日志器

又因为日志器模块是对前面多个模块的整合,创建一个日志器需要设置日志器名称,设置日志器的输出等级,设置日志器的输出格式,设置落地方向(可能存在多个,使用数组构建),整个日志器的创建过程较为复杂,为了保证良好的代码风格,编写出优雅的代码,我们选择使用建造者模式进行创建

日志器基类

	//日志器基类class Logger{public:using prt=std::shared_ptr<Logger>;Logger(const string& logger_name,LogLevel::value limit_level,Formater::prt& formatter,std::vector<LogSink::prt>& sinks):_logger_name(logger_name),_limit_level(limit_level),_formatter(formatter),_sinks(sinks.begin(),sinks.end()){}//获取日志器名const std::string& logger_name() { return _logger_name;}/* 构造日志消息对象过程, 并得到格式化后的日志消息字符串-- 然后进行落地输出*///为什么只需要传入这三个参数?logger_name是成员变量,level是默认等级,其他的都不用用户主动传递void debug(const std::string& filename,size_t line,const std::string& fmt,...){}void info(const std::string& filename,size_t line,const std::string& fmt,...){}void warn(const std::string& filename,size_t line,const std::string& fmt,...){}void error(const std::string& filename,size_t line,const std::string& fmt,...){}void fatal(const std::string& filename,size_t line,const std::string& fmt,...){}protected:/* 抽象接口完成实际的落地输出 -- 不同的日志器会有不同的实际落地方式 */virtual void log(const char* data, size_t len) = 0;protected:std::mutex _mutex;//定义锁std::string _logger_name;//日志器名称//由于每一条输出消息,都要与其比较,大于则输出,小与则不输出,所以调用非常频繁,即将其设置为原子级别,减少锁的调用std::atomic<LogLevel::value> _limit_level;Formater::prt _formatter;//传进来需要输出的格式,格式化模块对象,将LogMsg对象格式化成指定字符串std::vector<LogSink::prt> _sinks;//由于落地方向可能不止一种,即使用数组进行管理};

成员变量:

  • _mutex 互斥锁,保证日志输出是线程安全的,不会出现交叉日志(多个线程使用同一个日志器在同一时刻一起打印会产生数据的交叉污染)

  • _fomatter 格式化模块对象,将LogMsg对象格式化成指定字符串

  • _sinks 落地器对象数组(一个日志器的一条日志可能会在多个位置进行日志输出)

  • _limit_level 日志器默认的最低日志输出等级(通过日志等级过滤日志)

  • _logger_name 日志器名称 (用于标识日志器,方便用户查找对应日志器)

成员函数

构造函数:需要对日志器名称,日志器默认最低日志输出等级,格式化模块对象,落地器对象数组进行初始化

Logger(const string& logger_name,LogLevel::value limit_level,Formater::prt& formatter,std::vector<LogSink::prt>& sinks):_logger_name(logger_name),_limit_level(limit_level),_formatter(formatter),_sinks(sinks.begin(),sinks.end())
{}
  • const std::string& logger_name() : 获取日志器名称

//获取日志器名
const std::string& logger_name() { return _logger_name;}
  • void debug(const std::string& filename,size_t line,const std::string& fmt,...)

功能:对日志等级为Debug日志消息对象进行构造,获取格式化日志消息字符串,并进行落地输出

实现思路:

关于函数传参:我们需要获取到日志输出的文件以及行号,用户输入的日志信息的格式以及传入的不定参数

1、判断当前日志等级是否到达输出标准(低于输出标准的日志就可以直接跳过了)

2、对fmt格式化字符串和不定参进行字符串组织,得到日志消息的字符串

3、构造LogMsg对象

4、通过格式化工具_formatter对LogMsg进行格式化,获取格式化后的日志字符串

5、进行日志落地

/* 构造日志消息对象过程, 并得到格式化后的日志消息字符串-- 然后进行落地输出*/
//为什么只需要传入这三个参数?logger_name是成员变量,level是默认等级,其他的都不用用户主动传递
void debug(const std::string& filename,size_t line,const std::string& fmt,...)
{// 1、判断当前日志等级是否达到输出标准,小于则不输出if(LogLevel::value::DEBUG<_limit_level) return;// 2、 fmt是格式例:"%d%s",会根据fmt的格式从不定参中取对应对字符va_list ap;va_start(ap, fmt);//使ap指向fmtchar* ret=nullptr;//定义缓冲区int n = vasprintf(&ret, fmt.c_str(), ap);//根据fmt的格式,将可对应的可变参数放入ret缓冲区中if(n==-1) {cout<<"vasprintf false"<<endl;return;}va_end(ap);//关闭指针,防止内存泄露// 3、 构造LogMsg对象LogMsg msg(filename,_logger_name,line,LogLevel::value::DEBUG,ret);// 4、 通过格式化工具对LogMsg进行格式化,获得格式化后的日志字符串std::stringstream ss;_formatter->format(ss, msg);//例:[%d{%H:%M:%S}][%t][%p][%c][%f:%l] %m%n// 5、 进行日志落地log(ss.str().c_str(), ss.str().size());//传入需要输出的字符串和字符串的大小free(ret);   // vasprintf() 内部开辟空间了,是动态申请的,需要我们手动释放
}

进一步解释

步骤一:明明LogMsg字段非常多,为什么只需要传入这几个参数呢?因为,我们采用日志器输出日志的函数Debug,Info,Warn等其自身就代表了输出日志的日志等级。而日志器的名称是Logger对象的成员变量,而该函数一定是在由打印线程内部被调用的,所以我们可以在构建时采用对应函数进行获取。没必要都让用户传入,简化用户使用

步骤三:对fmt格式化字符串和不定参数进行字符串组织中我们使用了vasprintf()函数

在上文的前置知识中我们说到,函数调用会将函数实参压入函数的栈帧中,我们可以根据上一个参数的末尾找到下一个参数的开头。所以我们只需要知道fmt的位置就可以推断不定参数的起始位置,这个工作实际上就是由va_start()函数进行实现的

int vasprintf(char **strp, const char *fmt, va_list ap);
  • strp : 二级指针,vasprintf会自己动态开辟一块空间,将我们格式化好的数据放入其中。然后将我们传入的strp指针指向这块区域的起始位置(数据是动态开辟的,用完之后需要手动free)

  • fmt :用户传入的格式化字符串,就是printf中的第一个参数

  • ap : 一个char*类型的指针,指向不定参数的起始位置

  • 成功返回输出的字节数,失败返回-1

vasprintf就帮助我们解析fmt格式化字符串,取出不定参数,组织成指定字符串输出到内存中。不需要我们自己手动解析格式化字符串,使用va_arg()函数一个个取出不定参数,并自己拼接

步骤五:日志落地,因为我们的日志有同步以及异步等落地方式,所以我们将落地函数log设计成纯虚函数,由派生类自己重写,我们只需要将需要输出的字符串还有它的长度交给log函数就可以了

关于下面的info(), warn(),error(),fatal()等 函数设计思路和debug()是一样的

同步日志器

同步日志器没有自己的成员函数,构造函数中我们只需要显示调用父类的构造函数进行填充即可,以下是同步日志器的工作流程

同步日志器的日志落地函数直接使用落地器进行日志落地操作,我们的线程串行等待日志写入外设,等待全部写完后,继续执行后续业务逻辑

//同步日志器
class SynchLogger:public Logger
{
public:SynchLogger(const string& logger_name,LogLevel::value limit_level,Formater::prt& formatter,std::vector<LogSink::prt>& sinks):Logger(logger_name,limit_level,formatter,sinks){}
protected://将数据通过不同的落地方式,输出void log(const char* data,size_t len) override{std::unique_lock<std::mutex> lock(_mutex);//加锁,等log函数释放后,锁也会自动释放if(_sinks.empty()) return;for(auto& sink:_sinks){sink->log(data,len);}}
};

异步日志器

设计思路:

  • 异步日志器的日志落地并不由业务线程做,业务线程只负责将日志数据拷贝到日志缓冲区中,然后继续执行业务程序即可,无需等待日志数据写到外设

  • 日志数据的实际落地工作由异步任务处理器(lopper)进行处理,我们在异步日志器启动时创建异步任务处理器,并在异步日志器中写好日志的实际落地方案(realLog),将这个方案传递给我们的异步任务处理器。

  • 异步任务处理器启动后会创建异步线程,该线程的工作就是不断循环的从到我们的日志缓冲区中获取数据,并根据异步日志器传入的实际落地方案对获取到的数据进行处理,负责数据的实际落地

异步日志器整体框架:

	//异步日志器class AsynchLogger:public Logger{public:AsynchLogger(const string& logger_name,LogLevel::value limit_level{}//将数据放入缓冲区中void log(const char* data,size_t len){}//从缓冲区中拿到数据,并且通过不同的落地方式,进行输出void realLog(Buffer& buf){}pritate:AsynchWork::prt _work;};

成员变量:

  • _looper : 异步任务处理器,每一个异步日志器都要搭载一个异步任务处理器,负责日志的实际落地任务

成员函数:

构造函数:异步日志器和同步日志器的构造函数唯一的区别就是异步日志器需要构建一个异步任务处理器_looper,我们需要传递给异步任务处理器一个日志落地函数,以及异步任务处理器的类型(安全与非安全)

AsynchLogger(const string& logger_name,LogLevel::value limit_level,Formater::prt& formatter,std::vector<LogSink::prt>& sinks):Logger(logger_name,limit_level,formatter,sinks),_work(std::make_shared<AsynchWork>(std::bind(&AsynchLogger::realLog, this, std::placeholders::_1))){}
  • void log(const char *data, size_t len): 调用异步任务处理器将日志数据拷贝到缓冲区 

//将数据放入缓冲区中
void log(const char* data,size_t len)
{_work->push(data,len);
}
  •  void realLog(Buffer &buf): 由异步任务处理器内部日志处理线程调用,取出缓冲区中的数据使用异步落地器中的落地器数组对日志数据进行落地

这里我们使用了C++11中的包装器,由于realLog函数是一个成员函数,所以我们需要绑定一个函数,该函数的第一个参数绑定为异步日志器的this指针,这样我们在包装器中使用该函数时就只需要传递一个Buffer参数就好了

//从缓冲区中拿到数据,并且通过不同的落地方式,进行输出
void realLog(Buffer& buf)
{if(_sinks.empty()) return;for(auto& sink:_sinks){sink->log(buf.begin(),buf.readAbleSize());}
}

日志器建造类设计思路

使用建造者模式来构建日志器,不让用户直接去构造日志器,简化用户的使用复杂度

1、抽象一个日志器建造者类(完成日志器对象所需的零部件构造,然后再进行日志器构建)

 (一) 设置日志器类型

​ (二) 将不同的日志器的创建放到同一个日志器建造者类(基类)中完成

2、派生出具体的建造者类, 局部日志器建造者 & 全局日志器建造者。为了方便后边添加了全局单例日志器管理器之后,将日志器添加到全局管理中

步骤一:日志器建造者基类设计

设置日志器类型

我们有两种日志器,同步日志器 & 异步日志器,我们使用枚举类型将其封装一下

//同步或异步
enum class LoggerType
{LOGGER_SYNCH,//同步LOGGER_ASYNCH//异步
};

构建日志器基类

//日志器建造者——基类
class LoggerBuilder
{
public:LoggerBuilder():_logger_type(LoggerType::LOGGER_SYNCH),_logger_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) {_logger_level=level;}//将日志器的字符格式传入其中void buildLoggerFormatter(const std::string& formatter) {_formatter=std::make_shared<Formater>(formatter);}//将日志器的落地方式传入其中template<class SinkType,class...Args>void buildLoggerSink(Args...args){_sinks.push_back(SinkFactory::create<SinkType>(std::forward<Args>(args)...));}//建造日志器virtual Logger::prt build()=0;
protected:LoggerType _logger_type;			//日志器类型std::string _logger_name;			//日志器名字LogLevel::value _logger_level;		//日志器等级Formater::prt _formatter;			//日志器字符格式std::vector<LogSink::prt> _sinks;	//日志器落地方式
};

成员变量:

  • _logger_type : 日志器类型

  • _logger_name : 日志器名称

  • _logger _ level : 日志器最低输出等级

  • _formatter : 日志输出格式化器

  • _sinks : 落地器数组

基类的大部分构造成员函数都比较简单,就是将传入的一些参数构建对应的部件保存在建造器中,这里就不一一介绍了,看代码应该很容易理解

对于建造者模式的说明,因为日志器对于各个部件的构造顺序并没有要求,只需要各个部件齐全即可,所以没有必要构建指挥者,我们将指挥者中的build函数移到了建造者函数中,build函数负责建造日志器的工作

局部日志器构造

注意:日志器的名称是不能为空的,如果用户构建一个没有名字的日志器那么就不方便后续我们进行日志器查找,这是不行的,使用assert()断言一下

然后我们就根据建造者中构建日志器的类型判断构建同步日志器还是异步日志器,传入相关参数即可返回对应的日志器

//局部日志器建造者
class LocalLoggerBuilder:public LoggerBuilder
{
public:virtual Logger::prt build() override{assert(!_logger_name.empty());if(_formatter.get()==nullptr){_formatter=make_shared<Formater>();}//如果落地方式为空,则将<StdoutSink>类型放入if(_sinks.empty()){buildLoggerSink<StdoutSink>();}//如果为异步if(_logger_type==LoggerType::LOGGER_ASYNCH){return std::make_shared<AsynchLogger>(_logger_name, _logger_level, _formatter, _sinks);}return std::make_shared<SynchLogger>(_logger_name,_logger_level,_formatter,_sinks);}
};

全局日志器构造

全局日志器建造者和局部日志器建造者的唯一区别就是,全局日志器会自动添加构建出来的日志器到日志管理器单例对象当中

LoggerManager::getInstance().addLogger(logger);
//全局日志器建造者
class GlobalLoggerBuilder:public LoggerBuilder
{
public:virtual Logger::prt build() override{assert(!_logger_name.empty());if(_formatter.get()==nullptr){_formatter=make_shared<Formater>();}//如果落地方式为空,则将<StdoutSink>类型放入if(_sinks.empty()){buildLoggerSink<StdoutSink>();}Logger::prt _logger;//如果为异步if(_logger_type==LoggerType::LOGGER_ASYNCH){_logger=std::make_shared<AsynchLogger>(_logger_name, _logger_level, _formatter, _sinks);}else{_logger=std::make_shared<SynchLogger>(_logger_name,_logger_level,_formatter,_sinks);}//将创建的日志器,放入LoggerManager类中进行管理LoggerManager::GetInstance().AddLogger(_logger);return _logger;}
};
7.7 双缓冲区异步任务处理器设计

设计思想:异步处理线程+数据池

使用者将需要完成的任务添加到任务池中,由异步线程来完成任务的实际执行操作

任务池的设计思想:双缓冲区阻塞数据池

优势:避免了空间的频繁申请和释放,尽可能减少了生产者和消费者之间的锁冲突的概率,提高了任务处理的效率

在任务池的设计中,有很多备选方案,比如循环队列等,但是不管是哪一种都会涉及到锁冲突等情况,因为在生产者和消费者模型中,任何两个角色之间都必须具有互斥关系,因此每一次任务的添加与取出都有可能涉及到锁的冲突,而双缓冲区不同,双缓冲区是处理器将一个缓冲区中的任务全部处理完毕后,再交换两个缓冲区,重新对缓冲区的任务进行处理,虽然在多线程写入过程中也必须控制生产者串行写入,但是却大大减少了生产者和消费者之间的锁冲突,并且不需要频繁的申请释放空间

缓冲区类的设计:

整体框架:

class Buffer{public:Buffer(): _buffer(BUFFER_SIZE), _read_idx(0), _write_idx(0){}// 向缓冲区写入数据void push(const char *data, size_t len){}// 返回可写空间的长度size_t writeAbleSize(){}// 返回可读空间的长度size_t readAbleSize(){}// 返回可读数据的长度的首地址const char *begin(){}// 对于读指针向后偏移void moveReader(size_t len){}// 重制读写位置,初始化缓冲区void bufferReset(){}// 对_buffer实现交换的操作void bufferSwap(Buffer &buffer){}// 判断缓冲区是否为空bool bufferEmpty(){}private:// 对于写指针向后偏移void moveWriter(size_t len){}private:std::vector<char> _buffer; // 缓冲区size_t _read_idx;		   // 当前可读数据的下标size_t _write_idx;		   // 当前可写数据的下标};

1、_buffer : 管理一个存放字符串数据的缓冲区(使用vector进行空间管理)

2、_read_idx : 当前写入位置的指针(指向可写区域的起始位置,避免写入覆盖)

3、_write_idx : 当前读取数据位置的指针(指向刻度数据区域的起始位置,当读取指针和写入指针位置相同说明数据读完了)

  • void push(const char* data, size_t len) : 由异步线程器调用,将数据拷到我们的push poll中

实现思路:

  • 首先得判断缓冲区的剩余空间是否充足,若不足则阻塞

  • 将内存中处理好的日志数据拷贝到缓冲区中

  • 对读指针进行向后偏移操作

// 向缓冲区写入数据
void push(const char *data, size_t len)
{// 如果剩余空间不够则阻塞if (len > writeAbleSize())return;// 将数据拷贝到缓冲区中std::copy(data, data + len, &_buffer[_write_idx]);// 移动缓冲区中写的位置moveWriter(len);
}
  • size_t writeAbleSize():返回缓冲区剩余空间的大小

// 返回可写空间的长度
size_t writeAbleSize()
{return _buffer.size()-_write_idx;
}
  • size_t readAbleSize():返回可读空间的大小

// 返回可读空间的长度
size_t readAbleSize()
{return _write_idx-_read_idx;
}
  • const char *begin():返回可读数据的长度的首地址

// 返回可读数据的长度的首地址
const char *begin()
{return &_buffer[_read_idx];
}
  • void moveReader(size_t len): 对于读指针向后偏移

void moveReader(size_t len)
{assert(len <= readAbleSize());_read_idx += len;
}
  • void bufferReset():重制读写位置,初始化缓冲区

void bufferReset()
{_read_idx = 0;_write_idx = 0;
}
  • void bufferSwap(Buffer &buffer):对_buffer实现交换的操作

void bufferSwap(Buffer &buffer)
{std::swap(_buffer,buffer._buffer);std::swap(_read_idx,buffer._read_idx);std::swap(_write_idx,buffer._write_idx);
}
  • bool bufferEmpty():判断缓冲区是否为空

bool bufferEmpty()
{return _read_idx==_write_idx;
}
  • void moveWriter(size_t len):对于写指针向后偏移

void moveWriter(size_t len)
{assert(len <= writeAbleSize());_write_idx += len;
}
7.8 异步工作器设计

外界将任务数据添加到缓冲区中,异步线程对处理缓冲区中的数据进行处理,若处理缓冲区中没有数据了就交换缓冲区

#pragma once
#include "buffer.hpp"
#include <mutex>
#include <condition_variable>
#include <atomic> 
#include <thread>
#include <functional>namespace CH
{//异步工作器class AsynchWork{public:using prt=std::shared_ptr<AsynchWork>;//该函数是我们用户,传进来处理缓冲区数据的方法函数using Functor=std::function<void(Buffer&)>;AsynchWork(const Functor& callback):_callback(callback),_stop(false),_thread(&AsynchWork::ThreadEntry,this){}//停止运行异步工作器void Stop(){_stop=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():根据后面的表达式,为真则进入下一步,为假则释放锁并且阻塞,等待被唤醒_produce_cond.wait(lock,[&](){return len<=_produce_buffer.writeAbleSize();});//将数据放入生产缓冲区中_produce_buffer.push(data,len);//唤醒消费者对缓冲区中的数据进行处理_consumer_cond.notify_one();}//线程的入口函数,默认类中的函数都具有this指针,所以在将该函数设为入口函数时,需要传入this指针//对缓冲区中的数据进行处理,处理完后初始化缓冲区,交换缓冲区void ThreadEntry(){while(true){//给锁添加一个生命周期,当缓冲区交换完后,就解锁{std::unique_lock<std::mutex> lock(_mutex);//当有数据进来是会调用push函数,则会唤醒消费者,判断生产缓冲区中是否有数据,没有则阻塞等待_consumer_cond.wait(lock,[&](){return !_produce_buffer.bufferEmpty()||_stop;});if(_stop&&_produce_buffer.bufferEmpty()) break;//交换缓冲区_consumer_buffer.bufferSwap(_produce_buffer);//唤醒生产者_produce_cond.notify_one();}//走到这里,消费者缓冲区中有数据,则对,消费者缓冲区中的数据进行处理_callback(_consumer_buffer);//处理完数据后,则对缓冲区进行初始化_consumer_buffer.bufferReset();}}private:std::atomic<bool> _stop;   				  //控制异步工作器是关闭,还是打开std::mutex _mutex;                        //d定义锁,保证线程安全Buffer _produce_buffer;					  //生产者缓冲区Buffer _consumer_buffer;				  //消费者缓冲区std::condition_variable _produce_cond;	  //生产者条件变量std::condition_variable _consumer_cond;	  //消费者条件变量std::thread _thread;                      //创建线程Functor _callback;						  //定义回调函数};
}
7.9 单例日志器管理类设计和全局日志器建造者

日志的输出,我们希望在任意位置都可以进行,但是我们创建一个日志器后,就会受到日志器所在域的访问属性限制

因此为了突破访问区域的限制,我们创建一个日志器管理类,这个类是一个单例类,这样的话我们就可以在任意位置来通过管理器单例获取日志器来进行日志输出了

基于单例日志器管理器的设计思想,我们对于日志器建造者类进行继承,继承出一个全局日志器建造者类,实现一个日志器在创建完毕后,直接将其添加到单例的日志器管理器中,以便能够在任何位置通过日志器名称获取指定的日志器进行日志输出

//日志器管理者
class LoggerManager 
{
public://单例模式,将其设置为静态,在调用时只会实例化一次static LoggerManager& GetInstance(){static LoggerManager ret;return ret;}//将日志器添加进来void AddLogger(Logger::prt& logger){//判断是否存在,存在则不需要添加,直接返回if(IsLogger(logger->logger_name())) return;std::unique_lock<mutex> lock(_mutex);_loggers.insert(std::make_pair(logger->logger_name(),logger));}//判断该日志器是否在其中bool IsLogger(const std::string& logger_name){std::unique_lock<mutex> lock(_mutex);cout<<"8"<<endl;auto it=_loggers.find(logger_name);if(it==_loggers.end())return false;return true;}//查找日志器Logger::prt GetLogger(const std::string& logger_name){std::unique_lock<mutex> lock(_mutex);auto it=_loggers.find(logger_name);if(it==_loggers.end())return Logger::prt();return it->second;}//获取默认日志器Logger::prt RootLogger(){return _root_logger;}
private:LoggerManager(){std::unique_ptr<LoggerBuilder> builder(new LocalLoggerBuilder());builder->buildLoggerName("root");//构建默认日志器,并且返回_root_logger=builder->build();_loggers.insert(std::make_pair("root",_root_logger));}
private:std::mutex _mutex;					//保证线程安全Logger::prt _root_logger;			//默认日志器std::unordered_map<std::string,Logger::prt> _loggers;	//将日志器全部放入,统一管理
};
//全局日志器建造者
class GlobalLoggerBuilder:public LoggerBuilder
{
public:virtual Logger::prt build() override{assert(!_logger_name.empty());if(_formatter.get()==nullptr){_formatter=make_shared<Formater>();}//如果落地方式为空,则将<StdoutSink>类型放入if(_sinks.empty()){buildLoggerSink<StdoutSink>();}Logger::prt _logger;//如果为异步if(_logger_type==LoggerType::LOGGER_ASYNCH){_logger=std::make_shared<AsynchLogger>(_logger_name, _logger_level, _formatter, _sinks);}else{_logger=std::make_shared<SynchLogger>(_logger_name,_logger_level,_formatter,_sinks);}//将创建的日志器,放入LoggerManager类中进行管理LoggerManager::GetInstance().AddLogger(_logger);return _logger;}
};
7.10 日志宏&全局接口设计

提供全局的日志器获取接口

使用代理模式通过全局函数或宏函数来代理Logger类的log, debug. info, warn, error, fatal 等接口,以便控制源码文件名称和行号的输出控制,简化用户操作

当仅需标准输出日志的时候可以通过主日志器(默认日志器)打印日志。且操作时只需要通过宏函数直接进行输出即可

#pragma once
#include "logger.hpp"
#include <stdio.h>namespace CH
{// 1、提供获取指定日志器的全局接口(避免用户自己操作单例对象)Logger::prt GetGlobalLogger(const std::string logger_name){return CH::LoggerManager::GetInstance().GetLogger(logger_name);}//2、获取默认日志器的接口Logger::prt rootLogger(){return CH::LoggerManager::GetInstance().RootLogger();}//fmt表示接收字符串,我们将字符串传进来时传给fmt,我们的可变参数是传给__VA_ARGS__//加##是为了我们只传字符串,不传可变参数的情况,否则会有一个多余的逗号,#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, ...) CH::rootLogger()->debug(fmt, ##__VA_ARGS__)#define INFO(fmt, ...)  CH::rootLogger()->info (fmt, ##__VA_ARGS__)#define WARN(fmt, ...)  CH::rootLogger()->warn (fmt, ##__VA_ARGS__)#define ERROR(fmt, ...) CH::rootLogger()->error(fmt, ##__VA_ARGS__)#define FATAL(fmt, ...) CH::rootLogger()->fatal(fmt, ##__VA_ARGS__)}

8、单元测试

该模块编写的是项目编写过程中每一模块的测试代码,保证每个模块可以正常使用。可以用于测试日志器中各个模块的工作是否正常,测试一个日志器中包含所有的落地方向,观察是否每个方向都正常落地,分别测试同步方式和异步方式落地后数据是否正常

#include "util.hpp"
#include "level.hpp"
#include "formatter.hpp"
#include "message.hpp"
#include "logsink.hpp"
#include "logger.hpp"
#include <vector>
#include "chlog.hpp"void text_util()
{//返回当前时间戳std::cout<<CH::Util::date::now()<<std::endl;//返回目录std::cout<<CH::Util::file::file_path("./parh/mymkdir/text.txt")<<std::endl;//创建该目录CH::Util::file::create_directory("./parh/mymkdir/text.txt");}void text_level()
{std::cout<<CH::LogLevel::tostring(CH::LogLevel::value::DEBUG)<<std::endl;std::cout<<CH::LogLevel::tostring(CH::LogLevel::value::ERROR)<<std::endl;std::cout<<CH::LogLevel::tostring(CH::LogLevel::value::FATAL)<<std::endl;std::cout<<CH::LogLevel::tostring(CH::LogLevel::value::INFO)<<std::endl;std::cout<<CH::LogLevel::tostring(CH::LogLevel::value::OFF)<<std::endl;std::cout<<CH::LogLevel::tostring(CH::LogLevel::value::WARN)<<std::endl;
}string text_format()
{CH::LogMsg meg(__FILE__,"root",__LINE__,CH::LogLevel::value::DEBUG,"日志消息");CH::Formater fmt;//std::cout<<fmt.format(meg)<<std::endl;return fmt.format(meg);
}//落地方向测试
void text_sink()
{std::string str=text_format();CH::LogSink::prt stdout_lsp=CH::SinkFactory::create<CH::StdoutSink>();CH::LogSink::prt file_lsp=CH::SinkFactory::create<CH::FileSink>("./path/text.log");CH::LogSink::prt roll_lsp=CH::SinkFactory::create<CH::RollSizeSink>("./path/code-",1024*1024);stdout_lsp->log(str.c_str(),str.size());file_lsp->log(str.c_str(),str.size());size_t cursize = 0;size_t count = 0;while (cursize < 1024 * 1024 * 10) {std::string tmp = std::to_string(count++) + str;roll_lsp->log(tmp.c_str(), tmp.size());cursize += tmp.size();}
}//日志器测试
void text_logger()
{const std::string logger_name="sync_name";CH::LogLevel::value logger_level=CH::LogLevel::value::DEBUG;CH::Formater::prt fmt(new CH::Formater());CH::LogSink::prt stdout_lsp = CH::SinkFactory::create<CH::StdoutSink>();CH::LogSink::prt file_lsp = CH::SinkFactory::create<CH::FileSink>("./path/log.txt");CH::LogSink::prt roll_file_lsp = CH::SinkFactory::create<CH::RollSizeSink>("./path/roll-", 1024 * 1024);std::vector<CH::LogSink::prt> sinks = { stdout_lsp, file_lsp, roll_file_lsp };CH::Logger::prt logger_prt(new CH::SynchLogger(logger_name,logger_level,fmt,sinks));logger_prt->debug(__FILE__, __LINE__, "%s", "测试日志");logger_prt->info (__FILE__, __LINE__, "%s", "测试日志");logger_prt->warn (__FILE__, __LINE__, "%s", "测试日志");logger_prt->error(__FILE__, __LINE__, "%s", "测试日志");logger_prt->fatal(__FILE__, __LINE__, "%s", "测试日志");size_t cursize = 0, count = 0;std::string str = "测试日志";while (cursize < 1024 * 1024 * 10) {logger_prt->fatal(__FILE__, __LINE__, "测试日志-%d", count++);cursize += 20;}}//同步局部日志器测试
void text_LocalLoggerBuilder_synch()
{CH::LoggerBuilder::prt build(new CH::LocalLoggerBuilder());build->buildLoggerName("local_logger");build->buildLoggerLevel(CH::LogLevel::value::DEBUG);build->buildLoggerFormatter("%m%n");build->buildLoggerType(CH::LoggerType::LOGGER_SYNCH);build->buildLoggerSink<CH::StdoutSink>();build->buildLoggerSink<CH::FileSink>("./path/text.log");build->buildLoggerSink<CH::RollSizeSink>("./path/code-",1024*1024);CH::Logger::prt logger_prt=build->build();logger_prt->debug(__FILE__, __LINE__, "%s", "测试日志");logger_prt->info (__FILE__, __LINE__, "%s", "测试日志");logger_prt->warn (__FILE__, __LINE__, "%s", "测试日志");logger_prt->error(__FILE__, __LINE__, "%s", "测试日志");logger_prt->fatal(__FILE__, __LINE__, "%s", "测试日志");size_t cursize = 0, count = 0;std::string str = "测试日志";while (cursize < 1024 * 1024 * 3) {logger_prt->fatal(__FILE__, __LINE__, "测试日志-%d", count++);cursize += 20;}
}//异步局部日志器测试
void text_LocalLoggerBuilder_asynch() {CH::LoggerBuilder::prt builder(new CH::LocalLoggerBuilder());builder->buildLoggerType(CH::LoggerType::LOGGER_ASYNCH);builder->buildLoggerLevel(CH::LogLevel::value::WARN);builder->buildLoggerName("clx_asynch_logger");builder->buildLoggerFormatter("%m%n");builder->buildLoggerSink<CH::StdoutSink>();builder->buildLoggerSink<CH::FileSink>("./path/test.log");builder->buildLoggerSink<CH::RollSizeSink>("./path/roll-", 1024 * 1024);CH::Logger::prt logger_ptr = builder->build();logger_ptr->debug(__FILE__, __LINE__, "%s", "测试日志");logger_ptr->info (__FILE__, __LINE__, "%s", "测试日志");logger_ptr->warn (__FILE__, __LINE__, "%s", "测试日志");logger_ptr->error(__FILE__, __LINE__, "%s", "测试日志");logger_ptr->fatal(__FILE__, __LINE__, "%s", "测试日志");size_t cursize = 0, count = 0;std::string str = "测试日志";while (cursize < 1024 * 1024) {logger_ptr->fatal(__FILE__, __LINE__, "测试日志-%d", count++);cursize += 20;}
}//全局日志器测试
void text_global_LoggerManager() {CH::LoggerBuilder::prt builder(new CH::GlobalLoggerBuilder());builder->buildLoggerType(CH::LoggerType::LOGGER_ASYNCH);builder->buildLoggerLevel(CH::LogLevel::value::WARN);builder->buildLoggerName("clx_asynch_logger");builder->buildLoggerFormatter("%m%n");builder->buildLoggerSink<CH::StdoutSink>();builder->buildLoggerSink<CH::FileSink>("./path/test.log");builder->buildLoggerSink<CH::RollSizeSink>("./path/roll-", 1024 * 1024);builder->build();CH::Logger::prt logger_ptr = CH::LoggerManager::GetInstance().GetLogger("clx_asynch_logger");logger_ptr->debug(__FILE__, __LINE__, "%s", "测试日志");logger_ptr->info (__FILE__, __LINE__, "%s", "测试日志");logger_ptr->warn (__FILE__, __LINE__, "%s", "测试日志");logger_ptr->error(__FILE__, __LINE__, "%s", "测试日志");logger_ptr->fatal(__FILE__, __LINE__, "%s", "测试日志");size_t count = 0;std::string str = "测试日志";while (count < 1e5 * 5) {logger_ptr->fatal(__FILE__, __LINE__, "测试日志-%d", count++);}
}void text_chlog() {CH::LoggerBuilder::prt builder(new CH::GlobalLoggerBuilder());builder->buildLoggerLevel(CH::LogLevel::value::DEBUG);builder->buildLoggerName("clx_synch_logger");builder->build();CH::Logger::prt logger_ptr = CH::LoggerManager::GetInstance().GetLogger("clx_synch_logger");INFO("%s", "测试开始");DEBUG("%s", "测试日志");INFO("%s", "测试日志");WARN("%s", "测试日志");ERROR("%s", "测试日志");FATAL("%s", "测试日志");INFO("%s", "测试完毕");}int main()
{//text_util();//text_level();//text_format();//text_sink();//text_logger();//text_LocalLoggerBuilder();//text_LocalLoggerBuilder_asynch();//text_global_LoggerManager();text_chlog();return 0;
}

9、 性能测试

以下是对日志系统项目做的一个性能测试,测试一下平均每秒能公打印多少日志消息到文件

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

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

测试方法

#include "chlog.hpp"
#include <chrono>void bench(const std::string &logger_name, size_t thread_count, size_t msg_count, size_t msg_len) {/* 1.获取日志器           */CH::Logger::prt logger = CH::GetGlobalLogger(logger_name);if (logger.get() == nullptr) {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++) {threads.emplace_back([&, i](){/* 4.线程函数内部开始计时  */auto start = std::chrono::high_resolution_clock::now();/* 5.开始循环写日志       */for (int j = 0; j < msg_prt_thr; j++) {logger->fatal("%s", msg.c_str());}/* 6.线程函数内部结束计时  */auto end = std::chrono::high_resolution_clock::now();std::chrono::duration<double> cost = end - start;cost_array[i] = cost.count();std::cout << "线程[" << i << "]: " << "  输出日志数量:" << msg_prt_thr << ", 耗时:" << cost.count()  << "s" << std::endl;});}for (int i = 0; i < thread_count; i++) {threads[i].join();}/* 7.计算总耗时  多线程中,每个线程都有自己运行的时间,但是线程是并发处理的,因此耗时最多的那个才是总时间 */double max_cost = cost_array[0];for (int i = 0; i < thread_count; i++) {max_cost = max_cost > cost_array[i] ? max_cost : cost_array[i];}size_t msg_prt_sec = msg_count / max_cost;size_t size_prt_sec = (msg_count * msg_len) / (max_cost * 1024);/* 8.进行输出打印 */std::cout << "总耗时: " << max_cost << "s" << std::endl;std::cout << "每秒输出日志数量: " << msg_prt_sec  << " 条"  << std::endl;std::cout << "每秒输出日志大小: " << size_prt_sec << " KB" << std::endl; 
}void sync_bench() {CH::LoggerBuilder::prt builder(new CH::GlobalLoggerBuilder());builder->buildLoggerName("sync_logger");builder->buildLoggerFormatter("%m%n");builder->buildLoggerType(CH::LoggerType::LOGGER_SYNCH);builder->buildLoggerSink<CH::FileSink>("./logfile/sync.log");builder->buildLoggerSink<CH::RollSizeSink>("./logfile/roll-sync-by-size", 1024 * 1024);builder->build();bench("sync_logger", 1, 1000000, 100);bench("sync_logger", 4, 1000000, 100);
}void async_bench() {CH::LoggerBuilder::prt builder(new CH::GlobalLoggerBuilder());builder->buildLoggerName("async_logger");builder->buildLoggerFormatter("%m%n");builder->buildLoggerType(CH::LoggerType::LOGGER_ASYNCH);builder->buildLoggerSink<CH::FileSink>("./logfile/async.log");builder->buildLoggerSink<CH::RollSizeSink>("./logfile/roll-async-by-size", 1024 * 1024);builder->build();bench("async_logger", 1, 1000000, 100);bench("async_logger", 4, 1000000, 100);
}int main() {sync_bench();// async_bench();
// #define file_name "../logs/"
//     if (access(file_name, F_OK < 0)){
//         std::cout << "不存在" << std::endl;
//     }
//     std::cout << "存在" << std::endl;return 0;
}

测试结果

同步日志器/单线程

同步日志器/多线程

异步日志器/单线程

异步日志器/多线程

http://www.xdnf.cn/news/3999.html

相关文章:

  • WEB表单和表格标签综合案例
  • win10启动项管理在哪里设置?开机启动项怎么设置
  • Android工厂模式
  • 抽奖系统(基于Tkinter)
  • 微服务项目中网关服务挂了程序还可以正常运行吗
  • 数学复习笔记 2
  • JAVA在线考试系统考试管理题库管理成绩查询重复考试学生管理教师管理源码
  • JobHistory Server的配置和启动
  • LCD,LED
  • 期末项目Python
  • GoogleTest:GMock初识
  • 嵌入式开发学习日志Day13
  • window 系统 使用ollama + docker + deepseek R1+ Dify 搭建本地个人助手
  • C++笔记之接口`Interface`
  • 恶心的win11更新DIY 设置win11更新为100年
  • 《赤色世界》彩蛋
  • 数据封装的过程
  • 分析atoi(),atol()和atof()三个函数的功能
  • 【今日三题】小红的口罩(小堆) / 春游(模拟) / 数位染色(01背包)
  • 【Bootstrap V4系列】学习入门教程之 组件-卡片(Card)
  • Linux怎么更新已安装的软件
  • sudo useradd -r -s /bin/false -U -m -d /usr/share/ollama ollama解释这行代码的含义
  • 1.openharmony环境搭建
  • osquery在网络安全入侵场景中的应用实战(二)
  • 关于毕业论文,查重,AIGC
  • QT6 源(78):阅读与注释滑动条 QSlider 的源码,其是基类QAbstractSlider 的子类,及其刻度线的属性举例
  • 算法热题——等价多米诺骨牌对的数量
  • 【实战教程】React Native项目集成Google ML Kit实现离线水表OCR识别
  • 【云备份】服务端业务处理模块设计与实现
  • 2025-04-18-文本相似度-菜鸟