<项目> 日志系统

news2024/10/7 16:20:33

目录

前言:

一、项目介绍

二、为什么需要日志系统

三、日志系统技术实现

(一)同步写⽇志

(二)异步写⽇志

四、前置技术学习

(一)不定参宏函数

(二)C语言不定参数使用

(三)模拟实现printf

(四)C++风格不定参数使用

(五)设计模式

1. 六大原则

2. 单例模式

3. 工厂模式

4. 建造者模式

5. 代理模式

五、日志系统框架设计

(一)功能划分

(二)模块划分

(三)模块关系图

六、代码设计

(一)实用类设计

(二)日志等级类设计

(三)日志消息类设计

(四)日志输出格式化类设计

1. 格式化子项类的设计

2. 日志格式化类的设计

(五)日志落地类的设计

滚动文件的扩展

(六)日志器类的设计

1. 同步日志器类:

2. 构造日志器扩展

(七)异步日志器类:

1. 缓冲区设计思想:

单个缓冲区类的进一步设计

2. 异步工作器类设计

3. 异步日志器类设计

(八)单例日志器管理类设计

(九)日志全局接口设计

七、项目目录结构管理

(一)整体测试代码(功能样例代码)测试

(二)滚动文件扩展代码测试:

八、性能测试


前言:

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

作用:记录了程序运行状态信息,以便于程序员能够随时根据状态信息,对系统的运行状态进行分析。

功能:能够让用户非常简便的进行日志输出,以及控制。

一、项目介绍

本项⽬主要实现⼀个⽇志系统, 其主要⽀持以下功能:
  • ⽀持多级别⽇志消息
  • ⽀持同步⽇志和异步⽇志
  • ⽀持可靠写⼊⽇志到控制台、⽂件以及滚动⽂件中
  • ⽀持多线程程序并发写⽇志
  • ⽀持扩展不同的⽇志落地⽬标地

二、为什么需要日志系统

  • ⽣产环境的产品为了保证其稳定性及安全性是不允许开发⼈员附加调试器去排查问题, 可以借助日志系统来打印⼀些⽇志帮助开发⼈员解决问题。
  • 上线客户端的产品出现bug⽆法复现并解决,可以借助⽇志系统打印⽇志并上传到服务端帮助开发⼈员进⾏分析。
  • 对于⼀些高频操作(如定时器、⼼跳包)在少量调试次数下可能⽆法触发我们想要的⾏为,通过断点的暂停⽅式,我们不得不重复操作⼏⼗次、上百次甚⾄更多,导致排查问题效率是⾮常低下, 因此可以借助打印⽇志的⽅式查问题。
  • 在分布式、多线程/多进程代码中,出现bug⽐较难以定位,可以借助⽇志系统打印log帮助定位 bug。
  • 帮助⾸次接触项⽬代码的新开发⼈员理解代码的运⾏流程。

三、日志系统技术实现

⽇志系统的技术实现主要包括三种类型:

利⽤printfstd::cout等输出函数将⽇志信息打印到控制台

对于⼤型商业化项⽬, 为了⽅便排查问题,我们⼀般会将⽇志输出到⽂件或者是数据库系统⽅便查 询和分析⽇志, 主要分为同步⽇志异步⽇志⽅式:

(一)同步写⽇志

同步⽇志 是指 当输出⽇志时,必须等待⽇志输出语句执⾏完毕后,才能执⾏后⾯的业务逻辑语句,⽇志输出语句与程序的业务逻辑语句将在同⼀个线程运⾏。每次调⽤⼀次打印⽇志API就对应⼀次系统调⽤write写⽇志⽂件。

在⾼并发场景下,随着⽇志数量不断增加,同步⽇志系统容易产⽣系统瓶颈:
  • ⼀⽅⾯,⼤量的⽇志打印陷⼊等量的write系统调⽤,有⼀定系统开销。
  • 另⼀⽅⾯,使得打印⽇志的进程附带了⼤量同步的磁盘IO,影响程序性能。

(二)异步写⽇志

异步⽇志是指在进⾏⽇志输出时,⽇志输出语句与业务逻辑语句并不是在同⼀个线程中运⾏⽽是有专⻔的线程⽤于进⾏⽇志输出操作。业务线程只需要将⽇志放到⼀个内存缓冲区中不⽤等待即可继续执⾏后续业务逻辑(作为⽇志的⽣产者),而日志的落地操作交给单独的⽇志线程去完成(作为⽇志 的消费者),这是⼀个典型的⽣产-消费模型

这样做的好处是即使⽇志没有真的地完成输出也不会影响程序的主业务,可以提⾼程序的性能
  • 主线程调用日志打印接⼝成为⾮阻塞操作。
  • 同步的磁盘IO从主线程中剥离出来交给单独的线程完成。

四、前置技术学习

(一)不定参宏函数

上代码!

#include <stdio.h>

// 哪个文件的哪一行代码输出的日志
#define LOG(fmt, ...) printf("[%s:%d]" fmt, __FILE__, __LINE__, __VA_ARGS__);

int main()
{
    LOG("%d good morning\n", 100); // [test.cc:10]100 good morning

    return 0;
}
  • ​​​​fmt 格式字符串的占位符。
  • ... 表示这是一个可变参数宏,意味着你可以传递任意数量的参数给 LOG 宏,这些参数会被传递给 printf 函数(通过 __VA_ARGS__)。
  • __FILE__ 是一个预处理器宏,它在预处理时会被替换为当前源代码文件名(以字符串形式)。
  • __LINE__ 也是一个预处理器宏,它在预处理时会被替换为当前源代码行号(以整数形式)。
  • __VA_ARGS__ 在C和C++的宏定义中,__VA_ARGS__ 是一个GCC(GNU Compiler Collection)和其他一些C/C++编译器所支持的预处理特性,它允许宏接受可变数量的参数。当你有一个宏,其参数数量在每次调用时都可能不同时,这个特性就非常有用了。

但调用时如果只有格式字符串,没有可变参数,即:

LOG("good morning\n");

这样子就会导致编译错误,这行代码在宏展开后会是:

printf("[%s:%d]" "good morning\n", ); // 编译错误,注意这里多了一个逗号

所以__VA_ARGS__ 通常与 ## 操作符一起使用,以确保在宏调用中没有提供任何可变参数,那么宏展开后可能会在 printf 或其他函数的参数列表中留下一个额外的逗号,这会导致编译错误。

所以如果可变参数列表(...)为空,并且你希望在宏展开时不留下逗号,你可以使用 ## 操作符来“吃掉”前面的逗号。

#define LOG(fmt, ...) printf("[%s:%d]" fmt, __FILE__, __LINE__, ##__VA_ARGS__);
LOG("good morning\n"); // [test.cc:10]good morning

// 展开后
printf("[%s:%d]" "good morning\n"); // 正常运行,不存在逗号

(二)C语言不定参数使用

上代码!

#include <stdio.h>
#include <stdarg.h>

/* C语言中不定参函数的使用,不定参数据的访问 */
void printNum(int count, ...)
{
    va_list ap;
    va_start(ap, count); // 获取指定参数的起始地址,这里就是获取count参数之后的第一个参数的起始地址
    for(int i = 0; i < count; i++)
    {
        int num = va_arg(ap, int);
        printf("pargm[%d]:%d\n", i, num);
    }
    va_end(ap); // 将ap指针置空
}
int main()
{
    printNum(2, 100, 50);
    printNum(5, 1, 2, 3, 4, 5);
    return 0;
}

解释说明:

va_list

  • va_list是一个类型,用于声明一个对象,该对象可以持有可变参数的信息。你可以将它看作是一个指针或者一个能够存储关于参数列表的元数据的特殊类型。

va_start

  • va_start是一个,用于初始化 va_list 对象,并设置其指向可变参数列表中的第一个参数。它接受两个参数:一个是 va_list 对象,另一个是在可变参数列表之前的最后一个固定参数。

va_arg

  • va_arg是一个用于从 va_list 对象中获取下一个参数。它接受两个参数:一个是 va_list 对象,另一个是你想要获取的参数的类型va_arg 会更新 va_list 对象的内部状态,使其指向下一个参数。

va_end

  • va_end是一个,用于清理 va_list 对象,并在使用完毕后释放与之关联的任何资源。在调用 va_end 之后,你不应该再次使用同一个 va_list 对象,除非你又重新调用了 va_start 来初始化它。

(三)模拟实现printf

vasprintf 是一个 C 语言函数,它用于将格式化的字符串和可变参数列表组合成一个新的字符串,并返回这个新字符串的指针.

int vasprintf(char **strp, const char *fmt, va_list ap);
  • strp 是一个指向 char * 的指针,函数成功后会将新字符串的指针赋值给它。
  • fmt 是一个格式字符串,类似于 printf 函数的格式字符串。
  • ap 是一个 va_list 类型的参数,表示可变参数列表。

vasprintf 函数将按照 fmt 指定的格式和 ap 提供的参数生成一个字符串,并动态分配内存来存储这个字符串。然后,它将新字符串的指针存储在 *strp 中,并返回非负值表示成功。如果内存分配失败,vasprintf 将返回 -1,并且 *strp 的值不会被改变。

使用 vasprintf 函数的典型方式是与 va_startva_arg 和 va_end 一起,来处理可变参数列表。但需要注意的是,调用 vasprintf 后,你需要负责释放分配的内存,通常使用 free 函数来完成。

// #define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>

void myprintf(const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    char *res;
    int ret = vasprintf(&res, fmt, ap);
    if(ret != -1)
    {
        printf(res);
        free(res);
    }
    va_end(ap); // 将ap指针置空
}
int main()
{
    myprintf("[%s:%d]\n", __FILE__, __LINE__);
    myprintf("good morning!\n");
    return 0;
}

(四)C++风格不定参数使用

使用模版来扩展参数:

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>

/* C++中不定参函数的使用 */
void xprintf() { std::cout << std::endl; }

template<typename T, typename ...Args>
void xprintf(const T &v, Args &&...args)
{
    std::cout << v;
    if((sizeof ...(args)) > 0)
    {
        xprintf(std::forward<Args>(args)...);
    }
    else
    {
        xprintf();
    }
}

int main()
{
    xprintf(__FILE__, " ", __LINE__);
    xprintf("good morning!");
    return 0;
}

(五)设计模式

设计模式是前辈们对代码开发经验的总结,是解决特定问题的⼀系列套路。它不是语法规定,⽽是套⽤来提⾼代码可复⽤性、可维护性、可读性、稳健性以及安全性的解决⽅案。

1. 六大原则

单⼀职责原则(Single Responsibility Principle):
  • 类的职责应该单⼀,⼀个⽅法只做⼀件事。职责划分清晰了,每次改动到最⼩单位的⽅法或 类。
  • 使⽤建议:两个完全不⼀样的功能不应该放⼀个类中,⼀个类中应该是⼀组相关性很⾼的函数、数据的封装。
  • ⽤例:⽹络聊天:⽹络通信 & 聊天,应该分割成为⽹络通信类 & 聊天类。
开闭原则(Open Closed Principle):
  • 对扩展开放,对修改封闭。
  • 使⽤建议:对软件实体的改动,最好⽤扩展⽽⾮修改的⽅式。
  • ⽤例:超时卖货:商品价格---不是修改商品的原来价格,⽽是新增促销价格。
⾥⽒替换原则(Liskov Substitution Principle):
  • 通俗点讲,就是只要⽗类能出现的地⽅,⼦类就可以出现,⽽且替换为⼦类也不会产⽣任何错 误或异常。
  • 在继承类时,务必重写⽗类中所有的⽅法,尤其需要注意⽗类的protected⽅法,⼦类尽量不要暴露⾃⼰的public⽅法供外界调⽤。
  • 使⽤建议:⼦类必须完全实现⽗类的⽅法,孩⼦类可以有⾃⼰的个性。覆盖或实现⽗类的⽅法 时,输⼊参数可以被放⼤,输出可以缩⼩
  • ⽤例:跑步运动员类-会跑步,⼦类⻓跑运动员-会跑步且擅⻓⻓跑, ⼦类短跑运动员-会跑步且 擅⻓短跑
依赖倒置原则(Dependence Inversion Principle):
  • ⾼层模块不应该依赖低层模块,两者都应该依赖其抽象. 不可分割的原⼦逻辑就是低层模式,原⼦逻辑组装成的就是⾼层模块。
  • 模块间依赖通过抽象(接⼝)发⽣,具体类之间不直接依赖。
  • 使⽤建议:每个类都尽量有抽象类,任何类都不应该从具体类派⽣。尽量不要重写基类的⽅ 法。结合⾥⽒替换原则使⽤。
  • ⽤例:奔驰⻋司机类--只能开奔驰; 司机类 -- 给什么⻋,就开什么⻋; 开⻋的⼈:司机--依 赖于抽象。
迪⽶特法则(Law of Demeter),⼜叫“最少知道法则”:
  • 尽量减少对象之间的交互,从⽽减⼩类之间的耦合。⼀个对象应该对其他对象有最少的了解。 对类的低耦合提出了明确的要求:

    · 只和直接的朋友交流, 朋友之间也是有距离的。⾃⼰的就是⾃⼰的(如果⼀个⽅法放在本类 中,既不增加类间关系,也对本类不产⽣负⾯影响,那就放置在本类中)。
  • ⽤例:⽼师让班⻓点名--⽼师给班⻓⼀个名单,班⻓完成点名勾选,返回结果,⽽不是班⻓点 名,⽼师勾选。
接⼝隔离原则(Interface Segregation Principle):
  • 客⼾端不应该依赖它不需要的接⼝,类间的依赖关系应该建⽴在最⼩的接⼝上。
  • 使⽤建议:接⼝设计尽量精简单⼀,但是不要对外暴露没有实际意义的接⼝。
  • ⽤例:修改密码,不应该提供修改⽤⼾信息接⼝,⽽就是单⼀的最⼩修改密码接⼝,更不要暴 露数据库操作。
从整体上来理解六⼤设计原则,可以简要的概括为⼀句话,⽤抽象构建框架,⽤实现扩展细节,具体到每⼀条设计原则,则对应⼀条注意事项:
  • 单⼀职责原则告诉我们实现类要职责单⼀;
  • ⾥⽒替换原则告诉我们不要破坏继承体系;
  • 依赖倒置原则告诉我们要⾯向接⼝编程;
  • 接⼝隔离原则告诉我们在设计接⼝的时候要精简单⼀;
  • 迪⽶特法则告诉我们要降低耦合;
  • 开闭原则是总纲,告诉我们要对扩展开放,对修改关闭。

2. 单例模式

⼀个类只能创建⼀个对象,即单例模式,该设计模式可以保证系统中该类只有⼀个实例,并提供⼀个 访问它的全局访问点,该实例被所有程序模块共享。⽐如在某个服务器程序中,该服务器的配置信息存放在⼀个⽂件中,这些配置数据由⼀个单例对象统⼀读取,然后服务进程中的其他对象再通过这个 单例对象获取这些配置信息,这种⽅式简化了在复杂环境下的配置管理。
单例模式有两种实现模式:饿汉模式和懒汉模式
饿汉模式程序 启动时 就会创建⼀个唯⼀的实例对象。因为单例对象已经确定,所以⽐较适⽤于多线程环境中,多线程获取单例对象不需要加锁,可以有效的避免资源竞争,提⾼性能。
// 单例模式 

// 饿汉模式: 以空间换时间
class Singleton
{
private:
    static Singleton _eton;
    Singleton():_data(10) // 构造函数私有化
    {
        std::cout << "单例对象构造\n";
    }
    Singleton(const Singleton&) = delete; // 取消拷贝构造
    ~Singleton() {}
private:
    int _data;
public:
    static Singleton &getInstance() 
    {
        return _eton;
    }
    int getData() { return _data; }
};
Singleton Singleton::_eton;

int main()
{
    std::cout << Singleton::getInstance().getData() << std::endl;
    return 0;
}

懒汉模式第⼀次使⽤要使⽤单例对象的时候创建实例对象。如果单例对象构造特别耗时或者耗费济源(加载插件、加载⽹络资源等), 可以选择懒汉模式,在第⼀次使⽤的时候才创建对象。
  • 这⾥介绍的是《Effective C++》⼀书作者 Scott Meyers 提出的⼀种更加优雅简便的单例模式 Meyers' Singleton in C++。
  • C++11 Static local variables 特性以确保C++11起,静态变量将能够在满⾜ thread-safe 的前提下唯⼀地被构造和析构。
