首页 > 编程语言 >easylogging++的那些事(四)源码分析(九)异步日志

easylogging++的那些事(四)源码分析(九)异步日志

时间:2022-12-07 22:58:26浏览次数:66  
标签:异步 ++ void easylogging 源码 base logMessage 日志 data

目录

在上一篇我们介绍了 easylogging++的 崩溃处理相关 的内容。今天我们开始分析 easylogging++异步日志的实现。

目前异步日志在 easylogging++当中是实验性功能,不建议在生产环境中使用,而且经过测试,由同步日志直接切换为异步日志,程序会出现崩溃的情况。
今天我们仅仅看看 easylogging++异步日志目前的实现机制。

异步日志是什么?

    前面我们介绍的写日志的方式在未定义 ELPP_EXPERIMENTAL_ASYNC 宏的情况下,都是同步日志。
    1) 同步日志:当需要写出一条日志消息时,只有等到这条日志消息完全写出时才能执行后续的流程,其问题在于可能会阻塞在磁盘写操作上;
    2) 异步日志:当需要写日志消息时,只是将日志消息进行存储,当积累到一定量时或者达到时间间隔后,由后台线程自动将存储的所有日志进行实际的磁盘写操作;
    综上所述,异步日志的好处是前台线程不会阻塞在写日志上,后台线程真正写日志时,日志消息往往已经积累了很多,此时只需一次 IO 操作( easylogging++这里还是一条条写日志 ),从而减少了 IO 函数的调用次数,提高了效率。

在 easylogging++的 总体设计主流程 中我们简单介绍过异步日志相关的内容。

异步日志相关的类

AsyncLogItem 类

    AsyncLogItem 表示异步日志队列 AsyncLogQueue 中的一条日志AsyncLogQueue 类后面会进行介绍。
    AsyncLogItem 的实现如下:

class AsyncLogItem
{
public:
    explicit AsyncLogItem(const LogMessage &logMessage, const LogDispatchData &data, const base::type::string_t &logLine)
        : m_logMessage(logMessage), m_dispatchData(data), m_logLine(logLine) {}
    virtual ~AsyncLogItem() {}
    inline LogMessage *logMessage(void)
    {
        return &m_logMessage;
    }
    inline LogDispatchData *data(void)
    {
        return &m_dispatchData;
    }
    inline base::type::string_t logLine(void)
    {
        return m_logLine;
    }

private:
    LogMessage m_logMessage;
    LogDispatchData m_dispatchData;
    base::type::string_t m_logLine; // 经过LogBuilder替换日志格式指示器为实际的内容后的日志内容
};

    LogMessageCLOG其他相关类 中已经介绍过了。
    AsyncLogItem 类的实现并不复杂,这里就不多说了。

AsyncLogQueue 类

    AsyncLogQueue 类表示异步日志队列
    AsyncLogQueue 的实现如下:

class AsyncLogQueue : public base::threading::ThreadSafe
{
public:
    virtual ~AsyncLogQueue()
    {
        ELPP_INTERNAL_INFO(6, "~AsyncLogQueue");
    }

    inline AsyncLogItem next(void)
    {
        base::threading::ScopedLock scopedLock(lock());
        AsyncLogItem result = m_queue.front();
        m_queue.pop();
        return result;
    }

    inline void push(const AsyncLogItem &item)
    {
        base::threading::ScopedLock scopedLock(lock());
        m_queue.push(item);
    }
    inline void pop(void)
    {
        base::threading::ScopedLock scopedLock(lock());
        m_queue.pop();
    }
    inline AsyncLogItem front(void)
    {
        base::threading::ScopedLock scopedLock(lock());
        return m_queue.front();
    }
    inline bool empty(void)
    {
        base::threading::ScopedLock scopedLock(lock());
        return m_queue.empty();
    }

private:
    std::queue<AsyncLogItem> m_queue;
};

    AsyncLogQueue 就是个线程安全的 std::queue

