Skip to content
This repository
Browse code

Added ActiveSupport::BacktraceCleaner and Rails::BacktraceCleaner for…

… cutting down on backtrace noise (inspired by the Thoughtbot Quiet Backtrace plugin) [DHH]
  • Loading branch information...
commit f42c77f927eb49b00e84d355e07de48723d03fcb 1 parent a026b4c
David Heinemeier Hansson authored November 22, 2008
14  actionpack/lib/action_controller/rescue.rb
@@ -68,9 +68,8 @@ def log_error(exception) #:doc:
68 68
             logger.fatal(exception.to_s)
69 69
           else
70 70
             logger.fatal(
71  
-              "\n\n#{exception.class} (#{exception.message}):\n    " +
72  
-              clean_backtrace(exception).join("\n    ") +
73  
-              "\n\n"
  71
+              "\n#{exception.class} (#{exception.message}):\n  " +
  72
+              clean_backtrace(exception).join("\n  ") + "\n\n"
74 73
             )
75 74
           end
76 75
         end
@@ -151,13 +150,8 @@ def response_code_for_rescue(exception)
151 150
       end
152 151
 
153 152
       def clean_backtrace(exception)
154  
-        if backtrace = exception.backtrace
155  
-          if defined?(RAILS_ROOT)
156  
-            backtrace.map { |line| line.sub RAILS_ROOT, '' }
157  
-          else
158  
-            backtrace
159  
-          end
160  
-        end
  153
+        defined?(Rails) && Rails.respond_to?(:backtrace_cleaner) ? 
  154
+          Rails.backtrace_cleaner.clean(exception.backtrace) : exception.backtrace
161 155
       end
162 156
   end
163 157
 end
17  actionpack/lib/action_view/template_error.rb
@@ -20,7 +20,11 @@ def message
20 20
     end
21 21
 
22 22
     def clean_backtrace
23  
-      original_exception.clean_backtrace
  23
+      if defined?(Rails) && Rails.respond_to?(:backtrace_cleaner)
  24
+        Rails.backtrace_cleaner.clean(original_exception.backtrace)
  25
+      else
  26
+        original_exception.backtrace
  27
+      end
24 28
     end
25 29
 
26 30
     def sub_template_message
@@ -66,8 +70,8 @@ def line_number
66 70
     end
67 71
 
68 72
     def to_s
69  
-      "\n\n#{self.class} (#{message}) #{source_location}:\n" +
70  
-        "#{source_extract}\n    #{clean_backtrace.join("\n    ")}\n\n"
  73
+      "\n#{self.class} (#{message}) #{source_location}:\n" + 
  74
+      "#{source_extract}\n    #{clean_backtrace.join("\n    ")}\n\n"
71 75
     end
72 76
 
73 77
     # don't do anything nontrivial here. Any raised exception from here becomes fatal 
@@ -92,9 +96,4 @@ def source_location
92 96
         end + file_name
93 97
       end
94 98
   end
