一个简单又高效的日志系统

http://www.vckbase.com/index.php/wv/1579.html

  • 一个性能高,使用简单的日志解决方案。本模块实现日志信息的批量写入文件,定时自动flush到文件中,写入文件的日志级别可动态调整,单个日志文件大小可配置,循环对日志文件写入,这样不会造成机器空间被日志文件耗尽。

一、程序日志是商品程序中必不可少的部分。在正式商用的程序中一般对于日志都会有一些类似的要求:

1.性能要求

2.运行时日志级别可调整

3.日志文件空间使用安全性问题

下面逐一针对上面的问题一起分析程序实现。

二、性能问题。

客户对程序的要求当然是越高越好。如果对于日志打印采用普通的方法,来一条日志就写一条日志到文件中,这样性能是很低的。因为程序不断的与磁盘进行交付,对系统的冲击很大,有可能会影响到正常的磁盘IO请求。

对于这个问题,一般的,都是采用批量写入的方法来解决。每写一条日志,并不是把日志立即写入文件中,而是先写到一个缓冲区中。当这个缓冲区达到一定的量时,再一次批量写入到文件中。见如下代码实现:

01. if (!strLog.IsEmpty())
02. {
03. m_strWriteStrInfo += GetCurTimeStr();
04. // 增加日志级别信息
05. if (enLevel == ENUM_LOG_LEVEL_ERROR)
06. {
07. m_strWriteStrInfo += _T("Error! ");
08. }
09. m_strWriteStrInfo += strLog;
10. m_strWriteStrInfo += _T("\r\n");
11. }
12.  
13. if ( bForce
14. || m_strWriteStrInfo.GetLength() > MAX_STR_LOG_INFO_LEN
15. || m_iWriteBinLogLen > MAX_BIN_LOG_INFO_LEN/10)
16. {
17. // write info,达到一定量时才提交到文件中
18. WriteLogToFile();
19. }

但这样会带来一个问题,如果日志量比较少,很可能要很久才能达到批量提交的量,这样就会造成程序写了日志,但是日志写入器还是把消息写在缓冲区里,文件中没有及时体现出来。我们可以采用定时又定时的办法来输出日志。程序对缓冲区内的日志消息定时强制刷新到文件中去。为了体现程序的使用简单性,把这个功能放在日志模块中实现了,从而调用日志的程序就不用考虑定时来刷新文件了。见如下程序实现:

01. CSuperLog::CSuperLog(void)
02. {
03. // 初始化临界区变量
04. InitializeCriticalSection(&m_csWriteLog);
05. // 启动信息
06. m_strWriteStrInfo = WELCOME_LOG_INFO;
07. // Create the Logger thread.
08. m_hThread = (HANDLE)_beginthreadex( NULL, 0, &LogProcStart, NULL, 0, &m_uiThreadID );
09.  
10. }
11. unsigned __stdcall CSuperLog::LogProcStart( void* pArguments )
12. {
13. int nCount = 1;
14. do
15. {
16. Sleep(300);
17. if (++nCount % 10 == 0 )
18. {
19. WriteLog(strTemp, ENUM_LOG_LEVEL_ERROR, true); // 每隔三秒写一次日志
20. }
21. while (m_bRun);
22. }

采有一个全局日志类变量,在构造函数中启动线程,线程每隔三秒去刷新一次文件。

二、日志级别可动态调整

程序的日志一般会进行日志分类,比如说日志级别一般会有调试日志,运行日志,错误日志等分类。在程序发布后运行时一般都会设置在运行日志级别,这时程序中的调试日志就不会被打印出来。如果程序运行中需要定位分析问题时,又需要把日志级别调低,把一些调试信息打印出来。见如下程序实现:

1. int CSuperLog::WriteLog(CString &strLog,enLogInfoLevel enLevel/* = ENUM_LOG_LEVEL_RUN*/bool bForce /*= false*/)
2. {
3. if (enLevel < m_iLogLevel)
4. {
5. return -1;
6. }
7. 。。。
8. }

对于调整日志级别,我没有把实现放在调用者去设置。而是把这个日志级别信息存放在共享内存中,如果要调整日志级别,则需要一个小工具去改那一个共享内存。实际上在整个设计中我一直想把日志系统设计得更独立一点,尽量不和外部调用程序有更多牵连。

01. //创建共享文件。
02. m_hMapLogFile = CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE,0,1024, _T("SuperLogShareMem"));
03. if (m_hMapLogFile != NULL)
04. {
05. //拷贝数据到共享文件里。
06. m_psMapAddr = (LPTSTR)MapViewOfFile(m_hMapLogFile,FILE_MAP_ALL_ACCESS, 0,0,0);
07. if (m_psMapAddr != NULL)
08.  {
09. _tcscpy_s(m_psMapAddr, 1024, g_pszLogLevel[m_iLogLevel]);                 
10. FlushViewOfFile(m_psMapAddr, _tcslen(g_pszLogLevel[m_iLogLevel]));
11. WriteLog(_T("设置默认日志级别到共享内存中成功。"), ENUM_LOG_LEVEL_RUN);
12.  }
13. }

在线程中定时去检查这个日志级别有否有变化,有变化则立即调整当前的级别设置。

三、日志文件空间使用安全性问题

对于长期运行的商品程序来说,一定会要考虑到文件系统安全性的问题。如果程序不停的打印垃圾信息,用不了多太,日志文件可能会变得很大。如果把用户空间占满了,那有可能会引起更严重的问题。所以一定要限制日志文件的大小。程序中考虑到日志文件更换,采用了三个文件轮换写,写满一个时,更换一个文件再写,不用考虑到日志文件会耗尽磁盘。

