【实战指南】轻松自研嵌入式日志框架,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模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
1月前
|
XML 安全 Java
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
本文介绍了Java日志框架的基本概念和使用方法,重点讨论了SLF4J、Log4j、Logback和Log4j2之间的关系及其性能对比。SLF4J作为一个日志抽象层,允许开发者使用统一的日志接口,而Log4j、Logback和Log4j2则是具体的日志实现框架。Log4j2在性能上优于Logback,推荐在新项目中使用。文章还详细说明了如何在Spring Boot项目中配置Log4j2和Logback,以及如何使用Lombok简化日志记录。最后,提供了一些日志配置的最佳实践,包括滚动日志、统一日志格式和提高日志性能的方法。
336 30
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
|
2月前
|
Rust 前端开发 JavaScript
Tauri 开发实践 — Tauri 日志记录功能开发
本文介绍了如何为 Tauri 应用配置日志记录。Tauri 是一个利用 Web 技术构建桌面应用的框架。文章详细说明了如何在 Rust 和 JavaScript 代码中设置和集成日志记录,并控制日志输出。通过添加 `log` crate 和 Tauri 日志插件,可以轻松实现多平台日志记录,包括控制台输出、Webview 控制台和日志文件。文章还展示了如何调整日志级别以优化输出内容。配置完成后,日志记录功能将显著提升开发体验和程序稳定性。
125 1
Tauri 开发实践 — Tauri 日志记录功能开发
|
2月前
|
XML JSON Java
Logback 与 log4j2 性能对比:谁才是日志框架的性能王者?
【10月更文挑战第5天】在Java开发中,日志框架是不可或缺的工具,它们帮助我们记录系统运行时的信息、警告和错误,对于开发人员来说至关重要。在众多日志框架中,Logback和log4j2以其卓越的性能和丰富的功能脱颖而出,成为开发者们的首选。本文将深入探讨Logback与log4j2在性能方面的对比,通过详细的分析和实例,帮助大家理解两者之间的性能差异,以便在实际项目中做出更明智的选择。
332 3
|
1天前
|
监控 安全 Linux
启用Linux防火墙日志记录和分析功能
为iptables启用日志记录对于监控进出流量至关重要
|
1月前
|
Java Maven Spring
超实用的SpringAOP实战之日志记录
【11月更文挑战第11天】本文介绍了如何使用 Spring AOP 实现日志记录功能。首先概述了日志记录的重要性及 Spring AOP 的优势,然后详细讲解了搭建 Spring AOP 环境、定义日志切面、优化日志内容和格式的方法,最后通过测试验证日志记录功能的准确性和完整性。通过这些步骤,可以有效提升系统的可维护性和可追踪性。
|
2月前
|
Java 程序员 应用服务中间件
「测试线排查的一些经验-中篇」&& 调试日志实战
「测试线排查的一些经验-中篇」&& 调试日志实战
29 1
「测试线排查的一些经验-中篇」&& 调试日志实战
|
2月前
|
Java 程序员 API
Android|集成 slf4j + logback 作为日志框架
做个简单改造,统一 Android APP 和 Java 后端项目打印日志的体验。
138 1
|
3月前
|
设计模式 SQL 安全
PHP中的设计模式:单例模式的深入探索与实践在PHP的编程实践中,设计模式是解决常见软件设计问题的最佳实践。单例模式作为设计模式中的一种,确保一个类只有一个实例,并提供全局访问点,广泛应用于配置管理、日志记录和测试框架等场景。本文将深入探讨单例模式的原理、实现方式及其在PHP中的应用,帮助开发者更好地理解和运用这一设计模式。
在PHP开发中,单例模式通过确保类仅有一个实例并提供一个全局访问点,有效管理和访问共享资源。本文详细介绍了单例模式的概念、PHP实现方式及应用场景,并通过具体代码示例展示如何在PHP中实现单例模式以及如何在实际项目中正确使用它来优化代码结构和性能。
56 2
|
2月前
|
SQL XML 监控
SpringBoot框架日志详解
本文详细介绍了日志系统的重要性及其在不同环境下的配置方法。日志用于记录系统运行时的问题,确保服务的可靠性。文章解释了各种日志级别(如 info、warn、error 等)的作用,并介绍了常用的日志框架如 SLF4J 和 Logback。此外,还说明了如何在 SpringBoot 中配置日志输出路径及日志级别,包括控制台输出与文件输出的具体设置方法。通过这些配置,开发者能够更好地管理和调试应用程序。
|
14天前
|
监控 安全 Apache
什么是Apache日志?为什么Apache日志分析很重要?
Apache是全球广泛使用的Web服务器软件,支持超过30%的活跃网站。它通过接收和处理HTTP请求,与后端服务器通信,返回响应并记录日志,确保网页请求的快速准确处理。Apache日志分为访问日志和错误日志,对提升用户体验、保障安全及优化性能至关重要。EventLog Analyzer等工具可有效管理和分析这些日志,增强Web服务的安全性和可靠性。