95  
-end
96  
-
97  
-if defined?(Exception::TraceSubstitutions)
98  
-  Exception::TraceSubstitutions << [/:in\s+`_run_.*'\s*$/, '']
99  
-  Exception::TraceSubstitutions << [%r{^\s*#{Regexp.escape RAILS_ROOT}/}, ''] if defined?(RAILS_ROOT)
100  
-end
  99
+end
18  actionpack/test/controller/rescue_test.rb
@@ -291,24 +291,6 @@ def test_rescue_templates
291 291
     assert_equal 'template_error',    templates[ActionView::TemplateError.name]
292 292
   end
293 293
 
294  
-  def test_clean_backtrace
295  
-    with_rails_root nil do
296  
-      # No action if RAILS_ROOT isn't set.
297  
-      cleaned = @controller.send(:clean_backtrace, @exception)
298  
-      assert_equal @exception.backtrace, cleaned
299  
-    end
300  
-
301  
-    with_rails_root Dir.pwd do
302  
-      # RAILS_ROOT is removed from backtrace.
303  
-      cleaned = @controller.send(:clean_backtrace, @exception)
304  
-      expected = @exception.backtrace.map { |line| line.sub(RAILS_ROOT, '') }
305  
-      assert_equal expected, cleaned
306  
-
307  
-      # No action if backtrace is nil.
308  
-      assert_nil @controller.send(:clean_backtrace, Exception.new)
309  
-    end
310  
-  end
311  
-
312 294
   def test_not_implemented
313 295
     with_all_requests_local false do
314 296
       with_rails_public_path(".") do
2  activesupport/CHANGELOG
... ...
@@ -1,5 +1,7 @@
1 1
 *2.3.0 [Edge]*
2 2
 
  3
+* Added ActiveSupport::BacktraceCleaner to cut down on backtrace noise according to filters and silencers [DHH]
  4
+
3 5
 * Added Object#try. ( Taken from http://ozmm.org/posts/try.html ) [Chris Wanstrath]
4 6
 
5 7
 * Added Enumerable#none? to check that none of the elements match the block #1408 [Damian Janowski]
1  activesupport/lib/active_support.rb
@@ -29,6 +29,7 @@
29 29
 require 'active_support/core_ext'
30 30
 
31 31
 require 'active_support/buffered_logger'
  32
+require 'active_support/backtrace_cleaner'
32 33
 
33 34
 require 'active_support/gzip'
34 35
 require 'active_support/cache'
72  activesupport/lib/active_support/backtrace_cleaner.rb
... ...
@@ -0,0 +1,72 @@
  1
+module ActiveSupport
  2
+  # Many backtraces include too much information that's not relevant for the context. This makes it hard to find the signal
  3
+  # in the backtrace and adds debugging time. With a BacktraceCleaner, you can setup filters and silencers for your particular
  4
+  # context, so only the relevant lines are included.
  5
+  #
  6
+  # If you need to reconfigure an existing BacktraceCleaner, like the one in Rails, to show as much as possible, you can always
  7
+  # call BacktraceCleaner#remove_silencers!
  8
+  #
  9
+  # Example:
  10
+  #
  11
+  #   bc = BacktraceCleaner.new
  12
+  #   bc.add_filter   { |line| line.gsub(Rails.root, '') } 
  13
+  #   bc.add_silencer { |line| line =~ /mongrel|rubygems/ }
  14
+  #   bc.clean(exception.backtrace) # will strip the Rails.root prefix and skip any lines from mongrel or rubygems
  15
+  #
  16
+  # Inspired by the Quiet Backtrace gem by Thoughtbot.
  17
+  class BacktraceCleaner
  18
+    def initialize
  19
+      @filters, @silencers = [], []
  20
+    end
  21
+    
  22
+    # Returns the backtrace after all filters and silencers has been run against it. Filters run first, then silencers.
  23
+    def clean(backtrace)
  24
+      silence(filter(backtrace))
  25
+    end
  26
+
  27
+    # Adds a filter from the block provided. Each line in the backtrace will be mapped against this filter.
  28
+    #
  29
+    # Example:
  30
+    #
  31
+    #   # Will turn "/my/rails/root/app/models/person.rb" into "/app/models/person.rb"
  32
+    #   backtrace_cleaner.add_filter { |line| line.gsub(Rails.root, '') }
  33
+    def add_filter(&block)
  34
+      @filters << block
  35
+    end
  36
+
  37
+    # Adds a silencer from the block provided. If the silencer returns true for a given line, it'll be excluded from the
  38
+    # clean backtrace.
  39
+    #
  40
+    # Example:
  41
+    #
  42
+    #   # Will reject all lines that include the word "mongrel", like "/gems/mongrel/server.rb" or "/app/my_mongrel_server/rb"
  43
+    #   backtrace_cleaner.add_silencer { |line| line =~ /mongrel/ }
  44
+    def add_silencer(&block)
  45
+      @silencers << block
  46
+    end
  47
+
  48
+    # Will remove all silencers, but leave in the filters. This is useful if your context of debugging suddenly expands as
  49
+    # you suspect a bug in the libraries you use.
  50
+    def remove_silencers!
  51
+      @silencers = []
  52
+    end
  53
+
  54
+    
  55
+    private
  56
+      def filter(backtrace)
  57
+        @filters.each do |f|
  58
+          backtrace = backtrace.map { |line| f.call(line) }
  59
+        end
  60
+        
  61
+        backtrace
  62
+      end
  63
+      
  64
+      def silence(backtrace)
  65
+        @silencers.each do |s|
  66
+          backtrace = backtrace.reject { |line| s.call(line) }
  67
+        end
  68
+        
  69
+        backtrace
  70
+      end
  71
+  end
  72
+end
1  activesupport/lib/active_support/core_ext/exception.rb
@@ -6,6 +6,7 @@ module ActiveSupport
6 6
   end
7 7
 end
8 8
 
  9
+# TODO: Turn all this into using the BacktraceCleaner.
9 10
 class Exception # :nodoc:
10 11
   def clean_message
11 12
     Pathname.clean_within message
10  activesupport/lib/active_support/test_case.rb
@@ -21,15 +21,21 @@ class TestCase < ::Test::Unit::TestCase
21 21
       Assertion = MiniTest::Assertion
22 22
     end
23 23
 
  24
+    # TODO: Figure out how to get the Rails::BacktraceFilter into minitest/unit
24 25
   # Test::Unit compatibility.
25 26
   rescue LoadError
26 27
     require 'test/unit/testcase'
27 28
     require 'active_support/testing/default'
28 29
 
  30
+    if defined?(Rails)
  31
+      require 'rails/backtrace_cleaner'
  32
+      Test::Unit::Util::BacktraceFilter.module_eval { include Rails::BacktraceFilterForTestUnit }
  33
+    end
  34
+
29 35
     class TestCase < ::Test::Unit::TestCase
30 36
       Assertion = Test::Unit::AssertionFailedError
31 37
       include ActiveSupport::Testing::Default
32  
-    end
  38
+    end    
33 39
   end
34 40
 
35 41
   class TestCase
@@ -37,4 +43,4 @@ class TestCase
37 43
     include ActiveSupport::Testing::Assertions
38 44
     extend ActiveSupport::Testing::Declarative
39 45
   end
40  
-end
  46
+end
1  activesupport/test/abstract_unit.rb
@@ -3,6 +3,7 @@
3 3
 gem 'mocha', '>= 0.9.0'
4 4
 require 'mocha'
5 5
 
  6
+$:.unshift "#{File.dirname(__FILE__)}/../lib"
6 7
 require 'active_support'
7 8
 require 'active_support/test_case'
8 9
 
47  activesupport/test/clean_backtrace_test.rb
... ...
@@ -0,0 +1,47 @@
  1
+require 'abstract_unit'
  2
+
  3
+class BacktraceCleanerFilterTest < ActiveSupport::TestCase
  4
+  def setup
  5
+    @bc = ActiveSupport::BacktraceCleaner.new
  6
+    @bc.add_filter { |line| line.gsub("/my/prefix", '') }
  7
+  end
  8
+  
  9
+  test "backtrace should not contain prefix when it has been filtered out" do
  10
+    assert_equal "/my/class.rb", @bc.clean([ "/my/prefix/my/class.rb" ]).first
  11
+  end
  12
+  
  13
+  test "backtrace should contain unaltered lines if they dont match a filter" do
  14
+    assert_equal "/my/other_prefix/my/class.rb", @bc.clean([ "/my/other_prefix/my/class.rb" ]).first
  15
+  end
  16
+  
  17
+  test "backtrace should filter all lines in a backtrace" do
  18
+    assert_equal \
  19
+      ["/my/class.rb", "/my/module.rb"], 
  20
+      @bc.clean([ "/my/prefix/my/class.rb", "/my/prefix/my/module.rb" ])
  21
+  end
  22
+end
  23
+
  24
+class BacktraceCleanerSilencerTest < ActiveSupport::TestCase
  25
+  def setup
  26
+    @bc = ActiveSupport::BacktraceCleaner.new
  27
+    @bc.add_silencer { |line| line =~ /mongrel/ }
  28
+  end
  29
+  
  30
+  test "backtrace should not contain lines that match the silencer" do
  31
+    assert_equal \
  32
+      [ "/other/class.rb" ], 
  33
+      @bc.clean([ "/mongrel/class.rb", "/other/class.rb", "/mongrel/stuff.rb" ])
  34
+  end
  35
+end
  36
+
  37
+class BacktraceCleanerFilterAndSilencerTest < ActiveSupport::TestCase
  38
+  def setup
  39
+    @bc = ActiveSupport::BacktraceCleaner.new
  40
+    @bc.add_filter   { |line| line.gsub("/mongrel", "") }
  41
+    @bc.add_silencer { |line| line =~ /mongrel/ }
  42
+  end
  43
+  
  44
+  test "backtrace should not silence lines that has first had their silence hook filtered out" do
  45
+    assert_equal [ "/class.rb" ], @bc.clean([ "/mongrel/class.rb" ])
  46
+  end
  47
+end
2  activesupport/test/core_ext/array_ext_test.rb
@@ -21,7 +21,7 @@ def test_second_through_tenth
21 21
     assert_equal array[2], array.third
22 22
     assert_equal array[3], array.fourth
23 23
     assert_equal array[4], array.fifth
24  
-    assert_equal array[41], array.fourty_two
  24
+    assert_equal array[41], array.forty_two
25 25
   end
26 26
 end
27 27
 
2  railties/CHANGELOG
... ...
@@ -1,5 +1,7 @@
1 1
 *2.3.0 [Edge]*
2 2
 
  3
+* Added Rails.backtrace_cleaner as an accessor for the Rails::BacktraceCleaner instance used by the framework to cut down on backtrace noise and config/initializers/backtrace_silencers.rb to add your own (or turn them all off) [DHH]
  4
+
3 5
 * Switch from Test::Unit::TestCase to ActiveSupport::TestCase.  [Jeremy Kemper]
4 6
 
5 7
 * Added config.i18n settings gatherer to config/environment, auto-loading of all locales in config/locales/*.rb,yml, and config/locales/en.yml as a sample locale [DHH]
6  railties/Rakefile
@@ -197,8 +197,10 @@ task :copy_configs do
197 197
   
198 198
   cp "configs/routes.rb", "#{PKG_DESTINATION}/config/routes.rb"
199 199
 
200  
-  cp "configs/initializers/inflections.rb", "#{PKG_DESTINATION}/config/initializers/inflections.rb"
201  
-  cp "configs/initializers/mime_types.rb",  "#{PKG_DESTINATION}/config/initializers/mime_types.rb"
  200
+  cp "configs/initializers/backtrace_silencers.rb", "#{PKG_DESTINATION}/config/initializers/backtrace_silencers.rb"
  201
+  cp "configs/initializers/inflections.rb",         "#{PKG_DESTINATION}/config/initializers/inflections.rb"
  202
+  cp "configs/initializers/mime_types.rb",          "#{PKG_DESTINATION}/config/initializers/mime_types.rb"
  203
+  cp "configs/initializers/new_rails_defaults.rb",  "#{PKG_DESTINATION}/config/initializers/new_rails_defaults.rb"
202 204
 
203 205
   cp "configs/locales/en.yml", "#{PKG_DESTINATION}/config/locales/en.yml"
204 206
 
7  railties/configs/initializers/backtrace_silencers.rb
... ...
@@ -0,0 +1,7 @@
  1
+# Be sure to restart your server when you modify this file.
  2
+
  3
+# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
  4
+# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
  5
+
  6
+# You can also remove all the silencers if you're trying do debug a problem that might steem from framework code.
  7
+# Rails.backtrace_cleaner.remove_silencers!
8  railties/lib/initializer.rb
@@ -39,6 +39,14 @@ def logger
39 39
         nil
40 40
       end
41 41
     end
  42
+    
  43
+    def backtrace_cleaner
  44
+      @@backtrace_cleaner ||= begin
  45
+        # Relies on ActiveSupport, so we have to lazy load to postpone definition until AS has been loaded
  46
+        require 'rails/backtrace_cleaner'
  47
+        Rails::BacktraceCleaner.new
  48
+      end
  49
+    end
42 50
 
43 51
     def root
44 52
       if defined?(RAILS_ROOT)
34  railties/lib/rails/backtrace_cleaner.rb
... ...
@@ -0,0 +1,34 @@
  1
+module Rails
  2
+  class BacktraceCleaner < ActiveSupport::BacktraceCleaner
  3
+    ERB_METHOD_SIG = /:in `_run_erb_.*/
  4
+    
  5
+    VENDOR_DIRS  = %w( vendor/plugins vendor/gems vendor/rails )
  6
+    MONGREL_DIRS = %w( lib/mongrel bin/mongrel )
  7
+    RAILS_NOISE  = %w( script/server )
  8
+    RUBY_NOISE   = %w( rubygems/custom_require benchmark.rb )
  9
+
  10
+    ALL_NOISE    = VENDOR_DIRS + MONGREL_DIRS + RAILS_NOISE + RUBY_NOISE
  11
+  
  12
+
  13
+    def initialize
  14
+      super
  15
+      add_filter   { |line| line.sub(RAILS_ROOT, '') }
  16
+      add_filter   { |line| line.sub(ERB_METHOD_SIG, '') }
  17
+      add_silencer { |line| ALL_NOISE.any? { |dir| line.include?(dir) } }
  18
+    end
  19
+  end
  20
+
  21
+
  22
+  # For installing the BacktraceCleaner in the test/unit
  23
+  module BacktraceFilterForTestUnit #:nodoc:
  24
+    def self.included(klass)
  25
+      klass.send :alias_method_chain, :filter_backtrace, :cleaning
  26
+    end
  27
+  
  28
+    def filter_backtrace_with_cleaning(backtrace)
  29
+      backtrace = filter_backtrace_without_cleaning(backtrace)
  30
+      backtrace = backtrace.first.split("\n") if backtrace.size == 1
  31
+      Rails.backtrace_cleaner.clean(backtrace)
  32
+    end
  33
+  end
  34
+end
7  railties/lib/rails_generator/generators/applications/app/app_generator.rb
@@ -62,9 +62,10 @@ def manifest
62 62
       m.template "configs/routes.rb", "config/routes.rb"
63 63
 
64 64
       # Initializers
65  
-      m.template "configs/initializers/inflections.rb", "config/initializers/inflections.rb"
66  
-      m.template "configs/initializers/mime_types.rb", "config/initializers/mime_types.rb"
67  
-      m.template "configs/initializers/new_rails_defaults.rb", "config/initializers/new_rails_defaults.rb"
  65
+      m.template "configs/initializers/backtrace_silencers.rb", "config/initializers/backtrace_silencers.rb"
  66
+      m.template "configs/initializers/inflections.rb",         "config/initializers/inflections.rb"
  67
+      m.template "configs/initializers/mime_types.rb",          "config/initializers/mime_types.rb"
  68
+      m.template "configs/initializers/new_rails_defaults.rb",  "config/initializers/new_rails_defaults.rb"
68 69
 
69 70
       # Locale
70 71
       m.template "configs/locales/en.yml", "config/locales/en.yml"

1 note on commit f42c77f

Dan Croak

Very pleased to see this, especially the backtrace_silencers.rb initializer. Nice work!

Please sign in to comment.
Something went wrong with that request. Please try again.