一般在寫程式的時候,難免會需要把程式的執行狀態記錄下來、或是做輸出;而一般時候,最單純的方法,就是直接透過 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 的物件來使用了~而所傳入的 trace、debug 這些值,則是 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 的優點,包括了:
- 除了訊息外,也自動地提供了時間、執行序、以及層級的資訊
- 這個函式庫是 Thread-safe 的,就算在不同執行序同時輸出紀錄,也不會有訊息被切斷的狀況
- 可以套用 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(寬字元版、wstring 或 wchar)、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_sink、synchronous_sink、asynchronous_sink 這三種不同的類別可以使用。詳細的說明可以參考官方文件。
在 backend 的部分,則有
-
輸出到 ostream 的 text_ostream_backend
-
輸出到文字檔案的 text_file_backend 與 text_multifile_backend
-
輸出到 syslog 的 syslog_backend
-
輸出成 Windows Event 的 simple_event_log_backend
-
連結到 Visual Studio IDE 的 debug_output_backend
基本上,支援的功能相當地多!有興趣的話,可以參考官方說明。而如果有需要的話,這部分也是可以自己定義、擴展的。
使用範例
而如果要使用完整的功能呢?就如同文章一開始說的,Boost.Log 實在有點複雜,所以 Heresy 這邊就先依照自己的需求,寫了一個範例出來而已。更詳細的介紹?或許就等以後再說吧…
Heresy 這邊設想的需求,包括了:
- 可以同時輸出在 Console 和記錄檔
- 記錄檔可以附加在之前的檔案後
- 可以調整需要紀錄、輸出的東西
而之後,大致上就是變成下面這樣:
// 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 進去就好啊!