// 懒汉模式: 懒加载 - 延迟加载的思想 -- 一个对象在需要用的时候在进行实例化
class Singleton
{
private:
    Singleton():_data(10) // 构造函数私有化
    {
        std::cout << "单例对象构造\n";
    }
    Singleton(const Singleton&) = delete; // 取消拷贝构造
    ~Singleton() {}
private:
    int _data;
public:
    static Singleton &getInstance() 
    {
        static Singleton _eton; // 静态局部变量
        return _eton;
    }
    int getData() { return _data; }
};

int main()
{
    std::cout << Singleton::getInstance().getData() << std::endl;
    return 0;
}

3. 工厂模式

⼯⼚模式是⼀种创建型设计模式, 它提供了⼀种创建对象的最佳⽅式。在⼯⼚模式中,我们创建对象 时不会对上层暴露创建逻辑,⽽是通过使⽤⼀个共同结构来指向新创建的对象,以此实现创建-使⽤的 分离。
⼯⼚模式可以分为:
简单⼯⼚模式:简单⼯⼚模式实现由⼀个⼯⼚对象通过类型决定创建出来指定产品类的实例。假设
有个⼯⼚能⽣产出⽔果,当客⼾需要产品的时候明确告知⼯⼚⽣产哪类⽔果,⼯⼚需要接收⽤⼾提
供的类别信息,当新增产品的时候,⼯⼚内部去添加新产品的⽣产⽅式。
// 工厂模式
class Fruit
{
public:
    Fruit() {}
    virtual void show()
    {
        std::cout << "我是一个水果" << std::endl;
    }
};

class Apple : public Fruit
{
public:
    void show() override
    {
        std::cout << "我是一个苹果\n";
    }
};

class Banana : public Fruit
{
public:
    void show() override
    {
        std::cout << "我是一个香蕉\n";
    }
};

class FruitFactory
{
public:
    static std::shared_ptr<Fruit> create(const std::string &name)
    {
        if(name == "苹果")
        {
            return std::make_shared<Apple>();
        }
        else{
            return std::make_shared<Banana>();
        }
    }
};

int main()
{
    std::shared_ptr<Fruit> fruit = FruitFactory::create("苹果");
    fruit->show();
    fruit = FruitFactory::create("香蕉");
    fruit->show();
    
    return 0;
}

简单⼯⼚模式:通过参数控制可以⽣产任何产品。
  • 优点:简单粗暴,直观易懂。使⽤⼀个工厂⽣产同⼀等级结构下的任意产品。
  • 缺点:
    1. 所有东西⽣产在⼀起,产品太多会导致代码量庞⼤。
    2. 开闭原则遵循(开放拓展,关闭修改)的不是太好,要新增产品就必须修改⼯⼚⽅法。

这个模式的结构和管理产品对象的⽅式⼗分简单, 但是它的扩展性⾮常差,当我们需要新增产品的时 候,就需要去修改⼯⼚类新增⼀个类型的产品创建逻辑,违背了开闭原则。


⼯⼚⽅法模式:在简单⼯⼚模式下新增多个⼯⼚,多个产品,每个产品对应⼀个⼯⼚。假设现在有
A、B 两种产品,则开两个⼯⼚,⼯⼚ A 负责⽣产产品 A,⼯⼚ B 负责⽣产产品 B,⽤⼾只知道产品 的⼯⼚名,⽽不知道具体的产品信息,⼯⼚不需要再接收客⼾的产品类别,⽽只负责⽣产产品。
⼯⼚⽅法: 定义⼀个创建对象的接⼝,但是由⼦类来决定创建哪种对象,使⽤多个⼯⼚分别⽣产指定 的固定产品。
// 工厂方法模式

class Fruit
{
public:
    Fruit() {}
    virtual void show()
    {
        std::cout << "我是一个水果" << std::endl;
    }
};

class Apple : public Fruit
{
public:
    void show() override
    {
        std::cout << "我是一个苹果\n";
    }
};

class Banana : public Fruit
{
public:
    void show() override
    {
        std::cout << "我是一个香蕉\n";
    }
};

class FruitFactory
{
public:
    virtual std::shared_ptr<Fruit> create() = 0;
};

class AppleFactory : public FruitFactory
{
public:
    std::shared_ptr<Fruit> create() override
    {
        return std::make_shared<Apple>();
    }
};

class BananaFactory : public FruitFactory
{
public:
    std::shared_ptr<Fruit> create() override
    {
        return std::make_shared<Banana>();
    }
};

int main()
{
    std::shared_ptr<FruitFactory> ff(new AppleFactory());
    std::shared_ptr<Fruit> fruit = ff->create();
    fruit->show();
    ff.reset(new BananaFactory());
    fruit = ff->create();
    fruit->show();

    return 0;
}
  • 优点:
    1. 减轻了⼯⼚类的负担,将某类产品的⽣产交给指定的⼯⼚来进⾏。
    2. 开闭原则遵循较好,添加新产品只需要新增产品的⼯⼚即可,不需要修改原先的⼯⼚类。
  • 缺点:对于某种可以形成⼀组产品族的情况处理较为复杂,需要创建⼤量的⼯⼚类。
⼯⼚⽅法模式每次增加⼀个产品时,都需要增加⼀个具体产品类和⼯⼚类,这会使得系统中类的个数 成倍增加,在⼀定程度上增加了系统的耦合度。

抽象工厂模式:⼯⼚⽅法模式通过引⼊⼯⼚等级结构,解决了简单⼯⼚模式中⼯⼚类职责太重的问
题,但由于⼯⼚⽅法模式中的每个⼯⼚只⽣产⼀类产品,可能会导致系统中存在⼤量的⼯⼚类,势
必会增加系统的开销。此时,我们可以考虑将⼀些相关的产品组成⼀个产品族(位于不同产品等级
结构中功能相关联的产品组成的家族),由同⼀个⼯⼚来统⼀⽣产,这就是抽象⼯⼚模式的基本思
想。
// 抽象⼯⼚:围绕⼀个超级⼯⼚创建其他⼯⼚。每个⽣成的⼯⼚按照⼯⼚模式提供对象。
// 思想:将⼯⼚抽象成两层,抽象⼯⼚ & 具体⼯⼚⼦类, 在⼯⼚⼦类种⽣产不同类型的⼦产品

class Fruit
{
public:
    Fruit() {}
    virtual void show() = 0;
};

class Apple : public Fruit
{
public:
    void show() override
    {
        std::cout << "我是一个苹果\n";
    }
};

class Banana : public Fruit
{ 
public:
    void show() override
    {
        std::cout << "我是一个香蕉\n";
    }
};

class Animal
{
public:
    Animal() {}
    virtual void show() = 0;
};

class Dog : public Animal
{
public:
    void show() override
    {
        std::cout << "我是一个小狗\n";
    }
};

class Lamp : public Animal
{ 
public:
    void show() override
    {
        std::cout << "我是一个山羊\n";
    }
};

// 抽象⼯⼚模式
class Factory
{
public:
    virtual std::shared_ptr<Fruit> getFruit(const std::string &name) = 0;
    virtual std::shared_ptr<Animal> getAnimal(const std::string &name) = 0;
};

class FruitFactory : public Factory
{
public:
    std::shared_ptr<Animal> getAnimal(const std::string &name)
    {
        return std::shared_ptr<Animal>();
    }

    std::shared_ptr<Fruit> getFruit(const std::string &name)
    {
        if(name == "苹果")
        {
            return std::make_shared<Apple>();
        }
        else
        {
            return std::make_shared<Banana>();
        }
    }
};

class AnimalFactory : public Factory
{
public:
    std::shared_ptr<Fruit> getFruit(const std::string &name)
    {
        return std::shared_ptr<Fruit>();
    }

    std::shared_ptr<Animal> getAnimal(const std::string &name)
    {
        if(name == "小狗")
        {
            return std::make_shared<Dog>();
        }
        else
        {
            return std::make_shared<Lamp>();
        }
    }
};

class FactoryProducer
{
public:
    static std::shared_ptr<Factory> create(const std::string &name)
    {
        if(name == "水果")
        {
            return std::make_shared<FruitFactory>();
        }
        else
        {
            return std::make_shared<AnimalFactory>();
        }
    }
};

int main()
{
    std::shared_ptr<Factory> ff = FactoryProducer::create("水果");
    std::shared_ptr<Fruit> fruit = ff->getFruit("苹果");
    fruit->show();
    fruit = ff->getFruit("香蕉");
    fruit->show();

    std::shared_ptr<Factory> factory = FactoryProducer::create("动物");
    std::shared_ptr<Animal> animal = factory->getAnimal("小狗");
    animal->show();
    animal = factory->getAnimal("香蕉");
    animal->show();

    return 0;
}
抽象⼯⼚模式适⽤于⽣产多个⼯⼚系列产品衍⽣的设计模式,增加新的产品等级结构复杂,需要对原 有系统进⾏较⼤的修改,甚⾄需要修改抽象层代码,违背了“开闭原则”。

4. 建造者模式

建造者模式是⼀种创建型设计模式, 使⽤多个简单的对象⼀步⼀步构建成⼀个复杂的对象,能够将⼀ 个复杂的对象的构建与它的表⽰分离,提供⼀种创建对象的最佳⽅式。主要⽤于解决对象的构建过于复杂的问题。
建造者模式主要基于四个核⼼类实现:
  • 抽象产品类:
  • 具体产品类:⼀个具体的产品对象类。
  • 抽象Builder类:创建⼀个产品对象所需的各个部件的抽象接⼝。
  • 具体产品的Builder类:实现抽象接⼝,构建各个部件。
  • 指挥者Director类:统⼀组建过程,提供给调⽤者使⽤,通过指挥者来构造产品。
// 建造者模式
class Computer
{
public:
    Computer() {}
    void setBoard(const std::string &board)
    {
        _board = board;
    }
    void setDisplay(const std::string &display)
    {
        _display = display;
    }
    void showParamaters()
    {
        std::string param = "Computer Paramaters:\n";
        param += "\tBoard: " + _board + "\n";
        param += "\tDisolay: " + _display + "\n";
        param += "\tOS: " + _os + "\n";
        std::cout << param << std::endl;
    }
    virtual void setOS() = 0;
protected:
    std::string _board;
    std::string _display;
    std::string _os;
};

/* 具体产品类 */
class MackBook : public Computer
{
public:
    void setOS() override
    {
        _os = "Mac OS x12";
    }
};

/* 抽象建造者类:包含创建⼀个产品对象的各个部件的抽象接⼝ */
class Builder
{
public:
    virtual void buildBoard(const std::string &board) = 0;
    virtual void buildDisplay(const std::string &display) = 0;
    virtual void buildOS() = 0;
    virtual std::shared_ptr<Computer> build() = 0;
};

/* 具体产品的具体建造者类:实现抽象接⼝,构建和组装各个部件 */
class MacBookBuilder : public Builder
{
public:
    MacBookBuilder() : _computer(new MackBook()) {}
    void buildBoard(const std::string &board)
    {
        _computer->setBoard(board);
    }
    void buildDisplay(const std::string &display)
    {
        _computer->setDisplay(display);
    }
    void buildOS()
    {
        _computer->setOS();
    }
    std::shared_ptr<Computer> build()
    {
        return _computer;
    }
private:
    std::shared_ptr<Computer> _computer;
};

/* 指挥者类,提供给调⽤者使⽤,通过指挥者来构造复杂产品 */
class Director
{
public:
    Director(Builder *builder) : _builder(builder) {}
    void construct(const std:: string &board, const std::string &display)
    {
        _builder->buildBoard(board);
        _builder->buildDisplay(display);
        _builder->buildOS();
    }
private:
    std::shared_ptr<Builder> _builder;
};

int main()
{
    Builder *builder = new MacBookBuilder();
    std::unique_ptr<Director> director(new Director(builder));
    director->construct("华硕主板", "三星显示器");
    std::shared_ptr<Computer> computer = builder->build();
    computer->showParamaters();

    return 0;
}

5. 代理模式

代理模式指代理控制对其他对象的访问, 也就是代理对象控制对原对象的引⽤。在某些情况下,⼀个 对象不适合或者不能直接被引⽤访问,⽽代理对象可以在客⼾端和⽬标对象之间起到中介的作⽤。
代理模式的结构包括⼀个是真正的你要访问的对象(⽬标类)、⼀个是代理对象。⽬标对象与代理对象实 现同⼀个接⼝,先访问代理类再通过代理类访问⽬标对象。代理模式分为静态代理、动态代理
  • 静态代理指的是,在编译时就已经确定好了代理类和被代理类的关系。也就是说,在编译时就已经确定了代理类要代理的是哪个被代理类。
  • 动态代理指的是,在运⾏时才动态⽣成代理类,并将其与被代理类绑定。这意味着,在运⾏时才能确定代理类要代理的是哪个被代理类。

以租房为例,房东将房⼦租出去,但是要租房⼦出去,需要发布招租启⽰, 带⼈看房,负责维修,这些⼯作中有些操作并⾮房东能完成,因此房东为了图省事,将房⼦委托给中介进⾏租赁。 代理模式实现:

// 代理模式

/* 房东要把⼀个房⼦通过中介租出去理解代理模式 */
class RentHouse
{
public:
    virtual void rentHouse() = 0;
};

/* 房东类: 将房子租出去 */
class Landlord : public RentHouse
{
public:
    void rentHouse()
    {
        std::cout << "将房子租出去\n";
    }
};

/* 中介代理类: 将租房子进行功能加强,实现租房以外的其他功能 */
class Intermediary : public RentHouse
{
public:
    void rentHouse()
    {
        std::cout << "发布招租启示\n";
        std::cout << "带人看房\n";
        _landloard.rentHouse();
        std::cout << "负责租后维修\n";
    }
private:
    Landlord _landloard;
};

int main()
{
    Intermediary intermediary;
    intermediary.rentHouse();

    return 0;
}

五、日志系统框架设计

本项⽬实现的是⼀个多⽇志器⽇志系统,主要实现的功能是让程序员能够轻松的将程序运⾏⽇志信息落地到指定的位置,且⽀持同步与异步两种方式的日志落地方式
作用: 将一条信息,进行格式化称为指定格式的字符串后,写入到指定位置。

(一)功能划分

1. 日志要写入指定位置(标准输出,指定文件,滚动文件,。。。)。日志系统需要支持将日志消息落地到不同的位置 -- 多落地方向

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

  • 同步:业务线程自己负责日志的写入(流程简单,但是有可能会因为阻塞导致效率降低)。
  • 异步:业务线程将日志放入缓冲区内存,让其他异步线程负责将日志写入指定位置。

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

4. 日志器管理

(二)模块划分

日志等级模块对输出的日志的等级进行划分,以便于控制日志的输出,并提供等级枚举转字符串功能。
  • OFF:关闭。
  • DEBUG:调试,调试时的关键信息输出。
  • INFO:提⽰,普通的提⽰型⽇志信息。
  • WARN:警告,不影响运⾏,但是需要注意⼀下的⽇志。
  • ERROR:错误,程序运⾏出现错误的⽇志。
  • FATAL:致命,⼀般是代码异常导致程序⽆法继续推进运⾏的⽇志。
日志消息模块: 封装一条日志输出的所需的各项要素信息。
时间:描述本条⽇志的输出时间。
线程ID:描述本条⽇志是哪个线程输出的。
日志等级:描述本条⽇志的等级。
日志数据:本条⽇志的有效载荷数据。
日志文件名:描述本条⽇志在哪个源码⽂件中输出的。
日志行号:描述本条⽇志在源码⽂件的哪⼀⾏输出的。
日志消息格式化模块 :设置指定的日志输出格式,并提供对日志消息进行格式化功能。
系统的默认⽇志输出格式:
%d{%H:%M:%S}%T[%t]%T[%p]%T[%c]%T%f:%l%T%m%n   ——>
13:26:32 [2343223321] [FATAL] [root] main.c:76 套接字创建失败\n
  • %d{%H:%M:%S}:表⽰⽇期时间,花括号中的内容表⽰⽇期时间的格式。
  • %T:表示制表符缩进。
  • %t:表示线程ID
  • %p:表示⽇志级别
  • %c:表示⽇志器名称,不同的开发组可以创建⾃⼰的⽇志器进⾏⽇志输出,⼩组之间互不影
  • 响。
  • %f:表示⽇志输出时的源代码⽂件名。
  • %l:表示⽇志输出时的源代码⾏号。
  • %m:表示给与的⽇志有效载荷数据
  • %n:表示换⾏
