正如一位大师说过的“即使再简单的程序都有bug!”,所以当程序出现错误的时候能准确的定位问题则成了保证软件质量的关键,而如何才能准确的定位问题呢?这就需要程序出错时尽可能的吐出更多的消息,最好的情况就是把问题定位到具体的文件和语句以及当时的堆栈信息……,这样就可以帮助程序员快速的定位问题,这便是软件可测试性的最终目的。
我们平时会使用到一些测试手段,如在代码中使用ASSERT,在屏幕或者控件里打印一些程序的执行过程,还有就是利用debug去直接跟踪程序的语句等,但是以上这些简单的测试手段对于那些程序中隐藏比较深的Bug就显得有点溯手无策了,特别是对一些服务器一级的程序,他们都需要长时间的运行,有时候出了错误并允许马上停下来,这就需要程序必须把错误信息保存下来待以后再去分析它,windows系统内部就有很多的日志,这些日志不仅可以供开发人员定位问题,也可以帮助用户更好的使用系统。其实所有的程序对可测试性都有着一些相同的要求,于是我们可以把这些需求提取出来抽象成一个单独的系统(类),这样就可以为所有的程序都提供统一的测试接口了。
2. 设计日志类的一些要点
1) 能保存成独立的文件,就像上面分析的那样,一些程序要执行很久,必须要把程序执行的信息保存下来,同时程序关闭后测试的信息不能随着程序一起消失调,所以没有比把这些信息保存在磁盘更好的方法了。
2) 打印日志的方法必须是可重入的,这一点不许要更多的解释,试问现在哪款软件不是多线程的,为了让线程之间打印的信息不互相干扰,我们的打印方法必须支持多线程。
3) 对打印要求设定级别,级别的粒度因需要而定, 一般都要包括,错误信息、告警信息和一般信息三类,这样用户就可以根据需要,通过日志的配置文件来选择需要打印的信息,而不是一股脑的把所有的信息都打印出来,重点不突出反而不利于问题的定位。
4) 打印接口要设计的灵活,可以让用户自由的去打印任何信息。有些情况下打印的同时也要把当时的堆栈信息打印出来,这些信息可能是字符串、整形或浮点型等, 所以要求打印的接口使用类似printf的方法去实现。
5) 打印的内容要采用统一的格式,一般包括:
[时间][等级][文件/函数][信息内容]
3. 简单的代码实现
通过以上的介绍,下面我们来通过代码实现它, 这只是个简单的实现,甚至代码都不全,意在说明问题,读者可以在这基础上自行去完善。
/* MyLog.h 头文件*/
#ifndef MYLOG_H
#define MYLOG_H
/* 日志报警等级 */
typedef enum
{
LEVEL_NONE = 0, /* 不输出日志 */
LEVEL_ERROR,
LEVEL_WARNING,
LEVEL_INFO,
LEVEL_BUTT
} LEVEL_TYPE;
extern const char LOG_CONFIG_FILE[]; // 日志文件的配置文件名
const int MAX_LOG_LENGTH = 1024;
const int MAX_FILE_NAME_LENGTH = 255;
/× 不同等级对应的字符串 ×/
const char LOG_ERROR[][10] = {
"",
"Error",
"Warning",
"Info"
};
/* 配置文件中的配置项 */
const char LOG_CONFIG_SECTION[] = "LOG";
const char LOG_CONFIG_LEVELTYPE[] = "LEVELTYPE"; // 打印日志的等级
const char LOG_CONFIG_FILENAME[] = "FILENAME"; // 日志文件名
/* 简化调用日志接口的宏 */
#define WRITE_LOG CLog::GetInstance()->WriteLog
Class CMyLog
{
public:
CMyLog* GetInstance(); // 获得日志类的实例
void WriteLog(LEVEL_TYPE level, const char* szTitle, const char* format,...); // 日志打印接口,参数可变
protected:
CMyLog();
~CMyLog();
virtual void PrintLog(const char* szMsg, LEVEL_TYPE iLevel); // 打印输出信息
void GetDateString(char* szDate); // 获得日期, 实现省略
void GetTimeString(char* szTime); // 获得时间,实现省略
private:
LEVEL_TYPE m_iLevel;
char m_szFileName[MAX_FILE_NAME_LENGTH]; // 输出日志文件名
CRITICAL_SECTION m_cs; // 函数的重入控制
FILE *m_file; // 日志文件
}
#endif
/* MyLog.cpp 原文件*/
#include "MyLog.h"
#include "string.h"
#include "stdio.h"
CMyLog::CMyLog()
{
/* 初始化变量 */
m_iLevel = LEVEL_NONE;
InitializeCriticalSection(&m_cs);
/* 读取配置文件 */
char szConfigFile[MAX_FILE_NAME_LENGTH] = {0};
strcpy(szConfigFile, LOG_CONFIG_FILE);
m_iLevel = GetPrivateProfileInt(LOG_CONFIG_SECTION,
LOG_CONFIG_LEVELTYPE,
m_iLevel,
szConfigFile); // 读取.ini文件中的LEVELTYPE项,具体请参考MSDN
char szFileName[MAX_FILE_NAME_LENGTH + 1];
GetPrivateProfileString( LOG_CONFIG_SECTION,
LOG_CONFIG_FILENAME,
"",
szFileName,
MAX_FILE_NAME_LENGTH,
szConfigFile); // 读取.ini文件中的FILENAME项,具体请参考MSDN
strcpy(m_szFileName, szFileName);
strcat(m_szFileName, LOG_FILE_NAME);
}
CMyLog::~CMyLog()
{
if (NULL != m_file)
{
fclose(m_file);
m_file = NULL;
}
DeleteCriticalSection(&m_cs);
}
CMyLog* CMyLog::GetInstance()
{
static CMyLog log;
return &log;
}
void CMyLog::WriteLog(LEVEL_TYPE level, const char* szTitle, const char* format,...)
{
/* 输入检查 */
if (NULL == format)
{
return;
}
/* 等级控制 */
if (level > m_iLevel)
{
return;
}
/* 组成呢给输出字符串 */
char szMsg[MAX_LOG_LENGTH + 1];
/* 日期时间 */
strcpy(szMsg,"[");
GetTimeString(&szMsg[strlen(szMsg)]);
strcat(szMsg, "]");
/* 等级 */
strcat(szMsg, "[");
if (level >= LEVEL_BUTT || level <= LEVEL_NONE)
{
return;
}
else
{
strcat(szMsg, LOG_ERROR[level]);
}
strcat(szMsg, "]");
/* Title */
if (NULL != szTitle)
{
strcat(szMsg,"[");
strcat(szMsg, szTitle);
strcat(szMsg,"]: ");
}
/* 内容 */
va_list argp;
va_strat(argp,format);
_vsnprintf(&szMsg[strlen(szMsg)], MAX_LOG_LENGTH - strlen(szMsg), format, argp);
va_end(argp);
/* 输出日志 */
printLog(szMsg, level);
}
void CMyLog::PrintLog(const char* szMsg, LEVEL_TYPE iLevel)
{
/* 函数的重入控制 */
EnterCriticalSection(&m_cs);
/* 输出到文件 */
if (iLevel <= m_iLevel)
{
if (NULL == m_file)
{
m_file = fopen(m_szFileName, "a+");
}
if (NULL == m_file)
{
ASSERT(false);
}
else
{
fprintf(m_file, "%s\n", szMsg);
fflush(m_file);//将缓存区的内容立即写入磁盘
}
}
LeaveCriticalSection(&m_cs);
}
4. 如何在代码中使用Log
首先,要把以上log的头文件和原文件一起加入到工程中去编译,并且在可执行程序的路径下要有一个.ini的配置文件;然后去定义其中的全局变量,最后只要在代码中需要加打印的地方使用WRITE_LOG宏就可以了, 如下:
/* Log.ini 配置文件*/
[LOG]
LEVELTYPE = ERROR // 日志打印等级控制
FILENAME = Log.txt // 日志文件名
/* 在自己的代码中使用Log打印 */
... ...
#include "MyLog.h"
const char LOG_CONFIG_FILE[] = "Log.ini"; // 日志文件的配置文件名
...
int MySocket::SendTo(SOCKET sock, const char* szBuffer, DWORD dwSize)
{
if (SOCKET_ERROR == send(sock, szBuffer, dwSize, 0)
{
WRITE_LOG(LEVEL_ERROR, "MySocket::SendTo", "send %s failed! GetLastError : %d",
szBuffer, WSAGetLastError());
return -1;
}
return SUCCESS;
}
以上代码中就可以实现日志打印,在程序的执行过程中如果socket的send失败,就会在log.txt文件里打印一条信息帮助开发人员定位问题,如下:
[23:44:12][ERROR][MySocket::SendTo]Send hello word failed! GetLastError : 10060
5. 后期的改进
这个简单的log类只是提出了一个程序打印log的方法,还需要读者去不断的改进,比如如果程序执行时间比较长的话,log打印信息会非常的多,随之而来的便是log文件会非常的大,不利于分析,所以需要指定log文件的大小;频繁的在代码中打印字符串也会对程序的性能造成一定的影响,所以可以对一些固定的字符串进行编码,打印的时候只需要打印编码即可,当然也可以开发一个专门针对日志文件进行分析的软件,这样就更提高了问题定位的效率。
目前对代码可测试性研究已经在业界比较成熟了,有兴趣的读者可以去参考ACE相应的日志打印方法。