同步+异步日志系统(C++实现)

对于一个服务器而言,不论是在调试中还是在运行中,都需要通过打日志的方式来记录程序的运行情况。本文设计的日志系统实现了同步与异步两种功能,原理见下图:

同步+异步日志系统(C++实现)_第1张图片

 同步日志:日志写入函数与工作线程串行执行,由于涉及到I/O操作,当单条日志比较大的时候,同步模式会阻塞整个处理流程,服务器所能处理的并发能力将有所下降,尤其是在峰值的时候,写日志可能成为系统的瓶颈。

异步日志:将所写的日志内容先存入阻塞队列中,写线程从阻塞队列中取出内容,写入日志。

日志的运行流程:

1、使用单例模式(局部静态变量方法)获取实例Log::getInstance()。

2、通过实例调用Log::getInstance()->init()函数完成初始化,若设置阻塞队列大小大于0则选择异步日志,等于0则选择同步日志,更新isAysnc变量。

3、通过实例调用write_log()函数写日志,首先根据当前时刻创建日志(前缀为时间,后缀为".log",并更新日期today和当前行数lineCount。

4、在write_log()函数内部,通过isAsync变量判断写日志的方法:如果是异步,工作线程将要写的内容放进阻塞队列中,由写线程在阻塞队列中取出数据,然后写入日志;如果是同步,直接写入日志文件中。

日志的分级与分文件:

分级情况:

  • Debug,调试代码时的输出,在系统实际运行时,一般不使用。
  • Warn,这种警告与调试时终端的warning类似,同样是调试代码时使用。
  • Info,报告系统当前的状态,当前执行的流程或接收的信息等。
  • Erro,输出系统的错误信息

分文件情况:

  1. 按天分,日志写入前会判断当前today是否为创建日志的时间,若为创建日志时间,则写入日志,否则按当前时间创建新的log文件,更新创建时间和行数。
  2. 按行分,日志写入前会判断行数是否超过最大行限制,若超过,则在当前日志的末尾加lineCount / MAX_LOG_LINES为后缀创建新的log文件。

Log.h

 #ifndef LOG_H
 #define LOG_H

#include "blockqueue.h"
#include 
#include 
#include "buffer.h"
#include 
#include 
#include 
#include 
#include 

 class Log
 {
 public:
    static Log* getInstance()
    {
        static Log instance;
        return &instance;
    }
    //初始化日志实例(阻塞队列最大容量、日志保存路径、日志文件后缀)
    void init(int maxQueueCapacity = 1024,
            const char* path_="./log",
            const char* suffix_=".log");

    //异步写日志公有方法,调用私有方法asyncWrite
    static void flushLogThread()
    {
        Log::getInstance()->asyncWrite();
    }

    //将输出内容按照标准格式整理
   void writeLog(int level, const char* format, ...);

private:
    Log();
    ~Log();
    //异步写日志方法
    void asyncWrite();

private:
    const int LOG_NAME_LEN=256;   //日志文件最长文件名
    const int MAX_LOG_LINES=50000;//日志文件内的最长日志条数

    const char* path;      //路径名
    const char* suffix;    //后缀名  
    int lineCount;   //日志行数记录
    int today;             //按当天日期区分文件
    FILE* fp;              //打开log的文件指针
    Buffer buff;           //输出的内容
    std::unique_ptr> deque;  //阻塞队列
    std::unique_ptr writeThread;        //写线程
    bool isAsync;          //是否开启异步日志
    std::mutex mtx;        //同步日志必需的互斥量
};


//四个宏定义,主要用于不同类型的日志输出
#define LOG_DEBUG(format, ...) Log::getInstance()->writeLog(0, format, ##__VA_ARGS__)
#define LOG_INFO(format, ...)  Log::getInstance()->writeLog(1, format, ##__VA_ARGS__)
#define LOG_WARN(format, ...)  Log::getInstance()->writeLog(2, format, ##__VA_ARGS__)
#define LOG_ERROR(format, ...) Log::getInstance()->writeLog(3, format, ##__VA_ARGS__)

#endif // !LOG_H

Log.cpp

#include "log.h"

Log::Log():lineCount(0),
            today(0),
            fp(nullptr),
            deque(nullptr),
            writeThread(nullptr),
            isAsync(false){}

Log::~Log()
{
    if(writeThread&&writeThread->joinable())
    {
        while(!deque->empty())//清空阻塞队列中的全部任务
        {
            deque->flush();
        }
        deque->close();
        writeThread->join();//等待当前线程完成手中的任务
    }
    if(fp)//冲洗文件缓冲区,关闭文件描述符
    {
        std::lock_guard lock(mtx);
        fflush(fp);
        fclose(fp);
    }
}

void Log::init(int maxQueueCapacity,const char* path_,const char* suffix_)
{
    if(maxQueueCapacity>0)//异步方式
    {
        isAsync=true;
        if(!deque)
        {
            std::unique_ptr> newDeque(new BlockQueue(maxQueueCapacity));
            deque=std::move(newDeque);
            std::unique_ptr newThread(new std::thread(flushLogThread));
            writeThread=std::move(newThread);
        }
    }
    else//同步方式
    {
        isAsync=false;
    }
    lineCount=0;
    //生成日志文件名
    time_t timer=time(nullptr);
    struct tm* sysTime=localtime(&timer);
    struct tm t=*sysTime;
    path=path_;
    suffix=suffix_;
    char filename[LOG_NAME_LEN]={0};
    snprintf(filename,LOG_NAME_LEN-1,"%s/%04d_%02d_%02d%s",
            path,t.tm_year+1900,t.tm_mon+1,t.tm_mday,suffix);
    today=t.tm_mday;
    {
        std::lock_guard lock(mtx);
        buff.retrieveAll();
        if(fp)
        {
            fflush(fp);
            fclose(fp);
        }
        fp=fopen(filename,"a");
        if(fp==nullptr)
        {
            mkdir(path,0777);//先生成目录文件(最大权限)
            fp=fopen(filename,"a");
        }
        assert(fp!=nullptr);
    }
}

void Log::writeLog(int level, const char* format, ...)
{
    struct timeval now={0,0};
    gettimeofday(&now,nullptr);
    time_t tSec=now.tv_sec;
    struct tm* sysTime=localtime(&tSec);
    struct tm t=*sysTime;
    va_list vaList;

    if(today!=t.tm_mday||(lineCount&&(lineCount%MAX_LOG_LINES==0)))
    {
        //生成最新的日志文件名
        char newFile[LOG_NAME_LEN];
        char tail[36]={0};
        snprintf(tail,35,"%04d_%02d_%02d",t.tm_year+1900,t.tm_mon+1,t.tm_mday);
        if(today!=t.tm_mday)//时间不匹配,则替换为最新的日志文件名
        {
            snprintf(newFile,LOG_NAME_LEN-1,"%s/%s%s",path,tail,suffix);
            today=t.tm_mday;
            lineCount=0;
        }
        else//长度超过日志最长行数,则生成xxx-1、xxx-2文件
        {
            snprintf(newFile,LOG_NAME_LEN-1,"%s/%s-%d%s",path,tail,(lineCount/MAX_LOG_LINES),suffix);
        }

        if(fp)
        {
            std::lock_guard lock(mtx);
            fflush(fp);
            fclose(fp);
        }
        fp=fopen(newFile,"a");
        assert(fp!=nullptr);
    }
    
    //在buffer内生成一条对应的日志信息
    {
        std::lock_guard lock(mtx);
        lineCount++;
        //添加年月日时分秒微秒———"2022-12-29 19:08:23.406500"
        int n=snprintf(buff.beginWrite(),128,"%04d-%02d-%02d %02d:%02d:%02d.%06ld ",
                        t.tm_year+1900,t.tm_mon+1,t.tm_mday,
                        t.tm_hour,t.tm_min,t.tm_sec,now.tv_usec);
        buff.hasWritten(n);
        //添加日志等级———"2022-12-29 19:08:23.406539 [debug]: "
        switch(level) 
        {
        case 0:
            buff.append("[debug]: ", 9);
            break;
        case 1:
            buff.append("[info] : ", 9);
            break;
        case 2:
            buff.append("[warn] : ", 9);
            break;
        case 3:
            buff.append("[error]: ", 9);
            break;
        default:
            buff.append("[info] : ", 9);
            break;
        }
        //添加使用日志时的格式化输入———"2022-12-29 19:08:23.535531 [debug]: Test 222222222 8 ============= "
        va_start(vaList, format);
        int m = vsnprintf(buff.beginWrite(), buff.writableBytes(), format, vaList);
        va_end(vaList);
        buff.hasWritten(m);
        //添加换行符与字符串结尾
        buff.append("\n\0", 2);
    }
    
    if(isAsync&&deque&&!deque->full())//异步方式(加入阻塞队列中,等待写线程读取日志信息)
    {
        std::lock_guard lock(mtx);
        deque->push_back(buff.retrieveAllAsString());
    }
    else//同步方式(直接向文件中写入日志信息)
    {
        std::lock_guard lock(mtx);
        fputs(buff.peek(),fp);
    }
    {//清理buffer缓冲区
        std::lock_guard lock(mtx);
        buff.retrieveAll();
    }
}

void Log::asyncWrite()
{
    std::string str="";
    while (deque->pop(str))
    {
        std::lock_guard lock(mtx);
        fputs(str.c_str(),fp);
    }
}

测试程序:test.cpp

分别采用同步和异步方式,各写60000(15000*4)条日志信息。

#include "log.h"

void TestLog() 
{
    int cnt = 0;
    Log::getInstance()->init(0,"./testlog1");//同步日志
    for(int j = 0; j < 15000; j++ )
    {
        LOG_DEBUG("%s 111111111 %d ============= ", "Test", cnt++);
        LOG_INFO ("%s 111111111 %d ============= ", "Test", cnt++);
        LOG_WARN ("%s 111111111 %d ============= ", "Test", cnt++);
        LOG_ERROR("%s 111111111 %d ============= ", "Test", cnt++);
    }

    cnt = 0;
    Log::getInstance()->init(1024,"./testlog2");//异步日志
    for(int j = 0; j < 15000; j++ )
    {
        LOG_DEBUG("%s 222222222 %d ============= ", "Test", cnt++);
        LOG_INFO ("%s 222222222 %d ============= ", "Test", cnt++);
        LOG_WARN ("%s 222222222 %d ============= ", "Test", cnt++);
        LOG_ERROR("%s 222222222 %d ============= ", "Test", cnt++);
    }
}

int main() 
{
    TestLog();
}

实验结果:

同步+异步日志系统(C++实现)_第2张图片

同步日志:

由于共写60000条日志,一份日志文件设置最大行数为50000,所以分为两个文件

 同步+异步日志系统(C++实现)_第3张图片

同步+异步日志系统(C++实现)_第4张图片

 同步+异步日志系统(C++实现)_第5张图片

 异步日志:

 同步+异步日志系统(C++实现)_第6张图片

同步+异步日志系统(C++实现)_第7张图片

同步+异步日志系统(C++实现)_第8张图片

附:

缓冲区Buffer类的设计(参考Muduo实现)_{(sunburst)}的博客-CSDN博客

基于C++11实现的阻塞队列(BlockQueue)_{(sunburst)}的博客-CSDN博客

参考资料:

最新版Web服务器项目详解 - 09 日志系统(上)

最新版Web服务器项目详解 - 10 日志系统(下)

你可能感兴趣的:(webServer,单例模式,c++)