程式的記錄輔助工具:Boost Log

| | 0 Comments| 16:14
Categories:

一般在寫程式的時候,難免會需要把程式的執行狀態記錄下來、或是做輸出;而一般時候,最單純的方法,就是直接透過 STL 的 cout 或是 cerr,把訊息輸出在 console 視窗裡。但是實際上,這樣做在管理上的彈性算是比較小,而且也很難區分哪些訊息是 debug 訊息、哪些是真正要告訴使用者的。

所以,Boost C Library 在 1.54 板之後,引進了一個名為「Log」的新的函式庫,可以用來當作程式執行紀錄的輔助工具。他的頁面是:http://www.boost.org/doc/libs/release/libs/log/doc/html/index.html

不過說實話,個人是覺得 Boost.Log 的功能的確很強大,但是相對地使用上也很複雜…所以其實 Heresy 自己也還沒全部搞清楚,這邊基本上就是先大概介紹 Heresy 知道的東西了~

Trivial logging

Boost Log 設計的架構算是滿完整、相對也滿複雜的;不過如果只是要簡單地做成是執行階段的訊息輸出的話,他也有提供簡易的「Trivial」模式可以使用。在這種模式下,要使用 Boost Log 大概就會像下面這樣:

#include <boost/log/trivial.hpp>
 
int main(int, char*[])
{
BOOST_LOG_TRIVIAL(trace) << "A trace severity message";
BOOST_LOG_TRIVIAL(debug) << "A debug severity message";
BOOST_LOG_TRIVIAL(info) << "An informational severity message";
BOOST_LOG_TRIVIAL(warning) << "A warning severity message";
BOOST_LOG_TRIVIAL(error) << "An error severity message";
BOOST_LOG_TRIVIAL(fatal) << "A fatal severity message";
 
return 0;
}

基本上,就是先加入 Boost Log 的 trival.hpp 這個 header 之後,就可以把 BOOST_LOG_TRIVIAL 這個巨集、當作標準 output stream 的物件來使用了~而所傳入的 tracedebug 這些值,則是 Boost Log 預先定義好的記錄的嚴重層級(severity level),可以根據狀況、來設定這個訊息是哪一種。

上面的程式輸出的結果,則會像下面這樣:

[2014-03-18 09:59:11.546000] [0x000004d2] [trace]   A trace severity message
[2014-03-18 09:59:11.547001] [0x000004d2] [debug] A debug severity message
[2014-03-18 09:59:11.547001] [0x000004d2] [info] An informational severity message
[2014-03-18 09:59:11.548003] [0x000004d2] [warning] A warning severity message
[2014-03-18 09:59:11.548003] [0x000004d2] [error] An error severity message
[2014-03-18 09:59:11.548003] [0x000004d2] [fatal] A fatal severity message

可以看到,他除了有輸出給定的字串之外,前面還會自動加上時間、以及紀錄的層級。至於中間的「0x000004d2」,則是執行序的編號,如果是多執行序程式的話,可以透過這個值來區隔紀錄是由哪個執行序產生的。

根據官網的說法,這樣使用 Boost Log 的優點,包括了:

  1. 除了訊息外,也自動地提供了時間、執行序、以及層級的資訊
  2. 這個函式庫是 Thread-safe 的,就算在不同執行序同時輸出紀錄,也不會有訊息被切斷的狀況
  3. 可以套用 filter 來過濾要顯示的內容

基本架構

基本上,Trivial logging 只是 Boost.Log 最簡單的使用方法。而如果要能完全發揮它的功能的話,則是要了解他的架構,並且去根據需求來做設定。

在架構上,Boost.Log 使採取三層式的設計,分別是「來源」(source logger)、「核心」(core)和「水槽」(sink);其中,「來源」(source logger)就是用來產生紀錄的東西,而「核心」(core)則是用來彙整記錄、作一些分析處理的元件。最後的「水槽」(sink),則是用來做記錄輸出用的。

下圖就是 Boost.Log 的架構圖(參考)。

在最左側,就是發送紀錄的來源、logger;這邊可以有多個、多種不同的紀錄來源;在發出紀錄後,會統一把資料交給「Logging Core」、這塊核心的部分來作處理。而之後,則是可以根據自己的需求,設定不同的「sink」,來作資料的處理、儲存。

