軽量で拡張性の高いログフレームワーク – 基本ツールとログフォーマットモジュールの実装

基本ツールとログフォーマットモジュールの実装

目次

  • 基本ツールとログフォーマットモジュールの実装
    • 一、実用ツールクラスの設計
      • [1.1 システム時刻の取得](#1.1 システム時刻の取得)
      • [1.2 ファイル関連操作](#1.2 ファイル関連操作)
    • 二、ログレベルクラス
    • 三、ログメッセージクラス
    • 四、ログ出力フォーマットモジュール
      • [4.1 フォーマットサブ項目](#4.1 フォーマットサブ項目)
      • [4.2 フォーマット文字列ビルダー](#4.2 フォーマット文字列ビルダー)
        • メンバ変数
        • フォーマット文字列構築ルールの解析関数
        • コンストラクタ
        • フォーマット出力関数
      • [4.3 フォーマットモジュールのまとめ](#4.3 フォーマットモジュールのまとめ)
    • 五、まとめ

これは、C++ ログシステムの学習ノートであり、同期および非同期のデュアルモードログの設計と実装を網羅しています。主な特徴:モジュール化された階層構造(フォーマット→書き込み→ ロガー→ビルダーインターフェース)、ダブルバッファによるプロデューサー・コンシューマーモデル、各種デザインパターン(シングルトン/ファクトリ/プロキシ/ビルダー)の適用、C++11のマルチスレッドおよびスマートポインタの実践。コードはLinux上で直接コンパイル・テスト可能です。

これは本プロジェクトの2回目の開発ノートです。興味のある方は、以前の関連コンテンツ

もご覧ください。

一、ユーティリティクラスの設計

本節では、ログシステムプロジェクトのコード記述を本格的に開始します。まず、ユーティリティクラスを設計します。ユーティリティクラスには、システム時刻の取得、ファイルの存在確認、ファイルのディレクトリパスの取得、およびディレクトリの作成という4つの機能インターフェースが含まれます。

1.1 システム時刻の取得

この機能は Date クラスにカプセル化されており、静的メンバ関数 getTime を通じて提供されます。getTime関数の内部では、標準ライブラリのtime関数を呼び出してシステム時刻を取得し、time_t型の結果を返します。実装は非常に簡単で、ctime/time.hヘッダーファイルをインクルードするだけで済みます。Dateクラスも同様にlogsys::util名前空間下に配置されています。静的メンバ関数として設計されているため、呼び出し時にクラスのインスタンスを作成する必要はなく、Date::getTime () を通じて直接システム時刻を取得できます。この機能はログファイルの命名に使用され、時刻をログファイル名の一部として、時間単位でローテーションするログファイル管理戦略を実現します。

#include <ctime>
namespace logsys{
    namespace util{
        class Date{
            public:
            // 現在の時刻を取得
            static time_t getTime(){
                return time(nullptr) + 8LL * 3600;
            }
        };
    }
}

実際の使用において、この関数にはタイムゾーンの補正機能がないことが判明しました。我々は北京時間を使用しているため、time関数の後に8LL * 3600を追加する必要があります。

1.2 ファイル関連の操作

ファイルの存在確認インターフェースは、access関数またはstat関数を使用して実装されます。access関数はLinuxで利用可能ですが、stat関数はクロスプラットフォームでの互換性がより優れています。ファイルのパス取得インターフェースは、ファイルのパス名を受け取り、そのファイルが属するディレクトリのパスを返します。ディレクトリ作成インターフェースは、指定されたパスにディレクトリを作成するために使用されます。

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
class File{
public:
    // ファイルの存在確認
    static bool exist(const std::string& pathName){
        struct stat filestat;
        if(stat(pathName.c_str(), &filestat) < 0){
            return false;
        
}
        return true;
    }
    // ファイルパスを取得
    static std::string getFilePath(const std::string& pathName){
        // 最後の区切り文字を検索
        int pos = pathName.find_last_of(「/\\\\」);
        
if(pos == std::string::npos){
            //見つからなかった場合、現在のファイル内にあることを意味する
            return 「.」;
        }
        return pathName.substr(0, pos + 1);
    }
    //ファイルパスの作成
    static void creatDirectory(const std::string& pathName){
        int index = 0;
        
int pos = 0;

        while(index < pathName.size()){
            pos = pathName.find_first_of(「/\\\\」, index);
            
if(pos == std::string::npos){
                //見つからなかった場合、ファイルパスは1階層のみであることを意味する
                mkdir(pathName.c_str(), 0777);
                break;
            }

            std::string filePtah = pathName.substr(0, pos + 1);//毎回、先頭から順次作成する
            
if(exist(filePtah)){
                // ファイルパスがすでに存在する場合
                index = pos + 1;
                continue;
            }

            mkdir(filePtah.c_str(), 0777);
            index = pos + 1;
        }
    }
};

ファイルの存在を確認する2つの実装方法:

  • 1つ目は、accessシステムコール関数を使用する方法です。
  • 2つ目は、stat関数を使用してファイルの属性情報を取得する方法です。statの呼び出しが成功するとtrueが返され、ファイルが存在することを示し、失敗した場合はfalseが返されます。

2. ログレベルクラス

ログレベルクラスの主な機能は、ログシステムにおけるさまざまなログレベルとその意味を列挙し、ログレベルの列挙値を対応する文字列に変換するインターフェースを提供することです。

  • ログレベルには、未知レベル(UNKNOWN、0に設定)、デバッグレベル(DEBUG、プログラムのデバッグ用)、情報レベル(INFO、プログラムの実行中の通知用) 、警告レベル(WARNING、問題が発生する可能性がある状況用)、エラーレベル(ERROR、プログラムでエラーが発生した際用)、致命的なエラーレベル(FATAL、プログラムを強制終了させる必要がある重大なエラー用)。
  • ログシステムにはデフォルトの出力レベルが設定されており、ログレベルがデフォルトの閾値以上である場合にのみ出力されます。例えば、 デフォルトレベルが ERROR に設定されている場合、ERROR および FATAL レベルのログのみが出力されます。
  • ログレベルクラスには、独自に定義したログレベル(列挙型)を文字列に変換するインターフェースも含める必要があります。
#include <string>

namespace logsys
{
    class LogLevel
    {
    public:
        enum VALUE
        {
            UNKNOW = 0, // 未知のログレベル
            DEBUG, // デバッグログレベル
            INFO, // 情報ログレベル
            WARNING, // 警告ログレベル
            ERROR, // エラーログレベル
            
FATAL // 致命的なエラーのログレベル
        };

        static const std::string levToStr(VALUE level)
        {
            switch (level)
            {
            case UNKNOW:
                return 「UNKNOW」;
            case DEBUG:
                return 「DEBUG」;
            
case INFO:
                return 「INFO」;
            case WARNING:
                return 「WARNING」;
            case ERROR:
                return 「ERROR」;
            case FATAL:
                return 『FATAL』;
            default:
                return 「UNKNOW」;
            }
        }
    };
}

三、ログメッセージクラス

ログメッセージクラスは、1つのログメッセージを格納するために必要な各要素を保持することを目的として設計されています。これらの要素には、日時、ログレベル、ソースファイル名、ソースコードの行番号、スレッドID、ログ本文、およびロガー名が含まれます。

  • 日時:エラーが発生した時刻を記録します。必須項目です。
  • ログレベル:ログ情報のタイプ(ヒントや致命的なエラーなど)を表し、フィルタリングやトラブルシューティングに使用されます。
  • ソースファイル名とソースコードの行番号:エラーが発生した具体的なコードの位置を特定するために使用されます。
  • スレッドID:どのスレッドまたはプロセスがエラーを発生させたかを識別するために使用されます。
  • ログ本文:「ソケットの作成に失敗しました」など、具体的なエラーの説明です。
  • ロガー名:複数のロガーが存在するシステムにおいて、異なるプロジェクトグループのログ出力を区別するために使用されます。
#include 「logLevel.hpp」
#include 「util.hpp」
#include <thread>

namespace logsys
{
    struct LogMessage
    {
        time_t _time;
        size_t _line;
        std::thread::id _tid;
        std::string _loggerName;
        
std::string _fileName;
        LogLevel::VALUE _level;
        std::string _message;

        LogMessage() {}
        LogMessage(size_t line, const std::string &loggerName, const std::string &fileName,
                   LogLevel::VALUE level, const std::string &message)
            
: _line(line), _loggerName(loggerName), _fileName(fileName),
              _level(level), _message(message) {
                _time = util::Date::getTime();
                _tid = std::this_thread::get_id();
            }
    };
}

タイムスタンプは util::Date::getTime() を呼び出すことで自動的に設定され、スレッド ID は std::this_thread::get_id() によって自動的に取得されます。


4. ログ出力フォーマットモジュール

柔軟なログ出力フォーマットのカスタマイズ機能を提供するため、このモジュールの主な機能は、ログメッセージの文字列フォーマットを行い、指定された形式の文字列に整えて出力することです。

4.1 フォーマットサブクラス

このモジュールには、フォーマットサブクラスの基底クラス、およびこの基底クラスから派生した各サブクラスが含まれています。

1つのログ内の他の要素を拡張しやすくするため、ログメッセージに必要な各要素について、それぞれサブクラスを派生させてその要素のフォーマット出力を行っています(例:日付フォーマットサブクラス、スレッドIDフォーマットサブクラス、ロガー名フォーマットサブクラス、ログレベルフォーマットサブクラス、ファイル名および行番号フォーマットサブクラス、メッセージ内容フォーマットサブクラス、改行コードフォーマットサブクラス)。今後、ユーザーが他のタイプの要素を必要とした場合、新たに派生クラスを追加して拡張することができます。

namespace logsys
{
    // フォーマット内容のビルダーの基底クラス
    class FormatItem
    {
    public:
        using Ptr = std::shared_ptr<FormatItem>;
        virtual void format(std::ostream &out, LogMessage &msg) = 0;
    };
    
// 時刻のフォーマット
    class TimeFormatItem : public FormatItem
    {
    public:
        TimeFormatItem(const std::string &fmt = 「%H:%M:%S」) : _time_fmt(fmt) {}
        void format(std::ostream &out, LogMessage &msg) override
        
{
            struct tm t;
            localtime_r(&msg._time, &t);

            char s[32] = {0};
            strftime(s, 31, _time_fmt.c_str(), &t);
            out << s;
        }

    private:
        
std::string _time_fmt;
    };

    // 行番号のフォーマット
    class LineFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, LogMessage &msg) override
        {
            out << msg._line;
        }
    };
    
// スレッドIDのフォーマット
    class TidFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, LogMessage &msg) override
        {
            out << msg._tid;
        }
    };

    // ロガー名のフォーマット
    
class LoggerNameFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, LogMessage &msg) override
        {
            out << msg._loggerName;
        }
    };
    
