日志系统的设计 为什么需要日志 实际的软件项目产出都有一个流程,即先开发、测试,再发布生产,由于人的因素,既然是软件产品就不可能百分百没有 bug 或者逻辑错误,对于已经发布到生产的项目,一旦某个时刻产生非预期的结果,我们就需要去定位和排查问题。但是一般正式的生产环境的服务器或者产品是不允许开发人员通过附加调试器去排查问题的,主要有如下可能原因:
在很多互联网企业,开发部门、测试部分和产品运维部门是分工明确的,软件产品一旦发布到生产环境以后,将全部交由运维部门人员去管理和维护,而原来开发此产品的开发人员不再拥有相关的操作程序的权限。 对于已经上了生产环境的服务,其数据和程序稳定性是公司的核心产值所在,一般不敢或不允许被开发人员随意调试或者操作,以免造成损失。 发布到生产环境的服务,一般为了让程序执行效率更高、文件体积更小,都是去掉调试符号后的版本,不方便也不利于调试。 既然我们无法通过调试器去调试,这个时候为了跟踪和回忆当时的程序行为进而定位问题,我们就需要日志系统。
退一步说,即使在开发或者测试环境,我们可以把程序附加到调试器上去调试,但是对于一些特定的程序行为,我们无法通过设置断点,让程序在某个时刻暂停下来进行调试。例如,对于某些网络通信功能,如果暂停时间过长(相对于某些程序逻辑来说),通信的对端可能由于彼端没有在规定时间内响应而断开连接,导致程序逻辑无法进入我们想要的执行流中去;再例如,对于一些高频操作(如心跳包、定时器、界面绘制下的某些高频重复行为),可能在少量次数下无法触发我们想要的行为,而通过断点的暂停方式,我们不得不重复操作几十次、上百次甚至更多,这样排查问题效率是非常低下的。对于这类操作,我们可以通过打印日志,将当时的程序行为上下文现场记录下来,然后从日志系统中找到某次不正常的行为的上下文信息。这也是日志的另外一个作用。
本文将从技术和业务上两个方面来介绍日志系统相关的设计与开发,所谓技术上,就是如何从程序开发的角度设计一款功能强大、性能优越、使用方便的日志系统;而业务上,是指我们在使用日志系统时,应该去记录哪些行为和数据,既简洁、不啰嗦,又方便需要时快速准确地定位到问题。
日志系统的技术上的实现 日志的最初的原型即将程序运行的状态打印出来,对于 C/C++ 这门语言来说,即可以利用 printf、std::cout 等控制台输出函数,将日志信息输出到控制台,这类简单的情形我们不在此过多赘述。
对于商业项目,为了方便排查问题,我们一般不将日志写到控制台,而是输出到文件或者数据库系统。不管哪一种,其思路基本上一致,我们这里以写文件为例来详细介绍。
同步写日志 所谓同步写日志,指的是在输出日志的地方,将日志即时写入到文件中去。根据笔者的经验,这种设计广泛地用于相当多的的客户端软件。笔者曾从事过数年的客户端开发(包括 PC、安卓软件),设计过一些功能复杂的金融客户端产品,在这些系统中采用的就是这种同步写日志的方式。之所以使用这种方式其主要原因就是设计简单,而又不会影响用户使用体验。说到这里读者可能有这样一个疑问:一般的客户端软件,一般存在界面,而界面部分所属的逻辑就是程序的主线程,如果采取这种同步写日志的方式,当写日志时,写文件是磁盘 IO 操作,相比较程序其他部分是 CPU 操作,前者要慢很多,这样势必造成CPU等待,进而导致主线程“卡”在写文件处,进而造成界面卡顿,从而导致用户使用软件的体验不好。读者的这种顾虑确实是存在的。但是,很多时候我们不用担心这种问题,主要有两个原因:
对于客户端程序,即使在主线程(UI 线程)中同步写文件,其单次或者几次磁盘操作累加时间,与人(用户)的可感知时间相比,也是非常小的,也就是说用户根本感觉不到这种同步写文件造成的延迟。当然,这里也给您一个提醒就是,如果在 UI 线程里面写日志,尤其是在一些高频操作中(如 Windows 的界面绘制消息 WM_PAINT 处理逻辑中),一定要控制写日志的长度和次数,否则就会因频繁写文件或一次写入数据过大而对界面造成卡顿。 客户端程序除了 UI 线程,还有其他与界面无关的工作线程,在这些线程中直接写文件,一般不会对用户的体验产生什么影响。 说了这么多,我们给出一个具体的例子。
日志类的 .h 文件
/** *@desc: IULog.h *@author: zhangyl *@date: 2014.12.25 */ #ifndef __LOG_H__ #define __LOG_H__ enum LOG_LEVEL { LOG_LEVEL_INFO, LOG_LEVEL_WARNING, LOG_LEVEL_ERROR }; //注意:如果打印的日志信息中有中文,则格式化字符串要用_T()宏包裹起来, #define LOG_INFO(...) CIULog::Log(LOG_LEVEL_INFO, __FUNCSIG__,__LINE__, __VA_ARGS__) #define LOG_WARNING(...) CIULog::Log(LOG_LEVEL_WARNING, __FUNCSIG__, __LINE__,__VA_ARGS__) #define LOG_ERROR(...) CIULog::Log(LOG_LEVEL_ERROR, __FUNCSIG__,__LINE__, __VA_ARGS__) class CIULog { public: static bool Init(bool bToFile, bool bTruncateLongLog, PCTSTR pszLogFileName); static void Uninit(); static void SetLevel(LOG_LEVEL nLevel); //不输出线程ID号和所在函数签名、行号 static bool Log(long nLevel, PCTSTR pszFmt, ...); //输出线程ID号和所在函数签名、行号 static bool Log(long nLevel, PCSTR pszFunctionSig, int nLineNo, PCTSTR pszFmt, ...); //注意:pszFunctionSig参数为Ansic版本 static bool Log(long nLevel, PCSTR pszFunctionSig, int nLineNo, PCSTR pszFmt, ...); private: CIULog() = delete; ~CIULog() = delete; CIULog(const CIULog& rhs) = delete; CIULog& operator=(const CIULog& rhs) = delete; static void GetTime(char* pszTime, int nTimeStrLength); private: static bool m_bToFile; //日志写入文件还是写到控制台 static HANDLE m_hLogFile; static bool m_bTruncateLongLog; //长日志是否截断 static LOG_LEVEL m_nLogLevel; //日志级别 }; #endif // !__LOG_H__ 日志的 cpp 文件
...