【实战指南】轻松自研嵌入式日志框架,6大功能亮点一文读懂

2024-04-12  本文已影响0人  开源519

【实战指南】轻松自研嵌入式日志框架,6大功能亮点一文读懂

[TOC]

引言

  日志系统虽非项目直接功能,却是开发者背后的强大辅助。优秀的日志设计如同给程序安装了北斗定位,让问题排查变得直观快捷,极大提升开发效率与项目维护体验。本文旨在深入探讨并详细记载自主研发日志框架的具体技术和实施策略。

概述

  日志框架作为一种普遍应用于软件开发领域的关键工具,已发展出众多成熟案例,诸如Android平台的logcatglogLog4cpp等。汲取这些成熟的框架的经验,本篇主要从需求分析、设计方案、实现细节和难点、测试、总结部分记录自研嵌入式框架的细节。

需求分析

  在使用者的角度,对于日志功能的需求主要概括如下:

  1. 日志分级管理
    实现包括DEBUGINFOWARNINGERROR在内的多级别日志输出接口,并允许用户灵活配置和动态切换日志输出级别阈值。
  2. 异步处理与并发安全性
    系统应具备异步日志记录能力,确保在多线程或并发环境下,日志信息记录的完整性及正确性,且内部实现需保证线程安全。
  3. 详尽上下文信息记录
  1. 滚动日志归档策略
  1. 高效资源利用
    设计上注重轻量化,确保日志系统占用较低的内存和CPU资源,在不影响系统性能的前提下完成日志记录工作。
  2. 便捷API接口
    提供一套简洁明了、易于集成的API接口,使得开发人员能够轻松地在代码中添加和使用日志功能。

设计方案

  基于上述日志功能需求分析,以下是设计方案的概要框架:

  1. 日志分级管理设计
  1. 异步处理与并发安全性设计
  1. 详尽上下文信息记录设计
  1. 滚动日志归档策略设计
    为确保日志文件的有效管理和存储,设计了一套文件滚动机制。当当前日志文件sparrow.log的大小超出预设阈值时,系统将自动执行回滚操作:
  1. 资源效率优化设计
  1. 便捷API接口设计
    将日志接口封装成宏函数,方便各模块调用。

实现细节

  1. 日志框架组成
    基于上述设计方案的概括,日志框架主要划分为三个相互协作的部分,在实际环境运行示意图如下:
LogManager.jpg

注:箭头指示日志数据的传输路径及其流向

  1. 对外接口(API)
    这部分代码相对简洁,其核心在于对日志使用接口进行了一层逻辑封装,并进一步通过宏定义的形式转化为易于使用的宏接口,旨在为开发者提供更为便捷的日志调用方式。
#define LOGD(tag, fmt, args...)     SprLog::GetInstance()->d(tag, "%4d " fmt, __LINE__, ##args)
#define LOGI(tag, fmt, args...)     SprLog::GetInstance()->i(tag, "%4d " fmt, __LINE__, ##args)
#define LOGW(tag, fmt, args...)     SprLog::GetInstance()->w(tag, "%4d " fmt, __LINE__, ##args)
#define LOGE(tag, fmt, args...)     SprLog::GetInstance()->e(tag, "%4d " fmt, __LINE__, ##args)
  1. 日志核心管理(Core)
    此核心模块承载了日志功能的核心实现逻辑,面对的主要挑战集中在如下几个关键技术环节:
int LogManager::MainLoop()
{
    while (mRunning)
    {
        if (pLogMCacheMem->AvailData() <= 0) {
            usleep(10000);
            continue;
        }

        int32_t len = 0;
        int ret = pLogMCacheMem->read(&len, sizeof(int32_t));
        if (ret != 0 || len < 0) {
            SPR_LOGE("read memory failed! len = %d, ret = %d\n", len, ret);
            usleep(10000);
            continue;
        }

        std::string value;
        value.resize(len);
        char* data = const_cast<char*>(value.c_str());
        ret = pLogMCacheMem->read(data, len);
        if (ret != 0) {
            SPR_LOGE("read failed! len = %d\n", len);
        }

        RotateLogsIfNecessary(len);
        WriteToLogFile(value);
    }

    return 0;
}

MainLoop中,不停读取环形共享内存数据,并写入本地文件中。在写入过程中,发现长度超过文件阈值,则触发日志文件回滚策略。回滚策略业务在RotateLogsIfNecessary实现。

// E.g: sparrow.log sparrow.log.1 sparrow.log.2 ...
int LogManager::RotateLogsIfNecessary(uint32_t logDataSize)
{
    uint32_t curFileSize = static_cast<uint32_t>(mLogFileStream.tellp());
    if (curFileSize + logDataSize > mMaxFileSize) {
        mLogFileStream.close();

        UpdateSuffixOfAllFiles();
        mLogFileStream.open(mLogsDirPath + '/' + mCurrentLogFile, std::ios_base::app | std::ios_base::out);
        if (!mLogFileStream.is_open()) {
            SPR_LOGE("Open %s failed!", mCurrentLogFile.c_str());
        }
    }

    return 0;
}

在回滚过程中,涉及到历史日志文件迁移,在UpdateSuffixOfAllFiles实现,篇幅有限暂不列举。文末获取代码方法,需要自取。