设计思想:设计不同的⼦类,不同的⼦类从⽇志消息中取出不同的数据进⾏处理。
日志消息落地模块: 负责对日志消息进行指定方向的写入输出。可以是标准输出,也可以是⽇志⽂件,也可以滚动⽂ 件输出....)(工厂模式)
  • 标准输出:表⽰将⽇志进⾏标准输出的打印。
  • ⽇志⽂件输出:表⽰将⽇志写⼊指定的⽂件末尾。
  • 滚动⽂件输出:当前以⽂件⼤⼩进⾏控制,当⼀个⽇志⽂件⼤⼩达到指定⼤⼩,则切换下⼀个 ⽂件进⾏输出。

后期,也可以扩展远程⽇志输出,创建客⼾端,将⽇志消息发送给远程的⽇志分析服务器。

设计思想:设计不同的⼦类,不同的⼦类控制不同的⽇志落地⽅向。
日志器模块:对上面几个模块进行整合。(建造者模式)
此模块是对以上⼏个模块的整合模块,⽤⼾通过⽇志器进⾏⽇志的输出,有效降低⽤⼾的使⽤
难度。
包含有:⽇志消息落地模块对象,⽇志消息格式化模块对象,⽇志输出等级。
  • 同步日志器模块:完成日志的同步输出功能。
  • 异步日志器模块:完成日志的异步输出功能。
⽇志器管理模块: 对创建的所有⽇志器进⾏统⼀管理,并提供⼀个默认⽇志器提供标准输出的⽇志输出。
为了降低项⽬开发的⽇志耦合,不同的项⽬组可以有⾃⼰的⽇志器来控制输出格式以及落地⽅
向,因此本项⽬是⼀个多⽇志器的⽇志系统。
异步线程模块:负责异步日志的实际落地输出。(代理模式)
实现对⽇志的异步输出功能,⽤⼾只需要将输出⽇志任务放⼊任务池,异步线程负责⽇志的落
地输出功能,以此提供更加⾼效的⾮阻塞⽇志输出。
单例的日志器管理模块:
对日志进行全局的管理,以便于能够在项目的任何位置获取指定的日志器进行日志输出。

(三)模块关系图

六、代码设计

(一)实用类设计

提前完成⼀些零碎的功能接⼝,以便于项⽬中会⽤到。
  • 获取系统时间
  • 判断⽂件是否存在
  • 获取⽂件的所在⽬录路径
  • 创建⽬录

Util.hpp

整体框架:

成员接口设置成静态的,就不需实例化对象而是通过 类名::接口 直接使用即可:

#ifndef __M_UTIL_H__
#define __M_UTIL_H__

/* 实用工具类的设计: 
    1. 获取系统时间
    2. 判断文件是否存在
    3. 获取文件所在路径
    4. 创建目录
*/ 
#include <iostream>
#include <ctime>
#include <unistd.h>
#include <sys/stat.h>

namespace mylog
{
    namespace util
    {
        class Date
        {
        public:
            // 1. 获取系统时间
            static size_t nowTime()
            {
                return (size_t)time(nullptr);
            }
        };

        class File
        {
        public:
            // 2. 判断文件是否存在
            static bool exists(const std::string &pathname);
            // 3. 获取文件所在路径
            static std::string path(const std::string &pathname);
            // 4. 创建目录
            static void createDirectory(const std::string &pathname);
        };
    }
} 
#endif

接口具体实现:

获取当前时间

 // 1. 获取系统时间
static size_t nowTime()
{
    return (size_t)time(nullptr);
}

判断文件是否存在

stat 用于获取文件或文件系统的状态信息。可以通过这个系统函数判断文件是否存在;

也可以通过access函数调用,F_OK为文件存在性检查,但这个函数只有在Linux操作系统下才具有:

// 2. 判断文件是否存在
static bool exists(const std::string &pathname)
{
    // 此接口为POSIX(可移植操作系统接口)
    struct stat st;
    if (stat(pathname.c_str(), &st) < 0)
    {
        return false;
    }
    return true;

    // 此接口只有Linux下有,不具有可移植性
    // return (access(pathname.c_str(), F_OK) == 0);
}

获取文件路径:查找到最后一个 / 的位置,然后从起始位置截取到最后一个斜杠位置。

但需注意的是:Windows下文件路径使用 \ 分隔,Linux下为 / ,所以使用此接口查询所给字符串的任意一个字符:

// 3. 获取文件所在路径
static std::string path(const std::string &pathname)
{
    // ./abc/a.txt 
    
    size_t pos = pathname.find_last_of("/\\"); 
    if(pos == std::string::npos) return "."; // 没找到说明在当前目录下
    return pathname.substr(0, pos+1); // +1包含最后一个斜杠
}

创建目录:传入需创建目录的路径,查找第一级目录,没有则创建,有则进行查找下一级目录,直到创建指定目录成功:

// 4. 创建目录
static void createDirectory(const std::string &pathname)
{
    // ./abc/bcd/cde 

    size_t pos = 0, idx = 0;
    while(idx < pathname.size())
    {
        pos = pathname.find_first_of("/\\", idx);
        if(pos == std::string::npos)
        {
            mkdir(pathname.c_str(), 0777); // 赋予全部权限
        }
        std::string parent_dir = pathname.substr(0, pos+1);
        if(exists(parent_dir) == true) 
        {
            idx = pos+1;
            continue;
        }
        mkdir(parent_dir.c_str(), 0777); 
        idx = pos+1;
    }
}

测试:

#include "Util.hpp"

int main()
{
    std::cout << mylog::util::Date::nowTime() << std::endl;
    std::string pathname = "./abc/bcd/cde/a.txt";
    mylog::util::File::createDirectory(mylog::util::File::path(pathname));

    return 0;
}
前时间戳获取成功,多级目录也创建成功:

(二)日志等级类设计

1. 定义出日志系统所包含的所以日志等级
  • UNKNOW = 0,
  • DEBUG:调试等级的日志,
  • INFO:    提示等级的日志,
  • WARNL:警告等级的日志,
  • ERROR:错误等级的日志,
  • FATAL:  致命错误等级的日志,
  • OFF:      关闭等级
每一个项目中都会设置一个默认的日志输出等级,只有输出的日志等级大于等于默认限制等级的时候才可以进行输出。
2. 提供一个接口,将对应等级的枚举,转换为一个对应的字符串。DEBUG -----> "DEBUG"
Level.hpp
成员接口设置成静态的,就不需实例化对象而是通过 类名::接口 直接使用即可:
#ifndef __M_LEVEL_H__
#define __M_LEVEL_H__

/*
   1. 定义枚举类,枚举出日志等级
   2. 提供转换接口: 将枚举转换为对应字符串
*/
namespace mylog
{
    class LogLevel
    {
    public:
        enum class value
        {
            UNKNOW = 0,
            DEBUG,
            INFO,
            WARN,
            ERROR,
            FATAL,
            OFF
        };

        static const char *toString(LogLevel::value level)
        {
            switch (level)
            {
                case LogLevel::value::DEBUG: return "DEBUG";
                case LogLevel::value::INFO: return "INFO";
                case LogLevel::value::WARN: return "WARN";
                case LogLevel::value::ERROR: return "ERROR";
                case LogLevel::value::FATAL: return "FATAL";
                case LogLevel::value::OFF: return "OFF";
            }
            return "UNKNOW";
        } 
    };
} 
#endif

测试:

#include "Util.hpp"
#include "Level.hpp"

int main()
{
    std::cout << mylog::LogLevel::toString(mylog::LogLevel::value::DEBUG) << std::endl;
    std::cout << mylog::LogLevel::toString(mylog::LogLevel::value::INFO) << std::endl;
    std::cout << mylog::LogLevel::toString(mylog::LogLevel::value::WARN) << std::endl;
    std::cout << mylog::LogLevel::toString(mylog::LogLevel::value::ERROR) << std::endl;
    std::cout << mylog::LogLevel::toString(mylog::LogLevel::value::FATAL) << std::endl;
    std::cout << mylog::LogLevel::toString(mylog::LogLevel::value::OFF) << std::endl;

    return 0;
}

成功打印出各个日志等级:

(三)日志消息类设计

中间存储一条日志消息所需的各项要素:

  • 日志的输出时间        用于过滤日志输出时间
  • 日志等级                   用于进行日志过滤分析
  • 源文件名称
  • 源代码行号               用于定位出现错误的代码位置
  • 线程ID                      用于过滤出错的线程
  • 日志主体消息
  • 日志器名称(当前支持多日志器同时使用)

Message.hpp

#ifndef __M__MSG_H__
#define __M__MSG_H__

/* 中间存储一条日志消息所需的各项要素: */

#include "Level.hpp"
#include "Util.hpp"
#include <iostream>
#include <string>
#include <thread>

namespace mylog
{
    struct LogMsg
    {
        size_t _ctime;          // 日志产生的时间戳
        LogLevel::value _level; // 日志等级
        size_t _line;           // 行号
        std::thread::id _tid;   // 线程ID
        std::string _file;      // 源码文件名
        std::string _logger;    // 日志器名称
        std::string _payload;   // 有效消息数据

        LogMsg(LogLevel::value level,
            size_t line,
            const std::string file,
            const std::string logger,
            const std::string msg) : 
            _ctime(util::Date::nowTime()),
            _level(level),
            _line(line),
            _tid(std::this_thread::get_id()),
            _file(file),
            _logger(logger),
            _payload(msg)
        {}
    };
}
#endif

这个类没啥测试的~~

(四)日志输出格式化类设计

对日志消息进行格式化,组织为指定格式的字符串。其主要包含以下内容:

pattern 成员保存⽇志输出的格式字符串。
  • %d ⽇期
  • %T 缩进
  • %t 线程id
  • %p ⽇志级别
  • %c ⽇志器名称
  • %f ⽂件名
  • %l ⾏号
  • %m ⽇志消息
  • %n 换⾏

格式化字符串控制了日志的输出格式,定义格式化字符,是为了让日志系统进行日志格式化更加的灵活方便。

std::vector<FormatItem::ptr> items成员:用于按序保存格式化字符串对应的⼦格式化对象。

FormatItem类主要负责⽇志消息⼦项的获取及格式化。其包含以下⼦类:

  • MsgFormatItem :表⽰要从LogMsg中取出有效⽇志数据。
  • LevelFormatItem :表⽰要从LogMsg中取出⽇志等级。
  • NameFormatItem :表⽰要从LogMsg中取出⽇志器名称。
  • ThreadFormatItem :表⽰要从LogMsg中取出线程ID。
  • TimeFormatItem :表⽰要从LogMsg中取出时间戳并按照指定格式进⾏格式化。
  • CFileFormatItem :表⽰要从LogMsg中取出源码所在⽂件名。
  • CLineFormatItem :表⽰要从LogMsg中取出源码所在⾏号。
  • TabFormatItem :表⽰⼀个制表符缩进。
  • NLineFormatItem :表⽰⼀个换⾏。
  • OtherFormatItem :表⽰⾮格式化的原始字符串("["、"]"、":")。

格式化子项数组对格式化字符串进行解析,保存了日志消息要素的排序,不同的格式化子项,会从日志消息中取出指定的元素,转换为字符串。

 ⽰例:"[%d{%H:%M:%S}] %m%n"

pattern = "[%d{%H:%M:%S}] %m%n"
items = {
 {OtherFormatItem(), "["},
 {TimeFormatItem(), "%H:%M:%S"},
 {OtherFormatItem(), "]"},
 {MsgFormatItem (), ""},
 {NLineFormatItem (), ""}
}
LogMsg msg = {
 size_t _line = 22;
 size_t _ctime = 12345678;
 std::thread::id _tid = 0x12345678;
 std::string _name = "logger";
 std::string _file = "main.cpp";
 std::string _payload = "创建套接字失败";
 LogLevel::value _level = ERROR;
};
格式化的过程其实就是按次序从Msg中取出需要的数据进⾏字符串的连接的过程。
最终组织出来的格式化消息: "[22:32:54] 创建套接字失败\n"。

1. 格式化子项类的设计

对于格式化的日志消息,首先需先实现格式化子项类。

作用:从日志消息中取出指定的元素,追加到一块内存空间中。

设计思想

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

对于时间子类来说,需将时间戳转换成时间字符串,即分为两个步骤:

1. 时间戳 ——》 实际时间

用到的函数为 localtime_r localtime 是一个在多种编程语言和环境中常见的函数,通常用于将时间戳(Unix 时间戳,即从1970年1月1日00:00:00 UTC到现在的秒数)转换为本地时间(即考虑了时区的时间)。

在C语言中,localtime 函数是一个非线程安全的函数,因为它返回指向静态数据的指针。在多线程环境中,这可能导致数据竞争和不可预测的结果。为了解决这个问题,一些系统提供了localtime_r(或localtime_s在某些平台上)这样的线程安全版本。

localtime_r函数与localtime函数的功能相同,但它接受一个额外的参数,该参数是指向struct tm的指针,用于存储结果,而不是使用静态内存。这使得多个线程可以同时调用localtime_r而不会相互干扰。

struct tm *localtime_r(const time_t *timer, struct tm *result);
  • timer:指向时间戳(time_t类型)的指针。
  • result:指向struct tm的指针,用于存储转换后的本地时间。

2. 实际时间 ——》时间字符串

用到的函数为 strftime 是一个在多种编程语言中常见的函数,用于将日期和时间格式化为字符串。

size_t strftime(char *str, size_t maxsize, 
                const char *format, const struct tm *timeptr);
  • str:指向用于存储格式化时间的字符数组的指针。
  • maxsizestr 数组的大小。
  • format:指定日期和时间格式的 C 字符串。
  • timeptr:指向 struct tm 结构体的指针,该结构体包含了要格式化的日期和时间信息。

Format.hpp

#ifndef __M__FMT_H__
#define __M__FMT_H__

#include "Level.hpp"
#include "Message.hpp"
#include <vector>

namespace mylog
{
    // 抽象格式化子项基类
    class FormatItem
    {
    public:
        using ptr = std::shared_ptr<FormatItem>;
        virtual void format(std::ostream &out, LogMsg &msg) = 0;
    };

    // 派生格式化子项子类 -- 有效消息、等级、时间、文件名、行号、线程ID、日志器名、制表符、换行、其他

    // 有效消息子类
    class MsgFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, LogMsg &msg) override
        {
            out << msg._payload;
        }
    };

    // 等级子类
    class LevelFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, LogMsg &msg) override
        {
            out << LogLevel::toString(msg._level);
        }
    };

    // 时间子类
    class TimeFormatItem : public FormatItem
    {
    public:
        TimeFormatItem(const std::string &fmt = "%H:%M:%S") : _time_fmt(fmt) {}
        void format(std::ostream &out, LogMsg &msg) override
        {
            struct tm t;
            localtime_r(&msg._ctime, &t);
            char tmp[32] = {0};
            strftime(tmp, 31, _time_fmt.c_str(), &t);    
            out << tmp;
        }
    private:
        std::string _time_fmt; // %H:%M:%S
    };

    // 文件名子类
    class LevelFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, LogMsg &msg) override
        {
            out << msg._file;
        }
    };

    // 行号子类
    class LineFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, LogMsg &msg) override
        {
            out << msg._line;
        }
    };

    // 线程ID子类
    class ThreadFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, LogMsg &msg) override
        {
            out << msg._tid;
        }
    };

    // 日志器子类
    class LoggerFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, LogMsg &msg) override
        {
            out << msg._logger;
        }
    };

    // 制表符子类
    class TabFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, LogMsg &msg) override
        {
            out << "\t";
        }
    };

    // 换行子类
    class NLineFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, LogMsg &msg) override
        {
            out << "\n";
        }
    };

    // 其他子类 如: abcdefg[%d{%H}]
    class OtherFormatItem : public FormatItem
    {
    public:
        OtherFormatItem(const std::string &str) : _str(str) {}
        void format(std::ostream &out, LogMsg &msg) override
        {
            out << _str;
        }
    private:
        std::string _str;
    };
}

#endif

设计格式化完子项类就可以设计日志格式化类了。

2. 日志格式化类的设计

Format.hpp

整体框架:

#ifndef __M__FMT_H__
#define __M__FMT_H__

#include "Level.hpp"
#include "Message.hpp"
#include <vector>

namespace mylog
{
    /*
        %d 表示日期,包含子格式{%H:%M:%S}
        %t 表示线程ID
        %c 表示日志器名称
        %f 表示源码文件名
        %l 表示源码行号
        %p 表示日志级别
        %T 表示制表符缩进
        %m 表示主体消息
        %n 表示换行
    */
    class Formatter
    {
    public:
        using ptr = std::shared_ptr<Formatter>;
        Formatter(const std::string &pattern = "[%D{%H:%H:%S}][%t][%c][%f:%l][%p]%T%m%n") :
            _pattern(pattern) {}
            
        // 对msg进行格式化
        void format(std::ostream &out, LogMsg &msg);
        std::string format(LogMsg &msg);

