require 'spec_helper' require 'securerandom' require 'logstasher/log_subscriber' class MockController def user_id @user_id ||= SecureRandom.hex(16) end end class MockRequest def remote_ip '127.0.0.1' end end describe LogStasher::LogSubscriber do subject { described_class.new } let(:logger) { ::Logger.new('/dev/null') } let(:mock_controller) { MockController.new } let(:mock_request) { MockRequest.new } let(:context) {{ :controller => mock_controller, :request => mock_request }} around do |example| backup_logger = LogStasher.logger LogStasher.logger = logger Thread.current[:logstasher_context] = context Timecop.freeze { example.run } Thread.current[:logstasher_context] = nil LogStasher.logger = backup_logger end describe '#process_action' do let(:timestamp) { ::Time.new.utc.iso8601(3) } let(:duration) { 12.4 } let(:json_params) { JSON.dump(payload[:params]) } let(:payload) {{ :controller => 'users', :action => 'show', :params => { 'foo' => 'bar' }, :format => 'text/plain', :method => 'method', :path => '/users/1', :status => 200 }} let(:event) { double(:payload => payload, :duration => duration) } it 'logs the event in logstash format' do expect(logger).to receive(:<<) do |json| expect(JSON.parse(json)).to eq({ '@timestamp' => timestamp, '@version' => '1', 'tags' => ['request'], 'action' => payload[:action], 'controller' => payload[:controller], 'format' => payload[:format], 'params' => json_params, 'ip' => mock_request.remote_ip, 'method' => payload[:method], 'request_path' => payload[:path], 'route' => "#{payload[:controller]}##{payload[:action]}", 'status' => payload[:status], 'runtime' => { 'total' => duration } }) end subject.process_action(event) end it 'appends fields to the log' do ::LogStasher.append_fields do |fields| fields['user_id'] = user_id fields['other'] = 'stuff' end expect(logger).to receive(:<<) do |json| fields = JSON.parse(json) expect(fields['user_id']).to eq mock_controller.user_id expect(fields['other']).to eq 'stuff' end subject.process_action(event) end it 'can be configured to remove parameters from the log' do allow(::LogStasher).to receive(:include_parameters?).and_return(false) expect(logger).to receive(:<<) do |json| expect(JSON.parse(json)['params']).to be_nil end subject.process_action(event) end it 'can be configured to skip parameter serialization' do expect(::LogStasher).to receive(:serialize_parameters?).and_return(false) expect(logger).to receive(:<<) do |json| expect(JSON.parse(json)['params']).to eq payload[:params] end subject.process_action(event) end it 'includes redirect location in the log' do redirect_event = double(:payload => {:location => 'new/location'}) subject.redirect_to(redirect_event) expect(logger).to receive(:<<) do |json| expect(JSON.parse(json)['location']).to eq 'new/location' end subject.process_action(event) end it 'includes runtimes in the log' do payload.merge!({ :view_runtime => 3.3, :db_runtime => 2.1 }) expect(logger).to receive(:<<) do |json| runtime = JSON.parse(json)['runtime'] expect(runtime['view']).to eq 3.3 expect(runtime['db']).to eq 2.1 end subject.process_action(event) end it 'includes exception info in the log' do begin fail RuntimeError, 'it no work' rescue # Test inside the rescue block so $! is properly set payload.merge!({ :exception => ['RuntimeError', 'it no work'] }) expect(logger).to receive(:<<) do |json| log = JSON.parse(json) expect(log['error_class']).to eq('RuntimeError') expect(log['error_message']).to eq('it no work') expect(log['error_backtrace']).to be_instance_of(Array) expect(log['status']).to eq 500 expect(log['tags']).to include('exception') end subject.process_action(event) end end end describe '#redirect_to' do let(:location) { "users/#{SecureRandom.hex(16)}" } let(:payload) {{ :location => location }} let(:event) { double(:payload => payload) } it 'copies the location into the thread local logstasher context' do subject.redirect_to(event) expect(Thread.current[:logstasher_context][:location]).to eq location end end end