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

简介: 本文介绍了如何自研一个嵌入式日志框架,涵盖了6大功能亮点:日志分级管理、异步处理与并发安全性、详尽上下文信息记录、滚动日志归档策略、高效资源利用和便捷API接口。设计上,通过日志过滤器、共享环形缓冲区和独立的日志管理进程实现日志管理。在并发环境下,使用信号量保证线程安全。日志文件按大小滚动并有序归档,同时考虑了资源效率。对外提供简洁的API接口,便于开发人员使用。文章还简述了实现细节,包括实时存储、日志滚动和共享内存管理。测试部分验证了日志回滚和实时打印功能的正确性。

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

[TOC]

引言

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

概述

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

需求分析

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

  1. 日志分级管理
    实现包括DEBUGINFOWARNINGERROR在内的多级别日志输出接口,并允许用户灵活配置和动态切换日志输出级别阈值。
  2. 异步处理与并发安全性
    系统应具备异步日志记录能力,确保在多线程或并发环境下,日志信息记录的完整性及正确性,且内部实现需保证线程安全。
  3. 详尽上下文信息记录
  • 日志条目应包含精确的时间戳、进程ID、代码行号以及模块标识等关键上下文信息,以便于进行问题定位和性能诊断。
  • 各模块在使用日志接口时,传入对应的模块标识,方便调试过滤关键日志。
  1. 滚动日志归档策略
  • 实时存储与文件滚动:日志应即时被写入到本地文件,确保信息的完整留存和持久化。
  • 文件大小限制与自动分卷:单个日志文件具有预设的最大容量限制,当达到上限时,系统自动创建新文件进行存储。
  • 有序归档命名规则:采用“文件名.log.序号”形式命名历史日志文件,如sparrow.log.1sparrow.log.2等;当前活动日志文件统一命名为sparrow.log,不带序号,以此保持最新日志的访问便捷性。
  1. 高效资源利用
    设计上注重轻量化,确保日志系统占用较低的内存和CPU资源,在不影响系统性能的前提下完成日志记录工作。
  2. 便捷API接口
    提供一套简洁明了、易于集成的API接口,使得开发人员能够轻松地在代码中添加和使用日志功能。

设计方案

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

  1. 日志分级管理设计
  • 日志级别定义:
    设计一组枚举类型表示不同日志级别(DEBUGINFOWARNINGERROR),并构建相应的日志输出接口。
    配置模块可读取环境变量或配置文件,动态设置日志级别阈值,低于此阈值的日志将不会被记录。
  • 日志过滤器
    在日志输出前增加过滤逻辑,根据当前设置的日志级别决定是否需要输出指定级别的日志消息。
  1. 异步处理与并发安全性设计
  • 构建日志共享缓存区
    映射一片共享内存,通过环形buffer的形式管理,用于实时缓存产生的日志。模块使用时,只会实时的将日志丢到共享内存中,而不会参与日志存储业务,提升日志的写入效率。
  • 创建独立的日志管理进程LogManager
    此进程负责实时读取缓存区的日志,并写入到本地文件中。同时,此进程负责实现日志文件的管理等功能。
  • 通过信号量,实现并发同步
    多进程在写入共享内存时,通过信号量实现进程间同步,避免日志写入错乱。
  1. 详尽上下文信息记录设计
  • 日志输出格式封装
    将时间戳、进程ID、代码行号、模块名等信息封装到日志中,伴随每一条日志消息一起输出。
  1. 滚动日志归档策略设计
    为确保日志文件的有效管理和存储,设计了一套文件滚动机制。当当前日志文件sparrow.log的大小超出预设阈值时,系统将自动执行回滚操作:
  • 容量监测阶段
    实施动态监控sparrow.log文件大小,一旦触及预先设定的容量上限,即刻触发回滚流程。
  • 文件迁移过程
    将现有的sparrow.log文件通过原子操作进行重命名,新增后缀".1"变为sparrow.log.1,以保存历史日志内容。
  • 新文件初始化
    立即创建一个新的sparrow.log文件,用于接收接下来产生的实时日志信息。
  • 滚动策略连续执行
    sparrow.log文件再度达到阈值时,延续相同逻辑,依次将sparrow.log更新为sparrow.log.n+1(n为历史文件序号),并创建新的sparrow.log。
  • 历史日志管理
    设定保留的历史日志文件数量上限,超出限额的最老日志文件将被适时清理,确保存储空间的有效利用。
  • 日志文件压缩设计
    可选功能。若文件存储阈值定义较大,产生历史文件时,将其压缩存储。
  1. 资源效率优化设计
  • 共享环形缓冲区管理
    1)将实时日志先存入共享内存中,避免大量的文件读写操作。
    2)引入环形缓冲区管理共享内存,实现共享内存的循环使用,避免创建大容量共享内存。
    3) 控制缓存大小,环形设计避免共享内存写入溢出。
  1. 便捷API接口设计
    将日志接口封装成宏函数,方便各模块调用。

    实现细节

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

