はじめまして、ユニファ株式会社のWEBエンジニアのチョウと申します。
こんなことがないでしょうか。突然調査依頼が来て、あるユーザーについて調べてほしいです。大量のログから掘り出してみましたが、ユーザー何人がアクセスするによってログが混じってしまいました。RailsのログにプロセスのIDが入ってますが、1つのプロセスは連続で複数ユーザーに使われて、リクエストとリクエストの境界が見づらく、時間かかるケースよくあります。もっと問題なのは、システムは必ず1つのログファイルに出力するわけではない。リクエストログは複数のファイルに分けられてしまったら、調査自体が邪魔されるといっても過言ではない。
ここで問題をまとめましょう。
- ユーザーあるいはユーザーのリクエストを特定したい
- 1つのリクエストが複数のログファイルに分けても分かる
ここですぐ分かる方がいるかもしれません、1つのリクエストは複数行のログを出力しているので、この複数行のログをあるIDなどでつなげればいいのです。
もっと具体的には、リクエストごとにユニークをIDを作成し、ログを出力するところ、このIDを一緒に出せば連続の行でもわかります。
request-id-1: foo
request-id-2: bar
request-id-1: foo2
ここでrequest-id-1でgrepすればプロセスID関係なくこのリクエストのログだけフィルターできます。
Rails自体はlog_tagsという設定があります。
config.log_tags = [ :uuid ]
を有効にすれば、ログにX-Request-Idの内容を出力されます。X-Request-Idはリクエスト別で作成されましたので、要望1に合ってます。
Log tagging in Rails | We Are Stac
BEFORE
Started GET "/dashboard" for 127.0.0.1 at 2014-11-24 15:48:30 +0000
AFTER
[a00c54e0-874f-4358-a7b9-accef0c407c6] Started GET "/dashboard" for 127.0.0.1 at 2014-11-24 15:48:30 +0000
これで終わり?残念ながら、この方法はRails.loggerだけに効き、複数のログファイルに通用しないです。
しかし、log_tagsのように、ログ出力するとき、X-Request-Idを入れれば解決できると思います。
そもそもX-Request-Idはどこからですか。答えはRailsのRake Middleware:ActionDispatch::RequestIdです。このmiddlewareはX-Request-Idを決め、Rackのenvに設定してます。
envの中にあると、ログを出力するところ取得しにくいため、まずthread local variableとかに保存してみます。つまり、自分でmiddlewareを作り、X-Request-Idをthread local variableに設定します。
middlewareリスト
use ActionDispatch::Static
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007f9112ed8258>
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use BetterErrors::Middleware
use ActionDispatch::RemoteIp
use RailsDevTweaks::GranularAutoload::Middleware
use ActionDispatch::Callbacks
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use ActionDispatch::Head
use Rack::ConditionalGet
use Rack::ETag
use ActionDispatch::BestStandardsSupport
use ExceptionNotification::Rack
run Foo::Application.routes
application.rb
config.middleware.insert_after 'ActionDispatch::RequestId', 'MyLogContextCollector'
class MyLogContextCollector
def initialize(app)
@app = app
end
def call(env)
# request id from ActionDispatch::RequestId
request_id = env['action_dispatch.request_id']
MyLogContext.set_current_request_id(request_id)
@app.call(env)
ensure
MyLogContext.clear_current_request_id
end
end
ここでenvからX-Request-Idを取り出し、MyLogContextのthread local variableに設定します。
class MyLogContext
TRANS_VAR_REQUEST_ID = :request_id
attr_reader :severity # String
attr_reader :time # DateTime
attr_reader :program_name # String
attr_reader :message # Object
# @param [String] severity
# @param [DateTime] time
# @param [String] program_name
# @param [Object] message
def initialize(severity, time, program_name, message)
@severity = severity
@time = time
@program_name = program_name
@message = message
end
# @return [Integer]
def get_process_id
$$
end
# @param [String] format
# @return [String]
def get_formatted_time(format = nil)
return nil if @time.nil?
return @time.strftime(format) if format
"#{@time.strftime('%Y-%m-%d %H:%M:%S')}.#{'%06d' % @time.usec.to_s}"
end
# @return [String]
def message2string
case @message
when ::String
@message
when ::Exception
"#{ @message.message } (#{ @message.class })\n" <<
(@message.backtrace || []).join("\n")
else
@message.inspect
end
end
class << self
# @param [String] request_id
def set_current_request_id(request_id)
set_trans_var(TRANS_VAR_REQUEST_ID, request_id)
end
# @return [String]
def get_current_request_id
get_trans_var(TRANS_VAR_REQUEST_ID)
end
def clear_current_request_id
set_trans_var(TRANS_VAR_REQUEST_ID, nil)
end
# @param [Symbol] key
# @param [Object] value
def set_trans_var(key, value)
::Thread.current.thread_variable_set(key, value)
end
# @param [Symbol] key
def get_trans_var(key)
::Thread.current.thread_variable_get(key)
end
end
end
ログを出力する時、都度thread local variableであるX-Request-Idを入れるのはしたくないため、log formatのところすこし改造してみました。
logger = Logger.new('log/foo.log')
logger.formatter = MyLogFormatter.new(lambda{|context|
"[%s #%d] %5s -- %s: [%s] %s\n" % [
context.get_formatted_time,
context.get_process_id,
context.severity,
context.program_name,
MyLogContext.get_current_request_id,
context.message2string
]
})
class MyLogFormatter
def initialize(lambda)
@lambda = lambda
end
# @param [String] severity
# @param [DateTime] time
# @param [String] prog_name
# @param [Object] msg
# @return [String]
def call(severity, time, prog_name, msg)
context = ::Yoursmile::LogContext.new(severity, time, prog_name, msg)
@lambda.call(context)
end
end
これで全部終わりました。
==> log/1.log <==
[2016-11-25 17:26:11.350612 #66417] INFO -- : [b15ea4d9a43bc7aa390051e16c2dbe3b] 1
[2016-11-25 17:26:11.367210 #66417] INFO -- : [b15ea4d9a43bc7aa390051e16c2dbe3b] 2
[2016-11-25 17:26:11.367284 #66417] INFO -- : [b15ea4d9a43bc7aa390051e16c2dbe3b] 3
[2016-11-25 17:26:11.370316 #66417] DEBUG -- : [b15ea4d9a43bc7aa390051e16c2dbe3b] 4
[2016-11-25 17:26:11.372182 #66417] DEBUG -- : [b15ea4d9a43bc7aa390051e16c2dbe3b] 5
[2016-11-25 17:26:11.374109 #66417] DEBUG -- : [b15ea4d9a43bc7aa390051e16c2dbe3b] 6
==> log/2.log <==
[2016-11-25 17:26:11.383344 #66417] DEBUG -- : [b15ea4d9a43bc7aa390051e16c2dbe3b] 7
==> log/3.log <==
[2016-11-25 17:26:11.391745 #66417] DEBUG -- : [b15ea4d9a43bc7aa390051e16c2dbe3b] 8
==> log/1.log <==
[2016-11-25 17:26:11.415792 #66417] DEBUG -- : [b15ea4d9a43bc7aa390051e16c2dbe3b] 9
[2016-11-25 17:26:11.432047 #66417] INFO -- : [b15ea4d9a43bc7aa390051e16c2dbe3b] 10
[2016-11-25 17:26:11.441824 #66417] DEBUG -- : [b15ea4d9a43bc7aa390051e16c2dbe3b] 11
[2016-11-25 17:26:11.448600 #66417] DEBUG -- : [b15ea4d9a43bc7aa390051e16c2dbe3b] 12
[2016-11-25 17:26:11.450477 #66417] DEBUG -- : [b15ea4d9a43bc7aa390051e16c2dbe3b] 13
[2016-11-25 17:26:11.451106 #66417] INFO -- : [b15ea4d9a43bc7aa390051e16c2dbe3b] 14
よさそう。
まとめ
あとで本番にリリースし、調査がすこし楽になった感じがします。エラーはある原因で別のファイルに出力されましたから、本来エラーを起こすリクエストを特定するのは難しかったです。そこでこの機能のおかげて、エラーを出力する行のrequest idが分かればすぐgrepで見つかりました。
実際、X-Request-Idはアプリケーション内のログではなく、1つのリクエストが複数アプリケーションに関わる場合も利用できます。それは最初のアプリケーションあるいはWEBサーバーがRequest-Idを作成し、RPCのパラメータとしてほかのアプリケーションに通信し、処理のログを繋げます。つまり、request tracerみたいなものです。
最後に、以上の内容は役に立ちましたら幸いです。