Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

yaolog: A powerful, easy-use, cross-platform C++ log utility

4.82/5 (12 votes)
30 Mar 2013CPOL5 min read 164.8K   1.3K  
A powerful, easy-use, cross-platform C++ log utility.

Image 1

Why use yaolog 

Log is extremely important for debug. Many cppers use printf to output debug info, it's easy to use, but hard to get more info and control. On the other hand, some log libraries are powerful but too large for a simple project, and the complexity maybe increase learning costs. In my opinion, besides friendly interface, a good log utility should achieve three aims mainly: extra info, multi-output, behavior control. Here yaolog have already implemented these features: 

  • printf style, LOGA__, LOGW__, LOG__, LOGBIN__, LOGBIN_F__ interfaces
  • extra info (time, source file, function, line, etc.)  
  • each logger has its own behavior  
  • config ini file to change log behavior at run-time   
  • output to console, file, http server   
  • thread-safe    
  • cross-platform (Windows, Linux)   

And the most important thing is it's really easy-to-use

Design and Implement   

No 'log level'? 

Log level, like "info", "warning", "error", and so on, have been provided by many log libraries. But are these levels sufficient for your project? Maybe you just want a single level, maybe you want more levels like "socket buffer", "UI event", "oh God help me", etc. So yaolog use 'logger object' instead of 'log level', and you can create multi logger objects and control their behaviors respectively: 

C++
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); 

The first parameter is logID. You can use macros to define logID for convenience.

The interface  

<p>
</p><p>All the interfaces of yaolog are macro:</p><p>
<code>

<code>YAOLOG_INIT

<code>YAOLOG_EXIT

<code>YAOLOG_CREATE

<code>YAOLOG_DISABLE_ALL

<code>YAOLOG_SET_LOG_ATTR

<code>YAOLOG_SET_LOGFILE_ATTR

<code>YAOLOG_SET_ATTR_FROM_CONFIG_FILE

<code>LOG__

<code>LOGA__

<code>LOGW__

<code>LOGBIN__ 

<code>LOGBIN_F__

The reason is: this method will not increase your app's size if you define _NO_YAOLOG macro. 


The log interface LOGA__, LOGW__, LOG__, LOGBIN__LOGBIN_F__ are macros, to log char, wchar, tchar, and binary data(raw or formatted). LOG__ Expanded like this:  

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

__VA_ARGS__(##__VA_ARGS__ in Linux) is so-called Variadic Macros, it is replaced by all of the arguments that match the ellipsis, like a 'printf macro':  

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

Synchronous vs. Asynchronous 

I need a real-time log output to file or console because some debug info require be examined synchronously, e.g. UI events, so yaolog executes the 'local' log operation immediately in the same thread that user's. But if you specify the flag YaoUtil::OUT_FLAG_REMOTE to a logger, it's means you want to post the log data to a http server, and this operation execute in a background worker thread, never block the other threads. See the chart below:

Image 2

Change log behavior at run-time  

The behavior of a logger is refreshed by the last 'config operation', and 'config operation' in yaolog is one of the following: 

  1. call YAOLOG_CREATE to create a logger and enable or disable it 
  2. call YAOLOG_SET_LOG_ATTR & YAOLOG_SET_LOGFILE_ATTR to set a logger's attribute
  3. call YAOLOG_SET_ATTR_FROM_CONFIG_FILE to set a logger's attribute from a config file
  4. modify config file's content 
  5. call YAOLOG_DISABLE_ALL to disable all log actions (or cancel disable), the priority of this method is higher than 1&2&3&4.   

When your app is running, if you want to change a logger's behavior, just modify the config file and save, then the logger's behavior will be changed (the premise is YAOLOG_SET_ATTR_FROM_CONFIG_FILE has ever been called in app). As the diagram described above, the background worker thread check each logger's config file (if specified) every 2 seconds (by default), and update the loggers. 

Usage     

A yaolog's 'hello world' is like this:  

C++
// 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;  

The YAOLOG_INIT and YAOLOG_EXIT should just call once, but no bad infection with repetitious calls. If you want to output both to console and file:  

C++
// 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"); 

You can also config an ini file and change it at run-time to control log behavior, the format of ini file is in the bottom of yaolog.h.   

C++
// 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");

Sometimes we need to log binary data. You must set the third parameter to YaoUtil::LOG_TYPE_BIN or YaoUtil::LOG_TYPE_FORMATTED_BIN in YAOLOG_CREATE method to indicate this is a binary logger(raw or formatted), and this attribute cannot be change later:   

C++
// 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 can post log data to http server: 

C++
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"); 

The log data have used Base64Encode and UrlEncode for transfer. An HTTP server (C#) code is like this: 

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]);
}   

If you want to compile yaolog to DLL (Windows), uncomment this block in yaolog.h and define YAOLOG_EXPORTS for the DLL itself:

C++
/*
#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
*/ 

Control log in the official version of your software  

Generally we disable all logs in release. However, many bugs happened only in end-user's machine. How to get debug info in this case? yaolog considers three different situations: 

For simple, private projects, or not require high level security

Just set all bEnable parameters to false (at YAOLOG_CREATE, YAOLOG_SET_LOG_ATTR,  and in ini file). You can use macros to do this easily. And, ini file not exists equal to disable. When a user reports a bug, told him to deploying an ini file -- file name and path are specified by YAOLOG_SET_ATTR_FROM_CONFIG_FILE -- so loggers will work by ini file's content. Notice: this method will not work if you don't call YAOLOG_SET_ATTR_FROM_CONFIG_FILE ever in your code.  

For higher level security  

OK, I know your code is a commercial software and do not want users view debug info via just writing an ini file! yaolog provides a solution below:

  • call YAOLOG_DISABLE_ALL(true) to disable all logs in your release version. Then all logs are disabled and cannot be enable except for call YAOLOG_DISABLE_ALL(false).
  • Specify a protocal to tell server the user's machine ID -- from YaoUtil::MachineID::GetMachineID() -- when your app startup. And write this ID in a special file or show it in app's about dialog, by your choice.  
  • When a user reports a bug, told him find his machine ID to you. When his app startup again, your server identify this ID and response log config string (by your protocol) to his app and loggers are working now. 
  • Here is an example of response handler function. Notice: the behavior of a logger is set by the last 'config operation', so do not call other set functions after OnResponse(), or else your config in OnResponse() will be overwrite.  
C++
// 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, ...);
}  

For highest level security  

If you want no-log in release absolutely, define _NO_YAOLOG macro. Then all log-calls will not be compiled.  

History   

  • 2013-3-31: add ability to: 1. log formatted binary data  2. generate new log file daily 
  • 2013-1-24: defined YAOLOG_EXPORTS for compiling yaolog to DLL. 
  • 2013-1-14: all log-calls will not be compiled if _NO_YAOLOG is defined. 
  • 2012-12-19 : bug fixed: parsing ini file. 
  • 2012-12-16 : initial release.  

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)