class SharedRingBuffer
{
public:
    /**
     * @brief Constructs a master Shared Ring Buffer object.
     * @param path  The path to the shared memory.
     * @param capacity The buffer's capacity.
     *
     * Intended for use in master mode with shared memory refreshing.
     */
    SharedRingBuffer(std::string path, uint32_t capacity);

    /**
     * @brief Constructs a slave Shared Ring Buffer object
     * @param path The path to the shared memory.
     *
     * This constructor creates an instance of a slave Shared Ring Buffer, typically used by client applications.
     * It facilitates access and utilization of the shared buffer by referencing it through the specified path.
     */
    SharedRingBuffer(std::string path);
    ~SharedRingBuffer();

    bool    IsReadable()    const noexcept;
    bool    IsWriteable()   const noexcept;
    int     write(const void* data, int32_t len);
    int     read(void* data, int32_t len);
    int     DumpBuffer(void* data, int32_t len) const noexcept;

    int32_t AvailSpace()    const noexcept;
    int32_t AvailData()     const noexcept;


private:
    void    AdjustPosIfOverflow(uint32_t* pos, int32_t size) const noexcept;
    void    SetRWStatus(ECmdType type) const noexcept;
    void    DumpMemory(const char* pAddr, uint32_t size);
    void    DumpErrorInfo();

private:
    Root*       mRoot;
    void*       mData;
    uint32_t    mCapacity;
    std::mutex  mMutex;
    std::string mShmPath;
};
  1. 调试输出(Debug)
      这部分实现主要是将存储在缓存区的日志实时显示在终端上,便于调试时,观察实时打印。考虑到终端通过执行tail -f sparrow.log 能达到同样效果,故暂不实现。

测试

  1. 测试终端实时日志打印
    本地触发10ms一次的定时器,观察终端输出的日志间隔是否为10ms
$ tail -f /tmp/sprlog/sparrow.log
04-12 23:53:26.048  51958       TimerM D:   76 [0x0 -> 0x5] msg.GetMsgId() = SIG_ID_SYSTEM_TIMER_NOTIFY
04-12 23:53:26.048  51958       TimerM D:   76 [0x0 -> 0x0] msg.GetMsgId() = SIG_ID_TIMER_START_SYSTEM_TIMER
04-12 23:53:26.057  51958       TimerM D:   76 [0x0 -> 0x5] msg.GetMsgId() = SIG_ID_SYSTEM_TIMER_NOTIFY
04-12 23:53:26.058  51958       TimerM D:   76 [0x0 -> 0x0] msg.GetMsgId() = SIG_ID_TIMER_START_SYSTEM_TIMER
04-12 23:53:26.068  51958       TimerM D:   76 [0x0 -> 0x5] msg.GetMsgId() = SIG_ID_SYSTEM_TIMER_NOTIFY
04-12 23:53:26.068  51958       TimerM D:   76 [0x0 -> 0x0] msg.GetMsgId() = SIG_ID_TIMER_START_SYSTEM_TIMER
04-12 23:53:26.078  51958       TimerM D:   76 [0x0 -> 0x5] msg.GetMsgId() = SIG_ID_SYSTEM_TIMER_NOTIFY
04-12 23:53:26.078  51958       TimerM D:   76 [0x0 -> 0x0] msg.GetMsgId() = SIG_ID_TIMER_START_SYSTEM_TIMER
04-12 23:53:26.088  51958       TimerM D:   76 [0x0 -> 0x5] msg.GetMsgId() = SIG_ID_SYSTEM_TIMER_NOTIFY
04-12 23:53:26.088  51958       TimerM D:   76 [0x0 -> 0x0] msg.GetMsgId() = SIG_ID_TIMER_START_SYSTEM_TIMER
04-12 23:53:26.098  51958       TimerM D:   76 [0x0 -> 0x5] msg.GetMsgId() = SIG_ID_SYSTEM_TIMER_NOTIFY

通过观测,证实了每隔10毫秒的时间间隔均能得到预期反馈,确认该间隔时间设置准确无误。

  1. 测试日志回滚
    为了方便验证,日志文件阈值暂设置为1M。
$ ls /tmp/sprlog/ -lh
total 10M
-rw-r--r-- 1 dx dx 995K Apr 12 23:56 sparrow.log
-rw-r--r-- 1 dx dx 1.0M Apr 12 23:55 sparrow.log.1
-rw-r--r-- 1 dx dx 1.0M Apr 12 23:54 sparrow.log.2
-rw-r--r-- 1 dx dx 1.0M Apr 12 23:54 sparrow.log.3
-rw-r--r-- 1 dx dx 1.0M Apr 12 23:53 sparrow.log.4
-rw-r--r-- 1 dx dx 1.0M Apr 12 23:52 sparrow.log.5
-rw-r--r-- 1 dx dx 1.0M Apr 12 23:51 sparrow.log.6
-rw-r--r-- 1 dx dx 1.0M Apr 12 23:50 sparrow.log.7
-rw-r--r-- 1 dx dx 1.0M Apr 12 23:50 sparrow.log.8
-rw-r--r-- 1 dx dx 1.0M Apr 12 23:49 sparrow.log.9

观察结果显示,日志回滚机制正在正常运作,表现为sparrow.log文件大小随着新日志的实时写入而动态更新,始终保持存储最新内容。

总结

上一篇 下一篇

猜你喜欢

热点阅读