而在紀錄的部分,除了最基本的文字訊息之外,也可以再加入所謂的「屬性資料」(attribute)、方便後續的處理。像是上面 Trivial logging 的 severity level,就是一種屬性資料。而在 Core 的部分,則可以根據紀錄作篩選,看要處理那些紀錄、那些要略過。

在 sink 的部分,則是分為前端(frontend)和後端(backend)兩個部分,frontend 的部分基本上是由 Boost.Log 提供的,它包含了篩選、格式化、例外處理等功能。而 backend 的部分,則是真正在把紀錄作儲存的部分,是設計成用來擴展用的~除了可以把紀錄存成檔案之外,也可以根據自己的需求來做設計、作其他像是分析、回報之類的處理。


內建類別

由於 Boost.Log 採取了 根據需求的不同,也提供了不同類型的 logger、sink 可以直接使用;而如果有需要的話,也可以根據定義、自行擴展。

在 logger 的部分,Boost.Log 主要提供了下面幾種類型(參考):

  • logger
  • severity_logger
  • channel_logger
  • severity_channel_logger

其中,logger 算是最基本的型別,根據使用的狀況,還有 logger_mt(多執行序版本)、wlogger(寬字元版、wstringwchar)、wlogger_mt 這幾種變形,可以視需要來選擇要用哪種。

severity_logger 則是以 template 的形式,加入了 severity level 的功能,可以更快速地使用這個比較普遍可能會用到的 attribute。而和基本的 logger 一樣,他也有多執行序版、以及寬字元版的變形。

除了 severity level 之外,Boost.Log 也有提供有 channel 功能的 logger,型別是 channel_logger;他提供了一個額外的 attribute,可以用來區分紀錄產生的來源。當然,如果要同時使用 severity level 和 channel 的話,Boost.Log 也提供了 severity_channel_logger 可以使用。


而在 sink 的 fronetend 的部分,Boost.og 根據多執行序的同步方法,主要提供了 unlocked_sinksynchronous_sinkasynchronous_sink 這三種不同的類別可以使用。詳細的說明可以參考官方文件

在 backend 的部分,則有

  • 輸出到 ostreamtext_ostream_backend
  • 輸出到文字檔案的 text_file_backendtext_multifile_backend
  • 輸出到 syslog 的 syslog_backend
  • 輸出成 Windows Event 的 simple_event_log_backend
  • 連結到 Visual Studio IDE 的 debug_output_backend

基本上,支援的功能相當地多!有興趣的話,可以參考官方說明。而如果有需要的話,這部分也是可以自己定義、擴展的。


使用範例

而如果要使用完整的功能呢?就如同文章一開始說的,Boost.Log 實在有點複雜,所以 Heresy 這邊就先依照自己的需求,寫了一個範例出來而已。更詳細的介紹?或許就等以後再說吧…

Heresy 這邊設想的需求,包括了:

  1. 可以同時輸出在 Console 和記錄檔
  2. 記錄檔可以附加在之前的檔案後
  3. 可以調整需要紀錄、輸出的東西

而之後,大致上就是變成下面這樣:

// STL Header
#include <fstream>
 
// Boost Log Header
#include <boost/log/trivial.hpp>
#include <boost/log/expressions.hpp>
#include <boost/log/sinks/sync_frontend.hpp>
#include <boost/log/sinks/text_ostream_backend.hpp>
#include <boost/log/support/date_time.hpp>
#include <boost/log/utility/empty_deleter.hpp>
#include <boost/log/utility/setup/common_attributes.hpp>
 