AsyncLogDispatchCallback 类

    启用异步日志时,默认的 LogDispatchCallbackAsyncLogDispatchCallback,主要工作:日志需要写终端则写终端,日志需要写文件,则放入异步日志队列。
    AsyncLogDispatchCallback 类的实现如下:

class AsyncLogDispatchCallback : public LogDispatchCallback
{
protected:
    void handle(const LogDispatchData *data);
};
void AsyncLogDispatchCallback::handle(const LogDispatchData *data)
{
    // logLine:经过LogBuilder替换日志格式指示器为实际的内容后的日志内容
    base::type::string_t logLine = data->logMessage()->logger()->logBuilder()->build(data->logMessage(), data->dispatchAction() == base::DispatchAction::NormalLog);
    // 先判断是否需要终端输出,如果需要,则根据需要(调整彩色显示),然后终端输出
    if (data->dispatchAction() == base::DispatchAction::NormalLog && data->logMessage()->logger()->typedConfigurations()->toStandardOutput(data->logMessage()->level()))
    {
        if (ELPP->hasFlag(LoggingFlag::ColoredTerminalOutput))
            data->logMessage()->logger()->logBuilder()->convertToColoredOutput(&logLine, data->logMessage()->level());
        ELPP_COUT << ELPP_COUT_LINE(logLine);
    }
    // 需要写文件,则放进队列
    //  Save resources and only queue if we want to write to file otherwise just ignore handler
    if (data->logMessage()->logger()->typedConfigurations()->toFile(data->logMessage()->level()))
    {
        ELPP->asyncLogQueue()->push(AsyncLogItem(*(data->logMessage()), *data, logLine));
    }
}

    LogBuilderCLOG日志输出 中已经介绍过了。
    TypedConfigurations 类在 日志格式配置管理类 中已经介绍过了。

IWorker 类

    异步日志调度器的抽象类,只能被继承,派生类需要实现 start 接口。
    IWorker 的实现如下:

class IWorker {
    public:
    virtual ~IWorker() {}
    virtual void start() = 0;
};

AsyncDispatchWorker 类

    AsyncDispatchWorker 类是 easylogging++默认的异步日志调度器,主要职责:从异步日志队列中取出日志,执行真正的写日志动作。
    AsyncDispatchWorker 的声明如下:

class AsyncDispatchWorker : public base::IWorker, public base::threading::ThreadSafe
{
public:
    AsyncDispatchWorker();
    virtual ~AsyncDispatchWorker();

    bool clean(void);
    void emptyQueue(void);
    virtual void start(void);
    void handle(AsyncLogItem *logItem);
    void run(void);

    void setContinueRunning(bool value)
    {
        base::threading::ScopedLock scopedLock(m_continueRunningLock);
        m_continueRunning = value;
    }

    bool continueRunning(void) const
    {
        return m_continueRunning;
    }

private:
    std::condition_variable cv;
    bool m_continueRunning; // 控制轮询清空日志队列的线程是否退出的标志
    base::threading::Mutex m_continueRunningLock;
};

构造函数

AsyncDispatchWorker::AsyncDispatchWorker()
{
    setContinueRunning(false);
}

析构函数

AsyncDispatchWorker::~AsyncDispatchWorker()
{
    setContinueRunning(false);
    ELPP_INTERNAL_INFO(6, "Stopping dispatch worker - Cleaning log queue");
    clean();
    ELPP_INTERNAL_INFO(6, "Log queue cleaned");
}

启动异步日志

// 开启一个线程(线程处理函数run)处理日志队列
void AsyncDispatchWorker::start(void)
{
    base::threading::msleep(5000); // 5s (why?)
    setContinueRunning(true);
    std::thread t1(&AsyncDispatchWorker::run, this);
    t1.join();
}

// 线程处理函数
void AsyncDispatchWorker::run(void)
{
    while (continueRunning())
    {
        emptyQueue();
        base::threading::msleep(10); // 10ms
    }
}

// 依次取出每条日志,调用handle接口处理
void AsyncDispatchWorker::emptyQueue(void)
{
    while (!ELPP->asyncLogQueue()->empty())
    {
        AsyncLogItem data = ELPP->asyncLogQueue()->next();
        handle(&data);
        base::threading::msleep(100);
    }
}

    handle 接口在后面会详细分析。

写日志

// 处理一条日志
void AsyncDispatchWorker::handle(AsyncLogItem *logItem)
{
    LogDispatchData *data = logItem->data();
    LogMessage *logMessage = logItem->logMessage();
    Logger *logger = logMessage->logger();
    base::TypedConfigurations *conf = logger->typedConfigurations();
    // logLine:经过LogBuilder替换日志格式指示器为实际的内容后的日志内容
    base::type::string_t logLine = logItem->logLine();
    if (data->dispatchAction() == base::DispatchAction::NormalLog)
    {
        // 当前日志记录器的对应日志级别需要写文件
        if (conf->toFile(logMessage->level()))
        {
            // 获取当前日志记录器的对应日志级别对应的日志文件的文件流
            base::type::fstream_t *fs = conf->fileStream(logMessage->level());
            if (fs != nullptr)
            {
                // 文件存在则将日志写文件
                fs->write(logLine.c_str(), logLine.size());
                if (fs->fail())
                {
                    // 写失败则终端输出内部错误信息
                    ELPP_INTERNAL_ERROR("Unable to write log to file ["
                                            << conf->filename(logMessage->level()) << "].\n"
                                            << "Few possible reasons (could be something else):\n"
                                            << "      * Permission denied\n"
                                            << "      * Disk full\n"
                                            << "      * Disk is not writable",
                                        true);
                }
                else
                {
                    // 写成功时,检查是否需要立即刷新(配置了LoggingFlag::ImmediateFlush或者当前日志记录器的对应级别的未刷新次数达到刷新的阈值)
                    if (ELPP->hasFlag(LoggingFlag::ImmediateFlush) || (logger->isFlushNeeded(logMessage->level())))
                    {
                        logger->flush(logMessage->level(), fs);
                    }
                }
            }
            else
            {
                // 文件不存在,则控制台输出内部错误信息
                ELPP_INTERNAL_ERROR("Log file for [" << LevelHelper::convertToString(logMessage->level()) << "] "
                                                     << "has not been configured but [TO_FILE] is configured to TRUE. [Logger ID: " << logger->id() << "]",
                                    false);
            }
        }
    }
#if defined(ELPP_SYSLOG)
    // 如果定义了ELPP_SYSLOG宏,则判断日志类型是否为SysLog日志
    // 如果是,判断SysLog日志的级别,进行相关的SysLog日志输出
    else if (data->dispatchAction() == base::DispatchAction::SysLog)
    {
        // Determine syslog priority
        int sysLogPriority = 0;
        if (logMessage->level() == Level::Fatal)
            sysLogPriority = LOG_EMERG;
        else if (logMessage->level() == Level::Error)
            sysLogPriority = LOG_ERR;
        else if (logMessage->level() == Level::Warning)
            sysLogPriority = LOG_WARNING;
        else if (logMessage->level() == Level::Info)
            sysLogPriority = LOG_INFO;
        else if (logMessage->level() == Level::Debug)
            sysLogPriority = LOG_DEBUG;
        else
            sysLogPriority = LOG_NOTICE;
#if defined(ELPP_UNICODE)
        char *line = base::utils::Str::wcharPtrToCharPtr(logLine.c_str());
        syslog(sysLogPriority, "%s", line);
        free(line);
#else
        syslog(sysLogPriority, "%s", logLine.c_str());
#endif
    }
#endif // defined(ELPP_SYSLOG)
}

    handle 接口处理日志的流程与同步日志真正写日志的流程类似。
    同步日志真正写日志的流程已经在 CLOG日志输出DefaultLogDispatchCallback::dispatch 接口详细分析过了,这里就不多说了。

