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

日志系统

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

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

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写日志文件

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YQ1DlOq1-1690428346688)(/Users/chenlixin/Library/Application Support/typora-user-images/image-20230718141234144.png)]

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

  • 一方面,大量的日志打印陷入等量的write系统调用,具有一定的系统开销
  • 另一方面,使得打印日志的进程附带了大量同步的磁盘IO,影响程序性能

4.2.2 异步写日志

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ML4oE8Lu-1690428346688)(/Users/chenlixin/Library/Application Support/typora-user-images/image-20230718142819898.png)]

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

  • 主线程调用日志打印接口成为非阻塞操作
  • 同步的磁盘IO操作从主线程剥离出来交给单独的线程完成

5、相关技术知识

5.1 不定参函数

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

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

5.1.1 不定参宏函数

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GN4Jpsvg-1690428346688)(/Users/chenlixin/Library/Application Support/typora-user-images/image-20230718145341568.png)]

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

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

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

解释一下:

  • fmt(format):就是我们的格式化字符串,编译器就是以它为依据进行不定参数解析的
  • ...: 就是我们的不定参数
  • "[%s : %d] " fmt 因为这两个都是格式化字符串,C语言是支持直接连接的
  • __VA_ARGS__也是C语言给我们提供的宏函数,用于给我们的fmt传参
  • ##__VA_ARGS__ 加了##是为了告诉编译器,若我们只想传一个不定参数,可以省略前面的fmt参数的传递

##__VA_ARGS__的意思就是
本来我们只想传递一个不定参数需要这么写 LOG("%s", "clx"); 现在可以省略fmt参数的传递 LOG("clx");就可以了

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六大原则
  • 单一责任原则(Single Responsibility Principle)

    • 类的职责应该单一,一个方法只做一件事,职责划分清晰,每次改动到最小单位的方法或类
    • 使用建议:两个完全不一样的功能不应该放在一个类中,一个类中应该是一组相关性很高的函数、数据的封装
    • 用例:网络聊天类(❌)应该分割成网络通信类 + 聊天类
  • 开闭原则(Open Closed Principle)

    • 对扩展开放,对修改封闭(只添加新功能,不修改原有内容)
    • 使用建议:对软件实体的改动,最好用扩展而非修改的方式
    • 用例:超时卖货:商品价格—不是修改商品原来的价格,而是新增促销的价格
  • 里氏替换原则(Liskov Substitution Principle)

    • 凡事父类能够出现的地方,子类就可以出现,而且替换为子类也不会出现任何的错误或者异常
    • 在继承类时,务必重写父类中的所有方法,尤其注意父类的protected方法,子类尽量不要暴露自己的public方法供外界调用
    • 使用建议:子类无比完全实现父类的方法,还子类可以有自己的个性,覆盖或者实现父类的方法时,输入的参数可以被放大,输出也可以缩小
    • 用例:跑步运动员类:会跑步, 子类长跑运动员-会跑步且擅长长跑,子类短跑运动员:会跑步且擅长短跑
  • 依赖倒置原则(Dependence Inversion Principle)

    • 高层模块不应该依赖底层模块,两者都应该依赖其抽象,不可分隔的原子逻辑就是低层的模式,原子逻辑组装成的就是高层模块
    • 模块间依赖通过抽象(接口)发生,具体类之间不能直接依赖
    • 使用建议:每一个类都尽量有抽象类,任何类都不应该从具体类派生。尽量不要重写基类的方法。结合里氏替换原则使用
    • 用例:奔驰车司机 – 只能开奔驰,司机类:给什么车开什么车 : 开车的人 : 司机 – 依赖抽象
  • 迪米特法则(Law of Demeter) 最少知道法则

    • 尽量减少对象之间的交互,从而减少类之间的耦合。一个对象应该对其他对象有最少的了解,对类的低耦合提出了明确的要求:
      • 只喝直接的朋友交流,朋友间也是有剧烈的。自己的就是自己的(如果一个方法放在本类中,既不增加类间关系,也不对本类造成负面影响,那就放置在本类中)
    • 用例:老师让班长点名,老师给班长名单,班长点名勾选,返回结果。老师只和班长交互,同学们只和班长交互
  • 接口隔离原则

    • 客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上
    • 使用建议:接口设计尽量精简单一,但是不要对外暴露没有啥意义的接口
    • 用例:修改密码,不应该提供用户信息接口,而是单一使用修改密码接口

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

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. 日志要写入指定位置(标准输出,指定文件,滚动文件…)
    • 日志系统需要支持将日志消息落地到不同位置—多落地方向
    1. 日志写入指定位置,支持不同的写入方式(同步,异步)
    • 同步:业务线程自己负责日志写入(流程简单,但是可能因为阻塞导致效率降低)
    • 异步:业务线程将日志放入缓冲区内存,让其他异步线程负责将日志写入到指定位置(业务线程不会阻塞)
    1. 日志输出以日志器为单位,支持多日志器(不同的项目组有不同的输出策略)
    1. 日志器的管理

6.1 模块划分

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

  • OFF : 关闭
  • DEBUG : 调试,调试时的关键信息输出
  • INFO : 提示,普通的提示型日志信息
  • WARN : 警告,不影响运行,但是需要注意一下的小错误
  • ERROR : 错误,程序运行时出现错误的日志

6.2 模块关系图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FDgdwCMP-1690428346689)(/Users/chenlixin/Library/Application Support/typora-user-images/image-20230719101419628.png)]

7、代码设计

7.1 实用类设计

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

实用类整体框架

/*  实用工具类实现 :
    1、 获取系统时间
    2、 判断文件是否存在
    3、 获得文件所在路径
    4、 创建目录
*/

#include <iostream>
#include <ctime>

namespace clxlog{
    namespace util {

        class Data {
            public:
                static size_t getTime() { return static_cast<size_t>(time(nullptr)); }  // 获取当前系统时间戳
        };

        class File {
            public:
                static bool exits(const std::string &pathname);                         // 判断文件是否存在
                static std::string path(const std::string &pathname);  									// 获取文件所在路径
                static void createDirectory(const std::string& pathname);               // 创建指定目录
        };
    }
}

Data 类实现

class Data {
            public:
                static size_t now() { return static_cast<size_t>(time(nullptr)); }
        };

time_t time(time_t *t);

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

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

File 类实现


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

static bool exists(const std::string &pathname) {
    // 方法1 获取文件状态,若存在则可能获取成功,若不存在则一定失败
    struct stat st;
    if (stat(pathname.c_str(), &st) < 0) {
        return false;
    }
    return true;
    //  方法2 access  是一个系统调用接口,使用后代码移植性不好
    //  return (access(pathname.c_str(), F_OK) == 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 */
 };

方法二: 判断文件是否存在我们还可以使用access函数

access()检查调用进程是否可以访问文件路径名,模式指定要执行的可访问性检查,并且是值F_OK,或者是由R_OK、W_OK和X_OK中的一个或多个的按位OR组成的掩码。 F_OK测试文件是否存在。 R_OK、W_OK和X_OK分别测试文件是否存在并授予读取、写入和执行权限。

成功返回0,若文件不存在则或者有任意一个期望权限不存在返回-1,并设置错误码

#include <unistd.h> 
int access(const char *pathname, int mode);
// example
int main() {
#define file_name "../logs/buffer.hpp"
    if (access(file_name, F_OK < 0)){
        std::cout << "不存在" << std::endl;
    }
    std::cout << "存在" << std::endl;   // 存在
    return 0;
}

注意:access函数是Linux提供的系统调用接口,只能在Linux环境下使用,移植性不是很好

方法三:fopen函数用于判断文件是否存在可以使用 r 或者 rb模式 ,因为使用 其它方式的话,可能会自动建立文件。 返回值为NULL(打不开)和正数(能打开),用这种方法做出的判断是不完全正确的,因为有的文件存在,但是可能不可读。

 FILE *fopen(const char *path, const char *mode);

path()函数:用于获取文件所在目录路径

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);
}

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


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

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

static void createDirectory(const std::string& pathname) {
    // ./abc/a
    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.c_str())) {
            mkdir(parent_dir.c_str() , 0777);
        } 
        idx = pos + 1;
    }
}
int mkdir(const char *pathname, mode_t mode);

实用类完整代码

#ifndef __CLX_UTIL_H__
#define __CLX_UTIL_H__

/*  实用工具类实现 :
    1、 获取系统时间
    2、 判断文件是否存在
    3、 获得文件所在路径
    4、 创建目录
*/

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <ctime>

namespace clxlog{
    namespace util {

        class Data {
            public:
                static size_t now() { return static_cast<size_t>(time(nullptr)); }
        };

        class File {
            public:
                static bool exists(const std::string &pathname) {
                    // 方法1 获取文件状态,若存在则可能获取成功,若不存在则一定失败
                    struct stat st;
                    if (stat(pathname.c_str(), &st) < 0) {
                        return false;
                    }
                    return true;
                    //  方法2 access  是一个系统调用接口,使用后代码移植性不好
                    //  return (access(pathname.c_str(), F_OK) == 0);
                }
                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);
                }
                static void createDirectory(const std::string& pathname) {
                    // ./abc/a
                    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.c_str())) {
                            mkdir(parent_dir.c_str() , 0777);
                        } 
                        idx = pos + 1;
                    }
                }
        };
    }
}
#endif

7.2 日志等级类设计

设计思路:

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

  • UNKNOW : 最低等级日志
  • DEBUG : 调试等级日志
  • INFO : 提示等级日志
  • WARN : 警告等级日志
  • ERROR : 错误等级日志
  • FATAL : 致命错误等级日志
  • OFF : 最高等级,可用于禁止所有日志输出

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

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


日志等级类完整代码

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

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

注意:这里使用了#define 和 #undef 的语法,以及#variable_name可以将变量名转化为字符串输出,通常在枚举类型,宏转字符串时使用

7.3 日志消息类设计

日志消息类主要是为了封装一条完整的日志内容,其中各个字段用于存储日志的各个属性信息,只需简单提供构造函数即可
1、 日志的输出时间 可用于过滤日志
2、 日志的等级 用于进行日志的过滤分析
3、 源文件名称
4、 源代码行号 用于定位出现错误的代码位置
5、 线程ID 用于过滤出错的线程
6、 日志主题消息
7、 日志器名称 项目允许多日志器同时使用


日志消息类完整代码

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

#ifndef __CLX_MESSAGE_H__
#define __CLX_MESSAGE_H__

#include "level.hpp"
#include "util.hpp"
#include <time.h>
#include <iostream>
#include <string>
#include <thread>

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

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

7.4 日志输出格式化类设计

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

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

    /* 
        %d  表示日期,    子格式 {%H:%M:%S}  %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:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"):
                _pattern(pattern) {
                    assert(parsePattern());
                }
            /*  对msg进行格式化*/
            std::string format(const LogMsg &msg;
            void format(std::ostream &out, const 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;
    };
}

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

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 文件子项 调用 FileFormatItem 进行处理 输出 文件名到指定位置字符串

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

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

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

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

// 抽象格式化子项基类
class FormatItem {
public: 
    using ptr = std::shared_ptr<FormatItem>;												// 使用智能指针管理类对象
    virtual void format(std::ostream &out, const LogMsg &msg) = 0;  // 用于将LogMsg各个字段格式化到指定out流中
};

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

    // 派生格式化子项子类 -- 消息 等级 时间 文件名 行号 线程ID 日志器名 制表符 换行 其他
    class MsgFormatItem : 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 TimeFormatItem : public FormatItem {
    public:
        TimeFormatItem(const std::string &fmt = "%H:%M:%S") : _time_fmt(fmt) {};
        virtual void format(std::ostream &out, const 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 FileFormatItem : public FormatItem {
    public:
        virtual void format(std::ostream &out, const LogMsg &msg) override {
            out << msg._file;
        }
    };

    class LineFormatItem : public FormatItem {
    public:
        virtual void format(std::ostream &out, const LogMsg &msg) override {
            out << msg._line;
        }
    };

    class ThreadFormatItem : public FormatItem {
    public:
        virtual void format(std::ostream &out, const LogMsg &msg) override {
            out << msg._tid;
        }
    };

    class LoggerFormatItem : public FormatItem {
    public:
        void format(std::ostream &out, const LogMsg &msg) override {
            out << msg._logger;
        }
    };

    class TabFormatItem : public FormatItem {
    public:
        void format(std::ostream &out, const LogMsg &msg) override {
            out << '\t';
        }
    };

    class NewLineFormatItem : public FormatItem {
    public:
        void format(std::ostream &out, const LogMsg &msg) override {
            out << '\n';
        }
    };

    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;
    };


Formatter 类成员函数实现

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

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

 /* 
        %d  表示日期,    子格式 {%H:%M:%S}  %d{%H:%M:%S}
        %t  表示鲜橙ID 
        %c  表示日志器名称
        %f  表示源码文件名
        %l  表示源码行号
        %p  表示日志级别
        %T  表示制表符缩进
        %m  表示主体消息
        %n  表示换行
    */
    
Formatter(const std::string &pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%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将其变为字符串后返回

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

注意: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成员数组中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bo7E8MDk-1690428346689)(/Users/chenlixin/Library/Application Support/typora-user-images/image-20230719191654879.png)]

/* 对格式化字符串进行解析 */
bool parsePattern() {
    // 1、对格式化规则字符串进行解析
    std::vector<std::pair<std::string, std::string>> fmt_order;
    size_t pos = 0;
    std::string key, val;
    while (pos < _pattern.size()) {
        // (一) 处理原始字符串 -- 判断是否为%,不是就是原始字符串
        if (_pattern[pos] != '%') {
            val.push_back(_pattern[pos++]); continue;
        }
        if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%') {
            val.push_back('%'); pos += 2; continue;
        }
        // 万一出现第一个自符就是格式化字符串,那么处理原始字符串的操作就会向数组插入{"",""}
        // 虽然不会产生错误但是便于逻辑理解,最好还是判断处理一下
        if (!val.empty()) {
            fmt_order.push_back(std::make_pair("", val));
        }
        val.clear();

        // (二) 处理格式化字符串,代表原始字符串处理完毕
        pos += 1;
        if (pos == _pattern.size()) { 
            std::cout << "%之后没有对应的格式化字符" << std::endl;
            return false; 
        }
        key = _pattern[pos];
        pos += 1;
        // 此时pos指向格式化字符串后面的位置,判断是否有格式化子串
        if (pos < _pattern.size() && _pattern[pos] == '{') {
            pos += 1;   
            // 这时pos指向子规则的起始位置
            while (pos < _pattern.size() && _pattern[pos] != '}') {
                val.push_back(_pattern[pos++]);
            }
            // 若走到了末尾,还没有找到},则说明格式是错误的,跳出循环
            if (pos == _pattern.size()) {   
                std::cout << "子规则{}匹配出错" << std::endl;
                return false;
            }
            pos += 1; // 因为pos指向的是 } 位置,向后走一步就到了下一次处理的新位置
        }
        fmt_order.push_back(std::make_pair(key, val));
        key.clear(); val.clear();
    }
    // 2、根据解析得到的数据初始化格式子项数组成员
    for (auto &it : fmt_order) {
        _items.push_back(createItem(it.first, it.second));
    } 
    return true;
}

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

  • 我们的处理顺序是先非格式化字符串再格式化字符串。若开始即是格式化字符串(%d等)的话就可能会向数组中插入{"", ""}虽然之后调用子项处理器处理的时候只是会插入一个空字符串,并不会产生错误,但是为了便于逻辑理解,最好还是判断处理一下

    if (!val.empty()) {
                fmt_order.push_back(std::make_pair("", val));
    }
    
  • 当我们处理非格式化字符串时遇到了%,我们如何确定其就是格式化字符串还是就是%字符呢?在上述格式化字符串定义中我们认为%%为一个%,所以我们还需要判断下一个字符是否还是%,如果还是则就得继续非格式化字符串处理,并且这里的pos 一次要加上2

  • 处理格式化字符串过程中,如果碰到了只有%后就没有字符了,那么一定是出错了,返回false。如果碰到了子字符串{},如果我们走到结尾都没有找到子字符串结束标志},则说明格式是错误的,返回false


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

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<FileFormatItem>();
if (key == "l")  return std::make_shared<LineFormatItem>();
if (key == "p")  return std::make_shared<LevelFormatItem>();
if (key == "T")  return std::make_shared<TabFormatItem>();
if (key == "m")  return std::make_shared<MsgFormatItem>();
if (key == "n")  return std::make_shared<NewLineFormatItem>();
if (key.empty()) return std::make_shared<OtherFormatItem>(val);

