Rails2.3のログフォーマットの変更は、Railscasts - The Logger を見て
Logger!!!(ログにタイムスタンプを追加する方法) - うんたらかんたら日記
としていましたが、改めてソースを見ると
#activesupport-2.3.11/lib/active_support/core_ext/logger.rb This logger is deprecated in favor of ActiveSupport::BufferedLogger
と書かれていました。
Loggerでのフォーマット変更は結構柔軟(ミリ秒、プロセス、ログレベルなど)に書けたので
特に困ってないし、理由もわかんないのですが、deprecatedと言われると不安なので
ActiveSupport::BufferedLoggerでのフォーマット変更を試してみました。
結論
#config/environment.rb ActiveSupport::BufferedLogger.class_eval do SEVERITIES = self::Severity.constants.inject({}){ |val, con| val[eval("self::#{con}")] = con; val } def add_with_format(severity, message = nil, progname = nil, &block) time = Time.now format = "[%s #%d] %s -- %s: %s\n" formatted_message = format % ["#{time.strftime('%Y-%m-%d %H:%M:%S')}.#{'%06d' % time.usec.to_s}", $$, SEVERITIES[severity], progname, message] add_without_format(severity, nil) { formatted_message } end alias_method_chain :add, :format end
これでOK。
[2011-05-01 14:09:07.723610 #9165] INFO -- : Parameters: {"id"=>"1"} [2011-05-01 14:09:07.730061 #9165] WARN -- : Article Load (0.3ms) SELECT * FROM "articles" WHERE ("articles"."id" = 1) [2011-05-01 14:09:07.745981 #9165] INFO -- : Rendering template within layouts/articles [2011-05-01 14:09:07.746250 #9165] INFO -- : Rendering articles/edit [2011-05-01 14:09:07.756997 #9165] INFO -- : Completed in 32ms (View: 25, DB: 0) | 200 OK [http://localhost/articles/1/edit]
まずは
ActiveSupport::BufferedLoggerがどこで使用されるかを見ていきます。
#rails/lib/initializer.rb
initializer
RAILS_ENV = (ENV['RAILS_ENV'] || 'development').dup unless defined?(RAILS_ENV) module Rails class Initializer def self.run(command = :process, configuration = Configuration.new) yield configuration if block_given? initializer = new configuration initializer.send(command) initializer end
関係ないけど、最初にRAILS_ENVの設定をしてるんですね。
initializer = new configuration が謎ですが
configurationをnewして、processメソッドをsendするという流れ。
Configurationを見てみると
module Rails class Configuration def initialize set_root_path! self.frameworks = default_frameworks self.autoload_paths = default_autoload_paths self.autoload_once_paths = default_autoload_once_paths self.eager_load_paths = default_eager_load_paths self.log_path = default_log_path self.log_level = default_log_level self.view_path = default_view_path self.controller_paths = default_controller_paths self.preload_frameworks = default_preload_frameworks self.cache_classes = default_cache_classes self.dependency_loading = default_dependency_loading self.whiny_nils = default_whiny_nils self.plugins = default_plugins self.plugin_paths = default_plugin_paths self.plugin_locators = default_plugin_locators self.plugin_loader = default_plugin_loader self.database_configuration_file = default_database_configuration_file self.routes_configuration_file = default_routes_configuration_file self.gems = default_gems self.i18n = default_i18n for framework in default_frameworks self.send("#{framework}=", Rails::OrderedOptions.new) end self.active_support = Rails::OrderedOptions.new end
いろいろやってますが、log_levelの設定もここでやってます。
module Rails class Configuration private def default_log_level environment == 'production' ? :info : :debug end
process
module Rails class Initializer def process Rails.configuration = configuration check_ruby_version install_gem_spec_stubs set_load_path add_gem_load_paths require_frameworks set_autoload_paths add_plugin_load_paths load_environment preload_frameworks initialize_encoding initialize_database initialize_cache initialize_framework_caches initialize_logger initialize_framework_logging
ここもいろいろやってます。
initialize_logger
Rails.loggerやconfiguration.logger(つまりenvironment.rbでLogger.new(config.log_path)とかしてる)が設定されていない場合
ActiveSupport::BufferedLoggerをnewしています。
最後にRAILS_DEFAULT_LOGGERも設定してますね。ほぅ。
module Rails class Initializer def initialize_logger # if the environment has explicitly defined a logger, use it return if Rails.logger unless logger = configuration.logger begin logger = ActiveSupport::BufferedLogger.new(configuration.log_path) logger.level = ActiveSupport::BufferedLogger.const_get(configuration.log_level.to_s.upcase) if configuration.environment == "production" logger.auto_flushing = false end rescue StandardError => e logger = ActiveSupport::BufferedLogger.new(STDERR) logger.level = ActiveSupport::BufferedLogger::WARN logger.warn( "Rails Error: Unable to access log file. Please ensure that #{configuration.log_path} exists and is chmod 0666. " + "The log level has been raised to WARN and the output directed to STDERR until the problem is fixed." ) end end silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger } end
では、activesupportを見てみましょう
#activesupport-2.3.11/lib/active_support/core_ext
initialize
最後のforでaddメソッドが作成されています。
module ActiveSupport class BufferedLogger module Severity DEBUG = 0 INFO = 1 WARN = 2 ERROR = 3 FATAL = 4 UNKNOWN = 5 end include Severity def initialize(log, level = DEBUG) @level = level @buffer = {} @auto_flushing = 1 @guard = Mutex.new if log.respond_to?(:write) @log = log elsif File.exist?(log) @log = open(log, (File::WRONLY | File::APPEND)) @log.sync = true else FileUtils.mkdir_p(File.dirname(log)) @log = open(log, (File::WRONLY | File::APPEND | File::CREAT)) @log.sync = true @log.write("# Logfile created on %s" % [Time.now.to_s]) end end for severity in Severity.constants class_eval <<-EOT, __FILE__, __LINE__ + 1 def #{severity.downcase}(message = nil, progname = nil, &block) add(#{severity}, message, progname, &block) end def #{severity.downcase}? #{severity} >= @level end EOT end # def debug(message = nil, progname = nil, &block) # add(DEBUG, message, progname, &block) # end # # def debug? # DEBUG >= @level # end
add
ありました、ここですね。
def add(severity, message = nil, progname = nil, &block) return if @level > severity message = (message || (block && block.call) || progname).to_s # If a newline is necessary then create a new message ending with a newline. # Ensures that the original message is not mutated. message = "#{message}\n" unless message[-1] == ?\n buffer << message auto_flush message end
改行付与のついでに、フォーマット変更してみましょう。
修正してみた
module ActiveSupport class BufferedLogger module Severity DEBUG = 0 INFO = 1 WARN = 2 ERROR = 3 FATAL = 4 UNKNOWN = 5 end include Severity def add(severity, message = nil, progname = nil, &block) return if @level > severity message = (message || (block && block.call) || progname).to_s # If a newline is necessary then create a new message ending with a newline. # Ensures that the original message is not mutated. time = Time.now format = "[%s #%d] %s -- %s: %s\n" severity = Severity.constants.find{ |c| Severity.const_get(c) == severity } # severity_initial = Severity.constants.find{ |c| Severity.const_get(c) == severity }[0..0] message = "#{message}\n" unless message[-1] == ?\n message = format % ["#{time.strftime('%Y-%m-%d %H:%M:%S')}.#{'%06d' % time.usec.to_s}", $$, severity, progname, message] buffer << message auto_flush message end end end
severityが数値で渡ってくるので、DEBUGとかINFOとか文字列を出力するために
Severityも書いています。
さらに改変
ググってみると下記のようなやり方が紹介されていました。
Railsのログに関する覚書き - Ruby Study Go
ActiveSupport::BufferedLogger.class_eval do SEVERITIES = self::Severity.constants.inject({}){ |val, con| val[eval("self::#{con}")] = con; val } def add_with_format(severity, message = nil, progname = nil, &block) add_without_format(severity, nil) { "%s [%5s] %s" % [Time.now.strftime("%Y/%m/%d %H:%M:%S"), SEVERITIES[severity], message] } end alias_method_chain :add, :format end
これはcool。なるほど元メソッドを呼ぶ形であればclas_evalした上で
alias_method_chainしてあげればこんなに短く書ける。
感想
ということで、冒頭のソースになりました。
Rails側は、LoggerやLog4Rなどのloggerオブジェクトに柔軟に対応した作りになっていますが
ActiveSupport::BufferedLoggerのフォーマット変更は、ちょっと面倒。
もう少し書きやすい構造にしてくれると有り難いですね。
おまけ
loggerオブジェクトの中身です。
Loggerの場合
#<Logger:0x101609350 @default_formatter=#<Logger::Formatter:0x101608ec8 @datetime_format=nil>, @formatter=#<Logger::Formatter:0x101609198 @datetime_format=nil>, @level=0, @logdev= #<Logger::LogDevice:0x101608ce8 @dev=#<File:/Users/rochefort/work/rails/bloga/log/production.log>, @filename="/Users/rochefort/work/rails/bloga/log/production.log", @mutex= #<Logger::LogDevice::LogDeviceMutex:0x101608b80 @mon_count=0, @mon_entering_queue=[], @mon_owner=nil, @mon_waiting_queue=[]>, @shift_age=0, @shift_size=1048576>, @progname=nil>
ActiveSupport::BufferedLoggerの場合
#<ActiveSupport::BufferedLogger:0x102d76650 @auto_flushing=1000, @buffer= {#<Thread:0x102d7cbb8 run>=> ["\n\nProcessing ArticlesController#show (for 127.0.0.1 at 2011-04-30 06:17:06) [GET]\n", " Parameters: {\"id\"=>\"2\"}\n"]}, @guard=#<Mutex:0x102d6ba70>, @level=1, @log=#<File:/Users/rochefort/work/rails/bloga/log/production.log>>