有一個很簡單的需求,一開始我以為對 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 是一件很惱人的事。

,

novus 發表在 痞客邦 PIXNET 留言(0) 人氣()