assert(false);   return FormatItem::ptr();
}

日志输出格式化类完整代码

#ifndef __CLX_FORMAT_H__
#define __CLX_FORMAT_H__
#include <vector>
#include <sstream>
#include <cassert>
#include "util.hpp"
#include "level.hpp"
#include "message.hpp"

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

    // 派生格式化子项子类 -- 消息 等级 时间 文件名 行号 线程ID 日志器名 制表符 换行 其他
    class MsgFormatItem : 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 TimeFormatItem : public FormatItem {
    public:
        TimeFormatItem(const std::string &fmt = "%H:%M:%S") : _time_fmt(fmt) {};
        virtual void format(std::ostream &out, const 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 FileFormatItem : public FormatItem {
    public:
        virtual void format(std::ostream &out, const LogMsg &msg) override {
            out << msg._file;
        }
    };

    class LineFormatItem : public FormatItem {
    public:
        virtual void format(std::ostream &out, const LogMsg &msg) override {
            out << msg._line;
        }
    };

    class ThreadFormatItem : public FormatItem {
    public:
        virtual void format(std::ostream &out, const LogMsg &msg) override {
            out << msg._tid;
        }
    };

    class LoggerFormatItem : public FormatItem {
    public:
        void format(std::ostream &out, const LogMsg &msg) override {
            out << msg._logger;
        }
    };

    class TabFormatItem : public FormatItem {
    public:
        void format(std::ostream &out, const LogMsg &msg) override {
            out << '\t';
        }
    };

    class NewLineFormatItem : public FormatItem {
    public:
        void format(std::ostream &out, const LogMsg &msg) override {
            out << '\n';
        }
    };

    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;
    };

    /* 
        %d  表示日期,    子格式 {%H:%M:%S}  %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:%M:%S}][%t][%c][%f:%l][%p]%T%m%n"):
                _pattern(pattern) {
                    assert(parsePattern());
                }
            /*  对msg进行格式化*/
            std::string format(const LogMsg &msg) {
                std::stringstream ss;
                format(ss, msg);
                return ss.str();
            }
            void format(std::ostream &out, const LogMsg &msg) {
                for (auto &item : _items) {
                    item->format(out, msg);
                }
            }
            /* 对格式化字符串进行解析 */
            bool parsePattern() {
                // 1、对格式化规则字符串进行解析
                std::vector<std::pair<std::string, std::string>> fmt_order;
                size_t pos = 0;
                std::string key, val;
                while (pos < _pattern.size()) {
                    // (一) 处理原始字符串 -- 判断是否为%,不是就是原始字符串
                    if (_pattern[pos] != '%') {
                        val.push_back(_pattern[pos++]); continue;
                    }
                    if (pos + 1 < _pattern.size() && _pattern[pos + 1] == '%') {
                        val.push_back('%'); pos += 2; continue;
                    }
                    // 万一出现第一个自符就是格式化字符串,那么处理原始字符串的操作就会向数组插入{"",""}
                    // 虽然不会产生错误但是便于逻辑理解,最好还是判断处理一下
                    if (!val.empty()) {
                        fmt_order.push_back(std::make_pair("", val));
                    }
                    val.clear();

                    // (二) 处理格式化字符串,代表原始字符串处理完毕
                    pos += 1;
                    if (pos == _pattern.size()) { 
                        std::cout << "%之后没有对应的格式化字符" << std::endl;
                        return false; 
                    }
                    key = _pattern[pos];
                    pos += 1;
                    // 此时pos指向格式化字符串后面的位置,判断是否有格式化子串
                    if (pos < _pattern.size() && _pattern[pos] == '{') {
                        pos += 1;   
                        // 这时pos指向子规则的起始位置
                        while (pos < _pattern.size() && _pattern[pos] != '}') {
                            val.push_back(_pattern[pos++]);
                        }
                        // 若走到了末尾,还没有找到},则说明格式是错误的,跳出循环
                        if (pos == _pattern.size()) {   
                            std::cout << "子规则{}匹配出错" << std::endl;
                            return false;
                        }
                        pos += 1; // 因为pos指向的是 } 位置,向后走一步就到了下一次处理的新位置
                    }
                    fmt_order.push_back(std::make_pair(key, val));
                    key.clear(); val.clear();
                }
                // 2、根据解析得到的数据初始化格式子项数组成员
                for (auto &it : fmt_order) {
                    _items.push_back(createItem(it.first, it.second));
                } 
                return true;
            }
        private:
            /* 根据不同的格式化字符创建不同的格式化子项对象 */
            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<FileFormatItem>();
                if (key == "l")  return std::make_shared<LineFormatItem>();
                if (key == "p")  return std::make_shared<LevelFormatItem>();
                if (key == "T")  return std::make_shared<TabFormatItem>();
                if (key == "m")  return std::make_shared<MsgFormatItem>();
                if (key == "n")  return std::make_shared<NewLineFormatItem>();
                if (key.empty()) return std::make_shared<OtherFormatItem>(val);

                assert(false);   return FormatItem::ptr();
            }
        private:
            std::string _pattern;
            std::vector<FormatItem::ptr> _items;
    };
}

#endif

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

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

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

  • 标准输出:StdoutSink
  • 固定文件:FileSink
  • 滚动文件:RollBySizeSink(根据大小滚动)
  • 滚动文件:RollByTimeSink(根据时间滚动)

滚动日志文件的必要性:

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

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

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

日志落地类的实现思想:

1、抽象出落地模块类

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

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

抽象落地基类

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

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

StdoutSink 派生类

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

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

FileSink 派生类

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

class FileSink : public LogSink {
    public: 
        // 构造时传入文件名,并打开文件,将操作句柄管理起来
        FileSink(const std::string &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;
};

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

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

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

下图是打开文件的几种模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qTjR51eE-1690428346689)(/Users/chenlixin/Library/Application Support/typora-user-images/image-20230725223048924.png)]

RollBySizeSink

整体框架:

class RollBySizeSink : public LogSink {
        public:
            RollBySizeSink(const std::string &basename, size_t max_size);
            void log(const char *data, size_t len);
        private:
            std::string createNewFile();
        private:
            std::string _basename;   // 基础文件名 + 扩展文件名(以时间来生成) = 实际输出文件名/   
            std::ofstream _ofs;      // 操作句柄        
            size_t _max_fsize;       // 记录文件允许存储最大数据量
            size_t _cur_fsize;       // 记录当前文件已经写入数据大小
            size_t _name_count;
    };

成员变量

  • basename : 基础文件名
  • _ofs : 操作句柄,内存输出到的文件流
  • _max_fsize : 单文件的最大储存数据量
  • _cur_fsize : 当前文件储存数据量
  • _name_count. : 滚动文件数量

成员函数

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

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

RollBySizeSink(const std::string &basename, size_t max_size) 
    : _basename(basename), _max_fsize(max_size), _cur_fsize(0), _name_count(0)
{
    std::string pathname = createNewFile();
    util::File::createDirectory(util::File::path(pathname));
    _ofs.open(pathname, std::ios::binary | std::ios::app);
    assert(_ofs.is_open());
}

void log(const char *data, size_t len) : 将数据写入到指定文件

实现流程:

  • 首先需要判断文件是否还有足够空间,若空间足够就直接写入,若空间不足需要创建一个新的文件进行写入
  • 创建新文件需要先将原文件关闭,再打开新文件
  • 向文件中写入len字节数据后需要更新_cur_fsize字段
void log(const char *data, size_t len) {
    if (_cur_fsize + len >= _max_fsize) {
        _ofs.close();                         // 关闭原来已经打开的文件
        std::string pathname = createNewFile();
        _cur_fsize = 0;
        util::File::createDirectory(util::File::path(pathname));
        _ofs.open(pathname, std::ios::binary | std::ios::app);
        assert(_ofs.is_open());
    }
    _ofs.write(data, len);
    _cur_fsize += len;
    assert(_ofs.good());
}

std::string createNewFile() : 根据时间创建新的滚动文件

实现流程:

  • 获取当前时间,并使用localtime_r()函数对时间进行格式化
  • 使用字符流stringstream来构建文件名称
  • basename + 格式化时间 + 滚动文件个数 + .log(后缀) 构建文件名
std::string createNewFile() {
    _name_count += 1;
    // 获取系统时间,以时间来构建文件扩展名
    time_t t = util::Data::now();
    struct tm lt;
    localtime_r(&t, &lt);
    std::stringstream filename;
    filename << _basename;
    filename << (lt.tm_year + 1900);
    filename << "-";
    filename << (lt.tm_mon + 1);
    filename << "-";
    filename << lt.tm_mday;
    filename << " ";
    filename << lt.tm_hour;
    filename << ":";
    filename << lt.tm_min;
    filename << ":";
    filename << lt.tm_sec;
    filename << "-";
    filename << _name_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::ptr create(Args&&... args) {
            return std::make_shared<SinkType>(std::forward<Args>(args)...);
        }
};

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


日志落地器完整代码:

/* 
    日志落地模块的实现
        1、 抽象落地类
        2、 派生子类(根据不同的落地方向进行派生)
        3、 使用工厂模式进行创建和表示分离 
*/

#ifndef __CLX_SINK_H__
#define __CLX_SINK_H__

#include "util.hpp"
#include <fstream>
#include <sstream>
#include <memory>
#include <cassert>

namespace clxlog {
    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) {
            // 不要直接使用std::cout << 进行输出,因为不一定是字符串需要按照长度输出
                std::cout.write(data, len);
            }
    };

    // 落地方向: 指定文件
    class FileSink : public LogSink {
        public: 
            // 构造时传入文件名,并打开文件,将操作句柄管理起来
            FileSink(const std::string &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;
    };

    // 落地方向: 滚动文件(以大小进行滚动)
    class RollBySizeSink : public LogSink {
        public:
            RollBySizeSink(const std::string &basename, size_t max_size) 
                : _basename(basename), _max_fsize(max_size), _cur_fsize(0), _name_count(0)
            {
                std::string pathname = createNewFile();
                util::File::createDirectory(util::File::path(pathname));
                _ofs.open(pathname, std::ios::binary | std::ios::app);
                assert(_ofs.is_open());
            }

            void log(const char *data, size_t len) {
                if (_cur_fsize + len >= _max_fsize) {
                    _ofs.close();                         // 关闭原来已经打开的文件
                    std::string pathname = createNewFile();
                    _cur_fsize = 0;
                    util::File::createDirectory(util::File::path(pathname));
                    _ofs.open(pathname, std::ios::binary | std::ios::app);
                    assert(_ofs.is_open());
                }
                _ofs.write(data, len);
                _cur_fsize += len;
                assert(_ofs.good());
            }
        private:
            std::string createNewFile() {
                _name_count += 1;
                // 获取系统时间,以时间来构建文件扩展名
                time_t t = util::Data::now();
                struct tm lt;
                localtime_r(&t, &lt);
                std::stringstream filename;
                filename << _basename;
                filename << (lt.tm_year + 1900);
                filename << "-";
                filename << (lt.tm_mon + 1);
                filename << "-";
                filename << lt.tm_mday;
                filename << " ";
                filename << lt.tm_hour;
                filename << ":";
                filename << lt.tm_min;
                filename << ":";
                filename << lt.tm_sec;
                filename << "-";
                filename << _name_count;
                filename << ".log";
                return filename.str();
            }
        private:
            std::string _basename;   // 基础文件名 + 扩展文件名(以时间来生成) = 实际输出文件名/   
            std::ofstream _ofs;      // 操作句柄        
            size_t _max_fsize;       // 记录文件允许存储最大数据量
            size_t _cur_fsize;       // 记录当前文件已经写入数据大小
            size_t _name_count;
    };

    class SinkFactory {
        public:
            template<typename SinkType, typename ...Args>
            static LogSink::ptr create(Args&&... args) {
                return std::make_shared<SinkType>(std::forward<Args>(args)...);
            }
    };
}

#endif

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

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

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

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

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

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

日志器基类

日志器基类框架:

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);
    const std::string &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;
};

成员变量:

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

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

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

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

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

成员函数

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

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 &name() : 获取日志器名称

const std::string &name() { return _logger_name; }

void debug(const std::string& file, size_t line, const std::string &fmt, ...)

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

实现思路:

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

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

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

3、构造LogMsg对象

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

5、进行日志落地

/* 构造日志消息对象过程, 并得到格式化后的日志消息字符串-- 然后进行落地输出*/
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 = nullptr;
    int ret = vasprintf(&res, fmt.c_str(), ap);
    if (ret == -1) { std::cout << "vasprintf failed! " << std::endl; return;}
    va_end(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);   // 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()是一样的

同步日志器

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KGT7hxzf-1690428346689)(/Users/chenlixin/Library/Application Support/typora-user-images/image-20230718141234144.png)]

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

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

异步日志器

设计思路:

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KnB3FzYZ-1690428346689)(/Users/chenlixin/Library/Application Support/typora-user-images/image-20230718142819898.png)]

异步日志器整体框架:

class AsynchLogger : public Logger {
    public:
        AsynchLogger(const std::string &logger_name, LogLevel::value level, Formatter::ptr &formatter, std::vector<LogSink::ptr> &sinks, AsynchType lopper_type);
        /* 将数据写入缓冲区*/
        void log(const char *data, size_t len);
        /* 设计一个实际落地函数(将缓冲区中的数据落地) */
        void realLog(Buffer &buf);
    private: 
        AsynchLooper::ptr _looper;
};

