日志系统虽非项目直接功能,却是开发者背后的强大辅助。优秀的日志设计如同给程序安装了北斗定位,让问题排查变得直观快捷,极大提升开发效率与项目维护体验。本文旨在深入探讨并详细记载自主研发日志框架的具体技术和实施策略。
另外,有一则好消息:公众号文章留言功能现已重新开放。在阅读完每一篇文章后,欢迎随时于文末留言区留下您的想法、问题或反馈,我将积极关注并与您进行深入交流。
日志框架作为一种普遍应用于软件开发领域的关键工具,已发展出众多成熟案例,诸如Android平台的logcat
、glog
、Log4cpp
等。汲取这些成熟的框架的经验,本篇主要从需求分析、设计方案、实现细节和难点、测试、总结部分记录自研嵌入式框架的细节。
在使用者的角度,对于日志功能的需求主要概括如下:
DEBUG
、INFO
、WARNING
、ERROR
在内的多级别日志输出接口,并允许用户灵活配置和动态切换日志输出级别阈值。sparrow.log.1
、sparrow.log.2
等;当前活动日志文件统一命名为sparrow.log
,不带序号,以此保持最新日志的访问便捷性。基于上述日志功能需求分析,以下是设计方案的概要框架:
DEBUG
、INFO
、WARNING
、ERROR
),并构建相应的日志输出接口。配置模块可读取环境变量或配置文件,动态设置日志级别阈值,低于此阈值的日志将不会被记录。LogManager
此进程负责实时读取缓存区的日志,并写入到本地文件中。同时,此进程负责实现日志文件的管理等功能。sparrow.log
文件再度达到阈值时,延续相同逻辑,依次将sparrow.log更新为sparrow.log.n+1(n为历史文件序号),并创建新的sparrow.log。日志框架注:箭头指示日志数据的传输路径及其流向
#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)
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;
};
tail -f sparrow.log
能达到同样效果,故暂不实现。$ 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毫秒的时间间隔均能得到预期反馈,确认该间隔时间设置准确无误。
$ 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文件大小随着新日志的实时写入而动态更新,始终保持存储最新内容。
用心感悟,认真记录,写好每一篇文章,分享每一框干货。