// ファイル名のフォーマット
    class FileNameFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, LogMessage &msg) override
        {
            out << msg._fileName;
        }
    };
    
// ログレベルのフォーマット
    class LevelFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, LogMessage &msg) override
        {
            out << LogLevel::levToStr(msg._level);
        }
    };
    
// ログの主要情報
    class MsgFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, LogMessage &msg) override
        {
            out << msg._message;
        }
    };
    
// タブ区切りフォーマット
    class TabFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, LogMessage &msg) override
        {
            out << 「\\t」;
        }
    };

    // 改行フォーマット
    
class NLineFormatItem : public FormatItem
    {
    public:
        void format(std::ostream &out, LogMessage &msg) override
        {
            out << 「\\n」;
        }
    };

    // その他の文字のフォーマット
    class OtherFormatItem : public FormatItem
    {
    public:
        
OtherFormatItem(const std::string &str) : _str(str) {}
        void format(std::ostream &out, LogMessage &msg) override
        {
            out << _str;
        }

    private:
        std::string _str;
    };
}

4.2 フォーマット文字列コンストラクタ

このクラスが、フォーマットされたログ情報を生成するコンストラクタです。ユーザーがこのようなコンストラクタオブジェクトを作成する際には、フォーマット文字列の構築ルール(文字列型、例:"[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%
T%m%n"
)を指定する必要があります。その後、コンストラクタはこの引数を解析し、対応するフォーマット項目を順にフォーマット項目配列に追加します。具体的なログメッセージをフォーマットする必要がある場合、システムはこの配列内の項目の順序に従って、ログメッセージから対応するデータ要素を順次抽出し、指定された形式に従ってそれらを組み合わせて最終的な出力文字列を作成します。