成员变量:

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

成员函数:

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

AsynchLogger(const std::string &logger_name, LogLevel::value level, Formatter::ptr &formatter, 
            std::vector<LogSink::ptr> &sinks, AsynchType lopper_type)
                : Logger(logger_name, level, formatter, sinks), 
                _looper(std::make_shared<AsynchLooper>(std::bind(&AsynchLogger::realLog, this, std::placeholders::_1), lopper_type)){}

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


void log(const char *data, size_t len): 调用异步任务处理器将日志数据拷贝到缓冲区

 void log(const char *data, size_t len) override{
      _looper->push(data, len);
  }

void realLog(Buffer &buf): 由异步任务处理器内部日志处理线程调用,取出缓冲区中的数据使用异步落地器中的落地器数组对日志数据进行落地

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:
            using ptr = std::shared_ptr<LoggerBuilder>;
            LoggerBuilder() : _logger_type(LoggerType::LOGGER_SYNCH), _limit_level(LogLevel::value::DEBUG), _looper_type(AsynchType::ASYNC_SAFE){}
            void buildLoggerType(LoggerType type)                 { _logger_type = type; }
            void buildLoggerName(const std::string &name)         { _logger_name = name; }
            void buildLoggerLevel(LogLevel::value level)          { _limit_level = level;  }
            void buildLoggerFormatter(const std::string &pattern) {  _formatter.reset(new Formatter(pattern)); }
            void buildEnableUnSafeAsynch() { _looper_type = AsynchType::ASYNC_UNSAFE; } 
            template<typename SinkType, typename ...Args>
            void buildSink(Args&&... args) { _sinks.push_back(SinkFactory::create<SinkType>(std::forward<Args>(args)...)); }
            virtual Logger::ptr build() = 0; //  建造日志器
        protected:
            AsynchType       _looper_type;
            LoggerType       _logger_type;
            std::string      _logger_name;
            LogLevel::value  _limit_level;    // 需要频繁访问
            Formatter::ptr   _formatter;
            std::vector<LogSink::ptr>  _sinks;
   };

成员变量:

  • _looper_type : 异步日志器模式类型,异步日志器工作模式分为安全与非安全
  • _logger_type : 日志器类型
  • _logger_name : 日志器名称
  • _limit _ level : 日志器最低输出等级
  • _formatter : 日志输出格式化器
  • _sinks : 落地器数组

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

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

局部日志器构造

局部日志器对我们建造者中的build() 函数进行了重写。为了防止用户并没有对建造者对成员进行初始化,我们使用几个if语句将默认的日志输出格式化器,日志落地器进行传入。

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

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

class LocalLoggerBuilder : public LoggerBuilder {
  public:
      virtual Logger::ptr build() override {
          assert(!_logger_name.empty());      // 必须有日志器名称
          if (_formatter.get() == nullptr) { _formatter = std::make_shared<Formatter>(); }
          if (_sinks.empty()) { buildSink<StdoutSink>(); }
          if (_logger_type == LoggerType::LOGGER_ASYNCH) {
              return std::make_shared<AsynchLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);
          }
          return std::make_shared<SynchLogger>(_logger_name, _limit_level, _formatter, _sinks);
      }
};

全局日志器构造

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

LoggerManager::getInstance().addLogger(logger);
    /* 全局日志器建造者 -- 在局部的基础上新增:自动添加日志器到单例对象中 */
    class GlobalLoggerBuilder : public LoggerBuilder {
        public:
            virtual Logger::ptr build() override {
                assert(!_logger_name.empty());      // 必须有日志器名称
                if (_formatter.get() == nullptr) { _formatter = std::make_shared<Formatter>(); }
                if (_sinks.empty()) { buildSink<StdoutSink>(); }
                Logger::ptr logger;
                if (_logger_type == LoggerType::LOGGER_ASYNCH) {
                    logger = std::make_shared<AsynchLogger>(_logger_name, _limit_level, _formatter, _sinks, _looper_type);
                } else {
                    logger = std::make_shared<SynchLogger>(_logger_name, _limit_level, _formatter, _sinks);
                }
                LoggerManager::getInstance().addLogger(logger);   // 新增
                return logger;
            }
    };

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

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

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

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pk95E6tR-1690428346689)(/Users/chenlixin/Library/Application Support/typora-user-images/image-20230726184801601.png)]

缓冲区类的设计:

整体框架:

namespace clxlog {
    class Buffer{
    public:
        Buffer() : _buffer(DEFAULT_BUFFER_SIZE), _write_idx(0), _read_idx(0) {};
        /* 向缓冲区写入数据 */
        void push(const char* data, size_t len);
        /* 返回可写空间的长度 */
        // 对于扩容思路来说,不存在可写空间不足,因为总是可写,此接口仅仅针对固定大小的缓冲区提供
        size_t writeAbleSize();
        /* 返回可读数据的起始长度 */
        const char* begin();
        /* 返回可读数据的长度 */
        size_t readAbleSize();
        /* 对读写指针进行向后偏移操作 */
        void moveReader(size_t len);
        /* 重制读写位置,初始化缓冲区 */
        void bufferReset() { _read_idx = 0; _write_idx = 0; }
        /* 对buffer实现交换的操作*/
        void bufferSwap(Buffer &buffer);
        /* 判断缓冲区是否为空*/
        bool bufferEmpty();
    private:
        /* 对读写指针进行向后偏移操作 */
        void moveWriter(size_t len);
        /* 对空间进行扩容操作*/
        void ensureEnoughSize(size_t len);
    private:
        std::vector<char> _buffer;
        size_t _read_idx;           // 当前可读数据的指针
        size_t _write_idx;          // 当前可写数据的指针
    };
}

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

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

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

提供的操作:

1、push() : 向缓冲区中写入数据

2、moveReader() : 从缓冲区读取操作(还要拷贝数据得不偿失)

3、begin() : 获取可读数据起始地址的接口,获取刻度数据长度的接口,移动读写位置的接口

4、bufferReset() :初始化缓冲区操作(将读写位置初始化–将一个缓冲区所有数据处理完毕之后)

5、bufferSwap() :交换缓冲区


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

实现思路:

  • 首先得判断缓冲区的剩余空间是否充足,若不足则需要进行扩容
  • 将内存中处理好的日志数据拷贝到缓冲区中
  • 对读指针进行向后偏移操作
 void push(const char* data, size_t len) {
    /* 缓冲区剩余空间不够的情况: 1、扩容(极限性能测试) 2、阻塞/返回false */
    // if (len > writeAbleSize()) return;
    ensureEnoughSize(len);
    // 1、将数据拷贝进缓冲区
    std::copy(data, data + len, &_buffer[_write_idx]);
    // 2、将当前写入数据向后偏移
    moveWriter(len);
 }

扩容的思路:

先指数扩容后线性扩容,设置一个阈值,小于阈值时缓冲区大小成倍增长,大于阈值线性增长,直至缓冲区可以放下所有日志数据

#define DEFAULT_BUFFER_SIZE    (1 * 1024 * 1024)
#define THRESHOLD_BUFFER_SIZE  (8 * 1024 * 1024)
#define INCREMENT_BUFFER_SIZE  (1 * 1024 * 1024) 

void ensureEnoughSize(size_t len) {
      if (len <= writeAbleSize()) return;
      size_t new_size = 0;
      while (writeAbleSize() < len) {
          if (_buffer.size() < THRESHOLD_BUFFER_SIZE) {
              new_size = _buffer.size() * 2; // 小于阈值翻倍增长
          } else {
              new_size = _buffer.size() + INCREMENT_BUFFER_SIZE; // 大于阈值线性增长
          }
          _buffer.resize(new_size);
      }
  }