日志框架


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

  • 对外接口(API)
    此组件专为应用开发者设计,嵌入于使用者进程内部,承担着将产生的日志实时推送至共享缓存区的责任,通过简洁高效的API接口,确保开发者能够轻松、灵活地在不同模块中记录不同级别的日志。
  • 日志核心管理(LogManagerSrv)
    作为独立运行的日志管理进程,核心组件专注于日志的存储和文件管理任务。它负责从共享缓存区读取日志信息,执行日志文件的滚动归档策略,以及对文件大小、历史日志数量进行有效控制,确保日志数据的持久化存储和系统资源的高效利用。
  • 调试输出(终端Debug)
    调试输出进程主要用于开发和调试场景。在终端手动执行后,它实时监听共享缓存区中的日志数据,并将它们立即显示在终端界面,便于开发人员实时观测和分析程序运行状况。
  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实现,篇幅有限暂不列举。文末获取代码方法,需要自取。

  • 共享环形缓存区管理
    此部分实现了一个基于共享内存的环形缓冲区数据结构,其核心功能与常规环形缓冲区保持一致,但特别之处在于它位于可供多个进程共同访问的共享内存区域中。该模块提供的接口服务于日志数据的高效暂存和交换,确保在多进程环境下,日志信息能安全、顺畅地从生成者进程传递至消费者进程(如日志管理进程),并在此过程中有效地避免数据冲突和丢失。
    ```C++
    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;
};

4. **调试输出(Debug)**       
&emsp; 这部分实现主要是将存储在缓存区的日志实时显示在终端上,便于调试时,观察实时打印。考虑到终端通过执行```tail -f sparrow.log``` 能达到同样效果,故暂不实现。
## 测试
1. **测试终端实时日志打印**    
本地触发10ms一次的定时器,观察终端输出的日志间隔是否为10ms
```shell
$ 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文件大小随着新日志的实时写入而动态更新,始终保持存储最新内容。

总结

  • 对于项目来说,移植并使用像Android logcat这样的成熟日志框架至关重要,本文的实现正是以此为参考依据。
  • 虽然日志系统看似与实际功能模块无直接关联,但其重要性不可忽视;真正动手实现一套完善的日志系统,对个人技术水平的成长有着极大的提升。
  • 在实现日志系统框架时,发现了共享内存应用的一种独特方式:通过与特定数据结构的有机结合,能实现数据在意外断电情况下的记忆保留,这种方式颇具趣味性和实用性。