        // 对格式话规则字符串进行解析
        bool parsePattern();
    private:
        // 根据不同的格式化字符创建不同的格式化子项对象
        FormatItem::ptr createItem(const std::string &key, const std::string &val);
    private:
        std::string _pattern; // 格式化规则字符串
        std::vector<FormatItem::ptr> _items;
    };
}

#endif

各接口具体实现:

构造函数

Formatter(const std::string &pattern = "[%D{%H:%H:%S}][%t][%c][%f:%l][%p]%T%m%n") :
    _pattern(pattern) 
{
    assert(parsePattern());
}

格式化消息:使用字符串流来接收格式化的消息:

// 对msg进行格式化
void format(std::ostream &out, LogMsg &msg)
{
    for(auto &item : _items)
    {
        item->format(out, msg);
    }
}
std::string format(LogMsg &msg)
{
    std::stringstream ss;
    format(ss, msg);
    return ss.str();
}

根据不同的格式化字符创建不同的格式化子项对象:

// 根据不同的格式化字符创建不同的格式化子项对象
FormatItem::ptr createItem(const std::string &key, const std::string &val)
{
    if(key == "d") return std::make_shared<TimeFormatItem>(val);
    if(key == "t") return std::make_shared<ThreadFormatItem>();
    if(key == "c") return std::make_shared<LoggerFormatItem>();
    if(key == "f") return std::make_shared<FileFromatItem>();
    if(key == "l") return std::make_shared<LineFormatItem>();
    if(key == "p") return std::make_shared<LevelFormatItem>();
    if(key == "T") return std::make_shared<TabFormatItem>();
    if(key == "m") return std::make_shared<MsgFormatItem>();
    if(key == "n") return std::make_shared<NLineFormatItem>();
    return std::make_shared<OtherFormatItem>(val);
}

对格式化规则字符串进行解析:

举个例子:

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

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

在处理过程中,需将处理得到的信息保存下来,例如图中的字符串:

  1. key = nullptr,  val = abcde[
  2. key = d,           val = %H:%M:%S
  3. key = nullptr,  val = ][
  4. key = p,           val = nullptr
  5. key = nullptr,  val = ]
  6. key = T,           val = nullptr
  7. key = m,          val = nullptr
  8. key = n,           val = nullptr

拿到这个数组之后,根据这个数组内容创建对应格式化子项对象,添加到 _items成员数组中:

// 对格式化规则字符串进行解析
bool parsePattern()
{
    // 1. 对格式化规则字符串进行解析
    // ab%c%cde[%d{%H:%M:%S}][%p]%T%m%n
    std::vector<std::pair<std::string, std::string>> fmt_order;
    size_t pos = 0;
    std::string key, val;
    while(pos < _pattern.size())
    {
        // 1. 处理原始字符串 - 判断是否是 % ,不是就为原始字符串
        if(_pattern[pos] != '%')
        {
            val.push_back(_pattern[pos++]);
            continue;
        }
        // 能走下来就代表pos位置就是 % 字符,%% 称为一个原始 % 字符(转义字符)
        if(pos+1 < _pattern.size() && _pattern[pos+1] == '%')
        {
            val.push_back('%'); 
            pos += 2;
            continue;
        }
        // 能走下来就代表 % 后一个字符为格式化字符,代表原始字符串处理完毕,
        // 并将原始字符串添加到格式化数组内
        if(val.empty() == false)
        {
            fmt_order.push_back(std::make_pair("", val));
            val.clear();
        }

        // 这时候为格式化字符的处理
        // pos++指向的是 % 后一个字符位置,即格式化字符位置
        pos++;
        if(pos == _pattern.size())
        {
            std::cout << "%之后没有对应的格式化字符!\n";
            return false;
        }
        key = _pattern[pos];
        pos += 1; // 这时指向的是格式化字符后一个位置
        if(pos < _pattern.size() && _pattern[pos] == '{')
        {
            pos += 1; // 这时候pos指向 { 之后,子项的起始位置
            while(pos < _pattern.size() && _pattern[pos] != '}')
            {
                val.push_back(_pattern[pos++]);
            }
            // 走到了末尾跳出了循环,则代表没有遇到 } ,代表格式是错误的
            if(pos == _pattern.size())
            {
                std::cout << "子规则{}匹配出错!\n";
                return false; // 没有找到 }
            }
            pos += 1; // 这时pos指向的为 } 位置,向后走一步走到了下次处理的新位置
        }
        fmt_order.push_back(std::make_pair(key, val));
        key.clear();
        val.clear();
    }

    // 2. 根据解析得到的数据初始化格式化子项数组成员
    for(auto &it : fmt_order)
    {
        _items.push_back(createItem(it.first, it.second));
    }
    return true;
}

功能测试:

#include "Util.hpp"
#include "Level.hpp"
#include "Message.hpp"
#include "Format.hpp"

int main()
{
    mylog::LogMsg msg(mylog::LogLevel::value::INFO, 100, "test.c", "root", "格式化功能测试...");
    mylog::Formatter fmt;
    std::string str = fmt.format(msg);
    std::cout << str << std::endl;

    return 0;
}
成功打印出日志消息,注意,在链接时需链接上 -lpthread ,线程ID本质上是一个地址,所以会显得很大:

特殊情况测试:

双百分号:

    mylog::Formatter fmt("abc%%acb[%d{%H:%M:%S}] %m%n");


缺少 } 花括号:

    mylog::Formatter fmt("abc%%acb[%d{%H:%M:%S] %m%n");

%后缺少字符:
    mylog::Formatter fmt("abc%%acb[%d{%H:%M:%S}] %m%");

特殊字符处理:%g

    mylog::Formatter fmt("abc%%acb[%d{%H:%M:%S}] %m%n[%g%g%g]");

(五)日志落地类的设计

⽇志落地类主要负责落地⽇志消息到⽬的地。
实现思想:
  1. 抽象出落地模块基类。
  2. 不同落地方向从基类进行派生。
  3. 使用工厂模式进行创建与表示的分离。
扩展:支持落地方向的扩展。用户可以自己编写一个新的落地模块,将日志进行其他方向的落地。
Sink.hpp
整体框架:
#ifndef __M_SINK_H__
#define __M_SINK_H__
/*
    抽象出落地模块基类。
    不同落地方向从基类进行派生。
    使用工厂模式进行创建与表示的分离。
*/

#include "Util.hpp"
#include <memory>
#include <iostream>
#include <fstream>

namespace mylog
{
    class LogSink
    {
    public:
        using ptr = std::shared_ptr<LogSink>;
        LogSink();
        virtual ~LogSink();
        virtual void log(const char *data, size_t len) = 0;
    };

    // 落地方向: 标准输出
    class StdoutSink : public LogSink
    {
    public:
        // 将日志消息写入到标准输出
        void log(const char *data, size_t len);
    };

    // 落地方向: 指定文件
    class FileSink : public LogSink
    {
    public:
        // 构造是传入文件名,并打开文件,将操作句柄管理起来
        FileSink(const std::string &pathname);
        
        // 将日志消息写入到标准输出
        void log(const char *data, size_t len);
    private:
        std::string _pathname;
        std::ofstream _ofs;
    };

    // 落地方向: 滚动文件 (以大小进行滚动)
    class RollBySizeSink : public LogSink
    {
    public:
        // 构造时传入文件名,并打开文件,将操作句柄管理起来
        RollBySizeSink(const std::string &basename, size_t max_size);
        
        // 将日志消息写入到标准输出 - 写入前判断文件大小,超过了最大大小就需切换文件
        void log(const char *data, size_t len);
    private:
        // 进行大小判断,超过指定大小就创建新文件
        void createNewFile(); 
    private:
        // 通过基础文件名 + 扩展文件名(以时间生成)组成一个实际的当前输出文件名
        std::string _basename; // ./log/base-        ——> ./long/base-525623462346.log
        std::ofstream _ofs;
        size_t _max_fsize; // 记录最大大小,当前文件超过了这个大小就要切换文件
        size_t _cur_fsize; // 记录当前文件已经写入的数据大小
    };
}

#endif

落地标准输出方向类成员接口具体实现:

// 落地方向: 标准输出
class StdoutSink : public LogSink
{
public:
    // 将日志消息写入到标准输出
    void log(const char *data, size_t len)
    {
        std::cout.write(data, len); 
    }
};

落地指定文件方向类成员接口具体实现:

// 落地方向: 指定文件
class FileSink : public LogSink
{
public:
    // 构造是传入文件名,并打开文件,将操作句柄管理起来
    FileSink(const std::string &pathname) : _pathname(pathname)
    {
        // 1. 创建日志文件所在目录
        util::File::createDirectory(util::File::path(pathname));
        // 2. 创建并打开日志文件,以二进制格式和追加形式打开
        _ofs.open(_pathname, std::ios::binary | std::ios::app); 
        assert(_ofs.is_open());
    }
    
    // 将日志消息写入到标准输出
    void log(const char *data, size_t len)
    {
        _ofs.write(data, len);
        assert(_ofs.good());
    }
private:
    std::string _pathname;
    std::ofstream _ofs;
};

落地滚动文件方向类成员接口具体实现:

滚动⽇志⽂件输出的必要性:
  • 由于机器磁盘空间有限, 我们不可能⼀直⽆限地向⼀个⽂件中增加数据。
  • 如果⼀个⽇志⽂件体积太⼤,⼀⽅⾯是不好打开,另⼀⽅⾯是即时打开了由于包含数据巨 ⼤,也不利于查找我们需要的信息。
  • 所以实际开发中会对单个⽇志⽂件的⼤⼩也会做⼀些控制,即当⼤⼩超过某个⼤⼩时(如 1GB),我们就重新创建⼀个新的⽇志⽂件来滚动写⽇志。 对于那些过期的⽇志, ⼤部分企业内部都有专⻔的运维⼈员去定时清理过期的⽇志,或者设置系统定时任务,定时清理过 期⽇志。

⽇志⽂件滚动的条件有两个:文件大小时间。我们可以选择:

  • ⽇志⽂件在⼤于 1GB 的时候会更换新的⽂件。
  • 每天定点滚动⼀个⽇志⽂件。

本项⽬基于文件大小 的判断滚动⽣成新的⽂件。

注意:如果我们设置的最大大小太小(下面程序中设置成了1MB),导致程序可能会在一秒钟之内生成多个同名文件,所以我们可以添加一个计数器区分文件名。

// 落地方向: 滚动文件 (以大小进行滚动)
class RollBySizeSink : public LogSink
{
public:
    // 构造时传入文件名,并打开文件,将操作句柄管理起来
    RollBySizeSink(const std::string &basename, size_t max_size) :
        _basename(basename), _max_fsize(max_size), _cur_fsize(0), _name_coumt(0)
    {
        std::string pathname = createNewFile();
        // 1. 创建日志文件所在目录
        util::File::createDirectory(util::File::path(pathname));
        // 2. 创建并打开日志文件,以二进制格式和追加形式打开
        _ofs.open(pathname, std::ios::binary | std::ios::app); 
        assert(_ofs.is_open());
    }
    
    // 将日志消息写入到文件 - 写入前判断文件大小,超过了最大大小就需切换文件
    void log(const char *data, size_t len)
    {
        if(_cur_fsize >= _max_fsize)
        {
            _ofs.close(); // 关闭原来已经打开的文件
            std::string pathname = createNewFile();
            _ofs.open(pathname, std::ios::binary | std::ios::app); 
            assert(_ofs.is_open());
            _cur_fsize = 0;
        }
        _ofs.write(data, len);
        assert(_ofs.good());
        _cur_fsize += len;
    }
private:
    // 进行大小判断,超过指定大小就创建新文件
    std::string createNewFile()
    {
        // 获取系统时间,以时间来构造文件名扩展名
        time_t t = util::Date::nowTime();
        struct tm lt;
        localtime_r(&t, &lt);
        std::stringstream filename;
        filename << _basename;
        filename << lt.tm_year + 1900; // 时间戳从1900/01/01 00:00:00开始的
        filename << "-";
        filename << lt.tm_mon + 1;     // 系统月份默认为从 0 开始,所以需+1
        filename << "-";
        filename << lt.tm_mday;
        filename << " ";
        filename << lt.tm_hour;
        filename << ":";
        filename << lt.tm_min;
        filename << ":";
        filename << lt.tm_sec;
        filename << "-";
        filename << _name_coumt++;
        filename << ".log";
        return filename.str();
    }
private:
    // 通过基础文件名 + 扩展文件名(以时间生成)组成一个实际的当前输出文件名
    size_t _name_coumt; // 如果我们设置的最大大小太小,导致程序可能会在一秒钟之内生成多个同名文件,所以我们可以添加一个计数器区分文件名
    std::string _basename; // ./log/base-        ——> ./long/base-525623462346.log
    std::ofstream _ofs;
    size_t _max_fsize; // 记录最大大小,当前文件超过了这个大小就要切换文件
    size_t _cur_fsize; // 记录当前文件已经写入的数据大小
};

实现了以上功能类,还差一个工厂类来创建不同的功能类:

使用模版不定参来解决创建不同的功能类与其成员接口参数不一致的问题:

#ifndef __M_SINK_H__
#define __M_SINK_H__
/*
    抽象出落地模块基类。
    不同落地方向从基类进行派生。
    使用工厂模式进行创建与表示的分离。
*/

#include "Util.hpp"
#include <memory>
#include <iostream>
#include <fstream>
#include <sstream>
#include <cassert>

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

#endif
编写测试代码,并运行程序:
#include "Util.hpp"
#include "Level.hpp"
#include "Message.hpp"
#include "Format.hpp"
#include "Sink.hpp"

int main()
{
    mylog::LogMsg msg(mylog::LogLevel::value::INFO, 100, "test.c", "root", "格式化功能测试...");
    mylog::Formatter fmt;
    std::string str = fmt.format(msg);

    mylog::LogSink::ptr stdout_lsp = mylog::SinkFactory::create<mylog::StdoutSink>();
    mylog::LogSink::ptr file_lsp = mylog::SinkFactory::create<mylog::FileSink>("./logfile/test.log");
    mylog::LogSink::ptr roll_lsp = mylog::SinkFactory::create<mylog::RollBySizeSink>("./logfile/roll-", 1024*1024);

    stdout_lsp->log(str.c_str(), str.size());
    file_lsp->log(str.c_str(), str.size());
    size_t cursize = 0;
    size_t count = 0;
    while(cursize < 1024 * 1024 * 10)
    {
        std::string tmp = str + std::to_string(count++);
        roll_lsp->log(tmp.c_str(), tmp.size());
        cursize += tmp.size();
    }

    return 0;
}

可以看到:

标准输出打印成功

落地文件创建成功,再查看里面的日志:

滚动文件创建了10个,每个都超过了1MB,再选择一个来查看里面的日志:

滚动文件的扩展

扩展一个以 时间作为入职文件滚动切换类型的日志落地模块。

以时间进行文件滚动,实际上是以时间段进行滚动。
实现思想:以当前系统时间,取模时间段大小,可以得到当前时间是第几个时间段,每次以当前系统时间取模,判断与当前文件的时间是否一致,不一致代表不是同一个时间段。 
#include "Util.hpp"
#include "Level.hpp"
#include "Message.hpp"
#include "Format.hpp"
#include "Sink.hpp"
#include <unistd.h>

/* 
扩展一个以时间作为入职文件滚动切换类型的日志落地模块
    以时间进行文件滚动,实际上是以时间段进行滚动
    实现思想: 以当前系统时间,取模时间段大小,可以得到当前时间是第几个时间段,
    每次以当前系统时间取模,判断与当前文件的时间是否一致,不一致代表不是同一个时间段。 
*/

enum class TimeGap
{
    GAP_SECOND,
    GAP_MINUTE,
    GAP_HOUR,
    GAP_DAY
};

// 落地方向: 滚动文件 (以时间进行滚动)
class RollByTimeSink : public mylog::LogSink
{
public:
    // 构造时传入文件名,并打开文件,将操作句柄管理起来
    RollByTimeSink(const std::string &basename, TimeGap gap_type) :
        _basename(basename)
    {
        switch(gap_type)
        {
            case TimeGap::GAP_SECOND: _gap_size = 1; break;
            case TimeGap::GAP_MINUTE: _gap_size = 60; break;
            case TimeGap::GAP_HOUR: _gap_size = 3600; break;
            case TimeGap::GAP_DAY: _gap_size = 3600*24; break;
        }
        _cur_gap = _gap_size == 1 ? mylog::util::Date::nowTime() : mylog::util::Date::nowTime() % _gap_size; // 获取当前是第几个时间段
        std::string filename = createNewFile();
        // 1. 创建日志文件所在目录
        mylog::util::File::createDirectory(mylog::util::File::path(filename));
        // 2. 创建并打开日志文件,以二进制格式和追加形式打开
        _ofs.open(filename, std::ios::binary | std::ios::app); 
        assert(_ofs.is_open());
    }
    