void moveWriter(size_t len) { assert(len <= writeAbleSize()); _write_idx += len; }

void moveReader(size_t len) : 从缓冲区中获取数据

实现思路:我们只需要将读指针向偏移就可以了,没必要再拷贝数据。之后进行数据落地时一次性将所有数据拿取进行落地即可。不需再多拷贝一次数据,得不偿失。

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

begin() : 获取可读数据起始地址的接口,获取刻度数据长度的接口,移动读写位置的接口

实现思路:返回读指针所指向的地址即可

 const char* begin() { return &_buffer[_read_idx]; }

bufferReset() :初始化缓冲区操作(将读写位置初始化–将一个缓冲区所有数据处理完毕之后)

 void bufferReset() { _read_idx = 0; _write_idx = 0; }

实现思路:将读写指针都清空即可(清空缓冲区)


bufferSwap() :交换缓冲区

实现思路:将Buffer类的成员函数都交换一下

  void bufferSwap(Buffer &buffer) {
      _buffer.swap(buffer._buffer);
      std::swap(_read_idx, buffer._read_idx);
      std::swap(_write_idx, buffer._write_idx);
  }

7.8 异步工作器设计

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

成员变量:

  • _produce_buffer : push poll ( 生产者缓冲区 )

  • _consumer_buffer : pop poll ( 消费者缓冲区 )

  • _produce_cond : 生产缓冲区满了,则生产者将在该条件变量下等待

  • _consumer_cond : 生产缓冲区是空的,日志处理线程不需要交换缓冲区处理日志,等待生产者写入

  • _mutex : 维持生产者之间的互斥,缓冲区交换时的线程安全问题

  • _stop : 标记工作器是否停止

  • _thread : 日志处理线程

  • _lopper_type : 处理器的工作模式(安全和非安全)

  • _callBack : 回调函数(由异步日志器传入,进行日志的实际落地)

/* 
   异步工作器
*/

namespace clxlog{
  using Functor = std::function<void(Buffer &)>;
  enum class AsynchType{ ASYNC_SAFE, ASYNC_UNSAFE }; // 安全状态(缓冲区满了就阻塞)/非安全状态(缓冲区满了就扩容,不考虑资源耗尽的风险)
  class AsynchLooper {
    public:
      using ptr = std::shared_ptr<AsynchLooper>;
      AsynchLooper(const Functor &cb, AsynchType looper_type = AsynchType::ASYNC_SAFE) 
        :_stop(false), _thread(std::thread(&AsynchLooper::threadEntry, this)), _callBack(cb), _looper_type(looper_type) {}
      ~AsynchLooper(){ stop(); }
      void stop() { 
        _stop = true;                    // 退出标志设置为true 
        _consumer_cond.notify_all();     // 唤醒所有工作线程
        _thread.join();                  // 等待工作线程退出
      }
      void push(const char *data, size_t len) {
        // 1、无限扩容 非安全(资源耗尽,程序崩溃) 2、固定大小 - 生产缓冲区中数据满了就阻塞
        std::unique_lock<std::mutex> lock(_mutex);
        /* 安全状态 */
        if (_looper_type == AsynchType::ASYNC_SAFE) {
          _produce_cond.wait(lock, [&](){ return _produce_buffer.writeAbleSize() >= len; });
        }
        _produce_buffer.push(data, len);
        _consumer_cond.notify_one();   //  唤醒消费者对缓冲区中的数据进行处理
      }
    private:
      /* 线程的入口函数 -- 对消费缓冲区中的数据进行处理,处理完毕后,初始化缓冲区,交换缓冲区*/
      void threadEntry() {
        while (1) {
          {
            // 1、 判断生产缓冲区有没有数据,有则交换,无则阻塞
            std::unique_lock<std::mutex> lock(_mutex);
            /* 退出标志被设置了,且缓冲区没有数据,这时候退出,否则可能缓冲区中任然有数据未被完全处理 */
            if (_stop && _produce_buffer.bufferEmpty()) break;
            _consumer_cond.wait(lock, [&](){ return (_stop || !_produce_buffer.bufferEmpty()); });
            _produce_buffer.bufferSwap(_consumer_buffer);
            // 2、 唤醒生产者(只有安全状态生产者才会被阻塞)
            if (_looper_type == AsynchType::ASYNC_SAFE) {
              _produce_cond.notify_all();
            }
          }
          // 3、 被唤醒后,对消费缓冲区进行数据处理(处理过程无需加锁保护)
          _callBack(_consumer_buffer);
          // 4、 初始化消费缓冲区
          _consumer_buffer.bufferReset();
        }
      }  
      /* 回调函数 具体对缓冲区数据进行处理的回调函数, 由异步工作器的使用者传入 */
      Functor _callBack; 

    private:
      AsynchType _looper_type;
      std::atomic<bool> _stop;                 // 工作器停止标志
      std::mutex _mutex;          
      Buffer _produce_buffer;
      Buffer _consumer_buffer;
      std::condition_variable _produce_cond;
      std::condition_variable _consumer_cond;
      std::thread _thread;        
  };
}



7.9 单例日志器管理类设计

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

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

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

namespace clxlog{
    class LoggerManager {
        public:
            static LoggerManager& getInstance();
            void addLogger(Logger::ptr &logger); 
            void hasLogger(const std::string &name);
            Logger::ptr getLogger(const std::string &name);
            Logger::ptr rootLogger();
        private:
            LoggerManager() {

            }
        private:
            std::mutex _mutex;
            Logger::ptr _root_logger;          // 默认日志器
            std::vector<Logger::ptr> _loggers;
    };
}

#endif

7.10 日志宏&全局接口设计

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

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

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

/* 
    1、提供获取指定日志器的全局接口(避免用户自己操作单例对象)
    2、使用宏函数对日志器接口进行代理(代理模式)
    3、提供宏函数,直接通过默认日志器进行日志的标准输出打印(无需获取日志器)
*/

#ifndef __CLX_CLXLOG_H__
#define __CLX_CLXLOG_H__

