65.9K
CodeProject 正在变化。 阅读更多。
Home

yaolog:一个强大、易用、跨平台的 C++ 日志工具

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (11投票s)

2012年12月15日

CPOL

5分钟阅读

viewsIcon

165972

downloadIcon

1352

一个强大、易用、跨平台的 C++ 日志工具。

为什么使用 yaolog 

日志对于调试至关重要。许多 C++ 开发者使用 printf 来输出调试信息,这很容易使用,但很难获取更多信息和进行控制。另一方面,一些日志库功能强大但对于简单项目来说太庞大,并且复杂性可能会增加学习成本。在我看来,一个好的日志工具除了友好的界面外,主要应该实现三个目标:额外信息、多输出、行为控制。yaolog 已经实现了这些功能: 

  • printf 风格的 LOGA__LOGW__LOG__LOGBIN__LOGBIN_F__ 接口
  • 额外信息(时间、源文件、函数、行号等)  
  • 每个日志记录器都有自己的行为  
  • 通过配置文件(ini)在运行时更改日志行为   
  • 输出到控制台、文件、HTTP 服务器   
  • 线程安全    
  • 跨平台(Windows、Linux)  

最重要的是,它真的很易于使用。 

设计与实现   

没有“日志级别”? 

日志级别,如“info”、“warning”、“error”等,已经被许多日志库提供。但这些级别是否足以满足您的项目?也许您只需要一个级别,也许您需要更多的级别,比如“socket buffer”、“UI event”、“oh God help me”等等。因此,yaolog 使用“日志记录器对象”而不是“日志级别”,您可以创建多个日志记录器对象并分别控制它们的行为: 

YAOLOG_CREATE("I", true, YaoUtil::LOG_TYPE_TEXT); // info logger
YAOLOG_CREATE("B", true, YaoUtil::LOG_TYPE_BIN);  // binary logger
YAOLOG_CREATE("FB", true, YaoUtil::LOG_TYPE_FORMATTED_BIN); // formatted binary logger

// set info logger output to file
YAOLOG_SET_LOG_ATTR("I", true, YaoUtil::OUT_FLAG_FILE, true, false, true, true, NULL); 

第一个参数是 logID。您可以方便地使用宏来定义 logID。

接口  

yaolog 的所有接口都是宏

YAOLOG_INIT

YAOLOG_EXIT

YAOLOG_CREATE

YAOLOG_DISABLE_ALL

YAOLOG_SET_LOG_ATTR

YAOLOG_SET_LOGFILE_ATTR

YAOLOG_SET_ATTR_FROM_CONFIG_FILE

LOG__

LOGA__

LOGW__

LOGBIN__ 

LOGBIN_F__

原因是:如果您定义了 _NO_YAOLOG 宏,这种方法不会增加您的应用程序的大小。 

日志接口 LOGA__LOGW__LOG__LOGBIN__LOGBIN_F__ 是宏,用于记录 charwchartchar 和二进制数据(原始或格式化)。LOG__ 展开如下:  

#define LOG__(logID, szFormat, ...) \
           YaoUtil::BaseLog *p = YaoUtil::LogFactory::Get(logID);\
           p->Log(__FILE__, __FUNCTION__, __LINE__, szFormat, __VA_ARGS__); 