    // 将日志消息写入到文件 - 写入前判断文件时间段
    void log(const char *data, size_t len)
    {
        time_t cur = mylog::util::Date::nowTime();
        if((cur % _gap_size) != _cur_gap)
        {
            _ofs.close();
            std::string filename = createNewFile();
            _ofs.open(filename, std::ios::binary | std::ios::app);
            assert(_ofs.is_open());
        }
        _ofs.write(data, len);
        assert(_ofs.good());
    }
private:
    // 进行大小判断,超过指定大小就创建新文件
    std::string createNewFile()
    {
        // 获取系统时间,以时间来构造文件名扩展名
        time_t t = mylog::util::Date::nowTime();
        struct tm lt;
        localtime_r(&t, &lt);
        std::stringstream filename;
        filename << _basename;
        filename << lt.tm_year + 1900; // 时间戳从1900/01/01 00:00:00开始的
        filename << "-";
        filename << lt.tm_mon + 1;     // 系统月份默认为从 0 开始,所以需+1
        filename << "-";
        filename << lt.tm_mday;
        filename << " ";
        filename << lt.tm_hour;
        filename << ":";
        filename << lt.tm_min;
        filename << ":";
        filename << lt.tm_sec;
        filename << "-";
        filename << ".log";
        return filename.str();
    }
private:
    std::string _basename; 
    std::ofstream _ofs;
    size_t _cur_gap;  // 当前是第几个时间段
    size_t _gap_size; // 时间段的大小
};

int main()
{
    mylog::Formatter fmt;
    std::string str = fmt.format(msg);
    
    // 以一秒作为时间间隔
    mylog::LogSink::ptr time_lsp = mylog::SinkFactory::create<RollByTimeSink>("./logfile/roll-", TimeGap::GAP_SECOND);
    time_t old = mylog::util::Date::nowTime();
    while(mylog::util::Date::nowTime() < old + 5)
    {
        time_lsp->log(str.c_str(), str.size());
        usleep(1000);
    }

    return 0;
}

这里我们测试以一秒作为时间间隔,预期生成五个日志消息文件,查看运行结果:

这里43秒时的文件大小与其他时间段的文件大小差了很多,因为此时的43秒不是一个完整的时间段,即不是完整的一秒:

选择一个文件进行查看,写入了900+的消息:

(六)日志器类的设计

日志器主要是⽤来和前端交互, 当我们需要使⽤⽇志系统打印log的时候, 只需要创建Logger对象,调⽤该对象debug、info、warn、error、fatal等⽅法输出⾃⼰想打印的⽇志即可,⽀持解析可变参数列表和输出格式, 即可以做到像使⽤printf函数⼀样打印⽇志。

功能:对前边所以模块进行整合,向外提供接口完成不同等级日志的输出。

管理的成员:

  1. 格式化模块对象
  2. 落地模块对象数组(一个日志器可能会向多个位置进行日志输出)
  3. 默认的日志输出限制等级(大于等于限制等级的日志才能输出)
  4. 互斥锁(保证日志输出过程是线程安全的,不会出现交叉日志)
  5. 日志器名称(日志器的唯一标识,以便于查找)

提供的操作:

  • 获取日志器名称
  • debug等级日志的输出操作(分别会封装日志消息LogMsg--各个接口日志等级不同)
  • info等级日志的输出操作
  • warn等级日志的输出操作
  • error等级日志的输出操作
  • fatal等级日志的输出操作

实现:

  1. 抽象Logger基类(派生出同步日志器类 & 异步日志器类)。
  2. 因为两种不同的日志器,只有落地方式不同,因此将落地操作给抽象出来,不同的日志器调用各自的落地操作进行日志落地。
  3. 模块关联中使用基类指针对子类日志器对象进行日志管理和操作。

Logger.hpp

基类整体框架:

#ifndef __M_LOGG_H__
#define __M_LOGG_H__

/* 
    1. 抽象Logger基类(派生出同步日志器类 & 异步日志器类)
    2. 因为两种不同的日志器,只有落地方式不同,因此将落地操作给抽象出来,
    不同的日志器调用各自的落地操作进行日志落地。
*/

#include "Util.hpp"
#include "Level.hpp"
#include "Format.hpp"
#include "Sink.hpp"
#include <mutex>
#include <atomic>

namespace mylog
{
    class Logger
    {
    public:
        using ptr = std::shared_ptr<Logger>;
        Logger(const std::string &logger_name, 
               LogLevel::value level, 
               Formatter::ptr &formatter, 
               std::vector<LogSink::ptr> &sinks) :
               _logger_name(logger_name),
               _limit_level(level),
               _formatter(formatter),
               _sinks(sinks.begin(), sinks.end())
        {}
        // 获取日志器名称
        const std::string &getName() { return _logger_name; }

        // 完成构造日志消息对象过程并进行格式化,得到格式化后的日志消息字符串,然后进行落地输出
        void debug(const std::string &file, size_t line, const std::string &fmt, ...);
        void info(const std::string &file, size_t line, const std::string &fmt, ...);
        void warn(const std::string &file, size_t line, const std::string &fmt, ...);
        void error(const std::string &file, size_t line, const std::string &fmt, ...);
        void fatal(const std::string &file, size_t line, const std::string &fmt, ...);
    protected:
        // 抽象接口完成实际的落地输出 - 不同的日志器会有不同的实际落地方式
        virtual void log(const char *data, size_t len) = 0;
    protected:
        std::mutex _mutex;
        std::string _logger_name;
        // 输出大量日志消息时对于日志等级会进行大量并发访问,所以将其设置成为原子性的
        std::atomic<LogLevel::value> _limit_level;
        Formatter::ptr _formatter;
        std::vector<LogSink::ptr> _sinks;
    };
}
#endif

各个接口具体实现:

// 完成构造日志消息对象过程并进行格式化,得到格式化后的日志消息字符串,然后进行落地输出
void debug(const std::string &file, size_t line, const std::string &fmt, ...)
{
    // 通过传入的参数构造出一个日志消息对象,进行日志的格式化,最终落地
    // 1. 判断当前的日志是否到了输出等级
    if(LogLevel::value::DEBUG < _limit_level) { return ; }
    // 2. 对fmt格式化字符串和不定参进行字符串组织,得到的日志消息的字符串
    va_list ap;
    va_start(ap, fmt);
    char *res;
    int ret = vasprintf(&res, fmt.c_str(), ap);
    if(ret == -1)
    {
        std::cout << "vasprintf failed!\n";
        return;
    }
    va_end(ap); // 将ap指针置空
    // 3. 构造LogMsg对象
    LogMsg msg(LogLevel::value::DEBUG, line, file, _logger_name, res);
    // 4. 通过格式化工具对LogMsg进行格式化,得到格式化后的日志字符串
    std::stringstream ss;
    _formatter->format(ss, msg);
    // 5. 对日志落地
    log(ss.str().c_str(), ss.str().size());
    free(res);
}

每个等级的日志进行格式化的操作都有冗余,所以我们可以封装一个接口进行格式化:

void serialize(LogLevel::value level, const std::string &file, size_t line, char *str)
{
    // 3. 构造LogMsg对象
    LogMsg msg(level, line, file, _logger_name, str);
    // 4. 通过格式化工具对LogMsg进行格式化,得到格式化后的日志字符串
    std::stringstream ss;
    _formatter->format(ss, msg);
    // 5. 对日志落地
    log(ss.str().c_str(), ss.str().size());
}

各个等级消息格式化的实现:

// 完成构造日志消息对象过程并进行格式化,得到格式化后的日志消息字符串,然后进行落地输出
void debug(const std::string &file, size_t line, const std::string &fmt, ...)
{
    // 通过传入的参数构造出一个日志消息对象,进行日志的格式化,最终落地
    // 1. 判断当前的日志是否到了输出等级
    if(LogLevel::value::DEBUG < _limit_level) { return ; }
    // 2. 对fmt格式化字符串和不定参进行字符串组织,得到的日志消息的字符串
    va_list ap;
    va_start(ap, fmt);
    char *res;
    int ret = vasprintf(&res, fmt.c_str(), ap);
    if(ret == -1)
    {
        std::cout << "vasprintf failed!\n";
        return;
    }
    va_end(ap); // 将ap指针置空
    serialize(LogLevel::value::DEBUG, file, line, res);
    free(res);
}

void info(const std::string &file, size_t line, const std::string &fmt, ...)
{
    if(LogLevel::value::INFO < _limit_level) { return ; }
    va_list ap;
    va_start(ap, fmt);
    char *res;
    int ret = vasprintf(&res, fmt.c_str(), ap);
    if(ret == -1)
    {
        std::cout << "vasprintf failed!\n";
        return;
    }
    va_end(ap); // 将ap指针置空
    serialize(LogLevel::value::INFO, file, line, res);
    free(res);
}

void warn(const std::string &file, size_t line, const std::string &fmt, ...)
{
    if(LogLevel::value::WARN < _limit_level) { return ; }
    va_list ap;
    va_start(ap, fmt);
    char *res;
    int ret = vasprintf(&res, fmt.c_str(), ap);
    if(ret == -1)
    {
        std::cout << "vasprintf failed!\n";
        return;
    }
    va_end(ap); // 将ap指针置空
    serialize(LogLevel::value::WARN, file, line, res);
    free(res);
}

void error(const std::string &file, size_t line, const std::string &fmt, ...)
{
    if(LogLevel::value::ERROR < _limit_level) { return ; }
    va_list ap;
    va_start(ap, fmt);
    char *res;
    int ret = vasprintf(&res, fmt.c_str(), ap);
    if(ret == -1)
    {
        std::cout << "vasprintf failed!\n";
        return;
    }
    va_end(ap); // 将ap指针置空
    serialize(LogLevel::value::ERROR, file, line, res);
    free(res);
}

void fatal(const std::string &file, size_t line, const std::string &fmt, ...)
{
    if(LogLevel::value::FATAL < _limit_level) { return ; }
    va_list ap;
    va_start(ap, fmt);
    char *res;
    int ret = vasprintf(&res, fmt.c_str(), ap);
    if(ret == -1)
    {
        std::cout << "vasprintf failed!\n";
        return;
    }
    va_end(ap); // 将ap指针置空
    serialize(LogLevel::value::FATAL, file, line, res);
    free(res);
}

有了基类的抽象实现,接下来就可以派生出同步日志器类跟异步日志器类了:

1. 同步日志器类:

同步日志器是直接将日志消息进行格式化写入文件

namespace mylog
{
    // 基类code...

    // 同步日志器 - 将日志直接通过落地模块句柄进行日志落地
    class SyncLogger : public Logger
    {
    public:
        SyncLogger(const std::string &logger_name, 
               LogLevel::value level, 
               Formatter::ptr &formatter, 
               std::vector<LogSink::ptr> &sinks) :
               Logger(logger_name, level, formatter, sinks)
        {}
    protected:
        void log(const char *data, size_t len)
        {
            std::unique_lock<std::mutex> lock(_mutex);
            if(_sinks.empty()) return;
            for(auto &sink : _sinks)
            {
                sink->log(data, len);
            }
        }
    };
}

编写测试代码,运行并查看结果:

#include "Util.hpp"
#include "Level.hpp"
#include "Message.hpp"
#include "Format.hpp"
#include "Sink.hpp"
#include "Logger.hpp"
#include <unistd.h>
#include <memory>

int main()
{
    std::string logger_name = "sync_logger";
    mylog::LogLevel::value limit = mylog::LogLevel::value::WARN;
    mylog::Formatter::ptr fmt(new mylog::Formatter("[%d{%H:%M:%S}][%c][%f:%l][%p]%T%m%n"));
    mylog::LogSink::ptr stdout_lsp = mylog::SinkFactory::create<mylog::StdoutSink>();
    mylog::LogSink::ptr file_lsp = mylog::SinkFactory::create<mylog::FileSink>("./logfile/test.log");
    mylog::LogSink::ptr roll_lsp = mylog::SinkFactory::create<mylog::RollBySizeSink>("./logfile/roll-", 1024*1024);
    std::vector<mylog::LogSink::ptr> sinks = {stdout_lsp, file_lsp, roll_lsp};
    mylog::Logger::ptr logger(new mylog::SyncLogger(logger_name, limit, fmt, sinks));

    logger->debug(__FILE__, __LINE__, "%s", ":测试日志");
    logger->info(__FILE__, __LINE__, "%s", "测试日志");
    logger->warn(__FILE__, __LINE__, "%s", "测试日志");
    logger->error(__FILE__, __LINE__, "%s", "测试日志");
    logger->fatal(__FILE__, __LINE__, "%s", "测试日志");

    size_t cursize = 0, count = 0;
    while(cursize < 1024 * 1024 * 10)
    {
        logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
        cursize += 20;
    }

    return 0;
}

查看生成的目录:

查看输出到的指定文件test.log,可以看到就写入了WARN,ERRO,FATAL三个等级的日志消息,因为我们设置的最小限制等级为WARN

2. 构造日志器扩展

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

  1. 抽象一个日志器建造者类(派生出同步日志器类 & 异步日志器类)
    1. 设置日志器类型
    2. 将不同类型日志器的创建放到同一个日志器建造者类中完成
  2. 派生出具体的建造者类 -- 局部日志器的建造者 & 全局的日志器建造者(后边添加了全局单例管理器之后,将日志器添加全局管理)
namespace mylog
{
    // ...

    /* 使用建造者模式来建造日志器,而不是让用户直接去构造日志器,简化用户的使用复杂度 */
    // 1. 抽象一个日志器建造者类(派生出同步日志器类 & 异步日志器类)
    //    1. 设置日志器类型
    //    2. 将不同类型日志器的创建放到同一个日志器建造者类中完成
    enum LoggerType
    {
        LOGGER_SYNC,
        LOGGER_ASYNC
    };
    class LoggerBuilder
    {
    public:
        LoggerBuilder() : 
            _logger_type(LoggerType::LOGGER_SYNC),
            _limit_level(LogLevel::value::DEBUG)
        {}
        void buildLoggerType(LoggerType type);
        void buildLoggerName(const std::string &name);
        void buildLoggerLevel(LogLevel::value level);
        void buildFormatter(const std::string &pattern);
        template<typename SinkType, typename ...Args>
        void buildSing(Args &&...args);
        virtual void build() = 0;
    protected:
        LoggerType _logger_type;
        std::string _logger_name;
        LogLevel::value _limit_level;
        Formatter::ptr _formatter;
        std::vector<LogSink::ptr> _sinks;
    };
    // 2. 派生出具体的建造者类 -- 局部日志器的建造者 & 全局的日志器建造者(后边添加了全局单例管理器之后,将日志器添加全局管理)
    class LocalLoggerBuilder : public LoggerBuilder
    {
    public:
        void build() override;
    };
}

整体实现:


/* 使用建造者模式来建造日志器,而不是让用户直接去构造日志器,简化用户的使用复杂度 */
// 1. 抽象一个日志器建造者类(派生出同步日志器类 & 异步日志器类)
//    1. 设置日志器类型
//    2. 将不同类型日志器的创建放到同一个日志器建造者类中完成
enum class LoggerType
{
    LOGGER_SYNC,
    LOGGER_ASYNC
};
class LoggerBuilder
{
public:
    LoggerBuilder() : 
        _logger_type(LoggerType::LOGGER_SYNC),
        _limit_level(LogLevel::value::DEBUG)
    {}
    void buildLoggerType(LoggerType type) { _logger_type = type; }
    void buildLoggerName(const std::string &name) { _logger_name = name; }
    void buildLoggerLevel(LogLevel::value level) { _limit_level = level; }
    void buildFormatter(const std::string &pattern) 
    {
        _formatter = std::make_shared<Formatter>(pattern);
    }
    template<typename SinkType, typename ...Args>
    void buildSink(Args &&...args)
    {
        LogSink::ptr psink = SinkFactory::create<SinkType>(std::forward<Args>(args)...);
        _sinks.push_back(psink);
        
    }
    virtual Logger::ptr build() = 0;
protected:
    LoggerType _logger_type;
    std::string _logger_name;
    LogLevel::value _limit_level;
    Formatter::ptr _formatter;
    std::vector<LogSink::ptr> _sinks;
};
// 2. 派生出具体的建造者类 -- 局部日志器的建造者 & 全局的日志器建造者(后边添加了全局单例管理器之后,将日志器添加全局管理)
class LocalLoggerBuilder : public LoggerBuilder
{
public:
    Logger::ptr build() override
    {
        assert(_logger_name.empty() == false); // 必须有日志器名称
        if(_formatter.get() == nullptr)
        {
            _formatter = std::make_shared<Formatter>();
        }
        if(_sinks.empty())
        {
            buildSink<StdoutSink>();
        }
        if(_logger_type == LoggerType::LOGGER_ASYNC) { 
            // 异步先不做处理
        }
        // 同步
        return std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);
    }
};

编写测试代码,运行并查看结果:

#include "Util.hpp"
#include "Level.hpp"
#include "Message.hpp"
#include "Format.hpp"
#include "Sink.hpp"
#include "Logger.hpp"
#include <unistd.h>
#include <memory>

int main()
{
    std::unique_ptr<mylog::LoggerBuilder> builder(new mylog::LocalLoggerBuilder());
    builder->buildLoggerName("sync_logger");
    builder->buildLoggerLevel(mylog::LogLevel::value::WARN);
    builder->buildFormatter("%m%n");
    builder->buildLoggerType(mylog::LoggerType::LOGGER_SYNC);
    builder->buildSink<mylog::FileSink>("./logfile/test.log"); // 落地到指定文件
    builder->buildSink<mylog::StdoutSink>();                   // 落地到标准输出
    mylog::Logger::ptr logger = builder->build(); 

    logger->debug(__FILE__, __LINE__, "%s", ":测试日志");
    logger->info(__FILE__, __LINE__, "%s", "测试日志");
    logger->warn(__FILE__, __LINE__, "%s", "测试日志");
    logger->error(__FILE__, __LINE__, "%s", "测试日志");
    logger->fatal(__FILE__, __LINE__, "%s", "测试日志");

    size_t cursize = 0, count = 0;
    while(cursize < 1024 * 1024 * 10)
    {
        logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
        cursize += 20;
    }
    return 0;
}

成功落地到标准输出:

成功落地到指定文件 test.log:

(七)异步日志器类:

异步日志器:将日志消息放⼊缓冲区,由异步线程进⾏落地输出。

思想:为了避免因为写日志的过程阻塞,导致业务线程在写日志的时候影响效率,因此异步的思想就是不让业务线程进行日志的实际落地操作,而是将日志消息放到缓冲区中(一块指定1的内存),会有一个专门的异步线程去针对缓冲区的数据进行处理(实际的落地操作)

同步日志器:

异步日志器

1. 缓冲区设计思想:

要求:不能涉及到空间的频繁申请和释放,否则会降低效率。

使用的数据结构:使用环形队列(提前申请空间,然后对空间循环利用),缓存日志消息,逐条处理。

一个头指针,一个尾指针开始时都指向第一个节点,若写入了一个日志,则尾指针往后走一步,日志数据拿走时,头指针往后走一步;尾指针走到尾时再走一步只需指到环形队列头部就行,头指针同理。

引出问题:缓冲区的读写操作会涉及到多线程,因此缓冲区的操作必须保证线程安全。

线程安全实现:对于缓冲区的读写操作进行加锁

因为写日志操作,在实际开发中,并不会分配太多资源,所以工作线程只需要一个日志器有一个写日志线程就行。

涉及到的冲突:生产者与生产者的互斥 & 生产者与消费者的互斥。

引出问题:锁冲突较为严重,因为所有线程之间都存在互斥关系。

在任务池的设计中,有很多备选⽅案,⽐如循环队列等等,但是不管是哪⼀种都会涉及到锁冲突的情 况,因为在⽣产者与消费者模型中,任何两个⻆⾊之间都具有互斥关系,因此每⼀次的任务添加与取出都有可能涉及锁的冲突。
解决方案: 双缓冲区
双缓冲区是异步工作线程(消费者)将⼀个缓冲区中的任务全部处理完毕后,然后交换两个缓冲区,重新对新的缓冲区中的任务进⾏处理,虽然同时多线程写⼊也会冲突,但是冲突并不会像每次只处理⼀条的时候频繁(减少了⽣产者与消费者之间的锁冲突),且不涉及到空间的频繁申请释放所带来的消耗。


单个缓冲区类的进一步设计

设计一个缓冲区:直接存放格式化后的日志消息字符串。

好处:

  • 减少了LogMsg对象频繁的构造的消耗,提高效率。
  • 可以针对缓冲区中的日志消息,一次性进行IO操作,减少IO次数,提高效率。

缓冲区类的设计:

  1. 管理一个存放字符串数据的缓冲区(使用vector进行空间管理)。
  2. 当前的写入数据位置的指针(指向可写区域的起始位置,避免数据的写入覆盖)。
  3. 当前的读取数据位置的指针(指向可读数据区域的起始位置,当读取指针与写入指针指向相同位置则表示数据读取完了)。

提供的操作:

  • 向缓冲区写入数据
  • 获取可读数据起始地址的接口
  • 获取可读写数据长度的接口
  • 移动读写位置的接口
  • 初始化缓冲区的操作(将读写位置初始化 -- 将一个缓冲区所有数据处理完毕之后)
  • 提供缓冲区扩容的接口
  • 提供交换缓冲区的操作(交换空间地址,并不交换空间数据)

Buffer.hpp

整体框架:

#ifndef __M_BUFFER_H__
#define __M__BUFFER_H__

/* 异步日志缓冲区 */
#include "Util.hpp"
#include <vector>

namespace mylog
{   
    #define DEFAULT_BUFFER_SIZE (1024*1024*100)
    class Buffer
    {
    public:
        Buffer() : _buffer(DEFAULT_BUFFER_SIZE), _reader_idx(0), _writer_idx(0) {}
        // 向缓冲区写入数据
        void push(const char *data, size_t len);

        // 返回可读数据的起始地址
        const char *begin();

        // 返回可读写数据的长度
        size_t readAbleSize();
        size_t writeAbleSize();

        //对读指针进行向后偏移操作
        void moveReader(size_t len);

        // 重置读写位置,初始化缓冲区
        void reset();

        //对buffer实现交换操作
        void swap(Buffer &buffer);

        // 判断缓冲区是否为空
        bool empty();
    private:
        // 对空间进行扩容
        void ensureEnoughSize(size_t len);

        //对写指针进行向后偏移操作
        void moveWriter(size_t len);
    private:
        std::vector<char> _buffer;
        size_t _reader_idx; // 当前可读数据的指针
        size_t _writer_idx; // 当前可写数据的指针
    };
}

#endif

总体实现:

#ifndef __M_BUFFER_H__
#define __M__BUFFER_H__

/* 异步日志缓冲区 */
#include "Util.hpp"
#include <vector>
#include <cassert>

namespace mylog
{   
    #define DEFAULT_BUFFER_SIZE (1024*1024*100)  // 默认大小
    #define THRESHOLD_BUFFER_SIZE (80*1024*1024) // 阈值大小
    #define INCREMENT_BUFFER_SIZE (10*1024*1024) // 增量大小
    class Buffer
    {
    public:
        Buffer() : _buffer(DEFAULT_BUFFER_SIZE), _reader_idx(0), _writer_idx(0) {}
        // 向缓冲区写入数据
        void push(const char *data, size_t len)
        {
            // 缓冲区剩余空间不够的情况:
            // (1) 固定大小,则直接返回
            // if(len > writeAbleSize()) return; 不考虑 - 在lopper类已经考虑       
            // (2) 动态扩容,用于极限性能测试      
            ensureEnoughSize(len);

            // 1.将数据拷贝进缓冲区
            std::copy(data, data+len, &_buffer[_writer_idx]);
            // 2. 将当前写入位置向后偏移
            moveWriter(len);
        }

        // 返回可读数据的起始地址
        const char *begin()
        {
            return &_buffer[_reader_idx];
        }

        // 返回可读写数据的长度
        size_t readAbleSize()
        {
            // 因为当前实现的缓冲区设计思想是双缓冲区,处理完就交换,所以不存在空间循环使用的
            return (_writer_idx - _reader_idx);
        }
        size_t writeAbleSize()
        {
            // 对于扩容思路来说,不存在可写空间大小,因为总是可写
            // 因此这个接口仅仅针对固定大小缓冲区提供
            return (_buffer.size() - _writer_idx);
        }

        //对读指针进行向后偏移操作 
        void moveReader(size_t len)
        {
            assert(len <= readAbleSize());
            _reader_idx += len;
        }

        // 重置读写位置,初始化缓冲区
        void reset()
        {
            _writer_idx = 0; // 缓冲区所有空间都是空闲的
            _reader_idx = 0; // 与_write_idx相等表示没有数据可读
        }

        //对buffer实现交换操作
        void swap(Buffer &buffer)
        {
            _buffer.swap(buffer._buffer);
            std::swap(_reader_idx, buffer._reader_idx);
            std::swap(_writer_idx, buffer._writer_idx);
        }

        // 判断缓冲区是否为空
        bool empty()
        {
            return (_reader_idx == _writer_idx);
        }
    private:
        // 对空间进行扩容
        void ensureEnoughSize(size_t len)
        {
            if(len <= writeAbleSize()) return ; // 不需要扩容
            size_t new_size = 0;
            if(_buffer.size() < THRESHOLD_BUFFER_SIZE)
            {
                new_size = _buffer.size() * 2 + len; // 小于阈值则翻倍增长
            }
            else    
            {
                new_size = _buffer.size() + INCREMENT_BUFFER_SIZE + len; // 否则线性增长
            }    
            _buffer.resize(new_size);
        }
        //对写指针进行向后偏移操作
        void moveWriter(size_t len)
        {
            assert((len+_writer_idx) <= _buffer.size());
            _writer_idx += len;
        }
    private:
        std::vector<char> _buffer;
        size_t _reader_idx; // 当前可读数据的指针
        size_t _writer_idx; // 当前可写数据的指针
    };
}

#endif

编写测试代码,运行并查看结果:

测试思想:读取文件数据,一点一点写入缓冲区,最终将缓冲区数据写入文件,判断生成的新文件与源文件是否一致:

#include "Util.hpp"
#include "Level.hpp"
#include "Message.hpp"
#include "Format.hpp"
#include "Sink.hpp"
#include "Logger.hpp"
#include "Buffer.hpp"
#include <unistd.h>
#include <memory>

int main()
{
    // 读取文件数据,一点一点写入缓冲区,最终将缓冲区数据写入文件,判断生成的新文件与源文件是否一致
    std::ifstream ifs("./logfile/test.log", std::ios::binary);
    if (ifs.is_open() == false) { std::cout << "open faild!\n"; return -1; }
    ifs.seekg(0, std::ios::end); // 读写位置跳转到文件末尾
    size_t fsize = ifs.tellg(); // 获取当前读写位置相对于起始位置的偏移量
    ifs.seekg(0, std::ios::beg); // 重新跳转到起始位置
    std::string body;
    body.resize(fsize);
    ifs.read(&body[0], fsize);
    if(ifs.good() == false) { std::cout << "read error\n"; return -1; }
    ifs.close();

    std::cout << "源文件大小: " << fsize << std::endl;
    mylog::Buffer buffer;
    for(int i = 0; i < body.size(); i++)
    {
        buffer.push(&body[i], 1);
    }
    std::cout << "缓冲区大小: " << buffer.readAbleSize() << std::endl;

    std::ofstream ofs("./logfile/tmp.log", std::ios::binary); 
    // ofs.write(buffer.begin(), buffer.readAbleSize()); // 一次性写入
    size_t rsize = buffer.readAbleSize();
    for(int i = 0; i < rsize; i++)
    {
        ofs.write(buffer.begin(), 1);
        if(ofs.good() == false) { std::cout << "write error!\n"; return -1;}
        buffer.moveReader(1);
    }
    if(ofs.good() == false) {std::cout << "write error!\n"; return -1;}
    ofs.close();

    return 0;
}

可以看到logfile目录下生成了一个 tmp.log 文件,经过MD5值计算与源文件值一致,说明两个文件内容一致,从而说明文件写入成功:

2. 异步工作器类设计

异步工作使用双缓冲区思想:外界将任务数据添加到输入缓冲区中,异步线程对处理缓冲区中的数据进行处理,若处理缓冲区中没有数据了则交换缓冲区。

实现:

管理的成员:

  • 双缓冲区(生产,消费)
  • 互斥锁(保证线程安全)
  • 条件变量 - 生产&消费(生产缓冲区没有数据,处理完消费缓冲区数据后就休眠)
  • 回调函数(针对缓冲区中数据的处理接口 - 外界传入一个函数,告诉异步工作器数据该如何处理)

提供的操作:

  • 停止异步工作器
  • 添加数据到缓冲区

私有操作:

  • 创建线程,线程入口函数中,交换缓冲区,对消费缓冲区数据使用回调函数进行处理,处理完后再次交换。

Lopper.hpp

#ifndef __M_LOPPER_H__
#define __M_LOPPER_H__

/* 异步工作器 */
#include "Buffer.hpp"
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <memory>

namespace mylog
{
    using Functor = std::function<void(Buffer &)>;
    enum class AsyncType
    {
        ASYNC_SAFE, // 安全状态,表示缓冲区满了则阻塞,避免资源耗尽的风险
        ASYNC_UNSAFE // 不安全考虑资源耗尽的问题,无限扩容,常用于测试
    };
    class AsyncLooper
    {
    public:
        using ptr = std::shared_ptr<AsyncLooper>;
        AsyncLooper(const Functor &cb, AsyncType loop_type = AsyncType::ASYNC_SAFE) : 
            _looper_type(loop_type),
            _stop(false), 
            _thread(std::thread(&AsyncLooper::threadEntry, this)), 
            _callBack(cb)
        {}

        ~AsyncLooper() { stop(); }

        void stop()
        {
            _stop = true; // 将退出标志设置为true
            _cond_con.notify_all(); // 唤醒所有的工作线程
            _thread.join(); // 等待工作线程的退出
        }
        void push(const char *data, size_t len)
        {
            // 1. 无线扩容 - 非安全;    2. 固定大小 - 生产缓冲区中数据满了就阻塞
            std::unique_lock<std::mutex> lock(_mutex);
            // 条件变量空置,若缓冲区剩余空间大小大于数据长度,则可以添加数据
            if(_looper_type == AsyncType::ASYNC_SAFE)
                _cond_pro.wait(lock, [&](){ return _pro_buf.writeAbleSize() >= len; });
            // 能够走下来代表满足了条件,可以向缓冲区添加数据
            _pro_buf.push(data, len);
            // 唤醒消费者对缓冲区中的数据进行处理
            _cond_con.notify_one();
        }
    private:
        // 线程入口函数 - 对消费缓冲区中的数据进行处理,处理完毕后,初始化缓冲区,交换缓冲区
        void threadEntry() 
        {
            while(1)
            {
                // 为互斥锁设置一个生命周期,当缓冲区交换完毕后就解锁(并不对数据的处理过程加锁保护)
                {
                    // 1. 判断生产缓冲区有没有数据,有则交换,无则阻塞
                    std::unique_lock<std::mutex> lock(_mutex);

                    // 退出标志被设置。且生成缓冲区已无数据,这时候再退出,否则有可能会造成生产缓冲区中有数据,但是没有被完全处理
                    if(_stop && _pro_buf.empty()) break;
                    // 若当前是退出前被唤醒,或者有数据被唤醒,则返回真,继续向下运行,否则重新陷入休眠
                    _cond_con.wait(lock, [&](){ return _stop || !_pro_buf.empty(); });
                    _con_buf.swap(_pro_buf);

                    // 2. 唤醒生产者
                    if(_looper_type == AsyncType::ASYNC_SAFE)
                        _cond_pro.notify_all();
                }
                // 3. 被唤醒后,对消费缓冲区进行数据处理
                _callBack(_con_buf);
                // 4. 初始化消费缓冲区
                _con_buf.reset();
            }
        }
    private:
        Functor _callBack; // 具体对缓冲区数据进行处理的回调函数,由异步工作器使用者传入
    private:
        AsyncType _looper_type;
        bool _stop; // 工作器停止标志
        Buffer _pro_buf; // 生产缓冲区
        Buffer _con_buf; // 消费缓冲区
        std::mutex _mutex;
        std::condition_variable _cond_pro;
        std::condition_variable _cond_con;
        std::thread _thread; // 异步工作器对应的工作线程
    };
}

#endif

3. 异步日志器类设计

异步⽇志器类继承⾃⽇志器类, 并在同步⽇志器类上拓展了异步消息处理器。当我们需要异步输出⽇ 志的时候, 需要创建异步⽇志器和消息处理器, 调⽤异步⽇志器的log、error、info、fatal等函数输 出不同级别⽇志。

