Boost log庫
本文首先介紹了boost.log的幾個重要的概念,然後分析其框架結構,最後詳細解析了一段示例程式碼,並總結了將boost.log應用到自己的程式中時的步驟。
1. 幾個概念
- 日誌記錄:一個獨立的訊息包,這個訊息包還不是實際寫到日誌裡的訊息,它只是一個候選的訊息。
- 屬性:日誌記錄中的一個訊息片。
- 屬性值:那就是上面所說的屬性的值了,可以是各種資料型別。
- 日誌槽(LOG SINK):日誌寫向的目標,它要定義日誌被寫向什麼地方,以及如何寫。
- 日誌源:應用程式寫日誌時的入口,其實質是一個logger物件的例項。
- 日誌過濾器:決定日誌記錄是否要被記錄的一組判斷。
- 日誌格式化:決定日誌記錄輸出的實際格式。
- 日誌核心:維護者日誌源、日誌槽、日誌過濾器等之間的關係的一個全域性中的實體。主要在初始化logging library時用到。
2. 框架結構
如圖,
(1). 應用程式在圖的右側,通過一個或多個logger例項傳送日誌訊息。
(2). 應用程式也可以出現在左側,那就是一個日誌的顯示例項了。
(3). 一個日誌記錄的資料中會包括許多屬性。屬性基本上是一個函式,它的返回值就是屬性值。比如時間不是一個函式(也是一個屬性)。
(4). 有三種類型的屬性集:全域性的,特定執行緒的,特定源的。前兩個是由logging core來維護的,所以不用再初始化。
(4.1). 全域性屬性集中的屬性被連線到所以的日誌物件上。
(4.2). 執行緒屬性集中的屬性會連線到把它註冊到屬性集時的那個執行緒。
(4.3). 源屬性集由初始化日誌的源來維護的,它會連線到一個特定的源上。
(4.4). 當一個源初始化日誌物件的時候,它會從上述的三個屬性集的所有屬性中得到屬性值。這些值會在將來處理。
如果在不同的屬性集中有相同的屬性名字的時候就會造成衝突,解決衝突的方法是全域性屬性集的優先順序最低,源屬性集的優先順序最高。高優先順序的屬性會覆蓋低優先順序的屬性。
(5). 當組合屬性值的時候,logging core來決定一個屬性是否要被送到sink中,這就是過濾。有兩層過濾,首先應用的是全域性中過濾,全域性過濾用來快速的過濾掉那些不需要的日誌記錄。然後就是sink指定的過濾了。每個sink都有單獨的過濾器。sink過濾器允許將一個日誌記錄定向到一個指定的sink。
(6). 如果一個日誌記錄至少通過了一個sink的話,它就可以用了。這時候就是日誌訊息格式化的時候了。格式化完成的日誌訊息和屬性值一起被送到接收它們的sink中。
(7). 如上圖所示,sink被分為前端和後端兩個部分。這是為了抽象sink的通用功能,如過濾和執行緒同步。前端由日誌庫提供,使用者不大可能再去實現它。而後端很可能是在日誌庫的外面,它來實現對日誌記錄的處理。如寫檔案,傳送到網路等。日誌庫提供了大部分通常用到的後端程式。
3. 示例程式碼解析
這個示例程式碼在boost.log的basic_usage裡,檔案前面include就省略了。
namespace logging = boost::log;
namespace fmt = boost::log::formatters;
namespace flt = boost::log::filters;
namespace sinks = boost::log::sinks;
namespace attrs = boost::log::attributes;
namespace src = boost::log::sources;
using boost::shared_ptr;
// 這裡定義了一個日誌級別的enum,後面在日誌輸出時會是一個屬性值
enum severity_level
{
normal,
notification,
warning,
error,
critical
};
// 定義上面的級別輸出流操作的過載函式,在日誌輸出時會用到
template< typename CharT, typename TraitsT >
inline std::basic_ostream< CharT, TraitsT >& operator<< (
std::basic_ostream< CharT, TraitsT >& strm, severity_level lvl)
{
static const char* const str[] =
{ // 這裡的每一個值要與severity_level enum對應
"normal",
"notification",
"warning",
"error",
"critical"
};
//如果日誌的級別在enum裡,則輸出相應的文字
if (static_cast< std::size_t >(lvl) < (sizeof(str) / sizeof(*str)))
strm << str[lvl];
else //否則直接輸出數字值
strm << static_cast< int >(lvl);
return strm;
}
// 這個函式用來測試日誌巢狀輸出的功能
int foo(src::logger& lg)
{
BOOST_LOG_FUNCTION(); // 這裡會在Scope屬性中加入“foo”
BOOST_LOG(lg) << "foo is being called";
return 10;
}
int main(int argc, char* argv[])
{
// 建立一個sink: synchronous_sink是sink frontend, text_ostream_backend是sink backend
// text_ostream_backend可以將日誌以文字的方式輸出
// synchronous_sink可以處理執行緒同步的問題,也就是在多個執行緒同時使用這個sink時,
// 我們的應用程式不用再考慮執行緒同步的問題了。
typedef sinks::synchronous_sink< sinks::text_ostream_backend > text_sink;
shared_ptr< text_sink > pSink(new text_sink);
//好了,現在pSink就是一個text_sink型別的shared_ptr指標了
{ // 這裡限定的區域是為了下面的鎖
// 獲取一個backend的鎖指標.
// 因為有了synchronous_sink型別的frontend,我們這裡只要有這個locked_backend()
// 就保證在此處操作時不會有其它的執行緒同時操作.
text_sink::locked_backend_ptr pBackend = pSink->locked_backend();
// 既然backend是一個text_ostream型別的,我們就可以加入一些ostream型別的輸出流給他
// 日誌會同時輸出到這些輸出流中
// 先加一個std::clog給它
shared_ptr< std::ostream > pStream(&std::clog, logging::empty_deleter());
// shared_ptr會在指標不再使用時刪除它, 但std::clog是不能刪除的, 所以加logging::empty_deleter()
pBackend->add_stream(pStream);
// 再加一個std::ofstream給它
shared_ptr< std::ofstream > pStream2(new std::ofstream("sample.log"));
assert(pStream2->is_open());
pBackend->add_stream(pStream2);
}
// 好了,我們已經做好了一個sink, 現在將它加入到logging library裡
logging::core::get()->add_sink(pSink);
// 再建立一個logger, 我們就可以用它來輸出了.
src::logger lg;
// Hello, World 一下, 在sample.log檔案和控制檯上會同時顯示
BOOST_LOG(lg) << "Hello, World!";
// 格式化輸出, 也是用locked_backend來操作, 此時指定的屬性要在後臺逐一定義.
pSink->locked_backend()->set_formatter(fmt::ostrm
<< fmt::attr("LineID") // 這個是指日誌檔案的行號,不是程式原始檔的行號
<< " [" << fmt::date_time< boost::posix_time::ptime >("TimeStamp", "%d.%m.%Y %H:%M:%S.%f")
<< "] [" << fmt::attr< severity_level >("Severity") // 注意這裡的severity_level正是我們前面定義的enum
<< "] [" << fmt::time_duration< boost::posix_time::time_duration >("Uptime") // 這個屬性在後面會被定義成一個執行緒範圍的屬性
<< "] ["
<< fmt::attr< std::string >("Tag") // 這個Tag只是一個字串型別的屬性
<< "] ["
<< fmt::named_scope("Scope", fmt::keywords::scope_iteration = fmt::keywords::reverse) << "] " // 這個Scope屬性就是列印巢狀函式的東西了
<< fmt::message()); // 最後,別忘了將最重要的日誌內容列印了.
/*
// 這是另外一種格式化的方法, 好像更簡單一些.
pSink->locked_backend()->set_formatter(
fmt::format("%1% @ %2% [%3%] >%4%< Scope: %5%: %6%")
% fmt::attr("LineID")
% fmt::date_time< boost::posix_time::ptime >("TimeStamp", "%d.%m.%Y %H:%M:%S.%f")
% fmt::time_duration< boost::posix_time::time_duration >("Uptime")
% fmt::attr< std::string >("Tag")
% fmt::named_scope("Scope", fmt::keywords::scope_iteration = fmt::keywords::reverse, fmt::keywords::scope_depth = 2)
% fmt::message());
*/
// 下面開始設定屬性了.
// LineID是一個計數器,先建立一個初始值為1的計數器.
shared_ptr< logging::attribute > pCounter(new attrs::counter< unsigned int >(1));
// 將它加入到全域性屬性中,如果要求將不同的內容輸出到不同的日誌檔案中去,這裡設定為全域性屬性可能就是不太合適了.
logging::core::get()->add_global_attribute("LineID", pCounter);
// 下面是設定TimeStamp屬性
shared_ptr< logging::attribute > pTimeStamp(new attrs::local_clock());
logging::core::get()->add_global_attribute("TimeStamp", pTimeStamp);
// 設定Uptime屬性為執行緒級屬性,因為執行時間只能在一個執行緒內衡量才有意義
// attrs::timer應該是一個boost::posix_time::time_duration型別的值,會記錄上本次呼叫與上一次呼叫的時間差。
BOOST_LOG_SCOPED_THREAD_ATTR("Uptime", attrs::timer);
// Socpe也是一個執行緒級的屬性,add_thread_attribute是另外一個增加執行緒級屬性的方法
boost::shared_ptr< logging::attribute > pNamedScope(new attrs::named_scope());
logging::core::get()->add_thread_attribute("Scope", pNamedScope);
// 設定日誌的Scope,也就是“main”函式
BOOST_LOG_FUNCTION();
// 現在再輸出兩個日誌記錄,結果是這樣的:
// 1 [08.12.2009 11:16:42.750000] [] [00:00:00.000079] [] [int __cdecl main(int,char *[])] Some log line with a counter
// 2 [08.12.2009 11:16:42.765625] [] [00:00:00.016310] [] [int __cdecl main(int,char *[])] Another log line with the counter
BOOST_LOG(lg) << "Some log line with a counter";
BOOST_LOG(lg) << "Another log line with the counter";
// 注意到上面有兩個空的屬性,一個是severity_leve, 另一個是Tag
// 下面設定一下Tag.
{
BOOST_LOG_NAMED_SCOPE("Tagging scope"); // 這裡設定一個這個區域的名字為“Tagging scope”,輸出scope屬性值時會增加這個scope
// 現在增加給lg增加一個臨時的屬性.
// 每一個在當前scope裡用lg輸出的日誌記錄,它的“Tag”屬性值都是“Tagged line”
BOOST_LOG_SCOPED_LOGGER_TAG(lg, "Tag", std::string, "Tagged line");
// 也可以用下面的程式碼實現:
// attrs::constant< std::string > TagAttr("Tagged line");
// logging::scoped_attribute _ =
// logging::add_scoped_logger_attribute(lg, "Tag", TagAttr);
// 再輸出兩條看一下,結果是這樣的, 注意“Tagged line”和“Tagging scope”:
// 3 [08.12.2009 11:16:42.781250] [] [00:00:00.032886] [Tagged line] [Tagging scope<-int __cdecl main(int,char *[])] Some tagged log line
// 4 [08.12.2009 11:16:42.812500] [] [00:00:00.051012] [Tagged line] [Tagging scope<-int __cdecl main(int,char *[])] Another tagged log line
BOOST_LOG(lg) << "Some tagged log line";
BOOST_LOG(lg) << "Another tagged log line";
}
// 這裡再輸出一行,就沒有上面那個區域中的“Tag line”和“Tagging scope”了:
// 5 [08.12.2009 11:16:42.828125] [] [00:00:00.068013] [] [int __cdecl main(int,char *[])] Now the tag is removed
BOOST_LOG(lg) << "Now the tag is removed";
// 現在可以看一下過濾器的使用了。
// 過濾器的過濾條件是基於屬性的。
// 每一個過濾器其實就是一個返回值為bool型的函式物件.
// 過濾器可以指定到sink,也可以指定到全域性.
// 像下面這樣可以為一個sink設定過濾器:
//pSink->set_filter(
// flt::attr< severity_level >("Severity") >= warning // 輸出所有Severity屬性值大於等於warning的日誌記錄
// || flt::attr< std::string >("Tag").begins_with("IMPORTANT")); // 或者Tag屬性值以“IMPORTANT”開頭的
// 對於std::string或std::wstring型別的屬性有一些謂詞可以使用:
// "begins_with", "ends_with", "contains", "matches"
// 其中matches謂詞可以RegEx表示式
// 下面是設定全域性的過濾器
logging::core::get()->set_filter(
flt::attr< severity_level >("Severity") >= warning // Write all records with "warning" severity or higher
|| flt::attr< std::string >("Tag").begins_with("IMPORTANT"));
// 這時候,我們可以用“lg”來輸出日誌記錄了。
//
// 另外,還有一個severity_logger,可以直接使用它來做logger
// 如果想增加一些功能,可以派生於它的類
src::severity_logger< severity_level > slg;
// 由於我們前面設定了過濾器(不論是全域性的還是sink的都影響),所以下一行的normal日誌記錄將不會輸出。其它是設定為全域性和執行緒的屬性對於slg也同樣適用。
BOOST_LOG_SEV(slg, normal) << "A normal severity message, will not pass to the output";
BOOST_LOG_SEV(slg, error) << "An error severity message, will pass to the output";
{
// 這裡設定一個以“IMPORTANT”開頭的Tag屬性
BOOST_LOG_SCOPED_THREAD_TAG("Tag", std::string, "IMPORTANT MESSAGES");
// 下面再用slg輸出一個normal日誌。
// 這裡沒有指定level,但severity_logger預設級別為0,在這個程式裡就是normal
// 也可以指定severity_logger的預設級別
BOOST_LOG(slg) << "Some really urgent line";
}
// reset_filter()了sink的filter,如果前面設定了sink的過濾器,這裡會取消掉。但全域性的不會被reset
pSink->reset_filter();
// 下面會先輸出foo裡的日誌記錄,然後再輸出這個日誌記錄
BOOST_LOG(lg) << "The result of foo is " << foo(lg);
return 0;
}
4. 總結
boost.log框架主要是由日誌源,全域性庫,sink組成。
- 在上面的程式中,日誌源就是src::logger lg和src::severity_logger< severity_level > slg。
- 全域性庫就是logging::core::get()。
- sink就是pSink,這是一個sinks::synchronous_sink< sinks::text_ostream_backend >型別的指標。
sink的backend可以設定輸出流,可以設定輸出格式。
屬性可以設定為全域性的,執行緒的,日誌源的。也可以在一個區域中設定一個臨時的屬性。
問: 說,要將日誌放檔案裡, 總共分幾步?
- 答: 總共分三步
- 第一步、 建立一個sink, 向sink加入檔案輸出流;
- 第二步、將sink加入到logging library裡, 並建立一個logger;
- 第三步、向logger輸出日誌記錄。
5. 其它
- 如何限制日誌檔案的長度?
// 建立一個格式化物件, 另一種格式化的方法
boost::function< void (std::ostream&, logging::attribute_values_view const&, std::string const&) > formatter =
fmt::ostrm
<< fmt::attr< unsigned int >("LineID", "[%09u] ")
<< fmt::date_time< boost::posix_time::ptime >("TimeStamp") << " *"
<< fmt::message();
// 建立一個sink
boost::shared_ptr< sinks::synchronous_sink< sinks::text_ostream_backend > > sink(
new sinks::synchronous_sink< sinks::text_ostream_backend >);
// 增加常用屬性,也就是LineID和TimeStamp這兩個屬性。這個方便一點
logging::add_common_attributes();
// 設定sink的輸出格式
sink->locked_backend()->set_formatter(formatter);
// 日誌檔案將1個小時換一個,且檔案大小不會超過1MB。檔名從file_00.log開始
sink->locked_backend()->add_stream(boost::make_shared< boost::log::rotating_ofstream >(
"file_%02N.log", logging::keywords::rotation_interval = 3600, logging::keywords::rotation_size = 1048576));
// 將sink加到logging::core裡面
logging::core::get()->add_sink(sink);