基于Windows C++ 应用程序通用日志组件的使用详解
引言
在如何记录程序日志方面,通常有三种选择:
1、采用Log4CXX等公共开源日志组件:这类日志组件的特点是跨平台且功能比较强大,例如可以把日志发往另一台服务器或记录到数据库中等;另外,可配置性较高,可以通过配置文件或程序代码对日志进行很多个性化设置。但从另外一个角度看,由于这些优点往往也导致了在使用方面的缺点。首先,对于一般应用程序来说,它们并不需要太多的功能,通常只需要把日志记录到文件或反馈到应用程序,功能太多反正让用户使用起来觉得繁琐还得背负很多从来都用不到的代码。其次,这类日志组件通常是跨平台的,并不只是针对 Windows 或 VC 的应用程序,因此使用起来总会觉得有点别扭,例如他们的字符都是用 char 类型的,对于一个 Unicode 程序来说每次写日志都要做字符转换是很不爽的事情,本座在多年前曾经使用过 Log4Cpp ,程序执行时总是报告日志组件有内存泄露,虽然有可能是误报,但是使用起来总觉得很不舒服。
2、自己写几个简单的类或函数记录日志:这种方法的确很简单,通常都不用一两百行的代码。但这种方法通常缺乏规范性和通用性,其他程序需要记录类似的但有点差异的日志时,通常的作法是:Copy-Paste-Modify;另外,这类方法很可能也没有考虑性能或并发方面的问题,通常是直接在工作线程中写日志,对于那些性能要求较高的应用程序是绝对不允许的。
3、干脆不记录任何日志:的确,现在很多程序由于各种原因并没有记录任何日志。但本座以为,如果一个程序是有用的,具备一定功能,并且需要连续运行较长一段时间,那么记录日志是必须的;否则,得认真考虑该程序是否有存在的必要了。
设计
综上所述,编写一个通用的日志组件应该着重考虑三个方面:功能、可用性和性能。下面,本座详细说明在设计日志组件时对这些方面问题的考虑:
1、功能:本日志组件的目的是满足大多数应用程序记录日志的需求 —— 把日志输出到文件或发送到应用程序中,并不提供一些复杂但不常用的功能。本日志组件的功能包括:
把日志信息输出到指定文件
每日生成一个日志文件
对于 GUI 程序,可以把日志信息发送到指定窗口
对于Console应用程序,可以把日志信息发往标准输出 (std::cout)
支持 MBCS / UNICODE,Console / GUI 程序
支持动态加载和静态加载日志组件 DLL
支持 DEBUG/TRACE/INFO/WARN/ERROR/FATAL 等多个日志级别
2、可用性:本日志组件着重考虑了可用性,尽量让使用者用起来觉得简便、舒心:
简单纯净:不依赖任何程序库或框架
使用接口简单,不需复杂的配置或设置工作
提供 CStaticLogger 和 CDynamicLogger 包装类用于静态或动态加载以及操作日志组件,用户无需关注加载细节
程序如果要记录多个日志文件只需为每个日志文件创建相应的 CStaticLogger 或 CDynamicLogger 对象
只需调用 Log()/Debug()/Trace()/Info()/Warn()/Error()/Fatal() 等方法记录日志
日志记录方法支持可变参数
日志输出格式:<时间> <线程ID> <日志级别> <日志内容>
3、性能:性能是组件是否值得使用的硬指标,本组件从设计到编码的过程都尽量考虑到性能优化:
支持多线程同时发送写日志请求
使用单独线程在后台写日志,不影响工作线程的正常执行
采用批处理方式批量记录日志
接口
/******************************************************************************
Module: Logger.h
Notices: Copyright (c) 2012 Bruce Liang - http://www.cnblogs.com/ldcsaa/
Purpose: 记录程序日志。
1. 把日志信息输出到指定文件
2. 对于 GUI 程序,可以把日志信息发送到指定窗口
3. 对于Console应用程序,可以把日志信息发往标准输出 (std::cout)
Desc:
1、功能:
--------------------------------------------------------------------------------------
a) 把日志信息输出到指定文件
b) 每日生成一个日志文件
c) 对于 GUI 程序,可以把日志信息发送到指定窗口
d) 对于Console应用程序,可以把日志信息发往标准输出 (std::cout)
e) 支持 MBCS / UNICODE,Console / GUI 程序
f) 支持动态加载和静态加载日志组件 DLL
g) 支持 DEBUG/TRACE/INFO/WARN/ERROR/FATAL 等多个日志级别
2、可用性:
--------------------------------------------------------------------------------------
a) 简单纯净:不依赖任何程序库或框架
b) 使用接口简单,不需复杂的配置或设置工作
c) 提供 CStaticLogger 和 CDynamicLogger 包装类用于静态或动态加载以及操作日志组件,用户无需关注加载细节
d) 程序如果要记录多个日志文件只需为每个日志文件创建相应的 CStaticLogger 或 CDynamicLogger 对象
e) 只需调用 Log()/Debug()/Trace()/Info()/Warn()/Error()/Fatal() 等方法记录日志
f) 日志记录方法支持可变参数
g) 日志输出格式:<时间> <线程ID> <日志级别> <日志内容>
3、性能:
--------------------------------------------------------------------------------------
a) 支持多线程同时发送写日志请求
b) 使用单独线程在后台写日志,不影响工作线程的正常执行
c) 采用批处理方式批量记录日志
Usage:
方法一:(静态加载 Logger DLL)
--------------------------------------------------------------------------------------
0. 应用程序包含 StaticLogger.h 头文件
1. 创建 CStaticLogger 对象(通常为全局对象)
2. 调用 CStaticLogger->Init(...) 初始化日志组件
3. 使用 CStaticLogger->Log()/Debug()/Trace()/Info()/Warn()/Error()/Fatal() 等方法写日志
4. 调用 CStaticLogger->UnInit(...) 清理日志组件(CStaticLogger 对象析构时也会自动清理日志组件)
方法二:(动态加载 Logger DLL)
--------------------------------------------------------------------------------------
0. 应用程序包含 DynamicLogger.h 头文件
1. 创建 CDynamicLogger 对象(通常为全局对象)
2. 调用 CDynamicLogger->Init(...) 初始化日志组件
3. 使用 CDynamicLogger->Log()/Debug()/Trace()/Info()/Warn()/Error()/Fatal() 等方法写日志
4. 调用 CDynamicLogger->UnInit(...) 清理日志组件(CDynamicLogger 对象析构时也会自动清理日志组件)
方法三:(直接用导出函数加载 Logger DLL)
--------------------------------------------------------------------------------------
0. 应用程序包含 Logger.h 头文件
1. 手工调用 ILoger_Create() 和 ILoger_Create() 导出函数创建和销毁 ILogger 对象
(注:如果是动态加载,需手工调用 ::LoadLibrary()/::FreeLibrary() 系列 API 函数加载和卸载 Logger DLL)
[
***** 对于希望通过窗口接收日志信息的 GUI 程序 *****
A. 日志组件初始化成功后调用 SetGUIWindow(HWND) 设置收日志的窗口
B. 窗口须响应处理 LOG_MESSAGE 消息
C. 处理完 LOG_MESSAGE 消息后,调用 ILogger::FreeLogMsg() 销毁接收到的 TLogMsg
]
Environment:
1. Windows 2000 or later (_WIN32_WINNT >= 0x0500)
2. VC++ 2010 or later
Release:
1. Logger_C.dll - Console/MBCS/Release
2. Logger_CD.dll - Console/MBCS/Debug
3. Logger_CU.dll - Console/Unicode/Release
4. Logger_CUD.dll - Console/Unicode/Debug
5. Logger.dll - GUI/MBCS/Release
6. Logger_D.dll - GUI/MBCS/Debug
7. Logger_U.dll - GUI/Unicode/Release
8. Logger_UD.dll - GUI/Unicode/Debug
Examples:
1. TestGUILogger - GUI 版测试程序 (静态加载)
2. TestDynamicLogger - GUI 版测试程序 (动态加载)
3. TestConsoleLogger - Console 版测试程序 (静态加载)
******************************************************************************/
#pragma once
/**************************************************/
/********** imports / exports Logger.dll **********/
#ifdef LOGGER_EXPORTS
#define LOGGER_API __declspec(dllexport)
//#define TRY_INLINE inline
#else
#define LOGGER_API __declspec(dllimport)
//#define TRY_INLINE
#endif
/**************************************************/
/****************** 日志组件接口 *******************/
class LOGGER_API ILogger
{
public:
/***** 日志级别 *****/
enum LogLevel
{
LL_NONE = 0XFF,
LL_DEBUG = 1,
LL_TRACE = 2,
LL_INFO = 3,
LL_WARN = 4,
LL_ERROR = 5,
LL_FATAL = 6
};
/***** 操作错误码 *****/
enum ErrorCode
{
// 无错误
EC_OK = NO_ERROR,
// 文件操作相关的错误
EC_FILE_GENERIC,
EC_FILE_FILENOTFOUND,
EC_FILE_BADPATH,
EC_FILE_TOMANYOPERFILES,
EC_FILE_ACCESSDENIED,
EC_FILE_INVALIDFILE,
EC_FILE_REMOVECURRENTDIR,
EC_FILE_DIRECTORYFULL,
EC_FILE_BADSEEK,
EC_FILE_HARDIO,
EC_FILE_SHARINGVIOLATION,
EC_FILE_LOCKVIOLATION,
EC_FILE_DISKFULL,
EC_FILE_ENDOFFILE,
// 其他错误
EC_INVALID_STATE,
EC_INIT_LOGLEVEL,
EC_INIT_PRINTFLAG,
EC_INIT_CREATE_LOG_THREAD_FAIL
};
/******************************************
日志信息结构
*******************************************/
struct TLogMsg
{
DWORD m_dwSize; // 结构大小 - 跟据消息长度动态变化
LogLevel m_logLevel; // 日志级别
UINT m_uiThreadID; // 线程ID
SYSTEMTIME m_stMsgTime; // 记录时间
TCHAR m_psMsg[1]; // 消息内容
};
public:
ILogger(void);
virtual ~ILogger(void);
private:
ILogger(const ILogger&);
ILogger& operator = (const ILogger&);
public:
// 日志组件初始化方法
virtual BOOL Init(
LPCTSTR logFile = NULL // 日志文件. 默认: {AppPath}/logs/{AppName}-YYYYMMDD.log
, LogLevel ll = DEFAULT_LOG_LEVEL // 日志级别. 默认: [Debug -> LL_DEBUG] / [Release -> LL_INFO]
, int printFlag = DEFAULT_PRINT_FLAG // 输出掩码. 是否输出到文件和(或)屏幕. 默认: 只输出到文件
) = 0;
// 日志组件清理方法
virtual BOOL UnInit() = 0;
public:
// 写日志方法:传入日志内容字符串(对于不需要格式化的日志文本,用本方法效率最高)
virtual void Log_0 (LogLevel ll, LPCTSTR msg) = 0;
virtual void Debug_0(LPCTSTR msg);
virtual void Trace_0(LPCTSTR msg);
virtual void Info_0 (LPCTSTR msg);
virtual void Warn_0 (LPCTSTR msg);
virtual void Error_0(LPCTSTR msg);
virtual void Fatal_0(LPCTSTR msg);
// 写日志方法:传入格式化字符串和参数栈指针(通常只在组件内部使用)
virtual void LogV (LogLevel ll, LPCTSTR format, va_list arg_ptr);
// 写日志方法:传入格式化字符串和可变参数(非常灵活简便)
virtual void Log (LogLevel ll, LPCTSTR format, ...);
virtual void Debug (LPCTSTR format, ...);
virtual void Trace (LPCTSTR format, ...);
virtual void Info (LPCTSTR format, ...);
virtual void Warn (LPCTSTR format, ...);
virtual void Error (LPCTSTR format, ...);
virtual void Fatal (LPCTSTR format, ...);
// 写日志方法:传入格式化字符串和可变参数(与上一组方法类似,但在进行任何操作前会检查日志级别)
virtual void TryLog (LogLevel ll, LPCTSTR format, ...);
virtual void TryDebug (LPCTSTR format, ...);
virtual void TryTrace (LPCTSTR format, ...);
virtual void TryInfo (LPCTSTR format, ...);
virtual void TryWarn (LPCTSTR format, ...);
virtual void TryError (LPCTSTR format, ...);
virtual void TryFatal (LPCTSTR format, ...);
// 通用辅助方法
virtual BOOL HasInited () const = 0; // 是否已经初始化
virtual BOOL IsPrint2File () const = 0; // 是否把日志输出到文件
virtual BOOL IsPrint2Screen () const = 0; // 是否把日志输出到屏幕窗口
virtual int GetPrintFlag () const = 0; // 打印标志
virtual LogLevel GetLogLevel () const = 0; // 日志级别
virtual LPCTSTR GetLogFile() const = 0; // 日志文件
virtual ErrorCode GetLastError() const = 0; // 当前操作错误码
/****************************** GUI ******************************/
#ifdef _WINDOWS
public:
// 设置接收日志信息的窗口, hWndGUI == NULL 则取消接收
virtual void SetGUIWindow(HWND hWndGUI) = 0;
// 获取接收日志信息的窗口
virtual HWND GetGUIWindow() = 0;
// 销毁在发送 LOG_MESSAGE 消息时动态创建的 TLogMsg 对象
virtual void FreeLogMsg(const TLogMsg* pLogMsg);
// 虚拟窗口句柄标掩码:用于向 GUI 窗口发送 LOG_MESSAGE 消息时作为发送源标识
static const int LOGGER_FAKE_WINDOW_BASE = 0X80001111;
// 自定义日志消息:通过本消息向 GUI 窗口发送日志
// 其中:WPARAM -> ILogger 对象指针,LPARAM -> TLogMsg 结构体指针
static const int LOG_MESSAGE = WM_USER | (0x7FFF & LOGGER_FAKE_WINDOW_BASE);
#endif
public:
static const int PRINT_FLAG_FILE = 0x00000001; // 打印到文件
static const int PRINT_FLAG_SCREEN = 0x00000002; // 打印到屏幕
static const int DEFAULT_PRINT_FLAG = PRINT_FLAG_FILE; // 默认日志掩码
static const LogLevel DEFAULT_LOG_LEVEL =
#ifdef _DEBUG
LL_DEBUG
#else
LL_INFO
#endif
};
/**************************************************/
/************** Logger DLL 导出函数 ***************/
// 创建 ILogger 对象
EXTERN_C LOGGER_API ILogger* ILogger_Create();
// 销毁 ILogger 对象
EXTERN_C LOGGER_API void ILogger_Destroy(ILogger* p);
// 获取各日志级别的文字描述
EXTERN_C LOGGER_API LPCTSTR ILogger_GetLogLevelDesc (ILogger::LogLevel ll);
// 获取各操作错误码的文字描述
EXTERN_C LOGGER_API LPCTSTR ILogger_GetErrorDesc (ILogger::ErrorCode ec);
代码中的注释基本已经能够说明日志组件的使用方法,这里只做一些简单的概括:
版本:日志组件以 DLL 的形式提供,已编译成 Debug/Release、MBCS/Unicode、GUI/Console 8个版本
测试:三个测试程序 TestGUILogger、TestDynamicLogger 和 TestConsoleLogger 用于测试所有版本。其中 TestDynamicLogger 采用动态加载方式加载 Logger DLL
使用方法:
0. 应用程序包含 Logger.h 头文件
1. 调用 ILogger_Create() 导出函数创建 ILogger 对象
2. 调用 ILogger->Init(...) 初始化日志组件
3. 使用 ILogger->Log()/Debug()/Trace()/Info()/Warn()/Error()/Fatal() 等方法写日志
4. 调用 ILogger->UnInit(...) 清理日志组件
5. 调用 ILogger_Destroy() 导出函数销毁 ILogger 对象
2、CStaticLogger:ILogger 包装器(智能指针)—— 用于静态加载 Logger DLL
代码如下:
#pragma once
#include "Logger.h"
/**************************************************/
/********* http://www.cnblogs.com/ldcsaa/ *********/
/********** ILogger 包装器(智能指针) ***********/
/*********** 用于静态加载 Logger DLL ************/
class LOGGER_API CStaticLogger
{
public:
// 构造函数:如果 bCreate 为 TRUE,则在构建 CStaticLogger 实例的同时创建 ILogger 对象
CStaticLogger(BOOL bCreate = TRUE);
// 析构函数
~CStaticLogger();
private:
CStaticLogger(const CStaticLogger&);
CStaticLogger& operator = (const CStaticLogger&);
public:
inline void Reset (ILogger* pLogger); // 重设其封装的 ILogger 指针
inline BOOL IsValid () const; // 判断其封装的 ILogger 指针是否非空
inline ILogger* Get () const; // 获取 ILogger 指针
inline ILogger& operator * () const; // 获取 ILogger 引用
inline ILogger* operator -> () const; // 获取 ILogger 指针
inline operator ILogger* () const; // 转换为 ILogger 指针
private:
ILogger* m_pLogger;
};
CStaticLogger 为简化日志组件使用而设计,用于静态加载 Logger DLL 的场合。使用方法:
0. 应用程序包含 StaticLogger.h 头文件
1. 创建 CStaticLogger 对象(通常为全局对象)
2. 调用 CStaticLogger->Init(...) 初始化日志组件
3. 使用 CStaticLogger->Log()/Debug()/Trace()/Info()/Warn()/Error()/Fatal() 等方法写日志
4. 调用 CStaticLogger->UnInit(...) 清理日志组件(CStaticLogger 对象析构时也会自动清理日志组件)
3、CDynamicLogger:ILogger 包装器(智能指针)—— 用于动态加载 Logger DLL
代码如下:
#pragma once
#include "Logger.h"
/**************************************************/
/********* http://www.cnblogs.com/ldcsaa/ *********/
/************** Logger DLL 默认文件名 ***************/
#ifdef _DEBUG
#ifdef _UNICODE
#ifdef _WINDOWS
#define DEF_LOGGER_DLL_FILE_PATH _T("Logger_UD.dll")
#else
#define DEF_LOGGER_DLL_FILE_PATH _T("Logger_CUD.dll")
#endif
#else
#ifdef _WINDOWS
#define DEF_LOGGER_DLL_FILE_PATH _T("Logger_D.dll")
#else
#define DEF_LOGGER_DLL_FILE_PATH _T("Logger_CD.dll")
#endif
#endif
#else
#ifdef _UNICODE
#ifdef _WINDOWS
#define DEF_LOGGER_DLL_FILE_PATH _T("Logger_U.dll")
#else
#define DEF_LOGGER_DLL_FILE_PATH _T("Logger_CU.dll")
#endif
#else
#ifdef _WINDOWS
#define DEF_LOGGER_DLL_FILE_PATH _T("Logger.dll")
#else
#define DEF_LOGGER_DLL_FILE_PATH _T("Logger_C.dll")
#endif
#endif
#endif
/**************************************************/
/*************** Logger DLL 导出函数 ***************/
// 创建 ILogger 对象
typedef ILogger* (*FN_ILogger_Create) ();
// 销毁 ILogger 对象
typedef void (*FN_ILogger_Destroy) (ILogger* p);
// 获取各日志级别的文字描述
typedef LPCTSTR (*FN_ILogger_GetLogLevelDesc) (ILogger::LogLevel ll);
// 获取各操作错误码的文字描述
typedef LPCTSTR (*FN_ILogger_GetErrorDesc) (ILogger::ErrorCode ec);
/*************************************************/
/********** ILogger 包装器(智能指针) ***********/
/************ 用于动态加载 Logger DLL ************/
class CDynamicLogger
{
public:
// 构造函数:如果 bLoad 为 TRUE,则在构建 CDynamicLogger 示例的同时创建 ILogger 对象
CDynamicLogger(BOOL bLoad = TRUE, LPCTSTR lpszFilePath = DEF_LOGGER_DLL_FILE_PATH)
{
Reset();
if(bLoad)
Load(lpszFilePath);
}
// 析构函数
~CDynamicLogger()
{
Free();
}
private:
CDynamicLogger(const CDynamicLogger&);
CDynamicLogger& operator = (const CDynamicLogger&);
public:
// 创建 ILogger 对象
ILogger* ILogger_Create()
{return m_fnILoggerCreate();}
// 销毁 ILogger 对象
void ILogger_Destroy(ILogger* p)
{m_fnILoggerDestroy(p);}
// 获取各日志级别的文字描述
LPCTSTR ILogger_GetLogLevelDesc(ILogger::LogLevel ll)
{return m_fnILoggerGetLogLevelDesc(ll);}
// 获取各操作错误码的文字描述
LPCTSTR ILogger_GetErrorDesc(ILogger::ErrorCode ec)
{return m_fnILoggerGetErrorDesc(ec);}
// 加载 Logger DLL
BOOL Load(LPCTSTR lpszFilePath = DEF_LOGGER_DLL_FILE_PATH)
{
if(IsValid())
return FALSE;
BOOL isOK = FALSE;
m_hLogger = ::LoadLibrary(lpszFilePath);
if(m_hLogger)
{
m_fnILoggerCreate = (FN_ILogger_Create) ::GetProcAddress(m_hLogger, "ILogger_Create");
m_fnILoggerDestroy = (FN_ILogger_Destroy) ::GetProcAddress(m_hLogger, "ILogger_Destroy");
m_fnILoggerGetLogLevelDesc = (FN_ILogger_GetLogLevelDesc) ::GetProcAddress(m_hLogger, "ILogger_GetLogLevelDesc");
m_fnILoggerGetErrorDesc = (FN_ILogger_GetErrorDesc) ::GetProcAddress(m_hLogger, "ILogger_GetErrorDesc");
if(m_fnILoggerCreate && m_fnILoggerDestroy)
{
m_pLogger = ILogger_Create();
isOK = (m_pLogger != NULL);
}
}
if(!isOK)
Free();
return isOK;
}
// 卸载 Logger DLL
BOOL Free()
{
if(!IsValid())
return TRUE;
BOOL isOK = TRUE;
if(m_pLogger) ILogger_Destroy(m_pLogger);
if(m_hLogger) isOK = ::FreeLibrary(m_hLogger);
Reset();
return isOK;
}
BOOL IsValid () const {return m_pLogger != NULL;} // 判断其封装的 ILogger 指针是否非空
ILogger* Get () const {return m_pLogger;} // 获取 ILogger 指针
ILogger& operator * () const {return *m_pLogger;} // 获取 ILogger 引用
ILogger* operator -> () const {return m_pLogger;} // 获取 ILogger 指针
operator ILogger* () const {return m_pLogger;} // 转换为 ILogger 指针
private:
void Reset()
{
m_hLogger = NULL;
m_pLogger = NULL;
m_fnILoggerCreate = NULL;
m_fnILoggerDestroy = NULL;
m_fnILoggerGetLogLevelDesc = NULL;
m_fnILoggerGetErrorDesc = NULL;
}
private:
HMODULE m_hLogger;
ILogger* m_pLogger;
FN_ILogger_Create m_fnILoggerCreate;
FN_ILogger_Destroy m_fnILoggerDestroy;
FN_ILogger_GetLogLevelDesc m_fnILoggerGetLogLevelDesc;
FN_ILogger_GetErrorDesc m_fnILoggerGetErrorDesc;
};
CDynamicLogger 为简化日志组件使用而设计,用于动态加载 Logger DLL 的场合。使用方法:
0. 应用程序包含 DynamicLogger.h 头文件
1. 创建 CDynamicLogger 对象(通常为全局对象)
2. 调用 CDynamicLogger->Init(...) 初始化日志组件
3. 使用 CDynamicLogger->Log()/Debug()/Trace()/Info()/Warn()/Error()/Fatal() 等方法写日志
4. 调用 CDynamicLogger->UnInit(...) 清理日志组件(CDynamicLogger 对象析构时也会自动清理日志组件)