思想:

  1. 继承与Logger日志器类:对于写日志操作进行函数重写(不再将数据直接写入文件,而是通过异步消息处理器,放到缓冲区中)。
  2. realLog函数主要由异步线程进⾏调⽤(是为异步消息处理器设置的回调函数),进行日志数据的实际落地。

管理的成员:

  • 异步工作器(异步消息处理器)

Logger.hpp

namespace mylog
{
    // 异步落地器
    class AsyncLogger : public Logger
    {
    public:
        AsyncLogger(const std::string &logger_name, 
               LogLevel::value level, 
               Formatter::ptr &formatter, 
               std::vector<LogSink::ptr> &sinks,
               AsyncType looper_type) :
               Logger(logger_name, level, formatter, sinks),
               _looper(std::make_shared<AsyncLooper>(std::bind(&AsyncLogger::realLog, this, std::placeholders::_1), looper_type))
        {}

        // 将数据写入缓冲区
        void log(const char *data, size_t len)
        {
            _looper->push(data, len);
        }
        
        // 设计一个实际落地函数(将缓冲区中的数据落地)
        void realLog(Buffer &buf)
        {
            if(_sinks.empty()) return;
            for(auto &sink : _sinks)
            {
                sink->log(buf.begin(), buf.readAbleSize());
            }
        }
    private:
        AsyncLooper::ptr _looper;
    };
}

完成后,完善日志器建造者,进行异步日志器安全模式的选择,提供异步日志器的创建:

namespace mylog
{
    /* 使用建造者模式来建造日志器,而不是让用户直接去构造日志器,简化用户的使用复杂度 */
    // 1. 抽象一个日志器建造者类(派生出同步日志器类 & 异步日志器类)
    //    1. 设置日志器类型
    //    2. 将不同类型日志器的创建放到同一个日志器建造者类中完成
    enum class LoggerType
    {
        LOGGER_SYNC,
        LOGGER_ASYNC
    };
    class LoggerBuilder
    {
    public:
        LoggerBuilder() : 
            _logger_type(LoggerType::LOGGER_SYNC),
            _limit_level(LogLevel::value::DEBUG),
            _looper_type(AsyncType::ASYNC_SAFE)
        {}
        void buildLoggerType(LoggerType type) { _logger_type = type; }
        void buildEnableUnSafeAsync() { _looper_type = AsyncType::ASYNC_UNSAFE; }
        void buildLoggerName(const std::string &name) { _logger_name = name; }
        void buildLoggerLevel(LogLevel::value level) { _limit_level = level; }
        void buildFormatter(const std::string &pattern) 
        {
            _formatter = std::make_shared<Formatter>(pattern);
        }
        template<typename SinkType, typename ...Args>
        void buildSink(Args &&...args)
        {
            LogSink::ptr psink = SinkFactory::create<SinkType>(std::forward<Args>(args)...);
            _sinks.push_back(psink);
            
        }
        virtual Logger::ptr build() = 0;
    protected:
        AsyncType _looper_type;
        LoggerType _logger_type;
        std::string _logger_name;
        LogLevel::value _limit_level;
        Formatter::ptr _formatter;
        std::vector<LogSink::ptr> _sinks;
    };
    // 2. 派生出具体的建造者类 -- 局部日志器的建造者 & 全局的日志器建造者(后边添加了全局单例管理器之后,将日志器添加全局管理)
    class LocalLoggerBuilder : public LoggerBuilder
    {
    public:
        Logger::ptr build() override
        {
            assert(_logger_name.empty() == false); // 必须有日志器名称
            if(_formatter.get() == nullptr)
            {
                _formatter = std::make_shared<Formatter>();
            }
            if(_sinks.empty())
            {
                buildSink<StdoutSink>();
            }
            if(_logger_type == LoggerType::LOGGER_ASYNC) { 
                return std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);
            }
            // 同步
            return std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);
        }
    };
}

编写测试代码,测试异步日志器总体模块:

#include "Util.hpp"
#include "Level.hpp"
#include "Message.hpp"
#include "Format.hpp"
#include "Sink.hpp"
#include "Logger.hpp"
#include "Buffer.hpp"
#include "Looper.hpp"
#include <unistd.h>
#include <memory>

int main()
{
    std::unique_ptr<mylog::LoggerBuilder> builder(new mylog::LocalLoggerBuilder());
    builder->buildLoggerName("async_logger");
    builder->buildLoggerLevel(mylog::LogLevel::value::WARN);
    builder->buildFormatter("[%c]%m%n");
    builder->buildLoggerType(mylog::LoggerType::LOGGER_ASYNC);
    builder->buildEnableUnSafeAsync();
    builder->buildSink<mylog::FileSink>("./asynclogfile/async.log");
    builder->buildSink<mylog::StdoutSink>();
    mylog::Logger::ptr logger = builder->build(); 

    logger->debug(__FILE__, __LINE__, "%s", ":测试日志");
    logger->info(__FILE__, __LINE__, "%s", "测试日志");
    logger->warn(__FILE__, __LINE__, "%s", "测试日志");
    logger->error(__FILE__, __LINE__, "%s", "测试日志");
    logger->fatal(__FILE__, __LINE__, "%s", "测试日志");

    size_t count = 0;
    while(count < 500000)
    {
        logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
    }

    return 0;
}
运行结果查看日志消息,可以看到标准输出和指定文件都输出了50003条(1条warn、1条error、1条fatal、500000条fatal)数据,都符合预期:

(八)单例日志器管理类设计

日志的输出,我们希望能够在任意位置都可以进行,但是当我们创建了⼀个日志器之后,就会受到日志器所在作用域的访问属性限制。
因此,为了突破访问区域的限制,我们创建⼀个日志器管理类,且这个类是⼀个单例类,这样的话, 我们就可以在任意位置来通过管理器单例获取到指定的日志器来进行日志输出了。
基于单例日志器管理器的设计思想,我们对于日志器建造者类进⾏继承,继承出⼀个全局日志器建造者类,实现⼀个日志器在创建完毕后,直接将其添加到单例的日志器管理器中,以便于能够在任何位置通过日志器名称能够获取到指定的⽇志器进⾏日志输出。
作用:
  • 对所有创建的日志器进行管理。
  • 可以在程序的任意位置,获取相同的单例对象,获取其中的日志器进行日志输出。
拓展:单例管理器创建的时候,默认先创建一个日志器(用于进行标准输出的打印)。
目的:让用户在不创建任何日志的情况下,也能进行标准输出的打印,方便用户使用。
设计:
  • 管理的成员:
  • 默认日志器
  • 所管理的日志器数组
  • 互斥锁
提供的接口:
  • 添加日志器管理
  • 判断是否管理了指定名称的日志器
  • 获取指定名称的日志器
  • 获取默认日志器
namespace mylog
{
    // 日志器管理类
    class LoggerManager
    {
    public:
        static LoggerManager& getInstance()
        {
            // 在C++11之后,针对静态局部变量,编译器在编译的层面实现了线程安全
            // 当静态局部变量在没有构造完成之前,其他的线程进入就会阻塞
            static LoggerManager eton;
            return eton;
        }
        void addLogger(Logger::ptr &logger)
        {
            if(hasLogger(logger->getName())) return;
            std::unique_lock<std::mutex> lock(_mutex);
            _loggers.insert(std::make_pair(logger->getName(), logger));
        }
        bool hasLogger(const std::string &name)
        {
            std::unique_lock<std::mutex> lock(_mutex);
            auto it = _loggers.find(name);
            if(it == _loggers.end())
                return false;
            return true;
        }
        Logger::ptr getLogger(const std::string &name)
        {
            std::unique_lock<std::mutex> lock(_mutex);
            auto it = _loggers.find(name);
            if(it == _loggers.end())
                return Logger::ptr();
            return it->second;
        }
        Logger::ptr rootLogger() { return _root_logger; }
    private:
        LoggerManager() 
        {
            std::unique_ptr<mylog::LoggerBuilder> builder(new mylog::LocalLoggerBuilder());
            builder->buildLoggerName("root");
            _root_logger = builder->build();
            _loggers.insert(std::make_pair("root", _root_logger));
        }
    private:
        std::mutex _mutex;
        Logger::ptr _root_logger; // 默认日志器
        std::unordered_map<std::string, Logger::ptr> _loggers;
    };
}

实现了管理类后,就可以派生出具体的建造者类 -- 全局的日志器建造者(添加了全局单例管理器之后,将日志器添加全局管理):

namespace mylog
{
    // 2. 派生出具体的建造者类 -- 全局的日志器建造者(添加了全局单例管理器之后,将日志器添加全局管理)
    class GlobalLoggerBuilder : public LoggerBuilder
    {
    public:
        Logger::ptr build() override
        {
            assert(_logger_name.empty() == false); // 必须有日志器名称
            if(_formatter.get() == nullptr)
            {
                _formatter = std::make_shared<Formatter>();
            }
            if(_sinks.empty())
            {
                buildSink<StdoutSink>();
            }
            Logger::ptr logger;
            if(_logger_type == LoggerType::LOGGER_ASYNC) { 
                // 异步
                logger =  std::make_shared<AsyncLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);
            }else{
                // 同步
                logger =  std::make_shared<SyncLogger>(_logger_name, _limit_level, _formatter, _sinks);
            }
            LoggerManager::getInstance().addLogger(logger);
            return logger;
        }
    };
}

编写测试代码,运行并查看结果测试全局管理日志器:

#include "Util.hpp"
#include "Level.hpp"
#include "Message.hpp"
#include "Format.hpp"
#include "Sink.hpp"
#include "Logger.hpp"
#include "Buffer.hpp"
#include "Looper.hpp"
#include <unistd.h>
#include <memory>

// 测试全局管理日志器
void test_log()
{
    mylog::Logger::ptr logger = mylog::LoggerManager::getInstance().getLogger("async_logger");

    logger->debug(__FILE__, __LINE__, "%s", ":测试日志");
    logger->info(__FILE__, __LINE__, "%s", "测试日志");
    logger->warn(__FILE__, __LINE__, "%s", "测试日志");
    logger->error(__FILE__, __LINE__, "%s", "测试日志");
    logger->fatal(__FILE__, __LINE__, "%s", "测试日志");

    size_t count = 0;
    while(count < 500000)
    {
        logger->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
    }
}
int main()
{
    std::unique_ptr<mylog::LoggerBuilder> builder(new mylog::GlobalLoggerBuilder());
    builder->buildLoggerName("async_logger");
    builder->buildLoggerLevel(mylog::LogLevel::value::WARN);
    builder->buildFormatter("[%c]%m%n");
    builder->buildLoggerType(mylog::LoggerType::LOGGER_ASYNC);
    builder->buildEnableUnSafeAsync();
    builder->buildSink<mylog::FileSink>("./asynclogfile/async.log");
    builder->buildSink<mylog::StdoutSink>();
    builder->build();
    
    test_log();

    return 0;
}

运行结果查看日志消息,可以看到标准输出和指定文件都输出了50003条(1条warn、1条error、1条fatal、500000条fatal)数据,都符合预期:

(九)日志全局接口设计

提供全局接口和宏函数,对日志系统接口进行使用便捷性优化:
思想:
  • 提供获取指定日志器的全局接口(避免用户自己操作单例对象)。
  • 使用宏函数对日志器的接口进行代理(代理模式)。
  • 提供宏函数,直接通过默认日志器进行日志的标准输出打印(不用获取日志器了)。
#ifndef __M_MYLOG_H__
#define __M_MYLOG_H__

#include "Logger.hpp"

namespace mylog
{
    // 1. 提供获取指定日志器的全局接口(避免用户自己操作单例对象)
    Logger::ptr getLogger(const std::string &name)
    {
        return mylog::LoggerManager::getInstance().getLogger(name);
    }
    Logger::ptr rootLogger()
    {
        return mylog::LoggerManager::getInstance().rootLogger();
    }

    // 2. 使用宏函数对日志器的接口进行代理(代理模式)。
    #define debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
    #define info(fmt, ...) info(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
    #define warn(fmt, ...) warn(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
    #define error(fmt, ...) error(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
    #define fatal(fmt, ...) fatal(__FILE__, __LINE__, fmt, ##__VA_ARGS__)

    // 3. 提供宏函数,直接通过默认日志器进行日志的标准输出打印(不用获取日志器了)。
    #define DEBUG(fmt, ...) mylog::rootLogger()->debug(fmt, ##__VA_ARGS__)
    #define INFO(fmt, ...) mylog::rootLogger()->info(fmt, ##__VA_ARGS__)
    #define WARN(fmt, ...) mylog::rootLogger()->warn(fmt, ##__VA_ARGS__)
    #define ERROR(fmt, ...) mylog::rootLogger()->error(fmt, ##__VA_ARGS__)
    #define FATAL(fmt, ...) mylog::rootLogger()->fatal(fmt, ##__VA_ARGS__)
}

#endif

编写测试代码,测试通过宏函数能否落地到指定文件:

#include "mylog.h"
#include <unistd.h>

// 测试日志器
void test_log()
{
    mylog::Logger::ptr logger = mylog::LoggerManager::getInstance().getLogger("async_logger");
    logger->debug("%s", "测试日志");
    logger->info("%s", "测试日志");
    logger->warn("%s", "测试日志");
    logger->error("%s", "测试日志");
    logger->fatal("%s", "测试日志");
    size_t count = 0;
    while (count < 500000)
    {
        logger->fatal("测试日志-%d", count++);
    }
}
int main()
{
    std::unique_ptr<mylog::LoggerBuilder> builder(new mylog::GlobalLoggerBuilder());
    builder->buildLoggerName("async_logger");
    builder->buildLoggerLevel(mylog::LogLevel::value::WARN);
    builder->buildFormatter("[%c][%f:%l]%m%n");
    builder->buildLoggerType(mylog::LoggerType::LOGGER_ASYNC);
    builder->buildEnableUnSafeAsync();
    builder->buildSink<mylog::FileSink>("./asynclogfile/async.log");
    builder->buildSink<mylog::StdoutSink>();
    builder->build();
    
    test_log();

    return 0;
}
成功落地到标准输出和指定文件,符合预期:

测试通过默认日志器宏函数,能否进行标准输出打印:

// 测试日志器
void test_log()
{
    DEBUG("%s", "测试日志");
    INFO("%s", "测试日志");
    WARN("%s", "测试日志");
    ERROR("%s", "测试日志");
    FATAL("%s", "测试日志");

    size_t count = 0;
    while(count < 500000)
    {
        FATAL("测试日志-%d", count++);
    }
}
成功落地到标准输出打印,符合预期:

七、项目目录结构管理

最终 LogSystem就为我们的项目整体, logsys为我们使用该系统时所包含的源码目录,里面的 mylog.h为使用该系统时所包含的头文件:

(一)整体测试代码(功能样例代码)测试

各个组件功能打包成一个目录,进行测试:

/example/test.cc

#include "../logsys/mylog.h"
#include <unistd.h>

// 测试日志器
void test_log(const std::string &name)
{
    INFO("%s", "测试开始");

    mylog::Logger::ptr logger = mylog::LoggerManager::getInstance().getLogger(name);
    logger->debug("%s", "测试日志");
    logger->info("%s", "测试日志");
    logger->warn("%s", "测试日志");
    logger->error("%s", "测试日志");
    logger->fatal("%s", "测试日志");

    INFO("%s", "测试结束");
}
int main()
{
    std::unique_ptr<mylog::LoggerBuilder> builder(new mylog::GlobalLoggerBuilder());
    builder->buildLoggerName("async_logger");
    builder->buildLoggerLevel(mylog::LogLevel::value::DEBUG);
    builder->buildFormatter("[%c][%f:%l][%p]%m%n");
    builder->buildLoggerType(mylog::LoggerType::LOGGER_SYNC);
    builder->buildSink<mylog::FileSink>("./logfile/sync.log");
    builder->buildSink<mylog::StdoutSink>();
    builder->buildSink<mylog::RollBySizeSink>("./logfile/roll-sync-by-size.log", 1024*1024);
    builder->build();
    
    test_log("async_logger");

    return 0;
}

运行程序可以看到标准输出、指定文件、滚动文件都落地到位:

(二)滚动文件扩展代码测试:

#include "../logsys/mylog.h"
#include <unistd.h>

/* 
扩展一个以时间作为入职文件滚动切换类型的日志落地模块
    以时间进行文件滚动,实际上是以时间段进行滚动
    实现思想: 以当前系统时间,取模时间段大小,可以得到当前时间是第几个时间段,
    每次以当前系统时间取模,判断与当前文件的时间是否一致,不一致代表不是同一个时间段。 
*/