__VA_ARGS__(Linux 下是 ##__VA_ARGS__)是所谓的变长宏,它会被展开为所有匹配省略号的参数,就像一个“printf 宏”一样:  

LOG__("I", _T("Logging %!"), _T("tchar"));
LOGA__("I", "Logging %!", "char");
LOGW__("I", L"Logging %!", L"wchar"); 

同步与异步 

我需要实时将日志输出到文件或控制台,因为某些调试信息需要同步检查,例如 UI 事件。因此,yaolog 在用户线程中立即执行“本地”日志操作。但是,如果您为日志记录器指定了 YaoUtil::OUT_FLAG_REMOTE 标志,则意味着您希望将日志数据发布到一个 HTTP 服务器,这个操作将在后台工作线程中执行,绝不会阻塞其他线程。请参见下图:

在运行时更改日志行为  

日志记录器的行为由最后一个“配置操作”刷新,“配置操作”在 yaolog 中是以下之一: 

  1. 调用 YAOLOG_CREATE 来创建一个日志记录器并启用或禁用它 
  2. 调用 YAOLOG_SET_LOG_ATTR&YAOLOG_SET_LOGFILE_ATTR 来设置日志记录器的属性
  3. 调用 YAOLOG_SET_ATTR_FROM_CONFIG_FILE 来从配置文件设置日志记录器的属性
  4. 修改配置文件内容 
  5. 调用 YAOLOG_DISABLE_ALL 来禁用所有日志操作(或取消禁用),此方法的优先级高于 1&2&3&4。   

当您的应用程序运行时,如果您想更改日志记录器的行为,只需修改配置文件并保存,然后日志记录器的行为就会改变(前提是 YAOLOG_SET_ATTR_FROM_CONFIG_FILE 曾经在应用程序中调用过)。如上图所示,后台工作线程默认每 2 秒检查每个日志记录器的配置文件(如果已指定),并更新日志记录器。 

用法     

yaolog 的“hello world”大概是这样的:  

// call YAOLOG_INIT at the app entry point
YAOLOG_INIT;

// Create a new text log object, specify logID and enable it
YAOLOG_CREATE("log1", true, YaoUtil::LOG_TYPE_TEXT);

// logging with all-default settings, it will output to console
LOGA__("log1", "Hello world! My name is %s, I'm %d!", "neil", 29);
LOGW__("log1", L"Hello world! My name is %s, I'm %d!", L"neil", 29);
LOG__("log1", _T("Hello world! My name is %s, I'm %d!"), _T("neil"), 29);

// call YAOLOG_EXIT before app exit 
YAOLOG_EXIT;  

YAOLOG_INITYAOLOG_EXIT 应该只调用一次,但重复调用也不会产生不良影响。如果您想同时输出到控制台和文件:  

// use default log file path, it is "module file dir\log\logID_time.log"
YAOLOG_SET_LOG_ATTR("log1", true, YaoUtil::OUT_FLAG_STDOUT | YaoUtil::OUT_FLAG_FILE, true,         false, true, true, NULL);  

// use custom path  
// and, if you want to generate a new log file everyday(parameter 4 is true), 
// then the szLogFileName(last parameter) must be NULL or ""  
YAOLOG_SET_LOGFILE_ATTR(LOGID_I, false, false, false, "c:\\logfile", "tt.log"); 

您还可以配置一个 ini 文件并在运行时更改它以控制日志行为,ini 文件的格式在 yaolog.h 的底部。   

// if parameter szINI is a filename like "logconfig.ini", 
// then it must be in module file dir,
// or you can use an absolute path like "c:\logconfig.ini"
YAOLOG_SET_ATTR_FROM_CONFIG_FILE("log1", "logconfig.ini");

有时我们需要记录二进制数据。您必须在 YAOLOG_CREATE 方法中将第三个参数设置为 YaoUtil::LOG_TYPE_BINYaoUtil::LOG_TYPE_FORMATTED_BIN 以指示这是一个二进制日志记录器(原始或格式化),并且此属性之后无法更改:  

// log raw binary data 
YAOLOG_CREATE("log2", true, YaoUtil::LOG_TYPE_BIN);
YAOLOG_SET_ATTR_FROM_CONFIG_FILE("log2", "logconfig.ini");
char buf[10] = { 0,1,2,3,4,5,6,7,8,9 };
LOGBIN__("log2", buf, 10);

// maybe you want to log send&recv data(binary) of a socket communication, and in this case,
// it is hard to distinguish between send and recv data from a raw binary log file. 
// so you can use formatted binary log here: 
YAOLOG_CREATE("log3", true, YaoUtil::LOG_TYPE_FORMATTED_BIN);
YAOLOG_SET_ATTR_FROM_CONFIG_FILE("log3", "logconfig.ini"); 
char data1[4] = { 0,1,2,3 };
char data2[4] = { 4,5,6,7 };
LOGBIN_F__("log3", "send", data1, 4);
LOGBIN_F__("log3", "recv", data2, 4);

yaolog 可以将日志数据发布到 HTTP 服务器: 

YAOLOG_SET_LOG_ATTR("log1", true, YaoUtil::OUT_FLAG_FILE | YaoUtil::OUT_FLAG_REMOTE,
   true, true, true, true, "http://192.168.1.195/default.aspx"); 

日志数据已使用 Base64EncodeUrlEncode 进行传输。HTTP 服务器(C#)代码如下: 

protected void Page_Load(object sender, EventArgs e)
{
    string logID = GetValueOfKey("logID");
    string isText = GetValueOfKey("isText");
    string machineID = GetValueOfKey("machineID");
    string logData = GetValueOfKey("logData");

    if (logID.Length == 0) return;

    // the path_ should be valid and IIS writable
    string path_ = @"c:\rev_log\";
    byte[] bytes = Convert.FromBase64String(logData);
    if (isText == "0")
    {
        // binary log
        File.WriteAllBytes(path_ + logID + ".bl", bytes);
    }
    else
    {
        // text log
        string s = Encoding.Default.GetString(bytes);
        s = string.Format(
           "logID={0}\r\nmachineID={1}\r\nlogData={2}",
           logID, machineID, s);
        File.AppendAllText(path_ + logID + ".log", s);
    }

    Response.Write("ok");
}

protected string GetValueOfKey(string key)
{
    // .NET decode the url automatically
    return (Request.Form[key] == null ? "" : Request.Form[key]);
}   

如果您想将 yaolog 编译成 DLL(Windows),请取消注释 yaolog.h 中的这个块,并为 DLL 本身定义 YAOLOG_EXPORTS

/*
#ifdef _YAOLOG_WIN32_
    #ifdef YAOLOG_EXPORTS
        #define YAOLOG_EXPORT_API _declspec(dllexport)
    #else
        #define YAOLOG_EXPORT_API _declspec(dllimport)
    #endif
#else
    #define YAOLOG_EXPORT_API
#endif
*/ 

在软件的正式版本中控制日志  

通常我们在发布版本中禁用所有日志。然而,许多 bug 只会出现在最终用户的机器上。在这种情况下如何获取调试信息?yaolog 考虑了三种不同的情况: 

对于简单的、私有的项目,或不要求高安全级别

只需将所有 bEnable 参数设置为 false(在 YAOLOG_CREATEYAOLOG_SET_LOG_ATTR 和 ini 文件中)。您可以使用宏轻松做到这一点。而且,ini 文件不存在等于禁用。当用户报告 bug 时,告诉他部署一个 ini 文件——文件名和路径由 YAOLOG_SET_ATTR_FROM_CONFIG_FILE 指定——这样日志记录器将根据 ini 文件的内容工作。注意:如果您从未在代码中调用 YAOLOG_SET_ATTR_FROM_CONFIG_FILE,此方法将不起作用。  

对于更高安全级别  

好的,我知道您的代码是商业软件,不希望用户仅仅通过编写一个 ini 文件就能查看调试信息!yaolog 提供了以下解决方案:

  • 在您的发布版本中调用 YAOLOG_DISABLE_ALL(true) 来禁用所有日志。然后所有日志都被禁用,除非调用 YAOLOG_DISABLE_ALL(false) 否则无法启用。
  • 指定一个协议,在应用程序启动时告诉服务器用户的机器 ID——来自 YaoUtil::MachineID::GetMachineID()。并根据您的选择,将此 ID 写入特殊文件或显示在应用程序的关于对话框中。  
  • 当用户报告 bug 时,告诉他找到他的机器 ID 并提供给您。当他的应用程序再次启动时,您的服务器识别此 ID 并响应日志配置字符串(通过您的协议)给他的应用程序,此时日志记录器就可以工作了。 
  • 这是一个响应处理函数的示例。注意,日志记录器的行为由最后一个“配置操作”设置,因此在 OnResponse() 之后不要调用其他设置函数,否则您在 OnResponse() 中的配置将被覆盖。 
// pseudocode
void OnResponse(char *responseBuffer)
{
  string logID;
  bool enable = true;
  ...

  // parse the response buffer to log config variables...
  ...

  if (!enable)
  {
      YAOLOG_DISABLE_ALL(true);
      return;
  }

  YAOLOG_DISABLE_ALL(false);
  YAOLOG_SET_LOG_ATTR(logID, ...);
  YAOLOG_SET_LOGFILE_ATTR(logID, ...);
}  

对于最高安全级别  

如果您希望在发布版本中绝对不包含日志,请定义 _NO_YAOLOG 宏。然后所有日志调用都不会被编译。  

历史   

  • 2013-3-31:增加了功能:1. 记录格式化二进制数据  2. 每天生成新的日志文件 
  • 2013-1-24:定义了 YAOLOG_EXPORTS 以将 yaolog 编译为 DLL。 
  • 2013-1-14:如果定义了 _NO_YAOLOG,所有日志调用都不会被编译。 
  • 2012-12-19:修复了 bug:解析 ini 文件。 
  • 2012-12-16:首次发布。  
© . All rights reserved.