#include "logger.hpp"

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

    Logger::ptr rootLogger() {
        return clxlog::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(logger, fmt , ...) logger->debug(fmt, ##__VA_ARGS__)
    // #define DLOG(fmt, ...) DEBUG(rootLogger(), fmt, ##__VA_ARGS__)

    #define DEBUG(fmt, ...) clxlog::rootLogger()->debug(fmt, ##__VA_ARGS__)
    #define INFO(fmt, ...)  clxlog::rootLogger()->info (fmt, ##__VA_ARGS__)
    #define WARN(fmt, ...)  clxlog::rootLogger()->warn (fmt, ##__VA_ARGS__)
    #define ERROR(fmt, ...) clxlog::rootLogger()->error(fmt, ##__VA_ARGS__)
    #define FATAL(fmt, ...) clxlog::rootLogger()->fatal(fmt, ##__VA_ARGS__)

}
#endif

8、功能用例


9、单元测试

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

#include "../logs/util.hpp"
#include "../logs/level.hpp"
#include "../logs/message.hpp"
#include "../logs/format.hpp"
#include "../logs/sink.hpp"
#include "../logs/logger.hpp"
#include "../logs/buffer.hpp"
#include "../logs/clxlog.h"

void util_test() {
    std::cout << clxlog::util::Data::now() << std::endl;
    
    std::string pathname = "./abc/bcd/a.txt";
    if (clxlog::util::File::exists(pathname)) {
        std::cout << pathname << " exists" << std::endl;
    } else {
        std::cout << "pathname" << " no exists" << std::endl;
    }

    std::string file_path = clxlog::util::File::path(pathname);
    std::cout << "file in the " << file_path << std::endl;

    clxlog::util::File::createDirectory(clxlog::util::File::path(pathname));
}

void level_test() {
    std::cout << clxlog::LogLevel::toString(clxlog::LogLevel::value::DEBUG) << std::endl;
    std::cout << clxlog::LogLevel::toString(clxlog::LogLevel::value::INFO) << std::endl;
    std::cout << clxlog::LogLevel::toString(clxlog::LogLevel::value::WARN) << std::endl;
    std::cout << clxlog::LogLevel::toString(clxlog::LogLevel::value::ERROR) << std::endl;
    std::cout << clxlog::LogLevel::toString(clxlog::LogLevel::value::FATAL) << std::endl;
    std::cout << clxlog::LogLevel::toString(clxlog::LogLevel::value::OFF) << std::endl;
    std::cout << clxlog::LogLevel::toString(clxlog::LogLevel::value::UNKNOW) << std::endl;
}

std::string format_test() {

    clxlog::LogMsg msg(clxlog::LogLevel::value::INFO, __LINE__, __FILE__, "root", "格式化功能测试");
    clxlog::Formatter fmt;
    // clxlog::Formatter fmt("%g%gabc%%abc[%d{%H:%M:%S}] %m%n");
    std::string str = fmt.format(msg);
    std::cout << str << std::endl;
    return str;
}

void sink_test() {
    std::string str = format_test();
    clxlog::LogSink::ptr stdout_lsp = clxlog::SinkFactory::create<clxlog::StdoutSink>();
    clxlog::LogSink::ptr file_lsp = clxlog::SinkFactory::create<clxlog::FileSink>("./filelog/log.txt");
    clxlog::LogSink::ptr roll_file_lsp = clxlog::SinkFactory::create<clxlog::RollBySizeSink>("./rollfilelog/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 = std::to_string(count++) + str;
        roll_file_lsp->log(tmp.c_str(), tmp.size());
        cursize += tmp.size();
    }
}


// 落地扩展,以时间作为滚动的依据的滚动文件落地方式
enum class TimeGap {
    GAP_SECOND,
    GAP_MINUTE,
    GAP_HOUR,
    GAP_DAY
};

class RollByTimeSink : public clxlog::LogSink {
    public:
        RollByTimeSink(const std::string &basename, TimeGap gap_type) 
            : _basename(basename), _cur_gap(0)
        {
            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 = 60 * 60 ;        break;
                case TimeGap::GAP_DAY    : _gap_size = 60 * 60 * 24 ;   break;
            }
            _cur_gap = clxlog::util::Data::now() / _gap_size;
            std::string filename = createNewFile();
            clxlog::util::File::createDirectory(clxlog::util::File::path(filename));
            _ofs.open(filename, std::ios::binary | std::ios::app);
            assert(_ofs.good());
        }

        void log(const char* data, size_t len) {
            time_t cur = clxlog::util::Data::now();
            if ((cur / _gap_size) != _cur_gap) {
                _ofs.close();
                std::string filename = createNewFile();
                _ofs.open(filename, std::ios::binary | std::ios::app);
                assert(_ofs.good());
            }
            _ofs.write(data, len);
            assert(_ofs.good());
        }

    private:
        std::string createNewFile() {
            time_t t = clxlog::util::Data::now();
            struct tm lt;
            localtime_r(&t, &lt);
            std::stringstream filename;
            filename << _basename;
            filename << (lt.tm_year + 1900);
            filename << "-";
            filename << (lt.tm_mon + 1);
            filename << "-";
            filename << lt.tm_mday;
            filename << " ";
            filename << lt.tm_hour;
            filename << ":";
            filename << lt.tm_min;
            filename << ":";
            filename << lt.tm_sec;
            filename << ".log";
            return filename.str();
        }
    private:
        std::string _basename;
        size_t _cur_gap;
        size_t _gap_size;
        std::ofstream _ofs;
};

void expansion_sink_test() {
    std::string str = format_test();
    clxlog::LogSink::ptr roll_time_lsp = clxlog::SinkFactory::create<RollByTimeSink>("./roll_time_file_log/roll-time-", TimeGap::GAP_SECOND);
    size_t count = 0;
    while (count < 3) {
        std::string tmp = std::to_string(count++) + str;
        roll_time_lsp->log(tmp.c_str(), tmp.size());
        sleep(1);
    }
}

void logger_test() {
    std::string logger_name = "sync_logger";
    clxlog::LogLevel::value limit_level = clxlog::LogLevel::value::DEBUG;
    clxlog::Formatter::ptr fmt(new clxlog::Formatter());
    clxlog::LogSink::ptr stdout_lsp = clxlog::SinkFactory::create<clxlog::StdoutSink>();
    clxlog::LogSink::ptr file_lsp = clxlog::SinkFactory::create<clxlog::FileSink>("./filelog/log.txt");
    clxlog::LogSink::ptr roll_file_lsp = clxlog::SinkFactory::create<clxlog::RollBySizeSink>("./rollfilelog/roll-", 1024 * 1024);
    std::vector<clxlog::LogSink::ptr> sinks = { stdout_lsp, file_lsp, roll_file_lsp };

    clxlog::Logger::ptr logger_ptr(new clxlog::SynchLogger(logger_name, limit_level, fmt, sinks));

    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 * 10) {
        logger_ptr->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
        cursize += 20;
    }
}

void build_synch_logger_test() {
    clxlog::LoggerBuilder::ptr builder(new clxlog::LocalLoggerBuilder());
    builder->buildLoggerType(clxlog::LoggerType::LOGGER_SYNCH);
    builder->buildLoggerLevel(clxlog::LogLevel::value::WARN);
    builder->buildLoggerName("clx_synch_logger");
    builder->buildLoggerFormatter("%m%n");
    builder->buildSink<clxlog::StdoutSink>();
    builder->buildSink<clxlog::FileSink>("./file_log/test.log");
    builder->buildSink<clxlog::RollBySizeSink>("./roll_file_log/roll-", 1024 * 1024);
    clxlog::Logger::ptr 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 * 10) {
        logger_ptr->fatal(__FILE__, __LINE__, "测试日志-%d", count++);
        cursize += 20;
    }
}