清空异步日志队列

// 带条件变量判断是否满足日志队列非空,条件满足则调用emptyQueue清空队列,然后发出通知
bool AsyncDispatchWorker::clean(void)
{
    std::mutex m;
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, []
            { return !ELPP->asyncLogQueue()->empty(); });
    emptyQueue();
    lk.unlock();
    cv.notify_one();
    return ELPP->asyncLogQueue()->empty();
}

至此,异步日志相关的内容就介绍完了。

前面提过 easylogging++默认的日志回滚只能备份文件,但总是会清空当前的日志文件。
日常项目中的日志回滚一般是当日志文件达到一定的条件后,比如文件大小达到阈值或者超过了一定的时间,比如 24 小时,这时候我们会重新生成一个新的日志文件,原日志文件一般保持不变。

这样 easylogging++的默认实现显然不符合真实项目的需求,因此日志回滚的功能我们就需要根据实际的项目需求定制一下了。下一篇我们就来看看如何定制日志回滚以满足真实项目的需求。

标签:异步,++,void,easylogging,源码,base,logMessage,日志,data
From: https://www.cnblogs.com/DesignLife/p/16964781.html

相关文章

  • C++
    通讯录管理系统1、系统需求通讯录是一个可以记录亲人、好友信息的工具。本教程主要利用C++来实现一个通讯录管理系统系统中需要实现的功能如下:添加联系人:向通讯录中......
  • 备忘录APP源码和设计报告
    大作业文档项目名称:备忘录专业:班级:1学号:姓名:目 录一、项目功能介绍3二、项目运行环境31、开发环境32、运行环境33、是否需要联网3三、项目配置文件及工程结构31、工程配置......
  • 安卓APP源码和设计报告——运动健身教学
    实验报告课程名称实验名称指导教师专业班级学号姓名目录一、设计背景31.需求分析32.课题研究的目的和意义3二、系统需求分析与开发环境31.系统功能需求32.系统界面需......
  • 鞋子商店APP源码和设计报告
    实验报告课程名称实验名称指导教师专业班级学号姓名一、需求分析1.需求分析随着互联网和手机技术的蓬勃发展,网购已经成为许多人,尤其是年轻人的主要消费方式,这就对手机购......
  • 新闻APP(娱乐)源码和设计报告
    项目名称:新闻娱乐APP专业:班级:学号:姓名:目 录一、项目功能介绍3二、项目运行环境31、开发环境32、运行环境33、是否需要联网3三、项目配置文件及工程结构31、工程配置文件32......
  • C++《面向对象课程设计》题目
    C++《面向对象课程设计》题目面向对象编程课程设计参考内容课程名称:《面向对象课程设计》设计题目:学生选课系统;订票系统;运动会分数统计系统;通信录管理系统;歌咏比赛......
  • 从稍微懂一点开始的C++学习之路1: 智能指针
    从稍微懂一点开始的C++学习之路1智能指针因为之前一直是搞qt的,没有搞过纯c++,所以现在算得上是刚开始学纯C++。C++的大部分语法其实我都懂,主要的是一些规范,还有内存回收等......
  • [c++11新特性]12-类型萃取
    ​​c++11/14类型萃取​​类型萃取所谓类型萃取,就是依靠模板的方式,来判断一个类型是否拥有某些特性,比如A类型和B类型是否相同,C类型是否有某个成员变量,D类型是否有某个方法,或......
  • CompletableFuture源码解析
    前言JDK8为我们带来了CompletableFuture这个有意思的新类,它提供比Future更灵活更强大的回调功能,借助CompletableFuture我们可以更方便的编排异步任务。本着知其然......
  • C/C++计时函数
    计时函数介绍time函数原型`time_ttime(time_t*timer)`,time函数是c-runtime库里的函数。此函数返回从1970年1月1日00:00:00(UTC时间)到此刻所经过的总秒数,所以使用此函数......