相关实践学习
日志服务之数据清洗与入湖
本教程介绍如何使用日志服务接入NGINX模拟数据,通过数据加工对数据进行清洗并归档至OSS中进行存储。
相关文章
|
5天前
|
XML Java 测试技术
《手把手教你》系列基础篇(九十一)-java+ selenium自动化测试-框架设计基础-Logback实现日志输出-下篇(详解教程)
【7月更文挑战第9天】在Java项目中,使用Logback配置可以实现日志按照不同包名输出到不同的文件,并且根据日志级别分开记录。
20 4
|
6天前
|
XML Java 测试技术
《手把手教你》系列基础篇(九十)-java+ selenium自动化测试-框架设计基础-Logback实现日志输出-中篇(详解教程)
【7月更文挑战第8天】这篇教程介绍了如何使用Logback将Java应用的日志输出到文件中。首先,通过创建`logback.xml`配置文件,设置`FileAppender`来指定日志文件路径和格式。然后,提供了一个`RollingFileAppender`的例子,用于每日生成新的日志文件并保留一定天数的历史记录。文中包含配置文件的XML代码示例,并展示了控制台输出和生成的日志文件内容。教程最后提到了一些可能遇到的问题及解决建议。
15 0
《手把手教你》系列基础篇(九十)-java+ selenium自动化测试-框架设计基础-Logback实现日志输出-中篇(详解教程)
|
10天前
|
XML 测试技术 数据格式
《手把手教你》系列基础篇(八十五)-java+ selenium自动化测试-框架设计基础-TestNG自定义日志-下篇(详解教程)
【7月更文挑战第3天】TestNG教程展示了如何自定义日志记录。首先创建一个名为`TestLog`的测试类,包含3个测试方法,其中一个故意失败以展示日志。使用`Assert.assertTrue`和`Reporter.log`来记录信息。接着创建`CustomReporter`类,继承`TestListenerAdapter`,覆盖`onTestFailure`, `onTestSkipped`, 和 `onTestSuccess`,在这些方法中自定义日志输出。
27 6
|
7天前
|
Java 关系型数据库 测试技术
《手把手教你》系列基础篇(八十九)-java+ selenium自动化测试-框架设计基础-Logback实现日志输出-上篇(详解教程)
【7月更文挑战第7天】Apache Log4j2的安全漏洞促使考虑使用logback作为替代的日志框架。Logback由log4j创始人设计,提供更好的性能,更低的内存使用,并且能够自动重载配置文件。它分为logback-core、logback-classic(实现了SLF4J API)和logback-access(用于Servlet容器集成)三个模块。配置涉及Logger、Appender(定义日志输出目的地)和Layout(格式化日志)。
15 1
|
7天前
|
Python
Python编程实战:利用闭包与装饰器优化日志记录功能
【7月更文挑战第7天】Python的闭包和装饰器简化了日志记录。通过定义如`log_decorator`的装饰器,可以在不修改原函数代码的情况下添加日志功能。当@log_decorator用于`add(x, y)`函数时,调用时自动记录日志。进一步,`timestamp_log_decorator`展示了如何创建特定功能的装饰器,如添加时间戳。这些技术减少了代码冗余,提高了代码的可维护性。
15 1
|
9天前
|
Java 测试技术 Apache
《手把手教你》系列基础篇(八十六)-java+ selenium自动化测试-框架设计基础-Log4j实现日志输出(详解教程)
【7月更文挑战第4天】Apache Log4j 是一个广泛使用的 Java 日志框架,它允许开发者控制日志信息的输出目的地、格式和级别。Log4j 包含三个主要组件:Loggers(记录器)负责生成日志信息,Appenders(输出源)确定日志输出的位置(如控制台、文件、数据库等),而 Layouts(布局)则控制日志信息的格式。通过配置 Log4j,可以灵活地定制日志记录行为。
25 4
|
8天前
|
监控
若依修改-----其他功能,包括参数设置,通知公告,日志管理,验证码控制开关在参数设置里,若依的注册页面是隐藏的,在src的login.vue的97行注册开发,修改成true,通知公告,促进组织内部信
若依修改-----其他功能,包括参数设置,通知公告,日志管理,验证码控制开关在参数设置里,若依的注册页面是隐藏的,在src的login.vue的97行注册开发,修改成true,通知公告,促进组织内部信
|
17天前
|
监控 Java API
Java日志框架的纷争演进与传奇故事
Java日志框架的纷争演进与传奇故事
|
8天前
|
XML Java 测试技术
《手把手教你》系列基础篇(八十八)-java+ selenium自动化测试-框架设计基础-Log4j 2实现日志输出-下篇(详解教程)
【7月更文挑战第6天】本文介绍了如何使用Log4j2将日志输出到文件中,重点在于配置文件的结构和作用。配置文件包含两个主要部分:`appenders`和`loggers`。`appenders`定义了日志输出的目标,如控制台(Console)或其他文件,如RollingFile,设置输出格式和策略。`loggers`定义了日志记录器,通过`name`属性关联到特定的类或包,并通过`appender-ref`引用`appenders`来指定输出位置。`additivity`属性控制是否继承父logger的配置。
15 0
|
8天前
|
XML Java 测试技术
《手把手教你》系列基础篇(八十七)-java+ selenium自动化测试-框架设计基础-Log4j 2实现日志输出-上篇(详解教程)
【7月更文挑战第5天】Apache Log4j 2是一个日志框架,它是Log4j的升级版,提供了显著的性能提升,借鉴并改进了Logback的功能,同时修复了Logback架构中的问题。Log4j2的特点包括API与实现的分离,支持SLF4J,自动重新加载配置,以及高级过滤选项。它还引入了基于lambda表达式的延迟评估,低延迟的异步记录器和无垃圾模式。配置文件通常使用XML,但也可以是JSON或YAML,其中定义了日志级别、输出目的地(Appender)和布局(Layout)。
16 0

热门文章

最新文章