enum class TimeGap
{
    GAP_SECOND,
    GAP_MINUTE,
    GAP_HOUR,
    GAP_DAY
};

// 落地方向: 滚动文件 (以时间进行滚动)
class RollByTimeSink : public mylog::LogSink
{
public:
    // 构造时传入文件名,并打开文件,将操作句柄管理起来
    RollByTimeSink(const std::string &basename, TimeGap gap_type) :
        _basename(basename)
    {
        switch(gap_type)
        {
            case TimeGap::GAP_SECOND: _gap_size = 1; break;
            case TimeGap::GAP_MINUTE: _gap_size = 60; break;
            case TimeGap::GAP_HOUR: _gap_size = 3600; break;
            case TimeGap::GAP_DAY: _gap_size = 3600*24; break;
        }
        _cur_gap = _gap_size == 1 ? mylog::util::Date::nowTime() : mylog::util::Date::nowTime() % _gap_size; // 获取当前是第几个时间段
        std::string filename = createNewFile();
        // 1. 创建日志文件所在目录
        mylog::util::File::createDirectory(mylog::util::File::path(filename));
        // 2. 创建并打开日志文件,以二进制格式和追加形式打开
        _ofs.open(filename, std::ios::binary | std::ios::app); 
        assert(_ofs.is_open());
    }
    
    // 将日志消息写入到文件 - 写入前判断文件时间段
    void log(const char *data, size_t len)
    {
        time_t cur = mylog::util::Date::nowTime();
        if((cur % _gap_size) != _cur_gap)
        {
            _ofs.close();
            std::string filename = createNewFile();
            _ofs.open(filename, std::ios::binary | std::ios::app);
            assert(_ofs.is_open());
        }
        _ofs.write(data, len);
        assert(_ofs.good());
    }
private:
    // 进行大小判断,超过指定大小就创建新文件
    std::string createNewFile()
    {
        // 获取系统时间,以时间来构造文件名扩展名
        time_t t = mylog::util::Date::nowTime();
        struct tm lt;
        localtime_r(&t, &lt);
        std::stringstream filename;
        filename << _basename;
        filename << lt.tm_year + 1900; // 时间戳从1900/01/01 00:00:00开始的
        filename << "-";
        filename << lt.tm_mon + 1;     // 系统月份默认为从 0 开始,所以需+1
        filename << "-";
        filename << lt.tm_mday;
        filename << " ";
        filename << lt.tm_hour;
        filename << ":";
        filename << lt.tm_min;
        filename << ":";
        filename << lt.tm_sec;
        filename << "-";
        filename << ".log";
        return filename.str();
    }
private:
    std::string _basename; 
    std::ofstream _ofs;
    size_t _cur_gap;  // 当前是第几个时间段
    size_t _gap_size; // 时间段的大小
};

int main()
{
    std::unique_ptr<mylog::LoggerBuilder> builder(new mylog::GlobalLoggerBuilder());
    builder->buildLoggerName("async_logger");
    builder->buildLoggerLevel(mylog::LogLevel::value::WARN);
    builder->buildFormatter("[%c][%f:%l]%m%n");
    builder->buildLoggerType(mylog::LoggerType::LOGGER_ASYNC);
    builder->buildSink<RollByTimeSink>("./logfile/roll-async-by-size.log", TimeGap::GAP_SECOND);
    mylog::Logger::ptr logger = builder->build();

    size_t cur = mylog::util::Date::nowTime();
    while(mylog::util::Date::nowTime() < cur + 5)
    {
        logger->fatal("这是一条测试日志");
        usleep(1000);
    }
    
    return 0;
}

八、性能测试

测试三要素:

  • 测试环境:
  • 测试方法:
  • 测试结果:

测试环境:

CPU:AMD Ryzen 7 5800H with Radeon Graphics 3.20 GHz
RAM:16G DDR4 3200
ROM:512G-SSD
OS:ubuntu-22.04TLS虚拟机(2CPU核⼼/4G内存)

测试工具的编写:

  • 可以控制写日志线程数量
  • 可以控制写日志的总数量

方法:分别对于同步日志器 & 异步日志器在单多线程下生成指定数量的日志消息总耗时的性能测试:

  • 需要测试单写日志线程的性能
  • 需要测试多写日志线程的性能

实现:

  1. 封装一个接口,传入日志器名称,线程数量,日志数量,单条日志大小。
  2. 在接口内,创建指定数量的线程,各自负责一部分日志的输出。
  3. 在输出之前计时开始,在输出完毕后即使结束,所耗时间 = 结束时间 - 起始时间。
  4. 每秒输出量 = 日志数量 / 总耗时。
  5. 每秒输出大小 = 日志数量 * 单条日志大小 / 总耗时。

注意:异步日志输出启动非安全模式,纯内存写入(不去考虑实际落地的时间)。

#include "../logsys/mylog.h"
#include <chrono>

void bench(const std::string &logger_name, size_t thr_count, size_t msg_count, size_t msg_len)
{
    // 1. 获取日志器
    mylog::Logger::ptr logger = mylog::getLogger(logger_name);
    if(logger.get() == nullptr)
    {
        return ;
    }
    std::cout << "测试日志: " << msg_count << "条,总大小: " << (msg_count * msg_len) / 1024 << "KB\n";

    // 2. 组织指定长度的日志消息
    std::string msg(msg_len-1, 'A'); // 少一个字节是为了给末尾到时候添加换行

    // 3. 创建指定数量的线程
    std::vector<std::thread> threads;
    std::vector<double> cost_arry(thr_count);
    size_t msg_per_thr = msg_count / thr_count; // 总日志数量 / 线程数量就是每个线程要输出的日志数量
    for(int i = 0; i < thr_count; i++)
    {
        threads.emplace_back([&, i]()
        {
            // 4. 线程函数内部开始计时
            auto start = std::chrono::high_resolution_clock::now();
            // 5. 开始循环写日志
            for(int j = 0; j < msg_per_thr; j++)
            {
                logger->fatal("%s", msg.c_str());
            }
            // 6. 线程函数内部结束计时
            auto end = std::chrono::high_resolution_clock::now();
            std::chrono::duration<double> cost = end - start;
            cost_arry[i] = cost.count();
            std::cout << "\t线程" << i << ": " << "\t输出数量: " << msg_per_thr << ", 耗时: " << cost.count() << "s" << std::endl;
        });
    }
    for(int i = 0; i < thr_count; i++)
    {
        threads[i].join();
    }

    // 7. 计算总耗时: 在多线程中,每个线程都会耗费时间,但是线程时并发处理的,因此耗时最高的那个就是总时间
    double max_cost = cost_arry[0];
    for(int i = 0; i < thr_count; i++)
    {
        max_cost = max_cost < cost_arry[i] ? cost_arry[i] : max_cost;
    }
    size_t msg_per_sec = msg_count / max_cost;
    size_t size_per_sec = (msg_count * msg_len) / (max_cost * 1024);

    // 8. 进行输出打印
    std::cout << "\t总耗时: " << max_cost << "s\n";
    std::cout << "\t每秒输出日志数量: " << msg_per_sec << "条\n";
    std::cout << "\t每秒输出日志大小: " << size_per_sec << "KB\n";
}

void sync_bench()
{
    std::unique_ptr<mylog::LoggerBuilder> builder(new mylog::GlobalLoggerBuilder());
    builder->buildLoggerName("sync_logger");
    builder->buildFormatter("%m%n");
    builder->buildLoggerType(mylog::LoggerType::LOGGER_SYNC);
    builder->buildSink<mylog::FileSink>("./logfile/sync.log");
    builder->build();
    // bench("sync_logger", 1, 1000000, 100);
    bench("sync_logger", 3, 1000000, 100);
}

void async_bench()
{
    std::unique_ptr<mylog::LoggerBuilder> builder(new mylog::GlobalLoggerBuilder());
    builder->buildLoggerName("async_logger");
    builder->buildFormatter("%m%n");
    builder->buildLoggerType(mylog::LoggerType::LOGGER_ASYNC);
    builder->buildEnableUnSafeAsync(); // 开启非安全模式 - 主要是为了将落地时间排除在外
    builder->buildSink<mylog::FileSink>("./logfile/async.log");
    builder->build();
    // bench("async_logger", 1, 1000000, 100);
    bench("async_logger", 3, 1000000, 100);
}

int main()
{
    // sync_bench();
    async_bench();

}

测试结果:同步下多线程比单线程慢:单线程不涉及到锁冲突,每次都加锁、写日志,加锁、写日志...,是一个串行化的过程;但多线程就涉及到锁冲突,另外磁盘性能也上限了 ,导致多线程性能反而降低了:

测试结果:异步下可以看到安全与非安全模式二者消耗时间差不了多少,在非安全模式下也就少了一丢丢时间,也就是少了实际落地的时间:

测试结果:异步多线程下只考虑线程只往内存去写数据,不考虑磁盘的性能,这时考验的就是CPU和内存的性能了:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1871732.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

mysql安装创建数据库防止踩坑

为了安装MySQL的家人们走弯路&#xff0c;稍微有些啰嗦&#xff0c;讲述我安装的时遇到的问题&#xff0c;如何解决。仔细看看离成功不远。 mysql下载链接 MySQL :: Download MySQL Community Server windows下安装mysql-8.0.29-winx64&#xff0c;下载安装包后解压到文件夹中…

摄影后期色彩管理流程(Lightroom篇)

在摄影后期处理中&#xff0c;色彩管理是确保图像从捕捉到输出的一致性和准确性的关键。Lightroom 和 Photoshop 其实已经将这套色彩管理流程作为默认选项&#xff0c;如果实质操作时仍存在色彩偏差的问题&#xff0c;可参考以下内容。 ProPhoto RGB > Adobe RGB > sRGB …

clickhouse count和uniqCombined

count(distinct ) 和 uniqCombined 获取去重后的总数。 去重&#xff1a;order by distinct argMax group by 哪个好&#xff1f;&#xff1f; clickhouse数据去重函数介绍&#xff08;count distinct&#xff09;_clickhouse distinct-CSDN博客

[论文阅读笔记33] Matching Anything by Segmenting Anything (CVPR2024 highlight)

这篇文章借助SAM模型强大的泛化性&#xff0c;在任意域上进行任意的多目标跟踪&#xff0c;而无需任何额外的标注。 其核心思想就是在训练的过程中&#xff0c;利用strong augmentation对一张图片进行变换&#xff0c;然后用SAM分割出其中的对象&#xff0c;因此可以找到一组图…

注意力机制在大语言模型中的应用

在大语言模型中&#xff0c;注意力机制&#xff08;Attention Mechanism&#xff09;用于捕获输入序列中不同标记&#xff08;token&#xff09;之间的关系和依赖性。这种机制可以动态地调整每个标记对当前处理任务的重要性&#xff0c;从而提高模型的性能。具体来说&#xff0…

Qt通过句柄获取其它进程控件实例

1.通过spy获取想要获取控件的句柄id 通过spy获取另一个软件的文本框的句柄 2.Qt写代码&#xff0c; 根据句柄获取文本框的内容 void getTextFromExternalWindow(HWND hwnd) {const int bufferSize 256;TCHAR buffer[bufferSize];// 获取窗口文本内容int length GetWindowT…

svn明明都在环境变量中添加了,但还是无法在cmd中生效

svn明明都在环境变量中添加了&#xff0c;但还是无法在cmd中生效 cmd显示原因问题解决 cmd显示 svn不是内部或外部命令&#xff0c;也不是可运行的程序或批处理文件 原因 安装svn一直点下一步下一步…&#xff0c;没有勾选command line client。 问题解决 1.按下winx&…

CSS|04 复合选择器伪类选择器属性选择器美化超链接

基本选择器&#xff1a;见上篇基本选择器 复合选择器选择器1,选择器2{属性:值;} 多元素选择器&#xff0c;同时匹配选择器1和选择器2&#xff0c;多个选择器之间用逗号分隔举例&#xff1a; p,h1,h2{margin:0px;}E F{属性:值;} 后代元素选择器&#xff0c;匹配所有属于E元素后…

Linux实用命令练习

目录 一、常用命令 二、系统命令 三、用户和组 四、权限 五、文件相关命令 六、查找 七、正则表达式 八、输入输出重定向 九、进程控制 十、其他命令 1、远程文件复制&#xff1a;scp 2、locate查找 3、which命令 4、设置或显示环境变量&#xff1a;export 5、修…

解决所有终端中文输出乱码的问题

一、系统自带的cmd.exe 以及 Git的bash.exe、sh.exe、git-bash.exe和git-cmd.exe&#xff0c;和PowerShell默认使用“当前系统区域设置”设定好的936 (ANSI/OEM - 简体中文 GBK)语言编码。 1、[当前代码页] 的936 (ANSI/OEM - 简体中文 GBK) 是导致中文乱码的原因 在控制面板→…

[分布式网络通讯框架]----Protobuf安装配置--附带每一步截图

Protobuf Protobuf&#xff08;Protocol Buffers&#xff09;协议是一种由 Google 开发的二进制序列化格式和相关的技术&#xff0c;它用于高效地序列化和反序列化结构化数据&#xff0c;通常用于网络通信、数据存储等场景。 为什么要使用Protobuf Protobuf 在许多领域都得到…

网上零食销售系统

摘 要 随着互联网的快速发展&#xff0c;网上销售已成为零售业的重要组成部分。在众多的线上购物品类中&#xff0c;零食销售因其受众广泛、购买频率高、消费金额适中等特点&#xff0c;一直备受关注。然而&#xff0c;传统的零食销售方式&#xff0c;如实体店铺销售&#xff…

web前端——css(一篇教会网页制作)

目录 一、基本语法 1.行内样式表 2.内嵌样式表 3.外部样式表 二、选择器 1.标签选择器 2.类选择器 3.id 选择器 4.通配选择器 三、常见修饰 1.文本 2.背景 3.列表 4.伪类 5.透明度 6.块级、行级、行级块标签 7.div 和 span 四、盒子模型&#xff08;重点&…

Reqable实战系列:Flutter移动应用抓包调试教程

Flutter应用网络请求调试一直是业内难题&#xff0c;原因在于Dart语言标准库的网络请求不会走Wi-Fi代理&#xff0c;常规通过配置Wi-Fi代理来抓包的方式行不通。这给我们日常开发测试造成了很大的阻碍&#xff0c;严重降低工作效率。因此写一篇教程&#xff0c;讲解如何使用Req…

vue的ESLint 4格缩进 笔记

https://chatgpt.com/share/738c8560-5271-45c4-9de0-511fad862109 一&#xff0c;代码4格缩进设置 .eslintrc.js文件 module.exports { "rules": { "indent": ["error", 4] } }; 自动修复命令 npx eslint --fix "src/**/*.{…

ONLYOFFICE 8.1编辑器桌面应用程序来袭——在线全面测评

目录 ✈下载✈ &#x1f440;界面&#x1f440; &#x1f44a;功能&#x1f44a; &#x1f9e0;幻灯片版式的重大改进&#x1f9e0; ✂无缝切换文档编辑、审阅和查看模式✂ &#x1f3b5;在演示文稿中播放视频和音频文件&#x1f3b5; &#x1f917;版本 8.1&#xff1a…

CDGA数据治理:数字化时代的基石

随着数字化浪潮的汹涌而来&#xff0c;数据已成为当今世界的核心驱动力。无论是企业决策、市场趋势预测&#xff0c;还是个性化用户体验&#xff0c;都离不开数据的支撑。而在这一过程中&#xff0c;数据治理的重要性日益凸显&#xff0c;成为实现数字化转型、发挥数据价值的最…

Django 如何使用视图动态输出 CSV 以及 PDF

Django 如何使用视图动态输出 CSV 以及 PDF 这一篇我们需要用到 python 的 csv 和 reportLab 库&#xff0c;通过django视图来定义输出我们需要的 csv 或者 pdf 文件。 csv文件 打开我们的视图文件 testsite/members/views.py 。新增一个视图方法&#xff1a; import csv …

Java反射获取--类对象(class对象),3种方式

Java反射获取–类对象(class对象)&#xff0c;3种方式 前言 内容节选个人oneNote个人笔记&#xff0c;分享Java反射获取--类对象(class对象)&#xff0c;3种方式

mac 安装mysql启动报错 ERROR!The server quit without update PID file

发现问题&#xff1a; mac安装mysql初次启动报错&#xff1a; 一般出现这种问题&#xff0c;大多是文件夹权限&#xff0c;或者以前安装mysql卸载不干净导致。首先需要先确定问题出在哪&#xff1f;根据提示我们可以打开mysql的启动目录&#xff0c;查看启动日志。 问题解决&a…