有一個很簡單的需求,一開始我以為對 Boost.Log 這樣功能強大的程式庫應該輕而易舉,結果花了我一點時間才摸出門徑。條件是這樣子的:
- 必須支援命令列和檔案輸出。
- 在支援 ANSI color code 的環境中,允許使用者啟用彩色輸出,程式會依照 log record 的 severity level 改變輸出顏色。
- 若環境不支援 ANSI color code,程式不應該輸出色彩,否則可讀性會慘遭 ANSI color code 破壞。
- 無論如何檔案都不應該輸出 ANSI color code,理由同上,更何況各家的 log viewer 都已經有自動上色的功能了。
假設既有的片段如下,為了簡短起見,一些細節被我簡化了,這不是隨貼即用程式碼。
#include <boost/shared_ptr.hpp> #include <boost/phoenix/bind/bind_function_object.hpp> #include <boost/utility/empty_deleter.hpp> #include <boost/date_time/posix_time/posix_time_types.hpp> #include <boost/log/core.hpp> #include <boost/log/attributes.hpp> #include <boost/log/sinks.hpp> #include <boost/log/expressions.hpp> #include <boost/log/utility/setup/file.hpp> #include <boost/log/utility/setup/console.hpp> #include <boost/log/utility/setup/common_attributes.hpp> #include <boost/log/attributes/named_scope.hpp> #include <boost/log/support/date_time.hpp> #include <boost/log/expressions/keyword.hpp> #include <iostream> #include <fstream> #include <string> #define ANSI_CLEAR "\x1B[0;00m" #define ANSI_RED "\x1B[1;31m" #define ANSI_GREEN "\x1B[1;32m" #define ANSI_YELLOW "\x1B[1;33m" #define ANSI_BLUE "\x1B[1;34m" #define ANSI_MAGENTA "\x1B[1;35m" #define ANSI_CYAN "\x1B[1;36m" namespace blog = boost::log; namespace attrs = blog::attributes; namespace expr = blog::expressions; namespace keywords = blog::keywords; namespace sinks = blog::sinks; enum LogLevel {LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR, LOG_FATAL}; BOOST_LOG_INLINE_GLOBAL_LOGGER_DEFAULT( mainLogger, boost::log::sources::severity_logger_mt<LogLevel>); void initLog() { initConsoleLog(true); initFileLog("default.log"); blog::add_common_attributes(); }
現在我把重點放在如何實作 initConsoleLog() 上。
第一個嘗試
我的第一個嘗試參考了 Boost.Log 文件中關於擴充程式庫的範例 ,重點在於提供自訂的 formatter 和 formatter_factory:
class LogLevelFormatter { public: typedef void result_type; explicit LogLevelFormatter(const std::string& format) : useColor_(format == "color") {} void operator() (blog::formatting_ostream& strm, const blog::value_ref<LogLevel>& value) { if (value) { LogLevel level = value.get(); if (useColor_) { switch (level) { case LOG_DEBUG: strm << ANSI_GREEN; break; case LOG_ERROR: ..... } strm << level << ANSI_CLEAR; } else { strm << level; } } } private: bool useColor_; }; class LogLevelFormatterFactory : public blog::basic_formatter_factory<char, LogLevel> { public: formatter_type create_formatter( const blog::attribute_name& name, const args_map& args) { args_map::const_iterator it = args.find("format"); if (it != args.end()) { return boost::phoenix::bind( LogLevelFormatter(it->second), expr::stream, expr::attr<LogLevel>(name)); } else { return expr::stream << expr::attr<LogLevel>(name); } } };
然後 initConsoleLog() 大致上像這樣:
void initConsoleLog(bool useColor = true) { blog::register_formatter_factory( "Severity", boost::make_shared<LogLevelFormatterFactory>()); if (useColor) { blog::add_console_log( std::clog, keywords::format = "<%TimeStamp%> [%Severity(format=\"color\")%] %Message%"); } else { blog::add_console_log( std::clog, keywords::format = "<%TimeStamp%> [%Severity%] %Message%"); } }
這個做法看起來有點小麻煩,而且如果不只想幫 Severity 這個屬性加顏色,還希望將整條訊息用花俏的方式上色(例如,遇到 fatal 就把整條訊息的背景換成紅色),這個方法就顯得很吃力。
這個方法最大的好處在於,只要 formatter factory 設定好,我們可以利用 boost::log::init_from_stream() 直接從設定檔載入所有設定,後續的改變某種程度上不需要改動程式碼。
第二種做法
我比較喜歡簡單的方法:
void consoleColorFormatter(blog::record_view const& rec, blog::formatting_ostream& strm) { blog::formatter timestampFormat = expr::stream << expr::format_date_time<boost::posix_time::ptime>( "TimeStamp", "<%Y-%m-%d %H:%M:%S>"); timestampFormat(rec, strm); // If further formatting is not needed. // strm << blog::extract<boost::posix_time::ptime>("TimeStamp", rec).get(); LogLevel level = blog::extract<LogLevel>("Severity", rec).get(); switch (level) { case LOG_DEBUG: strm << ANSI_GREEN; break; case LOG_ERROR: ..... } strm << " [" << level << "] " << ANSI_CLEAR; strm << rec[expr::smessage]; } void initConsoleLog(bool useColor = true) { using boost::shared_ptr; typedef sinks::text_ostream_backend Backend; typedef sinks::synchronous_sink<Backend> Sink; shared_ptr<Sink> sink = blog::add_console_log(std::clog); if (useColor) { sink->set_formatter(&consoleColorFormatter); } else { sink->set_formatter( /*...whatever...*/ ); } }
這裡唯一需要注意的是,expr::stream 並不直接輸出,而是產生一個 function object。這個方法的優缺點剛好和第一種方法相反,愛怎麼上色就怎麼上色,但格式就綁在程式碼中了。
我沒有仔細研究過能不能直接用 Boost.Log 的 lambda expression 實作,畢竟三不五時學一下這種自有品牌的 lambda expression 是一件很惱人的事。
文章標籤
全站熱搜
留言列表