一、程序日志是商品程序中必不可少的部分。在正式商用的程序中一般對於日志都會有一些類似的要求:
性能要求
運行時日志級別可調整
日志文件空間使用安全性問題
下面逐一針對上面的問題一起分析程序實現。
二、性能問題。
客戶對程序的要求當然是越高越好。如果對於日志打印采用普通的方法,來一條日志就寫一條日志到文件中,這樣性能是很低的。因為程序不斷的與磁盤進行交付,對系統的沖擊很大,有可能會影響到正常的磁盤IO請求。
對於這個問題,一般的,都是采用批量寫入的方法來解決。每寫一條日志,並不是把日志立即寫入文件中,而是先寫到一個緩沖區中。當這個緩沖區達到一定的量時,再一次批量寫入到文件中。見如下代碼實現:
if (!strLog.IsEmpty())
{
m_strWriteStrInfo += GetCurTimeStr();
// 增加日志級別信息
if (enLevel == ENUM_LOG_LEVEL_ERROR)
{
m_strWriteStrInfo += _T("Error! ");
}
m_strWriteStrInfo += strLog;
m_strWriteStrInfo += _T("\r\n");
}
if ( bForce
|| m_strWriteStrInfo.GetLength() > MAX_STR_LOG_INFO_LEN
|| m_iWriteBinLogLen > MAX_BIN_LOG_INFO_LEN/10)
{
// write info,達到一定量時才提交到文件中
WriteLogToFile();
}
但這樣會帶來一個問題,如果日志量比較少,很可能要很久才能達到批量提交的量,這樣就會造成程序寫了日志,但是日志寫入器還是把消息寫在緩沖區裡,文件中沒有及時體現出來。我們可以采用定時又定時的辦法來輸出日志。程序對緩沖區內的日志消息定時強制刷新到文件中去。為了體現程序的使用簡單性,把這個功能放在日志模塊中實現了,從而調用日志的程序就不用考慮定時來刷新文件了。見如下程序實現:
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:[email protected]
本文配套源碼