int main(int, char*[])
{
// create sink backend
boost::shared_ptr< boost::log::sinks::text_ostream_backend > backend(
new boost::log::sinks::text_ostream_backend() );
 
// add stream
backend->add_stream( boost::shared_ptr< std::ostream >(
&std::clog, boost::empty_deleter()));
backend->add_stream( boost::shared_ptr< std::ostream >(
new std::ofstream("sample.log",std::ofstream::app)));
 
// other setting
backend->auto_flush(true);
 
// create sink frontend
typedef boost::log::sinks::synchronous_sink< boost::log::sinks::text_ostream_backend > sink_t;
boost::shared_ptr< sink_t > sink(new sink_t(backend));
 
// sink format
sink->set_formatter(
boost::log::expressions::stream
<< boost::log::expressions::format_date_time< boost::posix_time::ptime >("TimeStamp", "%Y-%m-%d %T")
<< "[" << boost::log::trivial::severity  << "]\t"
<< boost::log::expressions::smessage
);
 
// sink filter
sink->set_filter(
boost::log::trivial::severity > boost::log::trivial::info
);
 
// add sink
boost::shared_ptr< boost::log::core > core = boost::log::core::get();
core->add_sink(sink);
 
// setup common attributes
boost::log::add_common_attributes();
 
// start loggoing with trival logger
BOOST_LOG_TRIVIAL(trace) << "A trace severity message";
BOOST_LOG_TRIVIAL(debug) << "A debug severity message";
BOOST_LOG_TRIVIAL(info) << "An informational severity message";
BOOST_LOG_TRIVIAL(warning) << "A warning severity message";
BOOST_LOG_TRIVIAL(error) << "An error severity message";
BOOST_LOG_TRIVIAL(fatal) << "A fatal severity message";
 
return 0;
}

這邊為了架構上的完整,所以沒有去簡化 namespace,導致整個程式碼很長,如果善用 auto、namespace alias 或是 using namespace 的話,都是可以有效簡化那些又臭又長的類別的。

首先,Boost.Log 為了記憶體管理的方便,大多是使用 boost::shared_ptr<> 這種 smart pointer 來作操作,這樣的優點是可以避免 memory leak。


而在上面的程式裡面,Heresy 是使用 text_ostream_backend 來’當作 sink 的 backend,他可以透過 add_stream() 來增加要輸出的目標,而這邊 Heresy 就是真加兩個 ostream 的物件,一個是 std::clog、另一個則是一個 ofstream

接下來,則是建立一個 synchronous_sink 的 frontend,同時讓他使用剛剛建立出來的 backend 物件作輸出。而針對 frontend 的物件,則還可以設定他的輸出格是(set_formatter())以及篩選條件(set_filter());而這邊要輸入的,則是使用 Lambda expression 來做設定。

不過,這邊的 Lambda expression 並非 C 11 標準的語法(參考),而是要搭配 Boost.Log 提供的 placeholder 來使用;而如果不習慣這種 Lambda expression 的形式的話,也可以改用 Boost.Phoenix 的形式來撰寫。有需要的話,請參考官方的說明文件

而 Heresy 在這邊格式的部分,就是輸出紀錄的時間、severity level、以及紀錄的內容。而 filter 的部分,則是限制只輸出 info 以上層級的紀錄。

在都設定完後,則是要取得 Boost.Log 的 core 物件、並透過 add_sink() 這個函式,讓 Boost.Log 把收到的紀錄丟給我們的 sink 來作輸出。


最後,前面有提到,Heresy 這邊有輸出紀錄的時間,而實際上,時間是 Boost.Log 的一個 global attribute,所以其實是需要額外作設定的。為了簡化,這邊是直接使用 Boost.Log 提供的 add_common_attributes() 這個函式;他會加入行號、時間、process id、thread id 等資料。

而在這樣都設定完了之後,就可以直接用本來的 BOOST_LOG_TRIVIAL() 來做紀錄的輸出了!

基本上,Heresy 這邊是忽略了 source logger 的設定,而直接使用內建的 trivial logger 來做操作。而如果實際根據需求、建立一個 source logger 來用的話,是會有更多可以控制的東西的~


這篇就先寫到這樣了。說實話,自己也知道寫得很零散、很多地方也都沒寫到,不過,也就先這樣了。之後如果有控認真研究完的話,再來補齊吧。

不過說實話…個人還是不太喜歡 Boost.Log 這樣大量使用 macro 的程式寫法啊…
而且,他自訂的 lambda experssion…說實話,用不慣;可以的話,Heresy 希望可以直接傳一個標準的 function object 進去就好啊!

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *