のんびりSEの議事録

プログラミング系のポストからアプリに関してのポストなどをしていきます。まれにアニメ・マンガなど

rubyでエラーコード付きの独自例外クラスを作成 + カスタムloggerで出力

アプリケーション開発でエラーの管理は特に重要で、あるエラーに対しては、エラーコードxxxの用に管理していくことが多いかなと思いますが、JavaとかだったらExceptionを継承して独自の例外クラスを作ったりしたんだけど、Rubyだとどうしたら良いかなと考え、作成してみました。

要求

  • 例外補足時にエラーコード + メッセージを渡したい
  • loggerのERROR出力にもエラーコードを出力したい
  • ログをJson形式で出力したい(おまけ)

Sample

# coding: utf-8

require 'logger'
require 'json'

ENV["TZ"] = "Asia/Tokyo"

##
# 自作logger class
class MyLogger < Logger

  def debug(progname = nil, method_name = nil, msg)
    super(progname) { { method_name: method_name, message: msg } }
  end

  def info(progname = nil, method_name = nil, msg)
    super(progname) { { method_name: method_name, message: msg } }
  end

  def warn(progname = nil, method_name = nil, msg)
    super(progname) { { method_name: method_name, message: msg } }
  end

  # Point 1
  def error(progname = nil, method_name = nil, msg, error_code, backtrace)
    super(progname) { { method_name: method_name, message: msg, error_code: error_code, backtrace: backtrace } }
  end

  def fatal(progname = nil, method_name = nil, msg, error_code, backtrace)
    super(progname) { { method_name: method_name, message: msg, error_code: error_code, backtrace: backtrace } }
  end

  ##
  # logをjson形式で
  class JSONFormatter < Logger::Formatter

    # Point 2
    def call(severity, time, progname, msg)
      { level: severity, time: time, program_name: progname.to_s, content: msg }.to_json + "\n"
    end
  end

end

##
# 自作exception class
class MyException < StandardError

  def initialize(error_code, message)
    @code = error_code # Point 3
    super("[#{error_code}] - #{message}")
  end

  def self.exception(error_code, message)
    self.new(error_code, message)
  end

  def self.throw(error_code)
    self.new(error_code, error_message(error_code))
  end

  # Point 4
  def self.error_message(error_code)
    # error_codeをyaml等で管理しておくと便利
  end

end

##
# 例外補足にエラーコードを取れるように
Exception.class_eval do

  # Point 5
  def code
    @code ||= "---"
  end

end


class ExceptionTest

  def error
    begin
      raise MyException.exception("100", "Error1")
    rescue => e
      raise e
    end
  end

  def default_error

    begin
      raise StandardError.new "StandardError"
    rescue => e
      raise e
    end
  end

end

test = ExceptionTest.new
logger = MyLogger.new(STDOUT)

begin
  test.error
rescue MyException => e
  logger.error(self, e.message, e.code, e.backtrace)
rescue => e
  logger.error(self, e.message, e.code, e.backtrace)
end

logger.formatter = MyLogger::JSONFormatter.new
begin
  test.default_error
rescue => e
  logger.error(self, e.message, e.code, e.backtrace)
end

Output

E, [2016-04-13T23:36:11.820632 #32316] ERROR -- main: {:method_name=>nil, :message=>"[100] - Error1", :error_code=>"100", :backtrace=>["lib/sample_exception.rb:81:in `error'", "lib/sample_exception.rb:102:in `<main>'"]}
{"level":"ERROR","time":"2016-04-13 23:36:11 +0900","program_name":"main","content":{"method_name":null,"message":"StandardError","error_code":"---","backtrace":["lib/sample_exception.rb:90:in `default_error'","lib/sample_exception.rb:111:in `<main>'"]}}

Point

  1. loggerのerrorメソッドerror_code, msg, backtraceを渡せるように拡張
  2. loggerのoutputをJson形式で出力出来るように拡張(改行コードを入れないと改行されずにJsonオブジェクトが続けて出力されてしまう)
  3. エラーコードをインスタンス変数に保管しておく
  4. ここではエラーコード管理をyaml等別ファイルで管理しておくと便利
  5. 自作のException以外も考慮し、Exceptionクラスをモンキーパッチでcodeメソッドで呼び出せるように拡張