メンバ変数

このクラスには、フォーマット文字列の構築ルール _fmt と、フォーマットサブアイテムの配列 _formatItems の2つのメンバ変数があります。

ユーザーは必要に応じてフォーマット文字列をカスタマイズし、ログ出力の詳細度を調整できます。モジュールはフォーマット文字列を解析し、そこから各フォーマット文字を抽出して対応するフォーマットサブアイテムオブジェクトを作成し、最後にそれらのフォーマットサブアイテムを順序通りにフォーマットサブアイテム配列に追加します。

class Formater{private:    std::string _fmt; // フォーマット文字列の構築ルール    std::vector<FormatItem::Ptr> _formatItems;};
フォーマット文字列の構築ルールを解析する関数

parse 関数は、フォーマット文字列(例:「[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n」)を解析し、それを個々のフォーマットサブアイテムに分解する役割を担います。この関数は、フォーマット文字列 _fmt 内の通常の文字とフォーマット文字(% で始まる文字)を処理する必要があります。通常の文字については直接 OtherFormatItem オブジェクトを作成し、フォーマット文字については文字の型に応じて対応するフォーマットサブアイテムオブジェクトを作成します。特に、日付フォーマット文字列 %d については、その後に続く可能性のある時刻フォーマットサブ文字列 {%H:%M:%S} も解析する必要があります。解析が完了すると、生成されたフォーマットサブアイテムオブジェクトは items 配列に格納され、後続のフォーマット処理で使用されます。フォーマットは有効な解析結果に基づいて行わなければならないため、コンストラクタ内で parse 関数を呼び出した後、assert を使用して解析が成功したことを確認します。

基本的な考え方:フォーマット文字列は、元の文字列とフォーマット文字列の 2 種類に分類されます。

  • 元の文字列はパーセント記号で始まらないのに対し、フォーマット文字列はパーセント記号で始まります。
    • パーセント記号に遭遇した際、その後に別のパーセント記号があるかどうかを判断する必要があります。ある場合はエスケープ文字を表し、そうでない場合は、その後に続く文字がフォーマット文字となります。
  • フォーマット文字には、中括弧で囲まれたサブフォーマットが含まれる場合があります。

フォーマットルール文字列の解析における具体的な実装手順

解析の考え方は、先頭から順に文字単位で処理し、パーセント記号で始まるフォーマット文字と通常の原始文字列を識別することです。パーセント記号に遭遇した際は、その直後の文字が別のパーセント記号であるかどうかを確認し、エスケープの有無を判断します。フォーマット文字の後に、中括弧で囲まれたサブフォーマットの内容が続く場合があり、これには特別な処理が必要です。

解析の過程では、元の文字列、フォーマット文字の識別子、およびそのサブフォーマットの内容を一時配列に保存する必要があります。各処理ステップで得られる情報にはキーと値のペアが含まれます。元の文字列のキーは空で、値は文字列の内容です。フォーマット文字のキーは識別子で、値はサブフォーマットの内容または空です。解析が完了すると、一時配列の内容に基づいて、フォーマット済みサブ項目のオブジェクトを順に作成し、メンバー配列 items に追加します。

namespace logsys
{
    class Formater
    {
    public:
        // フォーマット済み文字列の構築ルールを解析し、フォーマット済みコンテンツのビルダーを構築する
        bool parsefmt()
        {
            
std::list<std::pair<std::string, std::string>> pairlist;
            size_t index = 0, pos = 0;
            while (index < _fmt.size())
            {
                pos = _fmt.find(「%」, index);
                
if (pos == std::string::npos)
                {
                    // 見つからなかった場合、それ以降のフォーマット情報はないが、他の文字が存在する可能性がある
                    pairlist.push_back({「」, _fmt.substr(index)});
                    break;
                }
                // 「%」が見つかった場合、まず%以前の文字列を処理する
                
if(pos > index)
                {
                    pairlist.push_back({「」, _fmt.substr(index, pos - index)});
                }

                if (pos + 1 >= _fmt.size())
                {
                    return false; // 一致するフォーマット規則が見つからない場合、フォーマット文字列に誤りがあり、解析に失敗したことを示す
                }
                
// 「%」か「%%」かを判定する必要がある
                if (_fmt[pos + 1] == 『%』)
                {
                    pairlist.push_back({「」, 「%」});
                    index = pos + 2;
                    continue;
                
}
                else
                {
                    // まず、その後に『{}』があるかどうかを識別する必要がある。これは%xのフォーマットサブアイテムである
                    std::string val = 「」;
                    if (pos + 2 < _fmt.size() && _fmt[pos + 2] == '' {'')
                    {
                        size_t bpos = _fmt.find(「}」, pos + 3);
                        if (bpos != std::string::npos)
                        {
                            val = _fmt.substr(pos + 3, bpos - (pos + 3));
                            // 次のループでは、bpos + 1 から「}」の次の文字を検索開始
                            index = bpos + 1;
                        }
                        else
                        {
                            return false; // 「}」が見つからない場合、フォーマット文字列に誤りがあり、解析に失敗したことを示す
                        }
                    }
                    else
                    {
                        index = pos + 2;
                    }
                    
if (_fmt[pos + 1] == 『d』)
                        pairlist.push_back({「d」, val});
                    
else if (_fmt[pos + 1] == 『T』)
                        pairlist.push_back({「T」, val});
                    else if (_fmt[pos + 1] == 『t』)
                        pairlist.push_back({「t」, val});
                    else if (_fmt[pos + 1] == 『p』)
                        
pairlist.push_back({「p」, val});
                    else if (_fmt[pos + 1] == 『c』)
                        pairlist.push_back({『c』, val});
                    else if (_fmt[pos + 1] == 『f』)
                        pairlist.push_back({「f」, val});
                    
else if (_fmt[pos + 1] == 『l』)
                        pairlist.push_back({「l」, val});
                    else if (_fmt[pos + 1] == 『m』)
                        pairlist.push_back({「m」, val}) ;
                    else if (_fmt[pos + 1] == 『n』)
                        pairlist.push_back({「n」, val});
                    else
                        return false; // 一致するフォーマットルールが見つからない場合、フォーマット文字列に誤りがあり、解析に失敗したことを示す
                
}
            }
            // 解析結果に基づいてフォーマット項目を作成する
            for (auto &pair : pairlist)
            {
                _formatItems.push_back(createFormatItem(pair.first, pair.second));
            }
            return true;
        }
    
private:
        FormatItem::Ptr createFormatItem(const std::string &key, const std::string &val)
        {
            if (key == 「d」)
                return std::make_shared<TimeFormatItem>(val);
            
if (key == 「T」)
                return std::make_shared<TabFormatItem>();
            if (key == 『t』)
                return std::make_shared<TidFormatItem>();
            if (key == 「p」)
                return std::make_shared<LevelFormatItem>();
            
if (key == 「c」)
                return std::make_shared<LoggerNameFormatItem>();
            
if (key == 「f」)
                return std::make_shared<FileNameFormatItem>();
            if (key == 『l』)
                return std::make_shared<LineFormatItem>();
            if (key == 「m」)
                return std::make_shared<MsgFormatItem> ();
            if (key == 「n」)
                return std::make_shared<NLineFormatItem>();
            return std::make_shared<OtherFormatItem>(val);
        }
    
};
}

フォーマット文字列には複数のフォーマット文字が含まれています:%d は日付を表し、%t はスレッド ID を表し、 %p はログレベルを表し、%c はロガー名を表し、%f はファイル名を表し、%l は行番号を表し、%m はログメッセージを表し、%n は改行を表します。これらのフォーマット文字を組み合わせることで、ユーザーはログの出力形式をカスタマイズできます。

コンストラクタ

このようなフォーマッタオブジェクトを作成する際、フォーマット文字列の構築ルール(文字列型)を渡す必要があります。

namespace logsys
{
    class Formater
    {
        // コンストラクタ。フォーマット文字列の構築ルールを設定します
        
using Ptr = std::shared_ptr<Formater>;
        Formater(const std::string &fmt = 「[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n」)
            
: _fmt(fmt)
        {
            assert(parsefmt()); // 構築ルールの解析は成功しなければならない
        }
    };
}
フォーマット出力関数

2つの異なるバージョンをオーバーロードしています。1つは出力ストリームを指定し、もう1つはフォーマット済みの文字列を直接返します。

LogMessage を通じて指定された形式の出力を取得する必要がある場合は、この関数を使用します。

namespace logsys
{
    class Formater
    {
        // 出力ストリームを指定し、logMessage をフォーマットする
        
void format(std::ostream &out, LogMessage &msg)
        {
            for (auto &item : _formatItems)
            {
                item->format(out, msg);
            }
        }
        // logMessageをフォーマットし、フォーマット後の文字列を返す
        std::string format(LogMessage &msg)
        
{            std::stringstream ss;            for (auto &item : _formatItems)            {                item->format(ss, msg);            }            return ss.str();        }    };}

4.3 フォーマットモジュールのまとめ

ログフォーマットモジュールは、主に2つのコアコンポーネントで構成されています:フォーマット文字列とフォーマットサブアイテム配列です。フォーマット文字列はユーザーが定義するテンプレートであり、ログメッセージの出力形式を指定するもので、各種フォーマット命令と通常のテキストが含まれています。フォーマットサブアイテム配列は、フォーマット文字列を解析して生成される一連の処理命令であり、 各命令は特定のデータ処理方法に対応しています。

モジュールが動作する際:

  1. まず、フォーマット文字列に対して語彙解析と構文解析を行い、それを複数のフォーマットサブアイテムに分解し、出現順に配列に格納します。
  2. 次に、具体的なログメッセージを処理する際、システムはこのサブアイテム配列を順に走査し、各サブアイテムについて、ログメッセージから対応するデータを抽出し、サブアイテムで定義された形式に従って処理を行い、その結果を出力バッファに追加します。

例えば、日付サブアイテムに遭遇した場合は、ログメッセージからタイムスタンプを抽出し、指定された日時形式に変換します。ファイル名サブアイテムに遭遇した場合は、ソースコードのファイル名を抽出します。メッセージ本文サブアイテムに遭遇した場合は、主要なログ内容のテキストを抽出します。すべてのサブアイテムの処理が完了すると、出力バッファには完全にフォーマットされたログメッセージが含まれるようになります。


五、まとめ

本稿では、ログシステムの4つの基本モジュールを実装しました。ユーティリティクラス(DateおよびFile)は、時刻の取得とファイル操作のサポートを提供します。ログレベルクラスは、6種類のログレベルとその文字列変換を定義しています;ログメッセージクラスは、1つのログに含まれるすべての重要な情報をカプセル化しています;フォーマットモジュールは、柔軟なフォーマット文字列解析メカニズムを通じて、ユーザーによるログ出力形式のカスタマイズをサポートしています。次の記事では、ログ保存モジュールとロガーモジュールの実装を続けます。