01. CSuperLog::enLogStatus CSuperLog::OpenLogFile(void)
02. {
03. EnterCriticalSection(&m_csWriteLog);
04. for (int iRunCount = 0; iRunCount < MAX_LOG_FILE_COUNT; iRunCount++)
05. {
06. if (m_pFile == NULL)
07. {
08. m_pFile = new CStdioFile;
09. if (m_pFile == NULL)
10. {
11. LeaveCriticalSection(&m_csWriteLog);
12. return m_enStatus = ENUM_LOG_INVALID;
13. }
14.  
15.   BOOL bRet = m_pFile->Open(
16. g_pszLogFileName[(m_iCurLogFileSeq++)%MAX_LOG_FILE_COUNT],
17. CFile::modeWrite | CFile::modeCreate | CFile::typeBinary | CFile::shareDenyNone | CFile::modeNoTruncate);
18. if (bRet)
19. {
20. WriteUnicodeHeadToFile(m_pFile);
21. }
22. else
23. {
24.     delete m_pFile;
25. m_pFile = NULL;
26. LeaveCriticalSection(&m_csWriteLog);
27. return m_enStatus = ENUM_LOG_INVALID;
28. }
29. }
30.  
31. if (m_pFile->GetLength() > MAX_LOG_FILE_LEN)
32. {
33. m_pFile->Close();
34. BOOL bRet = FALSE;
35. // 上一个文件是最大的那个文件或是写过一遍了的。
36. if (m_iCurLogFileSeq >= MAX_LOG_FILE_COUNT)
37. {
38. // 所有文件都是写满了,则强制从第一个文件开始写,同时先清空文件
39. bRet = m_pFile->Open(
40. g_pszLogFileName[(m_iCurLogFileSeq++)%MAX_LOG_FILE_COUNT],
41. CFile::modeWrite | CFile::modeCreate | CFile::typeBinary | CFile::shareDenyNone);
42. }
43. else
44. {
45. // 打开第二个文件,再检查是否过了最大值
46. bRet = m_pFile->Open(
47. g_pszLogFileName[(m_iCurLogFileSeq++)%MAX_LOG_FILE_COUNT],
48. CFile::modeWrite | CFile::modeCreate | CFile::typeBinary | CFile::shareDenyNone | CFile::modeNoTruncate);
49. }
50.  
51. if (bRet)
52. {
53. WriteUnicodeHeadToFile(m_pFile);
54. }
55. else
56. {
57. delete m_pFile;
58. m_pFile = NULL;
59. LeaveCriticalSection(&m_csWriteLog);
60. return m_enStatus = ENUM_LOG_INVALID;
61. }
62. }
63. else
64. {
65. break;
66. }
67. }
68.  
69. m_pFile->SeekToEnd();
70. LeaveCriticalSection(&m_csWriteLog);
71. return m_enStatus = ENUM_LOG_RUN;
72. }

四、其它部分

程序中使用了CStdioFile来处理文件写入,在实现中如果使用text模式打开文件写入,会发现无法写入中文字符的问题。查找了一些资料,发现是字符编码的问题。有一种解决方法是用二进制方式打开,在文件的开头处写入unicode头部标识。

01. int CSuperLog::WriteUnicodeHeadToFile(CFile * pFile)
02. {
03. if (pFile == NULL)
04. {
05. return -1;
06. }
07. try
08. {
09. if (pFile->GetLength() == 0)
10. {
11. m_pFile->Write("\377\376", 2); // 就是FF FE
12. if (m_enStatus == ENUM_LOG_RUN)
13. {
14. m_pFile->WriteString(WELCOME_LOG_INFO);
15. }
16. m_pFile->Flush();
17. }
18. }
19. catch (...)
20. {
21. return -1;
22. }
23. return 0;
24. }

为了保证调用者尽可能的简单,程序把类接口都实现为静态方法,调用都可以直接使用。

1. #define   WRITE_LOG           CSuperLog::WriteLog
2. #define   LOG_LEVEL_DEBUG     CSuperLog::ENUM_LOG_LEVEL_DEBUG
3. #define   LOG_LEVEL_RUN       CSuperLog::ENUM_LOG_LEVEL_RUN
4. #define   LOG_LEVEL_ERROR     CSuperLog::ENUM_LOG_LEVEL_ERROR

调用者使用如下:

1. // 包含头文件
2. #include "common/SuperLog.h"
3. WRITE_LOG(_T("短信发送失败,重试一次。"), LOG_LEVEL_ERROR);

日志线程是在全局变量的析构函数中通知退出的。这时有可能还要会打印日志。为了保证性能,在取得当前时间的字符串时使用了两个静态局部变量

1. CString& CSuperLog::GetCurTimeStr()
2. {
3. static CTime g_tmCurTime;
4. g_tmCurTime = CTime::GetCurrentTime();// time(NULL);
5. CString g_strTime;
6. g_strTime = g_tmCurTime.Format(_T("%Y-%m-%d %H:%M:%S "));
7. return g_strTime;
8. }

在使用中发现,每次退出时,如果还有日志打印,程序总会异常。后来分析发现,静态全局变量每次都会先于全局变量析构,导致strTime析构后无效访问。只好把这个变量变成了全局变量规避。

1. CString& CSuperLog::GetCurTimeStr()
2. {
3. g_tmCurTime = CTime::GetCurrentTime();// time(NULL);
4. g_strTime = g_tmCurTime.Format(_T("%Y-%m-%d %H:%M:%S "));
5. return g_strTime;
6. }

四、结束语

程序实现仓促,基本的功能都调试完毕,但目前还有带参数的写日志接口没有写,二进制内容日志信息的接口也没有实现。后续作者会及时完成。有兴趣的同不学可以发邮件联系。Email:y63508@vip.qq.com


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值