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

在上一篇我们介绍了 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++的默认实现显然不符合真实项目的需求,因此日志回滚的功能我们就需要根据实际的项目需求定制一下了。下一篇我们就来看看如何定制日志回滚以满足真实项目的需求。

posted @ 2022-12-07 22:41  节奏自由  阅读(229)  评论(0编辑  收藏  举报