void buffer_test() {
    std::ifstream ifs("./file_log/test.log", std::ios::binary);
    if (ifs.is_open() == false) exit(-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" << std::endl; exit(-1); }
    ifs.close();

    std::cout << fsize << std::endl;
    clxlog::Buffer buffer;
    for (int i = 0; i < body.size(); i++) {
        buffer.push(&body[i], 1);
    }
    std::cout << buffer.readAbleSize() << std::endl;
    
    int need_read_size = buffer.readAbleSize();
    std::ofstream ofs("./file_log/tmp.log", std::ios::binary);
    for (int i = 0; i < need_read_size; i++) {
        ofs.write(buffer.begin(), 1);
        if (ofs.good() == false) { std::cout << "wirte error ! " << std::endl; exit(-1); }
        buffer.moveReader(1);
    }
    ofs.close();
}

void build_asynch_logger_test() {
    clxlog::LoggerBuilder::ptr builder(new clxlog::LocalLoggerBuilder());
    builder->buildLoggerType(clxlog::LoggerType::LOGGER_ASYNCH);
    builder->buildLoggerLevel(clxlog::LogLevel::value::WARN);
    builder->buildLoggerName("clx_asynch_logger");
    builder->buildLoggerFormatter("%m%n");
    builder->buildSink<clxlog::StdoutSink>();
    builder->buildSink<clxlog::FileSink>("./file_log/test.log");
    builder->buildSink<clxlog::RollBySizeSink>("./roll_file_log/roll-", 1024 * 1024);
    clxlog::Logger::ptr 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 logger_manager_test_log() {
    clxlog::LoggerBuilder::ptr builder(new clxlog::GlobalLoggerBuilder());
    builder->buildLoggerType(clxlog::LoggerType::LOGGER_ASYNCH);
    builder->buildLoggerLevel(clxlog::LogLevel::value::WARN);
    builder->buildLoggerName("clx_asynch_logger");
    builder->buildLoggerFormatter("%m%n");
    builder->buildSink<clxlog::StdoutSink>();
    builder->buildSink<clxlog::FileSink>("./file_log/test.log");
    builder->buildSink<clxlog::RollBySizeSink>("./roll_file_log/roll-", 1024 * 1024);
    builder->build();

    clxlog::Logger::ptr logger_ptr = clxlog::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 clxlog_test() {
    clxlog::LoggerBuilder::ptr builder(new clxlog::GlobalLoggerBuilder());
    builder->buildLoggerType(clxlog::LoggerType::LOGGER_ASYNCH);
    builder->buildLoggerLevel(clxlog::LogLevel::value::WARN);
    builder->buildLoggerName("clx_asynch_logger");
    builder->buildLoggerFormatter("[%c][%f:%l]%m%n");
    builder->buildSink<clxlog::StdoutSink>();
    builder->buildSink<clxlog::FileSink>("./file_log/test.log");
    builder->buildSink<clxlog::RollBySizeSink>("./roll_file_log/roll-", 1024 * 1024);
    builder->build();

    clxlog::Logger::ptr logger_ptr = clxlog::LoggerManager::getInstance().getLogger("clx_asynch_logger");

    // logger_ptr->debug("%s", "测试日志");
    // logger_ptr->info ("%s", "测试日志");
    // logger_ptr->warn ("%s", "测试日志");
    // logger_ptr->error("%s", "测试日志");
    // logger_ptr->fatal("%s", "测试日志");

    // size_t count = 0;
    // while (count < 1e5 * 5) {
    //     logger_ptr->fatal(测试日志-%d", count++);
    // }

    DEBUG("%s", "测试日志");
    INFO("%s", "测试日志");
    WARN("%s", "测试日志");
    ERROR("%s", "测试日志");
    FATAL("%s", "测试日志");

    size_t count = 0;
    while (count < 1e5 * 5) {
        FATAL("测试日志-%d", count++);
    }
}

void demo() {
    clxlog::LoggerBuilder::ptr builder(new clxlog::GlobalLoggerBuilder());
    builder->buildLoggerLevel(clxlog::LogLevel::value::DEBUG);
    builder->buildLoggerName("clx_synch_logger");
    builder->build();
    clxlog::Logger::ptr logger_ptr = clxlog::LoggerManager::getInstance().getLogger("clx_synch_logger");

    INFO("%s", "测试开始");

    DEBUG("%s", "测试日志");
    INFO("%s", "测试日志");
    WARN("%s", "测试日志");
    ERROR("%s", "测试日志");
    FATAL("%s", "测试日志");

    INFO("%s", "测试完毕");

}


int main(){
    // format_test();
    // sink_test();
    // expansion_sink_test();
    // logger_test();
    // build_synch_logger_test();
    // buffer_test();
    // build_asynch_logger_test();
    // logger_manager_test_log();
    // clxlog_test();
    demo();
    return 0;
}

10、 性能测试

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

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

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

测试环境 :

  • CPU : Intel® Xeon® Platinum 8255C CPU @ 2.50GHz
  • RAM : 4G DIMM RAM
  • ROM: 70G
  • OS : Linux VM-20-6-centos 3.10.0-1160.62.1.el7.x86_64 (腾讯云服务器)

测试方法

#include "../logs/clxlog.h"
#include <chrono>

void bench(const std::string &logger_name, size_t thread_count, size_t msg_count, size_t msg_len) {
    /* 1.获取日志器           */
    clxlog::Logger::ptr logger = clxlog::getLogger(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() {
    clxlog::LoggerBuilder::ptr builder(new clxlog::GlobalLoggerBuilder());
    builder->buildLoggerName("sync_logger");
    builder->buildLoggerFormatter("%m%n");
    builder->buildLoggerType(clxlog::LoggerType::LOGGER_SYNCH);
    builder->buildSink<clxlog::FileSink>("./logfile/sync.log");
    builder->buildSink<clxlog::RollBySizeSink>("./logfile/roll-sync-by-size", 1024 * 1024);
    builder->build();
    bench("sync_logger", 1, 1000000, 100);
 	  bench("sync_logger", 4, 1000000, 100);
}

void async_bench() {
    clxlog::LoggerBuilder::ptr builder(new clxlog::GlobalLoggerBuilder());
    builder->buildLoggerName("async_logger");
    builder->buildLoggerFormatter("%m%n");
    builder->buildLoggerType(clxlog::LoggerType::LOGGER_ASYNCH);
    builder->buildSink<clxlog::FileSink>("./logfile/async.log");
    builder->buildSink<clxlog::RollBySizeSink>("./logfile/roll-async-by-size", 1024 * 1024);
    builder->buildEnableUnSafeAsynch();
    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;
}

测试结果

同步日志器/单线程

[clx@VM-20-6-centos bench]$ ./bench 
测试日志:1000000 条, 总大小:97656KB
线程[0]:   输出日志数量:1000000, 耗时:3.02191s
总耗时: 3.02191s
每秒输出日志数量: 330916 条
每秒输出日志大小: 32316 KB

同步日志器/多线程

[clx@VM-20-6-centos bench]$ ./bench 
测试日志:1000000 条, 总大小:97656KB
线程[1]:   输出日志数量:200000, 耗时:2.86996s
线程[0]:   输出日志数量:200000, 耗时:2.92905s
线程[4]:   输出日志数量:200000, 耗时:2.96115s
线程[2]:   输出日志数量:200000, 耗时:3.02792s
线程[3]:   输出日志数量:200000, 耗时:3.06229s
总耗时: 3.06229s
每秒输出日志数量: 326552 条
每秒输出日志大小: 31889 KB

异步日志器/单线程

[clx@VM-20-6-centos bench]$ ./bench 
测试日志:1000000 条, 总大小:97656KB
线程[0]:   输出日志数量:1000000, 耗时:2.5254s
总耗时: 2.5254s
每秒输出日志数量: 395977 条
每秒输出日志大小: 38669 KB

异步日志器/多线程

[clx@VM-20-6-centos bench]$ ./bench 
测试日志:1000000 条, 总大小:97656KB
线程[0]:   输出日志数量:200000, 耗时:1.65786s
线程[1]:   输出日志数量:200000, 耗时:1.87722s
线程[3]:   输出日志数量:200000, 耗时:1.88468s
线程[2]:   输出日志数量:200000, 耗时:1.94319s
线程[4]:   输出日志数量:200000, 耗时:1.96773s
总耗时: 1.96773s
每秒输出日志数量: 508199 条
每秒输出日志大小: 49628 KB

11、 扩展

  • 丰富Sink类型(落地方式)

    • 支持按照时间滚动文件

    • 支持将log通过网络传输落地到日志服务器(tcp/udp)

    • 支持在控制台通过日志等级渲染不同的颜色输出方便定位

    • 支持落地日志到数据库

    • 支持配置服务器地址,将日志落地到远程服务器

  • 实现日志服务器负责存储日志并提供检索、分析、展示等功能

12、参考

  • 9
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白在进击

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值