Hello, wonderful logging world!

この糞のような,素晴らしき(デバッグ)人生.

と言う程でもないのですが,少し真面目に(デバッグ)ログの残し方について学習します.「えーマジ Logger 知らないの!?Logger 知らないで許されるのは小学生までだよね!」と言われそうですが,「最近の小学生は賢いですね」と言って流す事にします.

Log4J

ざっと見ていると,Log4J のようなインターフェースが主流のようです.そう言えば,以前に C# のコードをデバッグしている時もこれに似た形の Logger でした.

Log4J には 3 つの主要なコンポーネントがあります。

  • Logger
  • Appender
  • Layout

Logger は Log4J パッケージの中心クラスで、ロギングを行う部分をグループ化し、必要なグループのログだけを出力したり、カテゴリーに優先順位をつけることにより様々な出力方法を指定することができます。

Appender はログの出力先を指定するものです。Appender で設定できる出力先は、冒頭でも述べたように、ファイル、OutputStream、Java.io.Writer、リモート Log4J サーバ、リモート Unix Syslog デーモン、Windows NT イベントログなどです。

Layout はその名の通り、ログの出力フォーマットを指定するものです。単純なテキスト出力、ユーザが指定したレイアウト、HTML のテーブルレイアウトなどを指定することができます。

C++Log4J と銘打った Logger プロジェクトはかなりの数が存在するようです.ちょっとググっただけでも以下のプロジェクトがヒットしました.

Boost.Log

Boost にはまだ Logger が存在しない(はず)のですが,いくつかの Logger プロジェクトが Boost 入りを狙っており,レビュー待ちと言う状況のようです(参考:Boostのレビュー状況 2009/12 - Faith and Brave - C++で遊ぼう).Boost と銘打っている Logger プロジェクトには,以下のようなものがありました.

Boost 系のものをざっと眺めたところ,現状では自分にはオーバースペックかなぁと言う印象が残りました.「デフォルト設定で出力するだけ」であれば簡単に使えるのですが,何らかの設定を行おうとすると覚えるまではリファレンスを見る機会が多そうなのが少し残念.出力レベルと出力先を指定する辺りまでは,もう少し簡単にできると良いかなぁと言う感想でした.

ぼくがかんがえたさいこうのろがー

嘘です.構想 1 時間,実装 1 時間.こんな感じなのかなぁと頭に浮かんだものをコードに落としてみました.インターフェースとしては,

辺りで終わる位が現状の力量では使いやすいと言う感じがしたので,上記に近い形のインターフェースで実装しました.

Example

取りあえず,先にサンプル・コード.コンパイル方法は,clx_logger_20100114.tar.gz を解凍し,clx_logger/logger.h をインクルードするのみです.ただし,shared_ptr を使用しているので,HAVE_TR1_MEMORY_H を定義するか( std::tr1::shared_ptr が存在する場合),Boost をインストールする必要があります.

#include <iostream>
#include "logger.h"

int main(int argc, char* argv[]) {
    clx::logger::configure(std::cout, clx::loglevel::warn);
    
    clx::logger::trace(DEBUGF("Hoge"));
    clx::logger::debug(DEBUGF("Hello, world!"));
    clx::logger::info(DEBUGF("Trivial info."));
    clx::logger::warn(DEBUGF("Warning! Warning!"));
    clx::logger::error(DEBUGF("Debug life is dead!"));
    clx::logger::fatal(DEBUGF("Mou DaMePo..."));
    
    /*
     * ファイルに出力.
     * 出力の直前に open() し,出力が終わると close() する.
     * プロセス実行中は,ずっとファイル・オープンの状態で良い場合は,
     * file_appender ではなく std::ofstream を引数に指定する.
     */
    clx::file_appender fapp("sample.log");
    clx::logger::configure(fapp, clx::loglevel::trace);
    
    clx::logger::trace(DEBUGF("Hoge"));
    clx::logger::debug(DEBUGF("Hello, world!"));
    clx::logger::info(DEBUGF("Trivial info."));
    clx::logger::warn(DEBUGF("Warning! Warning!"));
    clx::logger::error(DEBUGF("Debug life is dead!"));
    clx::logger::fatal(DEBUGF("Mou DaMePo..."));
    
    return 0;
}
[clown@stinger example]$ g++ -Wall example_logger.cpp 
[clown@stinger example]$ ./a
[WARN] example_logger.cpp:10:Warning! Warning!
[ERROR] example_logger.cpp:11:Debug life is dead!
[FATAL] example_logger.cpp:12:Mou DaMePo...
Logger
namespace clx {
    namespace loglevel {
        enum { trace = 10, debug = 20, info = 30, warn = 40, error = 50, fatal = 60 };
    }
    
