diff --git a/lib/logstasher.rb b/lib/logstasher.rb index 0255777..8614a24 100644 --- a/lib/logstasher.rb +++ b/lib/logstasher.rb @@ -5,12 +5,21 @@ require 'logstasher/action_view/log_subscriber' require 'logstasher/rails_ext/action_controller/base' require 'logstasher/custom_fields' +require 'logstasher/rails_ext/rack/debug_exceptions' require 'request_store' require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/string/inflections' require 'active_support/ordered_options' module LogStasher + class NullLogger < Logger + def initialize(*args) + end + + def add(*args, &block) + end + end + extend self STORE_KEY = :logstasher_data REQUEST_CONTEXT_KEY = :logstasher_request_context @@ -94,12 +103,12 @@ def setup_before(config) LogStasher::ActionView::LogSubscriber.attach_to :action_view if config.view_enabled end - def setup(config) + def setup(config, app) # Path instrumentation class to insert our hook if (! config.controller_monkey_patch && config.controller_monkey_patch != false) || config.controller_monkey_patch == true require 'logstasher/rails_ext/action_controller/metal/instrumentation' end - self.suppress_app_logs(config) + self.suppress_app_logs(config, app) self.logger_path = config.logger_path || "#{Rails.root}/log/logstash_#{Rails.env}.log" self.logger = config.logger || new_logger(self.logger_path) self.logger.level = config.log_level || Logger::WARN @@ -126,9 +135,10 @@ def called_as_console? defined?(Rails::Console) && true || false end - def suppress_app_logs(config) + def suppress_app_logs(config, app) if configured_to_suppress_app_logs?(config) require 'logstasher/rails_ext/rack/logger' + app.env_config["action_dispatch.logger"] = NullLogger.new() LogStasher.remove_existing_log_subscriptions end end @@ -138,6 +148,20 @@ def configured_to_suppress_app_logs?(config) !!(config.suppress_app_log.nil? ? config.supress_app_log : config.suppress_app_log) end + def modify_middleware(app) + if enabled + app.middleware.insert_after ::ActionDispatch::DebugExceptions, ::LogStasher::ActionDispatch::DebugExceptions, app + end + end + + def custom_fields + Thread.current[:logstasher_custom_fields] ||= [] + end + + def custom_fields=(val) + Thread.current[:logstasher_custom_fields] = val + end + # Log an arbitrary message. # # Usually invoked by the level-based wrapper methods defined below. diff --git a/lib/logstasher/active_support/log_subscriber.rb b/lib/logstasher/active_support/log_subscriber.rb index 7eeece6..822e6f4 100644 --- a/lib/logstasher/active_support/log_subscriber.rb +++ b/lib/logstasher/active_support/log_subscriber.rb @@ -86,7 +86,7 @@ def location(event) def extract_exception(payload) if payload[:exception] exception, message = payload[:exception] - status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception) + status = ::ActionDispatch::ExceptionWrapper.status_code_for_exception(exception) if LogStasher.backtrace backtrace = $!.backtrace.join("\n") else diff --git a/lib/logstasher/rails_ext/rack/debug_exceptions.rb b/lib/logstasher/rails_ext/rack/debug_exceptions.rb new file mode 100644 index 0000000..4eaa7b4 --- /dev/null +++ b/lib/logstasher/rails_ext/rack/debug_exceptions.rb @@ -0,0 +1,45 @@ +require 'action_dispatch' +require 'active_support/all' + +module LogStasher + module ActionDispatch + + class DebugExceptions < ::ActionDispatch::DebugExceptions + include ::LogStasher::ActionDispatch + + def initialize(app, routes_app = nil) + @app = app + @routes_app = routes_app + end + + def call(env) + begin + status, header, body = @app.call(env) + if header['X-Cascade'] == 'pass' + raise ::ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}" + end + rescue Exception => exception + log_error(env, ::ActionDispatch::ExceptionWrapper.new(env, exception)) + end + [status, header, body] + end + + def build_exception_hash(wrapper) + exception = wrapper.exception + trace = wrapper.application_trace + trace = wrapper.framework_trace if trace.empty? + + { error: + ({ exception: exception.class.name, message: exception.message, trace: trace}. + merge!( exception.respond_to?(:annotated_source_code) && { annotated_source_code: exception.annoted_source_code } || {} )) + } + end + + private + + def log_error(env, wrapper) + LogStasher.logger << LogStasher.build_logstash_event(build_exception_hash(wrapper), ["exception"]).to_json + "\n" + end + end + end +end diff --git a/lib/logstasher/railtie.rb b/lib/logstasher/railtie.rb index 79e5363..c295073 100644 --- a/lib/logstasher/railtie.rb +++ b/lib/logstasher/railtie.rb @@ -33,9 +33,9 @@ class Railtie < Rails::Railtie LogStasher.setup_before(app.config.logstasher) if app.config.logstasher.enabled end - initializer :logstasher do + initializer :logstasher do |app| config.after_initialize do - LogStasher.setup(config.logstasher) if config.logstasher.enabled + LogStasher.setup(config.logstasher, app) if config.logstasher.enabled end end end diff --git a/spec/lib/logstasher/rails_ext/rack/debug_exceptions_spec.rb b/spec/lib/logstasher/rails_ext/rack/debug_exceptions_spec.rb new file mode 100644 index 0000000..12441f3 --- /dev/null +++ b/spec/lib/logstasher/rails_ext/rack/debug_exceptions_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +shared_examples 'MyApp' do + before do + class MyApp + def initialize() + end + def call(*args) + raise Exception.new("My Exception") + end + end + end + + let(:app) { MyApp.new } + let(:environment) { { 'action_dispatch.show_exceptions' => true } } + let(:logger) { double } + subject{ described_class.new(app) } + + before(:each) do + allow(LogStasher).to receive(:logger).and_return(logger) + allow(LogStasher.logger).to receive(:'<<').and_return(true) + end +end + +describe ::LogStasher::ActionDispatch::DebugExceptions do + include_examples 'MyApp' + + describe '#build_exception_hash' do + let (:wrapper) { double(exception: Exception.new("My Exception"), application_trace: [ "line5" ]) } + it do + hash = subject.build_exception_hash(wrapper) + + expect(hash).to match({:error=>{:exception=>"Exception", :message=>"My Exception", :trace=>["line5"]}}) + end + end + + describe 'calls LogStasher.logger with json format exception' do + describe '#log_error' do + it do + expect(LogStasher).to receive(:build_logstash_event) + expect(LogStasher.logger).to receive(:'<<').and_return(true) + subject.call(environment) + end + end + end +end diff --git a/spec/lib/logstasher_spec.rb b/spec/lib/logstasher_spec.rb index c13481a..f72355b 100644 --- a/spec/lib/logstasher_spec.rb +++ b/spec/lib/logstasher_spec.rb @@ -133,9 +133,9 @@ after { LogStasher.source = @previous_source } # Need to restore old source for specs it 'defines a method in ActionController::Base' do expect(LogStasher).to receive(:require).with('logstasher/rails_ext/action_controller/metal/instrumentation') - expect(LogStasher).to receive(:suppress_app_logs).with(config.logstasher) + expect(LogStasher).to receive(:suppress_app_logs).with(config.logstasher, app) expect(logger).to receive(:level=).with('warn') - LogStasher.setup(config.logstasher) + LogStasher.setup(config.logstasher, app) expect(LogStasher.source).to eq (logstasher_source || 'unknown') expect(LogStasher).to be_enabled expect(LogStasher::CustomFields.custom_fields).to be_empty @@ -176,19 +176,33 @@ end describe '.suppress_app_logs' do - let(:logstasher_config){ double(:logstasher => double(:suppress_app_log => true))} - let(:app){ double(:config => logstasher_config)} - it 'removes existing subscription if enabled' do - expect(LogStasher).to receive(:require).with('logstasher/rails_ext/rack/logger') - expect(LogStasher).to receive(:remove_existing_log_subscriptions) - LogStasher.suppress_app_logs(app.config.logstasher) + let(:app){ double(config: logstasher_config, env_config: {})} + + context 'when enabled' do + let(:logstasher_config){ double(logstasher: double(:suppress_app_log => true))} + it 'removes existing subscription if enabled' do + expect(LogStasher).to receive(:require).with('logstasher/rails_ext/rack/logger') + expect(LogStasher).to receive(:remove_existing_log_subscriptions) + LogStasher.suppress_app_logs(app.config.logstasher, app) + end + + it 'changes "action_dispatch.logger" to NullLogger' do + LogStasher.suppress_app_logs(app.config.logstasher, app) + expect(app.env_config).to include('action_dispatch.logger' => an_instance_of(::LogStasher::NullLogger)) + end end context 'when disabled' do let(:logstasher_config){ double(:logstasher => double(:suppress_app_log => false)) } + it 'does not remove existing subscription' do expect(LogStasher).to_not receive(:remove_existing_log_subscriptions) - LogStasher.suppress_app_logs(app.config.logstasher) + LogStasher.suppress_app_logs(app.config.logstasher, app) + end + + it 'leaves "action_dispatch.logger" as before' do + LogStasher.suppress_app_logs(app.config.logstasher, app) + expect(app.env_config).to match({}) end describe "backward compatibility" do @@ -196,7 +210,7 @@ let(:logstasher_config){ double(:logstasher => double(:suppress_app_log => nil, :supress_app_log => false)) } it 'does not remove existing subscription' do expect(LogStasher).to_not receive(:remove_existing_log_subscriptions) - LogStasher.suppress_app_logs(app.config.logstasher) + LogStasher.suppress_app_logs(app.config.logstasher, app) end end end