一个简单又高效的日志系统
摘要:本文給出一個性能高,使用簡單的日志解決方案。本模塊實現日志信息的批量寫入文件,定時自動flush到文件中,寫入文件的日志級別可動態調整,單個日志文件大小可配置,循環對日志文件寫入,這樣不會造成機器空間被日志文件耗盡。
關鍵字:日志 性能 日志級別
一、程序日志是商品程序中必不可少的部分。在正式商用的程序中一般對于日志都會有一些類似的要求:
下面逐一針對上面的問題一起分析程序實現。
二、性能問題。
客戶對程序的要求當然是越高越好。如果對于日志打印采用普通的方法,來一條日志就寫一條日志到文件中,這樣性能是很低的。因為程序不斷的與磁盤進行交付,對系統的沖擊很大,有可能會影響到正常的磁盤IO請求。
對于這個問題,一般的,都是采用批量寫入的方法來解決。每寫一條日志,并不是把日志立即寫入文件中,而是先寫到一個緩沖區中。當這個緩沖區達到一定的量時,再一次批量寫入到文件中。見如下代碼實現:
但這樣會帶來一個問題,如果日志量比較少,很可能要很久才能達到批量提交的量,這樣就會造成程序寫了日志,但是日志寫入器還是把消息寫在緩沖區里,文件中沒有及時體現出來。我們可以采用定時又定時的辦法來輸出日志。程序對緩沖區內的日志消息定時強制刷新到文件中去。為了體現程序的使用簡單性,把這個功能放在日志模塊中實現了,從而調用日志的程序就不用考慮定時來刷新文件了。見如下程序實現:
CSuperLog::CSuperLog(void){ // 初始化臨界區變量 InitializeCriticalSection(&m_csWriteLog); // 啟動信息 m_strWriteStrInfo = WELCOME_LOG_INFO; // Create the Logger thread. m_hThread = (HANDLE)_beginthreadex( NULL, 0, &LogProcStart, NULL, 0, &m_uiThreadID );}unsigned __stdcall CSuperLog::LogProcStart( void* pArguments ){ int nCount = 1; do { Sleep(300); if (++nCount % 10 == 0 ) { WriteLog(strTemp, ENUM_LOG_LEVEL_ERROR, true); // 每隔三秒寫一次日志 } } while (m_bRun);}采有一個全局日志類變量,在構造函數中啟動線程,線程每隔三秒去刷新一次文件。
二、日志級別可動態調整
程序的日志一般會進行日志分類,比如說日志級別一般會有調試日志,運行日志,錯誤日志等分類。在程序發布后運行時一般都會設置在運行日志級別,這時程序中的調試日志就不會被打印出來。如果程序運行中需要定位分析問題時,又需要把日志級別調低,把一些調試信息打印出來。見如下程序實現:
int CSuperLog::WriteLog(CString &strLog,enLogInfoLevel enLevel/* = ENUM_LOG_LEVEL_RUN*/, bool bForce /*= false*/){ if (enLevel < m_iLogLevel) { return -1; } 。。。}對于調整日志級別,我沒有把實現放在調用者去設置。而是把這個日志級別信息存放在共享內存中,如果要調整日志級別,則需要一個小工具去改那一個共享內存。實際上在整個設計中我一直想把日志系統設計得更獨立一點,盡量不和外部調用程序有更多牽連。
//創建共享文件。 m_hMapLogFile = CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE,0,1024, _T("SuperLogShareMem")); if (m_hMapLogFile != NULL) { //拷貝數據到共享文件里。 m_psMapAddr = (LPTSTR)MapViewOfFile(m_hMapLogFile,FILE_MAP_ALL_ACCESS, 0,0,0); if (m_psMapAddr != NULL) { _tcscpy_s(m_psMapAddr, 1024, g_pszLogLevel[m_iLogLevel]); FlushViewOfFile(m_psMapAddr, _tcslen(g_pszLogLevel[m_iLogLevel])); WriteLog(_T("設置默認日志級別到共享內存中成功。"), ENUM_LOG_LEVEL_RUN); } }在線程中定時去檢查這個日志級別有否有變化,有變化則立即調整當前的級別設置。
三、日志文件空間使用安全性問題
對于長期運行的商品程序來說,一定會要考慮到文件系統安全性的問題。如果程序不停的打印垃圾信息,用不了多太,日志文件可能會變得很大。如果把用戶空間占滿了,那有可能會引起更嚴重的問題。所以一定要限制日志文件的大小。程序中考慮到日志文件更換,采用了三個文件輪換寫,寫滿一個時,更換一個文件再寫,不用考慮到日志文件會耗盡磁盤。
CSuperLog::enLogStatus CSuperLog::OpenLogFile(void){ EnterCriticalSection(&m_csWriteLog); for (int iRunCount = 0; iRunCount < MAX_LOG_FILE_COUNT; iRunCount++) { if (m_pFile == NULL) { m_pFile = new CStdioFile; if (m_pFile == NULL) { LeaveCriticalSection(&m_csWriteLog); return m_enStatus = ENUM_LOG_INVALID; } BOOL bRet = m_pFile->Open( g_pszLogFileName[(m_iCurLogFileSeq++)%MAX_LOG_FILE_COUNT], CFile::modeWrite | CFile::modeCreate | CFile::typeBinary | CFile::shareDenyNone | CFile::modeNoTruncate); if (bRet) { WriteUnicodeHeadToFile(m_pFile); } else { delete m_pFile; m_pFile = NULL; LeaveCriticalSection(&m_csWriteLog); return m_enStatus = ENUM_LOG_INVALID; } } if (m_pFile->GetLength() > MAX_LOG_FILE_LEN) { m_pFile->Close(); BOOL bRet = FALSE; // 上一個文件是最大的那個文件或是寫過一遍了的。 if (m_iCurLogFileSeq >= MAX_LOG_FILE_COUNT) { // 所有文件都是寫滿了,則強制從第一個文件開始寫,同時先清空文件 bRet = m_pFile->Open( g_pszLogFileName[(m_iCurLogFileSeq++)%MAX_LOG_FILE_COUNT], CFile::modeWrite | CFile::modeCreate | CFile::typeBinary | CFile::shareDenyNone); } else { // 打開第二個文件,再檢查是否過了最大值 bRet = m_pFile->Open( g_pszLogFileName[(m_iCurLogFileSeq++)%MAX_LOG_FILE_COUNT], CFile::modeWrite | CFile::modeCreate | CFile::typeBinary | CFile::shareDenyNone | CFile::modeNoTruncate); } if (bRet) { WriteUnicodeHeadToFile(m_pFile); } else { delete m_pFile; m_pFile = NULL; LeaveCriticalSection(&m_csWriteLog); return m_enStatus = ENUM_LOG_INVALID; } } else { break; } } m_pFile->SeekToEnd(); LeaveCriticalSection(&m_csWriteLog); return m_enStatus = ENUM_LOG_RUN;}四、其它部分
程序中使用了CStdioFile來處理文件寫入,在實現中如果使用text模式打開文件寫入,會發現無法寫入中文字符的問題。查找了一些資料,發現是字符編碼的問題。有一種解決方法是用二進制方式打開,在文件的開頭處寫入unicode頭部標識。
int CSuperLog::WriteUnicodeHeadToFile(CFile * pFile){ if (pFile == NULL) { return -1; } try { if (pFile->GetLength() == 0) { m_pFile->Write("\377\376", 2); // 就是FF FE if (m_enStatus == ENUM_LOG_RUN) { m_pFile->WriteString(WELCOME_LOG_INFO); } m_pFile->Flush(); } } catch (...) { return -1; } return 0;}為了保證調用者盡可能的簡單,程序把類接口都實現為靜態方法,調用都可以直接使用。
#define WRITE_LOG CSuperLog::WriteLog#define LOG_LEVEL_DEBUG CSuperLog::ENUM_LOG_LEVEL_DEBUG#define LOG_LEVEL_RUN CSuperLog::ENUM_LOG_LEVEL_RUN#define LOG_LEVEL_ERROR CSuperLog::ENUM_LOG_LEVEL_ERROR調用者使用如下:
// 包含頭文件#include "common/SuperLog.h"WRITE_LOG(_T("短信發送失敗,重試一次。"), LOG_LEVEL_ERROR);日志線程是在全局變量的析構函數中通知退出的。這時有可能還要會打印日志。為了保證性能,在取得當前時間的字符串時使用了兩個靜態局部變量
CString& CSuperLog::GetCurTimeStr(){ static CTime g_tmCurTime; g_tmCurTime = CTime::GetCurrentTime();// time(NULL); CString g_strTime; g_strTime = g_tmCurTime.Format(_T("%Y-%m-%d %H:%M:%S ")); return g_strTime;}在使用中發現,每次退出時,如果還有日志打印,程序總會異常。后來分析發現,靜態全局變量每次都會先于全局變量析構,導致strTime析構后無效訪問。只好把這個變量變成了全局變量規避。
CString& CSuperLog::GetCurTimeStr(){ g_tmCurTime = CTime::GetCurrentTime();// time(NULL); g_strTime = g_tmCurTime.Format(_T("%Y-%m-%d %H:%M:%S ")); return g_strTime;}四、結束語
程序實現倉促,基本的功能都調試完畢,但目前還有帶參數的寫日志接口沒有寫,二進制內容日志信息的接口也沒有實現。后續作者會及時完成。有興趣的同不學可以發郵件聯系。Email:y63508@vip.qq.com
轉載于:https://www.cnblogs.com/cccc123/archive/2010/04/17/1714146.html
總結
以上是生活随笔為你收集整理的一个简单又高效的日志系统的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: AE 动效工作流技巧 —— 减少 Bod
- 下一篇: 【解决方案】HIKSDK/大华SDK/E