一个简单又高效的日志系统
摘要:本文给出一个性能高,使用简单的日志解决方案。本模块实现日志信息的批量写入文件,定时自动flush到文件中,写入文件的日志级别可动态调整,单个日志文件大小可配置,循环对日志文件写入,这样不会造成机器空间被日志文件耗尽。
关键字:日志 性能 日志级别
一、程序日志是商品程序中必不可少的部分。在正式商用的程序中一般对于日志都会有一些类似的要求:
- 性能要求
- 运行时日志级别可调整
- 日志文件空间使用安全性问题
下面逐一针对上面的问题一起分析程序实现。
二、性能问题。
客户对程序的要求当然是越高越好。如果对于日志打印采用普通的方法,来一条日志就写一条日志到文件中,这样性能是很低的。因为程序不断的与磁盘进行交付,对系统的冲击很大,有可能会影响到正常的磁盘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:y63508@vip.qq.com
转载于:https://www.cnblogs.com/cccc123/archive/2010/04/17/1714146.html
一个简单又高效的日志系统相关推荐
- clistctrl控件最后插入在后面_用图表控件做一个简单的员工信息查询系统
前几天在上课的时候有同学说在做人员的信息查询的时候,经常的要去做查找搜索很麻烦,能不能做一个简单的人员信息查询系统,只需要选择人员的编号就可以查询到这个员工的信息.其实要实现这个同学的需求在EXCEL ...
- 设计一个简单分页存储管理系统_【系统架构】如何设计一个简单灵活的收银系统?看这里!(1)...
在电商项目中,收银系统是一个不可或缺的功能,因为你不仅要通过它来进行收款.退款,而且也要通过它进行财务的对账.报税等.因此,如何设计一个简单灵活的收银系统,对于开发电商项目来说非常重要. 那如何设计一 ...
- 安卓 spinner下拉框 做模糊查询_用图表控件做一个简单的员工信息查询系统
前几天在上课的时候有同学说在做人员的信息查询的时候,经常的要去做查找搜索很麻烦,能不能做一个简单的人员信息查询系统,只需要选择人员的编号就可以查询到这个员工的信息.其实要实现这个同学的需求在EXCEL ...
- 一个简单好用的日志框架NLog
之前我介绍过如何使用log4net来记录日志,但最近喜欢上了另一个简单好用的日志框架NLog. 关于NLog和log4net的比较这里就不多讨论了,感兴趣的朋友可以参看.NET日志工具介绍和log4n ...
- java计算机毕业设计vue开发一个简单音乐播放器MyBatis+系统+LW文档+源码+调试部署
java计算机毕业设计vue开发一个简单音乐播放器MyBatis+系统+LW文档+源码+调试部署 java计算机毕业设计vue开发一个简单音乐播放器MyBatis+系统+LW文档+源码+调试部署 本源 ...
- 支付退款流程设计_【系统架构】如何设计一个简单灵活的收银系统?看这里!(1)...
在电商项目中,收银系统是一个不可或缺的功能,因为你不仅要通过它来进行收款.退款,而且也要通过它进行财务的对账.报税等.因此,如何设计一个简单灵活的收银系统,对于开发电商项目来说非常重要. 那如何设计一 ...
- 自己动手搭网站(六):javaweb搭建一个简单的个人博客系统
目录 前言 一.一点建网站的背景知识 二.个人博客系统介绍 1.核心功能和数据库 2.前端页面 3.后端 servlet service层 dao层 配置文件 参考资料 前言 这篇博主会介绍下我用ja ...
- 虚拟服务器能挂跟单系统,格物策略:跟单控是一个简单、高效的科技跟单系统...
技术是为了市场服务的,社会的层次越高,就越会往专业化发展,也就会越依赖于技术的进步.互联网金融的快速发展,众多群体都热衷于基于互联网技术的交易软件.而交易软件只是一种工具,就意味着如果你不专业,用什么 ...
- c语言点餐系统感悟,一个简单C语言点餐系统的学习心得
首先附上源代码: 1 #include 2 3 voidmain_munu(){4 //打印主菜单 5 char a[6][20]={"菜单","凉菜",&qu ...
最新文章
- ARKit从入门到精通-ARKit工作原理及流程介绍
- 获取文件最后修改时间的VC代码
- 007-迅雷定时重启AutoHotkey脚本-20190411
- 杨超越第一,Python第二
- SAP MM MIRO的时候,发票价格与物料主数据的价格(移动平均价)有差异的处理
- html5网站测试工具,基于HTML5网页的前端技术测试技巧
- 我对孩子学习编程的一点思考
- Android Animation实现元素在屏幕上按照指定轨迹运动,以及出现NullPointerException的解决方案
- mahout学习笔记4
- Semaphore及其用法
- 甘蔗是怎么变成白糖的?
- mysql 代码结构_MySQL代码执行结构
- mysql 写入性能_MySQL存储写入性能严重抖动分析
- ora-28500 ora-02063 mysql_ORA-01017/ORA-02063 DbLink建立错误问题分析及解决
- Windows部署WSUS补丁服务器
- 软考中级网络工程师知识点笔记
- 通过计算机英语怎么说,通过英文怎么说
- 如何在CentOS 8上安装Docker
- ue4-UMG和HUD绘制UI
- 【牛客网OJ题】计算糖果