【ツール作成】Messageモジュールを作る
前置き
今回は、コンソールへのメッセージ出力、ログへの出力を制御する Messageモジュールについてです。
なんでモジュール化?
コンソールへの出力なら、puts
でいいし、ログ出力も loggerモジュールがあるので、その都度出力すればいいわけですが、今回、Messageモジュールとしてまとめた理由としては、、、
- コンソールだけに出力したいメッセージ、例外の詳細など、ログだけに出力したいメッセージ、両方に出力したいメッセージがあって、それぞれ一回の呼び出しでできるようにしたかった。
I18n
やlogger
の使用が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処理を中止します。"