【實(shí)戰(zhàn)指南】輕松自研嵌入式日志框架,6大功能亮點(diǎn)一文讀懂
[TOC]
引言
? 日志系統(tǒng)雖非項(xiàng)目直接功能岁忘,卻是開發(fā)者背后的強(qiáng)大輔助辛慰。優(yōu)秀的日志設(shè)計(jì)如同給程序安裝了北斗定位,讓問(wèn)題排查變得直觀快捷干像,極大提升開發(fā)效率與項(xiàng)目維護(hù)體驗(yàn)昆雀。本文旨在深入探討并詳細(xì)記載自主研發(fā)日志框架的具體技術(shù)和實(shí)施策略。
概述
? 日志框架作為一種普遍應(yīng)用于軟件開發(fā)領(lǐng)域的關(guān)鍵工具蝠筑,已發(fā)展出眾多成熟案例狞膘,諸如Android平臺(tái)的logcat
、glog
什乙、Log4cpp
等挽封。汲取這些成熟的框架的經(jīng)驗(yàn),本篇主要從需求分析臣镣、設(shè)計(jì)方案辅愿、實(shí)現(xiàn)細(xì)節(jié)和難點(diǎn)智亮、測(cè)試、總結(jié)部分記錄自研嵌入式框架的細(xì)節(jié)点待。
需求分析
? 在使用者的角度阔蛉,對(duì)于日志功能的需求主要概括如下:
-
日志分級(jí)管理
實(shí)現(xiàn)包括DEBUG
、INFO
癞埠、WARNING
状原、ERROR
在內(nèi)的多級(jí)別日志輸出接口,并允許用戶靈活配置和動(dòng)態(tài)切換日志輸出級(jí)別閾值苗踪。 -
異步處理與并發(fā)安全性
系統(tǒng)應(yīng)具備異步日志記錄能力颠区,確保在多線程或并發(fā)環(huán)境下,日志信息記錄的完整性及正確性通铲,且內(nèi)部實(shí)現(xiàn)需保證線程安全毕莱。 - 詳盡上下文信息記錄
- 日志條目應(yīng)包含精確的時(shí)間戳、進(jìn)程ID颅夺、代碼行號(hào)以及模塊標(biāo)識(shí)等關(guān)鍵上下文信息朋截,以便于進(jìn)行問(wèn)題定位和性能診斷。
- 各模塊在使用日志接口時(shí)吧黄,傳入對(duì)應(yīng)的模塊標(biāo)識(shí)质和,方便調(diào)試過(guò)濾關(guān)鍵日志。
- 滾動(dòng)日志歸檔策略
- 實(shí)時(shí)存儲(chǔ)與文件滾動(dòng):日志應(yīng)即時(shí)被寫入到本地文件稚字,確保信息的完整留存和持久化饲宿。
- 文件大小限制與自動(dòng)分卷:?jiǎn)蝹€(gè)日志文件具有預(yù)設(shè)的最大容量限制,當(dāng)達(dá)到上限時(shí)胆描,系統(tǒng)自動(dòng)創(chuàng)建新文件進(jìn)行存儲(chǔ)瘫想。
- 有序歸檔命名規(guī)則:采用“文件名.log.序號(hào)”形式命名歷史日志文件,如
sparrow.log.1
昌讲、sparrow.log.2
等国夜;當(dāng)前活動(dòng)日志文件統(tǒng)一命名為sparrow.log
,不帶序號(hào)短绸,以此保持最新日志的訪問(wèn)便捷性车吹。
-
高效資源利用
設(shè)計(jì)上注重輕量化,確保日志系統(tǒng)占用較低的內(nèi)存和CPU資源醋闭,在不影響系統(tǒng)性能的前提下完成日志記錄工作窄驹。 -
便捷API接口
提供一套簡(jiǎn)潔明了、易于集成的API接口证逻,使得開發(fā)人員能夠輕松地在代碼中添加和使用日志功能乐埠。
設(shè)計(jì)方案
? 基于上述日志功能需求分析,以下是設(shè)計(jì)方案的概要框架:
- 日志分級(jí)管理設(shè)計(jì)
- 日志級(jí)別定義:
設(shè)計(jì)一組枚舉類型表示不同日志級(jí)別(DEBUG
、INFO
丈咐、WARNING
瑞眼、ERROR
),并構(gòu)建相應(yīng)的日志輸出接口棵逊。
配置模塊可讀取環(huán)境變量或配置文件伤疙,動(dòng)態(tài)設(shè)置日志級(jí)別閾值,低于此閾值的日志將不會(huì)被記錄辆影。 - 日志過(guò)濾器
在日志輸出前增加過(guò)濾邏輯徒像,根據(jù)當(dāng)前設(shè)置的日志級(jí)別決定是否需要輸出指定級(jí)別的日志消息。
- 異步處理與并發(fā)安全性設(shè)計(jì)
- 構(gòu)建日志共享緩存區(qū)
映射一片共享內(nèi)存秸歧,通過(guò)環(huán)形buffer的形式管理厨姚,用于實(shí)時(shí)緩存產(chǎn)生的日志衅澈。模塊使用時(shí)键菱,只會(huì)實(shí)時(shí)的將日志丟到共享內(nèi)存中,而不會(huì)參與日志存儲(chǔ)業(yè)務(wù)今布,提升日志的寫入效率经备。 - 創(chuàng)建獨(dú)立的日志管理進(jìn)程
LogManager
此進(jìn)程負(fù)責(zé)實(shí)時(shí)讀取緩存區(qū)的日志,并寫入到本地文件中部默。同時(shí)侵蒙,此進(jìn)程負(fù)責(zé)實(shí)現(xiàn)日志文件的管理等功能。 - 通過(guò)信號(hào)量傅蹂,實(shí)現(xiàn)并發(fā)同步
多進(jìn)程在寫入共享內(nèi)存時(shí)纷闺,通過(guò)信號(hào)量實(shí)現(xiàn)進(jìn)程間同步,避免日志寫入錯(cuò)亂份蝴。
- 詳盡上下文信息記錄設(shè)計(jì)
- 日志輸出格式封裝
將時(shí)間戳犁功、進(jìn)程ID、代碼行號(hào)婚夫、模塊名等信息封裝到日志中浸卦,伴隨每一條日志消息一起輸出。
-
滾動(dòng)日志歸檔策略設(shè)計(jì)
為確保日志文件的有效管理和存儲(chǔ)案糙,設(shè)計(jì)了一套文件滾動(dòng)機(jī)制限嫌。當(dāng)當(dāng)前日志文件sparrow.log的大小超出預(yù)設(shè)閾值時(shí),系統(tǒng)將自動(dòng)執(zhí)行回滾操作:
- 容量監(jiān)測(cè)階段
實(shí)施動(dòng)態(tài)監(jiān)控sparrow.log文件大小时捌,一旦觸及預(yù)先設(shè)定的容量上限怒医,即刻觸發(fā)回滾流程。 - 文件遷移過(guò)程
將現(xiàn)有的sparrow.log文件通過(guò)原子操作進(jìn)行重命名奢讨,新增后綴".1"變?yōu)閟parrow.log.1裆熙,以保存歷史日志內(nèi)容。 - 新文件初始化
立即創(chuàng)建一個(gè)新的sparrow.log文件,用于接收接下來(lái)產(chǎn)生的實(shí)時(shí)日志信息入录。 - 滾動(dòng)策略連續(xù)執(zhí)行
當(dāng)sparrow.log
文件再度達(dá)到閾值時(shí)蛤奥,延續(xù)相同邏輯,依次將sparrow.log更新為sparrow.log.n+1(n為歷史文件序號(hào))僚稿,并創(chuàng)建新的sparrow.log凡桥。 - 歷史日志管理
設(shè)定保留的歷史日志文件數(shù)量上限,超出限額的最老日志文件將被適時(shí)清理蚀同,確保存儲(chǔ)空間的有效利用缅刽。 - 日志文件壓縮設(shè)計(jì)
可選功能。若文件存儲(chǔ)閾值定義較大蠢络,產(chǎn)生歷史文件時(shí)衰猛,將其壓縮存儲(chǔ)。
- 資源效率優(yōu)化設(shè)計(jì)
- 共享環(huán)形緩沖區(qū)管理
1)將實(shí)時(shí)日志先存入共享內(nèi)存中刹孔,避免大量的文件讀寫操作啡省。
2)引入環(huán)形緩沖區(qū)管理共享內(nèi)存,實(shí)現(xiàn)共享內(nèi)存的循環(huán)使用髓霞,避免創(chuàng)建大容量共享內(nèi)存卦睹。- 控制緩存大小,環(huán)形設(shè)計(jì)避免共享內(nèi)存寫入溢出方库。
-
便捷API接口設(shè)計(jì)
將日志接口封裝成宏函數(shù)结序,方便各模塊調(diào)用。
實(shí)現(xiàn)細(xì)節(jié)
-
日志框架組成
基于上述設(shè)計(jì)方案的概括纵潦,日志框架主要?jiǎng)澐譃槿齻€(gè)相互協(xié)作的部分徐鹤,在實(shí)際環(huán)境運(yùn)行示意圖如下:
注:箭頭指示日志數(shù)據(jù)的傳輸路徑及其流向
- 對(duì)外接口(API)
此組件專為應(yīng)用開發(fā)者設(shè)計(jì),嵌入于使用者進(jìn)程內(nèi)部邀层,承擔(dān)著將產(chǎn)生的日志實(shí)時(shí)推送至共享緩存區(qū)的責(zé)任返敬,通過(guò)簡(jiǎn)潔高效的API接口,確保開發(fā)者能夠輕松被济、靈活地在不同模塊中記錄不同級(jí)別的日志救赐。 - 日志核心管理(LogManagerSrv)
作為獨(dú)立運(yùn)行的日志管理進(jìn)程,核心組件專注于日志的存儲(chǔ)和文件管理任務(wù)只磷。它負(fù)責(zé)從共享緩存區(qū)讀取日志信息经磅,執(zhí)行日志文件的滾動(dòng)歸檔策略,以及對(duì)文件大小钮追、歷史日志數(shù)量進(jìn)行有效控制预厌,確保日志數(shù)據(jù)的持久化存儲(chǔ)和系統(tǒng)資源的高效利用。 - 調(diào)試輸出(終端Debug)
調(diào)試輸出進(jìn)程主要用于開發(fā)和調(diào)試場(chǎng)景元媚。在終端手動(dòng)執(zhí)行后轧叽,它實(shí)時(shí)監(jiān)聽共享緩存區(qū)中的日志數(shù)據(jù)苗沧,并將它們立即顯示在終端界面,便于開發(fā)人員實(shí)時(shí)觀測(cè)和分析程序運(yùn)行狀況炭晒。
-
對(duì)外接口(API)
這部分代碼相對(duì)簡(jiǎn)潔待逞,其核心在于對(duì)日志使用接口進(jìn)行了一層邏輯封裝,并進(jìn)一步通過(guò)宏定義的形式轉(zhuǎn)化為易于使用的宏接口网严,旨在為開發(fā)者提供更為便捷的日志調(diào)用方式识樱。
#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)
-
日志核心管理(Core)
此核心模塊承載了日志功能的核心實(shí)現(xiàn)邏輯,面對(duì)的主要挑戰(zhàn)集中在如下幾個(gè)關(guān)鍵技術(shù)環(huán)節(jié):
-
實(shí)時(shí)存儲(chǔ)至本地
負(fù)責(zé)實(shí)現(xiàn)日志數(shù)據(jù)從內(nèi)存到磁盤文件的實(shí)時(shí)高效寫入震束,確保日志信息的完整性和可靠性怜庸。
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
中,不停讀取環(huán)形共享內(nèi)存數(shù)據(jù)垢村,并寫入本地文件中割疾。在寫入過(guò)程中,發(fā)現(xiàn)長(zhǎng)度超過(guò)文件閾值嘉栓,則觸發(fā)日志文件回滾策略宏榕。回滾策略業(yè)務(wù)在RotateLogsIfNecessary
實(shí)現(xiàn)胸懈。
-
日志文件滾動(dòng)
設(shè)計(jì)并執(zhí)行一套有效的日志文件回滾機(jī)制担扑,當(dāng)單個(gè)日志文件達(dá)到預(yù)設(shè)大小時(shí)恰响,需自動(dòng)創(chuàng)建新的日志文件并遷移日志趣钱,同時(shí)維護(hù)好歷史日志文件的有序命名和存儲(chǔ)。
// 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;
}
在回滾過(guò)程中胚宦,涉及到歷史日志文件遷移首有,在UpdateSuffixOfAllFiles
實(shí)現(xiàn),篇幅有限暫不列舉枢劝。文末獲取代碼方法井联,需要自取。
-
共享環(huán)形緩存區(qū)管理
此部分實(shí)現(xiàn)了一個(gè)基于共享內(nèi)存的環(huán)形緩沖區(qū)數(shù)據(jù)結(jié)構(gòu)您旁,其核心功能與常規(guī)環(huán)形緩沖區(qū)保持一致烙常,但特別之處在于它位于可供多個(gè)進(jìn)程共同訪問(wèn)的共享內(nèi)存區(qū)域中。該模塊提供的接口服務(wù)于日志數(shù)據(jù)的高效暫存和交換鹤盒,確保在多進(jìn)程環(huán)境下蚕脏,日志信息能安全、順暢地從生成者進(jìn)程傳遞至消費(fèi)者進(jìn)程(如日志管理進(jìn)程)侦锯,并在此過(guò)程中有效地避免數(shù)據(jù)沖突和丟失驼鞭。
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;
};
-
調(diào)試輸出(Debug)
? 這部分實(shí)現(xiàn)主要是將存儲(chǔ)在緩存區(qū)的日志實(shí)時(shí)顯示在終端上,便于調(diào)試時(shí)尺碰,觀察實(shí)時(shí)打印挣棕∫氚考慮到終端通過(guò)執(zhí)行tail -f sparrow.log
能達(dá)到同樣效果,故暫不實(shí)現(xiàn)洛心。
測(cè)試
-
測(cè)試終端實(shí)時(shí)日志打印
本地觸發(fā)10ms一次的定時(shí)器固耘,觀察終端輸出的日志間隔是否為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
通過(guò)觀測(cè),證實(shí)了每隔10毫秒的時(shí)間間隔均能得到預(yù)期反饋词身,確認(rèn)該間隔時(shí)間設(shè)置準(zhǔn)確無(wú)誤玻驻。
-
測(cè)試日志回滾
為了方便驗(yàn)證,日志文件閾值暫設(shè)置為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
觀察結(jié)果顯示璧瞬,日志回滾機(jī)制正在正常運(yùn)作,表現(xiàn)為sparrow.log文件大小隨著新日志的實(shí)時(shí)寫入而動(dòng)態(tài)更新渐夸,始終保持存儲(chǔ)最新內(nèi)容嗤锉。
總結(jié)
- 對(duì)于項(xiàng)目來(lái)說(shuō),移植并使用像Android logcat這樣的成熟日志框架至關(guān)重要墓塌,本文的實(shí)現(xiàn)正是以此為參考依據(jù)瘟忱。
- 雖然日志系統(tǒng)看似與實(shí)際功能模塊無(wú)直接關(guān)聯(lián),但其重要性不可忽視苫幢;真正動(dòng)手實(shí)現(xiàn)一套完善的日志系統(tǒng)访诱,對(duì)個(gè)人技術(shù)水平的成長(zhǎng)有著極大的提升。
- 在實(shí)現(xiàn)日志系統(tǒng)框架時(shí)韩肝,發(fā)現(xiàn)了共享內(nèi)存應(yīng)用的一種獨(dú)特方式:通過(guò)與特定數(shù)據(jù)結(jié)構(gòu)的有機(jī)結(jié)合触菜,能實(shí)現(xiàn)數(shù)據(jù)在意外斷電情況下的記憶保留,這種方式頗具趣味性和實(shí)用性哀峻。