1. 程式人生 > >Boost log庫

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