ぎょーぼのぶろぐ

IT系の話を書いていくブログです。今はRubyの勉強中。

【ツール作成】Messageモジュールを作る

前置き

今回は、コンソールへのメッセージ出力、ログへの出力を制御する Messageモジュールについてです。

なんでモジュール化?

コンソールへの出力なら、puts でいいし、ログ出力も loggerモジュールがあるので、その都度出力すればいいわけですが、今回、Messageモジュールとしてまとめた理由としては、、、

  • コンソールだけに出力したいメッセージ、例外の詳細など、ログだけに出力したいメッセージ、両方に出力したいメッセージがあって、それぞれ一回の呼び出しでできるようにしたかった。
  • I18nlogger の使用がMessageモジュール内だけに限定できるので、コードが見やすくなる。
  • I18nモジュールで、文言部分を別ファイルで管理したかったが、出力のたびに I18n.t を書かないといけないのが少し煩わしかった。
  • 想定外の例外出力の際、Backtrace を出力するようにしたかったが、各メソッドでいちいち出力フォーマットを書くのが面倒くさかった。それなら、メソッドに必要な引数を渡したら、それだけで定型で出力できるようにしたかった。

といったところです。

・・・これって、「ヘルパーメソッド」的な感じなのかな・・・

実装するメソッド

  • self.init(sys)
    SystemSetting インスタンスを引数にして、loggerの設定初期化を行うメソッドです。Messageモジュールを使用する前に必ず実行する必要があります。
    I18n' の設定は、Messageモジュールをrequire`したときに読み込まれるようにしています。

  • self.show(label, *args) コンソールだけにメッセージを出力するメソッドです。内容は、puts するだけです。I18nで文章を取得するので、引数はI18n.tメソッドで必要な引数を取っています。

  • self.log(level, label, *args) ログだけにメッセージを出力するメソッドです。loggerオブジェクト生成→出力→close を行います。必要な引数は、logレベルと I18n.tメソッドで必要な引数です。

  • self.show_and_log(level, label, *args) コンソールとログの両方にメッセージを出力するメソッドです。単にself.showメソッドとself.logメソッドを順に呼び出しているだけです。

  • self.show_exception(method, ex) 想定外の例外が発生したときに、例外が発生したメソッドと例外の詳細、バックトレースを固定のフォーマットで出力するメソッドです。必要な情報(発生メソッド名と例外オブジェクト)を受け取ってフォーマットを生成し、self.show_and_logに渡しています。

他、プライベートメソッドを二つ(実際にログファイルに書き込む処理のメソッド、I18n.t へ引数を渡すメソッド)、実装しました。

実装コード

実装コードは、以下の通りです。

module Message
    require 'I18n'
    require 'logger'

    # i18n 初期設定
    begin
        I18n.load_path = Dir["./locale/**/*.yml"]
        I18n.locale = :ja
    rescue I18n::InvalidLocaleData
        # ロケールファイルのオープンエラー
        puts "ロケールファイルの読み込みに失敗しました。\n処理を中止します。"
        exit(false)
    rescue I18n::InvalidLocale
        # 不正なロケールを指定した
        puts "ロケールの設定「:ja」が見つかりません。\n処理を中止します。"
        exit(false)
    end

    #
    # self.init
    # システム設定を受け取り、Messageクラスの設定を行う。
    #   引数
    #       system  :   設定のハッシュ。下記項目が入っていることを想定。
    #                   log_path        :   ログファイル出力パス
    #                   log_date_format :   ログ出力時の日付フォーマット
    #   戻り値
    #       なし
    #
    def self.init(sys)
        # system.ini の内容で初期化する。
        @log_path = sys.log_path
        @log_date_format = sys.log_date_format

        # logへ最初の書き込みを行ってみる。
        self.show_and_log(:info, :info01, desc: Time.new.strftime("%Y-%m-%d %H:%M"))

        return true
    end

    #
    # self.show
    #   コンソールへ表示する。
    #   引数
    #       label   :   ロケールファイルのラベル(シンボル)
    #       *args   :   パラメータ(I18n仕様)
    #
    def self.show(label, *args)
        puts t(label, *args)
    end

    #
    # self.log
    #   ログへ出力する。
    #   引数
    #       level   :   ログレベル(通常外の値を指定した場合は :unknown で出力)
    #       label   :   ロケールファイルのラベル(シンボル)
    #       *args   :   パラメータ(I18n仕様)
    #
    def self.log(level, label, *args)
        # ログファイルへ書き込み
        write_log(level, t(label, *args))
    end

    #
    # self.show_and_log
    #   コンソールとログへ出力する。(show と log を順に呼び出すだけ)
    #   引数
    #       level   :   ログレベル(通常外の値を指定した場合は :unknown で出力)
    #       label   :   ロケールファイルのラベル(シンボル)
    #       *args   :   パラメータ(I18n仕様)
    #
    def self.show_and_log(level, label, *args)
        show(label, *args)
        log(level, label, *args)
    end

    #
    # self.exception
    #   例外情報を出力する。
    #       コンソールとログの両方に出力する。
    #       メソッド名、Exceptionクラス、Exceptionメッセージ、backtrace を出力する。
    #   引数
    #       method      :   発生したメソッド名(__method__ で渡してもらう)
    #       ex          :   Exception オブジェクト
    #
    def self.exception(method, ex)        
        # コンソールとログへ出力(レベルは :fatal, 文言は:exception固定)
        show_and_log(:fatal, :exception, method: method, class: ex.class, message: ex.message, backtrace: ex.backtrace.join("\n"))
    end

    private

    #
    # self.write_log
    # ログファイルへの書き込み
    #   引数
    #       level   :   ログレベル(:debug, :info, :warn, :error, :fatal, :unknown)
    #       message :   出力する内容
    #
    #   ※ 出力直前にオープンし、出力後に必ずcloseするようにする。
    #
    def self.write_log(level, message)
        # loggerオブジェクト生成
        @logger = Logger.new(@log_path)
        @logger.datetime_format = @log_date_format

        # 指定のログレベル以外の値が来た場合は、:unknown に置き換える。
        unless [:debug, :info, :warn, :error, :fatal].include?(level) then
            level = :unknown
        end

        # 出力実行
        @logger.send(level, message)
        @logger.close

    rescue => ex
        # ここで例外が発生した場合、ログに出力できない状態なので、コンソールに直接出力する。
        puts t(:err06, path: sys.log_path)
        puts t(:exception, method: __method__, class: ex.class, message: ex.message, backtrace: ex.backtrace.join("\n"))
        exit(false)
    end

    #
    # self.t
    # メッセージ文を取得する。I18n.t へそのまま引き継ぐ。
    #   引数
    #       label   :   ロケールファイルのラベル(シンボル)
    #       *args   :   引数リスト
    #
    def self.t(label, *args)
        return I18n.t(label, *args)
    end
