Sinatra でアプリケーションログをファイルに書く方法。
Sinatra はじめて触ったんだけど、ログをファイルに書く方法がなかなかわからなかった。
ログを、ただファイルに書きたいのです。
結論
独自のミドルウェアで env['app.logger'] を設定し、アプリケーションのloggerメソッドをオーバーライドする。
サンプル
以下でログがファイルに出力される。アクセスログとアプリケーションログを別のファイルに出力することもできる。
config.ru
Loggerを作ってミドルウェアに渡す。
require 'logger' require 'sampleapp' require 'applog' logdir = File.dirname(__FILE__) + "/logs" logger = ::Logger.new(logdir + '/app.log') # CommonLoggerに渡すのに必要。 def logger.write(msg) self << msg end use AppLog, logger # rackup(rack/server.rb)が設定するCommonLoggerは$stderrに、 # sinatra(sinatra/base.rb) が設定するCommonLoggerはenv['rack.errors'] に # 出力しようとする。ファイルに書くため、独自に use する。 use Rack::CommonLogger, logger run SampleApp
applog.rb
env['app.logger'] を設定するミドルウェア。app.loggerは自分が適当につけた名前。
class AppLog def initialize(app, logger) @app, @logger = app, logger end def call(env) env["app.logger"] = @logger @app.call(env) end end
sampleapp.rb
アプリケーション。loggerメソッドは元々、request.loggerを返していて、request.loggerはenv['rack.logger']を返している。app.logger があればそちらを優先するようにする。
require 'sinatra/base' class SampleApp < Sinatra::Base def logger env['app.logger'] || env['rack.logger'] end get '/' do logger.info "OK" 'OK' end end
Rackのしくみ
アプリケーションをラップするミドルウェアがタマネギ状になってて、外から call(env) が順によばれていく。コアにあるアプリがenvからリクエストを得てレスポンスを返す。
ミドルウェアが call(env) で env['rack.logger'] を設定している。アプリはこのロガーを使いたいのだが、なぜか標準エラーにしか出力してくれない。
三種の logger
- Rack::CommonLogger
- Rack::Logger
- Rack::NullLogger
このうち、下の2つが env['rack.logger'] を設定していた。Rack::Loggerだと下のような感じ。
def call(env) logger = ::Logger.new(env['rack.errors']) logger.level = @level env['rack.logger'] = logger @app.call(env) end
NullLogger は何も出力しないロガー。
Rack::CommonLogger とは
Rack::CommonLogger はアプリケーションの実行後に、Apache形式でアクセスログを書くロガー。アプリケーションログと関係ない。
sinatra/base.rb と rack/server.rb に CommonLogger を設定する処理があり、環境によって、どちらか一方が使われる。
Sinatra が足す CommonLogger
Sinatra は base.rb の setup_default_middleware というメソッドでロガーを足している。
- -E オプションで指定できる環境が development や deployment の場合は設定しない。
- CommonLogger に引数は渡されておらず、デフォルトの env['rack.errors'] が出力先。
Rack が足す CommonLogger
rackup を実行すると、 rack/server.rb の buildapp メソッドが CommonLogger を追加する。
- -E オプションで指定できる環境が development や deployment の場合に設定する。
- Sinatra と条件が逆。Rack が設定しないなら Sinatra が設定する。
- CommonLogger の引数は $stderr 。env['rack.errors'] ではない。
- rackup が足す、その他のミドルウェアは self.logging_middleware と self.middleware メソッドのソースを参照。
Sinatra と Rack 、どっちにしてもエラー出力にログを出力する。
Rack::Logger とは
Rack::Logger は、標準の Logger を env['rack.logger'] に設定するミドルウェア。Logger の引数は env['rack.errors'] で固定。エラー出力にログを出力する。
Rack::Logger は、 Sinatra が追加していた。setup_default_middleware メソッドから呼ばれる setup_logging → setup_custom_logger メソッドを参照。enable :logging をしても、出力先は env['rack.errors'] になってしまう。
env['rack.errors'] を設定するのは誰なのか
自分の環境だと Thin が設定してた。サーバが設定する値。
の最後の回答で言われているように、標準エラー出力をファイルにリダイレクトする選択肢もあるようだが、もうちょっとマシな方法を探った。
CommonLogger が $stderr を使っているので、 evn['rack.errors'] を上書きするだけではダメだろうと思う。
ミドルウェアの呼ばれる順序とログ出力
独自のミドルウェアで env['rack.logger'] を入れ替えればいいのでは、と思ったのだが、config.ru にミドルウェアを足すと、call(env)される順序は、
- rack/server.rb が設定した Rack::CommonLogger
- config.ru が設定したミドルウェア
- sinatra/base.rb が設定した Rack::Logger
になってしまう。自分が設定した rack.logger が Sinatra の Logger で上書きされてしまう。
結果として
アプリケーションでLoggerを直接使う。あるいは、上記サンプルのように、 app.logger を設定して loggerメソッドを上書きするのが、いくらかスマートだと思う。
CommonLogger はそのままだと使えないので、別に自前のをもう一個設定する。
追記
確認に使ったバージョンは以下のとおり。
- Rack 1.4.1
- Sinatra 1.3.2
- Thin 1.3.1
やっぱり冷静に考えると rack.logger が使えないのはおかしいので、サーバで rack.errors を制御するのが、設計の意図なのかもしれない。ただ、現バージョンの rackup -D でデーモンとして起動する場合に、rack.errors を設定する方法はみつけられなかった。