    class logger {
    public:
        typedef std::size_t size_type;
        typedef char char_type;
        typedef std::basic_string<char> string_type;
        typedef std::basic_ostream<char_type> ostream_type;
        
        static void configure(ostream_type& out, size_type lv = loglevel::fatal);
        
        template <class AppenderT>
        static void configure(const AppenderT& cp, size_type lv = loglevel::fatal);
        
        static size_type& level();
        
        static bool put(const string_type& message, size_type lv);
        static bool trace(const string_type& message);
        static bool debug(const string_type& message);
        static bool info(const string_type& message);
        static bool warn(const string_type& message);
        static bool error(const string_type& message);
        static bool fatal(const string_type& message);
    };
}

configure() で出力先と出力レベルを指定し,その後は trace(), debug(), info(), warn(), error(), fatal() で出力していきます.logger クラス自身は,「指定された文字列を出力するかどうか」を判断する(判断して出力する)機能に留め,それ以外の機能に関しては Appender や Layout に任せます.

例えば,Lotate するかどうかは(現状では,実装してませんが) Append に,どのような書式で出力するかについては Layout に任せます.

Appender
namespace clx {
    class appender {
    public:
        typedef char char_type;
        typedef std::basic_ostream<char_type> ostream_type;
        
        appender() {}
        virtual ~appender() throw() {}
        
        virtual ostream_type& get() = 0;
        virtual bool preprocess() { return true; }
        virtual bool postprocess() { return true; }
    };
    
    typedef shared_ptr<appender> appender_ptr;
}

Appender のインターフェースです. Logger は,get() メソッドで取得した出力ストリームに対して,文字列を出力しようとします.

Logger の trace(), debug(), ... などの各メソッドは,文字列を出力する直前に preprocess() を呼び,出力した直後に postprocess() を呼びます.ファイルを開いたり(閉じたり),Rotate したりなど何らかの処理が必要な場合は,これらのメソッドに記述します.

namespace clx {
    class ostream_appender : public appender {
    public:
        explicit ostream_appender(ostream_type& out);
        virtual ~ostream_appender() throw();
        virtual ostream_type& get();
        
    private:
        ostream_type& out_;
        ostream_appender(const ostream_appender& cp);
        ostream_appender& operator=(const ostream_appender& cp);
    };
}

出力ストリームをラップするための Appender です.Non-copyable な為,通常ユーザが直接このクラスを利用する事はありません.

namespace clx {
    class file_appender : public appender {
    public:
        typedef std::basic_string<char_type> string_type;
        
        explicit file_appender(const string_type& path);
        virtual ~file_appender() throw();
        virtual ostream_type& get();
        virtual bool preprocess();
        virtual bool postprocess();
    };
}

Logger が文字列を出力するたびに,open/close を行う Appender です.プロセスが非常に長い間起動し続けているなど,ファイルを開きっぱなしにできない場合に使用します.

Layout

Layout に関しては,特にインターフェースは設けていません.Logger の各種出力メソッドが文字列 (std::string) の 1 引数しか受け付けないので,それと整合性の合うようなクラス/関数を必要に応じて作成します.

namespace clx {
    class debug_layout {
    public:
        typedef char char_type;
        typedef std::basic_string<char_type> string_type;
        
        debug_layout(const char_type* file, int line);
        ~debug_layout() throw();
        string_type operator()(const string_type& message) const;
    };
}

#define DEBUGF clx::debug_layout(__FILE__, __LINE__)

debug_layout は,ユーザが指定した文字列の先頭に,ファイル名と行番号を付与した文字列を返す関数オブジェクトです.通常は,DEBUGF("文字列") のような形で使用します(参考:いずみのぶろぐ : 恐るべし関数オブジェクト).

Logger に関しては,どんな要求があるのかなどまだ良く分かってない部分が多いので,もう少しいろいろ読んでみようと思います.