有一個很簡單的需求,一開始我以為對 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 "[0;00m"
#define ANSI_RED "[1;31m"
#define ANSI_GREEN "[1;32m"
#define ANSI_YELLOW "[1;33m"
#define ANSI_BLUE "[1;34m"
#define ANSI_MAGENTA "[1;35m"
#define ANSI_CYAN "[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 是一件很惱人的事。
文章標籤
全站熱搜