end

実装のポイント

I18n モジュールの設定で、ロードパスの設定に Dir["./locale/**/*.yml"] という書き方をしていますが、これは、「localeディレクトリ配下のサブディレクトリを含めてymlを拡張子に持つすべてのファイル」という意味になります。具体的には、/**/の部分が、「その配下のサブディレクトリ内も含めて」という意味になります。

例外処理については、I18n モジュールは最初の初期設定が通れば「基本的に使えるもの」として扱っています。ログ出力側は、self.write_logプライベートメソッドで例外が出る場合のみ、例外内容をコンソールに出力(puts)するようにしています。

I18n で使用するメッセージファイル(messages.yml)

I18n のメッセージは、下記のようにまとめています。文言のところ、本当はダブルクォーテーションは不要なのですが、末尾の空白とか分かり辛かったりするので、あえて括っています。

ja:
    err01: "【エラー】%{path} ファイルが見つかりません。ファイルのパスを確認してください。\n処理を中止します。"
    err02: "【エラー】%{path} ファイルの読み取りに失敗しました。%{path} ファイルの文字コードがUTF-8になっているか、確認してください。\n処理を中止します。"
    err03: "【エラー】%{param}の値が設定されていません。%{path}ファイルの内容を確認してください。\n処理を中止します。"
    err04: "【エラー】バックアップ先のディレクトリがありません。%{path} を確認してください。\n処理を中止します。" 
    err05: "【エラー】dest_prefix に使用できない文字が含まれています。「a-zA-Z0-9_-=()」以外の文字は使用できません。\n処理を中止します。"
    err06: "【エラー】ログファイルへの書き込みに失敗しました。%{path} が正しいか、確認してください。\n処理を中止します。"
    err07: "【エラー】バックアップ元のディレクトリが一つもありません。設定を確認してください。\n処理を中止します。"
    err08: "【エラー】バックアップ先ディレクトリに書き込みができません。アクセス権を確認してください。%{path}\n処理を中止します。"

    info01: "===== ファイルバックアップツール FileBackup.rb ===== %{desc}"
    info02: "【バックアップ元フォルダー】\n%{desc}"
    info03: "【バックアップ先フォルダー】\n %{desc}"
    info04: "【完了】"
    info05: "バックアップを完了しました。バックアップ先:%{path}"
    info06: " Total: %{size}Bytes, ファイル数: %{fcount}, ディレクトリ数: %{dcount}"
    info07: "【ディレクトリ作成】%{dir}"
    info08: "【ファイルコピー】%{file}"
    info09: " %{key}: %{value} \n"

    warn01: "【注意】設定されたパスは、ディレクトリではありません。 %{desc}" 
    
    confirm01: "処理を開始してよろしいですか?[Y/n]"
    confirm02: "処理を開始します。"
    confirm03: "処理を中止します。"
    confirm04: "【エラー】`y`か`n`で入力してください。"

    exception: "【エラー】例外が発生しました。例外の内容を確認してください。\n%{method}: %{class}: %{message}\n%{backtrace}\n処理を中止します。"