error_stalker 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +30 -0
  3. data/Gemfile.lock +76 -0
  4. data/README.rdoc +3 -0
  5. data/Rakefile +19 -0
  6. data/bin/create_indexes +14 -0
  7. data/bin/error_stalker_server +16 -0
  8. data/error_stalker.gemspec +21 -0
  9. data/lib/error_stalker.rb +4 -0
  10. data/lib/error_stalker/backend.rb +14 -0
  11. data/lib/error_stalker/backend/base.rb +14 -0
  12. data/lib/error_stalker/backend/in_memory.rb +25 -0
  13. data/lib/error_stalker/backend/log_file.rb +33 -0
  14. data/lib/error_stalker/backend/server.rb +41 -0
  15. data/lib/error_stalker/client.rb +62 -0
  16. data/lib/error_stalker/exception_group.rb +29 -0
  17. data/lib/error_stalker/exception_report.rb +116 -0
  18. data/lib/error_stalker/plugin.rb +42 -0
  19. data/lib/error_stalker/plugin/base.rb +24 -0
  20. data/lib/error_stalker/plugin/email_sender.rb +60 -0
  21. data/lib/error_stalker/plugin/lighthouse_reporter.rb +95 -0
  22. data/lib/error_stalker/plugin/views/exception_email.erb +18 -0
  23. data/lib/error_stalker/plugin/views/report.erb +18 -0
  24. data/lib/error_stalker/server.rb +152 -0
  25. data/lib/error_stalker/server/public/exception_logger.css +173 -0
  26. data/lib/error_stalker/server/public/grid.css +338 -0
  27. data/lib/error_stalker/server/public/images/background.png +0 -0
  28. data/lib/error_stalker/server/public/jquery-1.4.4.min.js +167 -0
  29. data/lib/error_stalker/server/views/_exception_message.erb +1 -0
  30. data/lib/error_stalker/server/views/_exception_table.erb +34 -0
  31. data/lib/error_stalker/server/views/index.erb +31 -0
  32. data/lib/error_stalker/server/views/layout.erb +18 -0
  33. data/lib/error_stalker/server/views/search.erb +41 -0
  34. data/lib/error_stalker/server/views/show.erb +32 -0
  35. data/lib/error_stalker/server/views/similar.erb +6 -0
  36. data/lib/error_stalker/sinatra_link_renderer.rb +25 -0
  37. data/lib/error_stalker/store.rb +11 -0
  38. data/lib/error_stalker/store/base.rb +75 -0
  39. data/lib/error_stalker/store/in_memory.rb +109 -0
  40. data/lib/error_stalker/store/mongoid.rb +318 -0
  41. data/lib/error_stalker/version.rb +4 -0
  42. data/test/test_helper.rb +8 -0
  43. data/test/unit/backend/base_test.rb +9 -0
  44. data/test/unit/backend/in_memory_test.rb +22 -0
  45. data/test/unit/backend/log_file_test.rb +25 -0
  46. data/test/unit/client_test.rb +67 -0
  47. data/test/unit/exception_report_test.rb +24 -0
  48. data/test/unit/plugins/email_sender_test.rb +12 -0
  49. data/test/unit/server_test.rb +141 -0
  50. data/test/unit/stores/in_memory_test.rb +58 -0
  51. metadata +109 -0
@@ -0,0 +1 @@
1
+ <%= h @exception.exception.to_s %>
@@ -0,0 +1,34 @@
1
+ <%= will_paginate @rows, :params => params %>
2
+ <table id="exceptions" cellpadding="0">
3
+ <tr>
4
+ <th class="count">&nbsp;</th>
5
+ <th class="last_occurred">Occurred</th>
6
+ <th class="exception">Exception</th>
7
+ <th class="links">&nbsp;</th>
8
+ </tr>
9
+ <% @rows.each_with_index do |report, i| %>
10
+ <tr id="<%= report.id %>" class="<%= i.even? ? 'even' : 'odd' %>">
11
+ <td class="count">&nbsp;</td>
12
+ <td class="last_occurred">
13
+ <span class="timestamp"><%= h report.timestamp %></span><br />
14
+ <%= h report.application %><br />
15
+ <%= h report.machine %><br />
16
+ </td>
17
+ <td class="exception">
18
+ <div>
19
+ <% @exception = report %>
20
+ <%= erb :_exception_message %>
21
+ <% if report.backtrace %>
22
+ <br />
23
+ <a href="javascript:$('tr#<%= report.id%> .stacktrace').toggle();">Stack trace</a>
24
+ <div class="stacktrace" style="display:none">
25
+ <pre><code><%= h report.backtrace.join("\n") %></code></pre>
26
+ </div>
27
+ <% end %>
28
+ </div>
29
+ </td>
30
+ <td class="links"><a href="/exceptions/<%= report.id %>.html">View</a></td>
31
+ </tr>
32
+ <% end %>
33
+ </table>
34
+ <%= will_paginate @rows %>
@@ -0,0 +1,31 @@
1
+ <h1>Recent Exceptions</h1>
2
+
3
+ <% if store.empty? %>
4
+ <p>No exceptions yet. You're just that awesome.</p>
5
+ <% else %>
6
+ <p><a href="/search">Search</a></p>
7
+
8
+ <%= will_paginate @records %>
9
+ <table id="exceptions" cellpadding="0">
10
+ <tr>
11
+ <th class="count">Count</th>
12
+ <th class="last_occurred">Last Occurred</th>
13
+ <th class="exception">Exception</th>
14
+ <th class="links">&nbsp;</th>
15
+ </tr>
16
+ <% @records.each_with_index do |group, i| %>
17
+ <tr class="<%= i.even? ? 'even' : 'odd' %>">
18
+ <td class="count"><%= group.count.to_i %></td>
19
+ <td class="last_occurred">
20
+ <span class="timestamp"><%= h group.most_recent_report.timestamp %></span><br />
21
+ <%= h group.most_recent_report.application %><br />
22
+ <%= h group.most_recent_report.machine %><br />
23
+ </td>
24
+ <% @exception = group.most_recent_report %>
25
+ <td class="exception"><div><%= erb :_exception_message %></div></td>
26
+ <td class="links"><a href="/exceptions/<%= group.most_recent_report.id %>.html">View</a> | <a href="/similar/<%= group.digest %>.html">See all</a></td>
27
+ </tr>
28
+ <% end %>
29
+ </table>
30
+ <%= will_paginate @records %>
31
+ <% end %>
@@ -0,0 +1,18 @@
1
+ <html>
2
+
3
+ <head>
4
+ <title>Exception Reporting</title>
5
+ <link href="<%=url 'grid.css' %>" media="screen" rel="stylesheet" type="text/css" />
6
+ <link href="<%=url 'exception_logger.css' %>" media="screen" rel="stylesheet" type="text/css" />
7
+ <script type="text/javascript" src="<%=url 'jquery-1.4.4.min.js' %>"></script>
8
+ </head>
9
+
10
+ <body>
11
+
12
+ <div id="content" class="container_12">
13
+ <%= yield %>
14
+ </div>
15
+
16
+ </body>
17
+
18
+ </html>
@@ -0,0 +1,41 @@
1
+ <p><a href="/">Back to list</a></p>
2
+ <h1>Search</h1>
3
+
4
+ <div id="search_form" class="clearfix">
5
+ <form method="get" action="/search">
6
+ <label for="application">Application</label>
7
+ <select id="application" name="application">
8
+ <option value=""></option>
9
+ <% store.applications.each do |application| %>
10
+ <option value="<%= application %>" <%= "selected='selected'" if params['application'] == application %>><%= application %></option>
11
+ <% end %>
12
+ </select>
13
+
14
+ <label for="machine">Machine</label>
15
+ <select id="machine" name="machine">
16
+ <option value=""></option>
17
+ <% store.machines.each do |machine| %>
18
+ <option value="<%= machine %>" <%= "selected='selected'" if params['machine'] == machine %>><%= machine %></option>
19
+ <% end %>
20
+ </select>
21
+
22
+ <label for="exception">Message</label>
23
+ <input id="exception" type="text" name="exception" value="<%= params[:exception] %>" />
24
+
25
+ <label for="type">Type</label>
26
+ <input id="type" type="text" name="type" value="<%= params[:type] %>" />
27
+
28
+ <% if store.supports_extended_searches? %>
29
+ <label for="data">Extra data (key:"value")</label>
30
+ <input id="data" type="text" name="data" value="<%= params[:data] %>" />
31
+ <% end %>
32
+
33
+ <input type="submit" name="Search" value="Search" />
34
+ </form>
35
+ </div>
36
+
37
+ <% if @results %>
38
+ <h2>Search results</h2>
39
+ <% @rows = @results%>
40
+ <%= erb :_exception_table %>
41
+ <% end %>
@@ -0,0 +1,32 @@
1
+ <p><a href="/">Back to list</a></p>
2
+ <h1>Exception in <%= h @report.application %></h1>
3
+ <h3>on <%= h @report.machine %> @ <%= h @report.timestamp %></h3>
4
+
5
+ <dl class="group_details">
6
+ <dt>Count</dt><dd><%= h(@group.count) %></dd>
7
+ <dt>First occurred</dt><dd><%= h(@group.first_timestamp) %></dd>
8
+ <dt>Most recently occurred</dt><dd><%= h(@group.most_recent_timestamp) %></dd>
9
+ <dt>On machines</dt><dd><%= h(@group.machines.join(', ')) %></dd>
10
+ <dt>Exception class</dt><dd><%= h(@group.type) %></dd>
11
+ </dl>
12
+ <p><a href="/similar/<%= @report.digest%>.html">See all</a></p>
13
+ <% plugins.each do |plugin| %>
14
+ <% if plugin.respond_to?(:exception_links) %>
15
+ <% plugin.exception_links(@report).each do |name, href| %>
16
+ <p><a href="<%= href %>"><%= name %></a></p>
17
+ <% end %>
18
+ <% end %>
19
+ <% end %>
20
+
21
+ <% @exception = @report %>
22
+ <h2><%= erb :_exception_message %></h2>
23
+
24
+ <% if @report.backtrace %>
25
+ <h3>Stack trace</h3>
26
+ <pre><code><%= h @report.backtrace.join("\n") %></code></pre>
27
+ <% end %>
28
+
29
+ <% if @report.data && !@report.data.empty? %>
30
+ <h3>Additional information</h3>
31
+ <pre><code><%= h @report.data.to_yaml %></code></pre>
32
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <p><a href="/">Back to list</a></p>
2
+ <h2>Exceptions similar to:</h2>
3
+ <h1><%= h cutoff(@group.first.exception) %></h1>
4
+
5
+ <% @rows = @group%>
6
+ <%= erb :_exception_table %>
@@ -0,0 +1,25 @@
1
+ require 'will_paginate/view_helpers/link_renderer'
2
+
3
+ module ErrorStalker
4
+ # +will_paginate+ doesn't have built-in sinatra support, so this
5
+ # LinkRenderer subclass implements the +url+ method to work with
6
+ # sinatra-style request hashes.
7
+ class SinatraLinkRenderer < WillPaginate::ViewHelpers::LinkRenderer
8
+ protected
9
+
10
+ # Returns the URL for the given +page+
11
+ def url(page)
12
+ url = @template.request.url
13
+ params = @template.request.params.dup
14
+ if page == 1
15
+ params.delete("page")
16
+ else
17
+ params["page"] = page
18
+ end
19
+
20
+ @template.request.path + "?" + params.map {|k, v| "#{Rack::Utils.escape(k)}=#{Rack::Utils.escape(v)}"}.join("&")
21
+ end
22
+ end
23
+ end
24
+
25
+ WillPaginate::ViewHelpers.pagination_options[:renderer] = ErrorStalker::SinatraLinkRenderer
@@ -0,0 +1,11 @@
1
+ # Exception stores are places that the ErrorStalker server will keep
2
+ # reported exceptions. Stores provide functionality like grouping
3
+ # exceptions, keeping track of exceptions, searching exceptions, and
4
+ # gathering recent exceptions. All ErrorStalker stores should inherit
5
+ # from ErrorStalker::Store::Base, and must implement all the methods
6
+ # defined on that class.
7
+ module ErrorStalker::Store
8
+ autoload :Base, 'error_stalker/store/base'
9
+ autoload :Mongoid, 'error_stalker/store/mongoid'
10
+ autoload :InMemory, 'error_stalker/store/in_memory'
11
+ end
@@ -0,0 +1,75 @@
1
+ require 'error_stalker/exception_group'
2
+
3
+ module ErrorStalker::Store
4
+ # The base store that all other exception stores should inherit
5
+ # from. All methods on this class must be inherited by subclasses,
6
+ # and the methods that return multiple objects must support
7
+ # pagination.
8
+ class Base
9
+
10
+ # Store this +exception_report+, however the store wishes to.
11
+ def store(exception_report)
12
+ raise NotImplementedError, "Must be implemented by child class"
13
+ end
14
+
15
+ # Return the most recent exception groups. Should return an array
16
+ # of ErrorStalker::ExceptionGroup objects.
17
+ def recent
18
+ raise NotImplementedError, "Must be implemented by child class"
19
+ end
20
+
21
+ # Have any exceptions been logged? If not, return +true+
22
+ def empty?
23
+ raise NotImplementedError, "Must be implemented by child class"
24
+ end
25
+
26
+ # Find an exception report with the given id
27
+ def find(id)
28
+ raise NotImplementedError, "Must be implemented by child class"
29
+ end
30
+
31
+ # Returns the ExceptionGroup matching +digest+
32
+ def group(digest)
33
+ raise NotImplementedError, "Must be implemented by child class"
34
+ end
35
+
36
+ # Returns a list of exceptions whose digest is +digest+.
37
+ def reports_in_group(digest)
38
+ raise NotImplementedError, "Must be implemented by child class"
39
+ end
40
+
41
+ # A list of all the applications that have seen exceptions
42
+ def applications
43
+ raise NotImplementedError, "Must be implemented by child class"
44
+ end
45
+
46
+ # A list of all the machines that have seen exceptions
47
+ def machines
48
+ raise NotImplementedError, "Must be implemented by child class"
49
+ end
50
+
51
+ # Does this store support searching through the data blob?
52
+ def supports_extended_searches?
53
+ false
54
+ end
55
+
56
+ # Returns the total number of exceptions
57
+ def total
58
+ raise NotImplementedError, "Must be implemented by child class"
59
+ end
60
+
61
+ # Returns the total number of exceptions since +timestamp+
62
+ def total_since(timestamp)
63
+ raise NotImplementedError, "Must be implemented by child class"
64
+ end
65
+
66
+ # Searches for exception reports maching +params+. Search should
67
+ # support searching by application name, machine name, exception
68
+ # name, and exception type. The keys in +params+ should match
69
+ # attributes of ErrorStalker::ExceptionReport, and the results
70
+ # should be ordered by timestamp from newest to oldest.
71
+ def search(params)
72
+ raise NotImplementedError, "Must be implemented by child class"
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,109 @@
1
+ require 'error_stalker/store/base'
2
+ require 'set'
3
+
4
+ # The simplest exception store. This just stores each reported
5
+ # exception in a list held in memory. This, of course, means that the
6
+ # exception list will disappear when the server goes down, the server
7
+ # might take up tons of memory, and searching will probably be
8
+ # slow. In other words, this is a terrible choice for production. On
9
+ # the other hand, this store is especially useful for tests.
10
+ class ErrorStalker::Store::InMemory < ErrorStalker::Store::Base
11
+
12
+ # The list of exceptions reported so far.
13
+ attr_reader :exceptions
14
+
15
+ # A hash of exceptions indexed by digest.
16
+ attr_reader :exception_groups
17
+
18
+ # All the machines that have seen exceptions
19
+ attr_reader :machines
20
+
21
+ # All the applications that have seen exceptions
22
+ attr_reader :applications
23
+
24
+ # Creates a new instance of this store.
25
+ def initialize
26
+ clear
27
+ end
28
+
29
+ # Store +exception_report+ in the exception list. This also indexes
30
+ # the exception into the appropriate exception group.
31
+ def store(exception_report)
32
+ @exceptions << exception_report
33
+ self.machines << exception_report.machine
34
+ self.applications << exception_report.application
35
+ exception_report.id = exceptions.length - 1
36
+ exception_groups[exception_report.digest] ||= []
37
+ exception_groups[exception_report.digest] << exception_report
38
+ exception_report.id
39
+ end
40
+
41
+ # Returns a list of exceptions whose digest is +digest+.
42
+ def reports_in_group(digest)
43
+ exception_groups[digest]
44
+ end
45
+
46
+ # returns the exception group matching +digest+
47
+ def group(digest)
48
+ build_group_for_exceptions(reports_in_group(digest))
49
+ end
50
+
51
+ # Empty this exception store. Useful for tests!
52
+ def clear
53
+ @exceptions = []
54
+ @exception_groups = {}
55
+ @machines = Set.new
56
+ @applications = Set.new
57
+ end
58
+
59
+ # Searches for exception reports maching +params+.
60
+ def search(params = {})
61
+ results = exceptions
62
+ results = results.select {|e| e.machine == params[:machine]} if params[:machine] && !params[:machine].empty?
63
+ results = results.select {|e| e.application == params[:application]} if params[:application] && !params[:application].empty?
64
+ results = results.select {|e| e.exception.to_s =~ /#{params[:exception]}/} if params[:exception] && !params[:exception].empty?
65
+ results = results.select {|e| e.type.to_s =~ /#{params[:type]}/} if params[:type] && !params[:type].empty?
66
+ results.reverse
67
+ end
68
+
69
+ # Find an exception report with the given id.
70
+ def find(id)
71
+ exceptions[id.to_i]
72
+ end
73
+
74
+ # Have we logged any exceptions?
75
+ def empty?
76
+ exceptions.empty?
77
+ end
78
+
79
+ # Return recent exceptions grouped by digest.
80
+ def recent
81
+ data = []
82
+ exception_groups.map do |digest, group|
83
+ data << build_group_for_exceptions(group)
84
+ end
85
+
86
+ data.reverse
87
+ end
88
+
89
+ def total
90
+ @exceptions.count
91
+ end
92
+
93
+ def total_since(timestamp)
94
+ @exceptions.select { |e| e.timestamp >= timestamp.to_s }.length
95
+ end
96
+
97
+ protected
98
+
99
+ def build_group_for_exceptions(group)
100
+ ErrorStalker::ExceptionGroup.new.tap do |g|
101
+ g.count = group.length
102
+ g.digest = group.first.digest
103
+ g.machines = group.map(&:machine).uniq
104
+ g.first_timestamp = group.first.timestamp
105
+ g.most_recent_timestamp = group.last.timestamp
106
+ g.most_recent_report = group.last
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,318 @@
1
+ require 'mongoid'
2
+ require 'error_stalker/store/base'
3
+
4
+ # Store exceptions using MongoDB. This store provides fast storage and
5
+ # querying of exceptions, and long-term persistence. It also allows
6
+ # querying based on arbitrary data stored in the +data+ hash of the
7
+ # exception report, which allows for crazy things like searching
8
+ # reports by URL or IP address.
9
+ class ErrorStalker::Store::Mongoid < ErrorStalker::Store::Base
10
+
11
+ # Configure mongoid from the mongoid config file found in
12
+ # +config_file+. This mongoid config file should be similar to the
13
+ # one on http://mongoid.org/docs/installation/, and must be indexed
14
+ # by environment name. +config_file+ is relative to either wherever
15
+ # you start the server from or the config.ru file, unless you pass a
16
+ # full file path.
17
+ def initialize(config_file)
18
+ filename = File.expand_path(config_file)
19
+ settings = YAML.load(ERB.new(File.new(filename).read).result)
20
+
21
+ Mongoid.configure do |config|
22
+ config.from_hash(settings[ENV['RACK_ENV']])
23
+ end
24
+ Thread.new { migrate_data }
25
+ end
26
+
27
+ # Store +exception_report+ in the database.
28
+ def store(exception_report)
29
+ report = ExceptionReport.create_from_exception_report(exception_report)
30
+ update_caches(report)
31
+ report.id
32
+ end
33
+
34
+ # Have we logged any exceptions?
35
+ def empty?
36
+ ExceptionGroup.where(:most_recent_timestamp.gt => 7.days.ago).count == 0
37
+ end
38
+
39
+ # Return the last 7 days worth of unique exception reports grouped
40
+ # by exception group.
41
+ def recent
42
+ # Needs to be wrapped in a PaginationHelper because we'll call
43
+ # paginate on the collection returned from this method. We don't
44
+ # want to use mongoid pagination here because we don't know what
45
+ # parameters we want to paginate on, and we don't want to return
46
+ # an array here because we don't want to load all the mongid
47
+ # models in memory. This is also made trickier because of my
48
+ # hacked-up :include stuff I built into ExceptionGroup.
49
+ ExceptionGroup::PaginationHelper.new(ExceptionGroup.where(:most_recent_timestamp.gt => 7.days.ago).order_by(:most_recent_timestamp.desc))
50
+ end
51
+
52
+ # Find an exception report with the given id.
53
+ def find(id)
54
+ ExceptionReport.find(id).to_exception_report
55
+ end
56
+
57
+ # All applications that have been seen by this store
58
+ def applications
59
+ Application.all.order_by(:name.asc).map(&:name)
60
+ end
61
+
62
+ # All machines that have been seen by this store
63
+ def machines
64
+ Machine.all.order_by(:name.asc).map(&:name)
65
+ end
66
+
67
+ # Returns all the exceptions in a group, ordered by
68
+ # most_recent_timestamp
69
+ def reports_in_group(digest)
70
+ ExceptionReport.where(:digest => digest).order_by(:timestamp.desc)
71
+ end
72
+
73
+ # returns the ExceptionGroup object corresponding to a particular
74
+ # digest
75
+ def group(digest)
76
+ ExceptionGroup.where(:digest => digest).first
77
+ end
78
+
79
+ # Does this store support searching through the data blob?
80
+ def supports_extended_searches?
81
+ true
82
+ end
83
+
84
+ def total
85
+ ExceptionReport.count()
86
+ end
87
+
88
+ def total_since(timestamp)
89
+ ExceptionReport.where(:timestamp.gte => timestamp).count()
90
+ end
91
+
92
+ # Searches for exception reports maching +params+. Supports querying
93
+ # by arbitrary data in the +data+ hash associated with the exception, with the format:
94
+ #
95
+ # REMOTE_ADDR:127.0.0.1 PATH:/test
96
+ def search(params)
97
+ scope = ExceptionReport.all
98
+
99
+ [:application, :machine].each do |param|
100
+ if params[param] && !params[param].empty?
101
+ scope.where(param => params[param])
102
+ end
103
+ end
104
+
105
+ [:exception, :type].each do |param|
106
+ if params[param] && !params[param].empty?
107
+ scope.where(param => /#{params[param]}/)
108
+ end
109
+ end
110
+
111
+ if params[:data] && !params[:data].empty?
112
+ params[:data].split.each do |keyvalue|
113
+ key, value = keyvalue.split(':')
114
+ scope.where("data" => {"#{key}" => "#{value}"})
115
+ end
116
+ end
117
+
118
+ scope.order_by(:timestamp.desc)
119
+ end
120
+
121
+ # Creates the MongoDB indexes used by this driver. Should be called
122
+ # at some point after deciding to use the mongoid store. Can be
123
+ # called either manually, or by running <tt>bin/create_indexes</tt>
124
+ def create_indexes
125
+ ErrorStalker::Store::Mongoid::ExceptionReport.create_indexes
126
+ ErrorStalker::Store::Mongoid::ExceptionGroup.create_indexes
127
+ end
128
+
129
+ # Migrate the data in the mongoid database to a newer format. This
130
+ # should eventually be more robust, like the Rails version, but for
131
+ # now this should be fine.
132
+ def migrate_data
133
+ if SchemaMigrations.where(:version => 1).empty?
134
+ ExceptionGroup.all.order_by(:timestamp).desc.each do |group|
135
+ exceptions = ExceptionReport.where(:digest => group.digest).order_by(:timestamp)
136
+ unless exceptions.empty?
137
+ exceptions = exceptions.to_a
138
+ group.attributes[:timestamp] = nil
139
+ group.first_timestamp = exceptions[0].timestamp
140
+ group.most_recent_timestamp = exceptions[-1].timestamp
141
+ group.machines = exceptions.map(&:machine).uniq
142
+ group.save
143
+ end
144
+ end
145
+ SchemaMigrations.create(:version => 1)
146
+ end
147
+ end
148
+
149
+ protected
150
+
151
+ # In order to make ErrorStalker super-fast, we keep a bunch of cached
152
+ # data (like exception report groups, machines, and
153
+ # applications). +update_caches+ updates all of this cached data
154
+ # when an exception report comes in.
155
+ def update_caches(report)
156
+ ExceptionGroup.collection.update(
157
+ {:digest => report.digest},
158
+ {
159
+ '$inc' => {:count => 1},
160
+ '$set' => {:most_recent_report_id => report.id, :most_recent_timestamp => report.timestamp},
161
+ '$addToSet' => {:machines => report.machine}
162
+ },
163
+ :upsert => true)
164
+
165
+ # Make sure the first_timestamp parameter is set. Unfortunately
166
+ # mongoid doesn't have an $add modifier yet, so we have to do another query.
167
+ ExceptionGroup.collection.update(
168
+ {:digest => report.digest, :first_timestamp => nil},
169
+ {'$set' => {:first_timestamp => report.timestamp}})
170
+
171
+ # Update indexes to pre-populate the search dropdowns.
172
+ Machine.collection.update(
173
+ {:name => report.machine},
174
+ {:name => report.machine},
175
+ :upsert => true)
176
+
177
+ Application.collection.update(
178
+ {:name => report.application},
179
+ {:name => report.application},
180
+ :upsert => true)
181
+
182
+ report.id
183
+ end
184
+ end
185
+
186
+ # Keeps track of the migrations we've run so far.
187
+ class ErrorStalker::Store::Mongoid::SchemaMigrations
188
+ include Mongoid::Document
189
+ field :version
190
+ end
191
+
192
+ # A cache of all the applications that have had exception reports seen
193
+ # by this server, so we don't have to search the entire DB to populate
194
+ # the search dropdown.
195
+ class ErrorStalker::Store::Mongoid::Application
196
+ include Mongoid::Document
197
+ field :name
198
+ end
199
+
200
+ # A cache of all the machines that have had exception reports seen by
201
+ # this server, so we don't have to search the entire DB to populate
202
+ # the search dropdown.
203
+ class ErrorStalker::Store::Mongoid::Machine
204
+ include Mongoid::Document
205
+ field :name
206
+ end
207
+
208
+ # Aggregates exceptions for for the 'recent exceptions' list. This is
209
+ # way faster than mapreducing on demand, although it requires some
210
+ # crazy code to preload all the exceptions.
211
+ class ErrorStalker::Store::Mongoid::ExceptionGroup < ErrorStalker::ExceptionGroup
212
+ include Mongoid::Document
213
+ field :count, :type => Integer
214
+ field :digest
215
+ field :machines, :type => Array
216
+ field :first_timestamp, :type => Time
217
+ field :most_recent_timestamp, :type => Time
218
+ field :most_recent_report_id, :type => Integer
219
+ index :digest
220
+ index :most_recent_timestamp
221
+
222
+ # Cache most recent report so we can preload a bunch at once
223
+ attr_accessor :most_recent_report
224
+
225
+ # When we display the list of grouped recent exceptions, we paginate
226
+ # them. We also need to display information about the most recent
227
+ # exception report. This helper class wraps +paginate+, doing a
228
+ # hacked-in +:include+ to get the most recent reports for the requested
229
+ # exception groups without running into the N+1 problem.
230
+ class PaginationHelper
231
+
232
+ # Wraps +criteria+ in a new PaginationHelper, which will include
233
+ # the most recent exception reports when +paginate+ is called.
234
+ def initialize(criteria)
235
+ @criteria = criteria
236
+ end
237
+
238
+ # Override the built-in pagination to support preloading the
239
+ # associated exception reports.
240
+ def paginate(pagination_opts = {})
241
+ recent = @criteria.paginate(pagination_opts)
242
+ exceptions = ErrorStalker::Store::Mongoid::ExceptionReport.where(:_id.in => recent.map(&:most_recent_report_id))
243
+ # Fake association preloading
244
+ id_map = {}.tap do |h|
245
+ exceptions.each do |ex|
246
+ h[ex.id] = ex
247
+ end
248
+ end
249
+
250
+ recent.each do |r|
251
+ r.most_recent_report = id_map[r.most_recent_report_id]
252
+ end
253
+
254
+ recent
255
+ end
256
+ end
257
+ end
258
+
259
+ # The mongoid version of ErrorStalker::ExceptionReport. This class
260
+ # is used for mongo-specific querying and persistence of
261
+ # ExceptionReports, while base ExceptionReports are store-agnostic.
262
+ class ErrorStalker::Store::Mongoid::ExceptionReport
263
+ include Mongoid::Document
264
+ field :application
265
+ field :machine
266
+ field :timestamp, :type => Time
267
+ field :type
268
+ field :exception
269
+ field :data, :type => Array
270
+ field :backtrace, :type => Array
271
+ field :digest
272
+
273
+ index :digest
274
+ index :data
275
+ index :timestamp
276
+
277
+ # Generates an ErrorStalker::ExceptionReport from this model,
278
+ # converting the +data+ field from a list of key-value pairs to a
279
+ # full-fledged hash. Internally, we store it as a list of key->value
280
+ # to support fast multiattribute indexing, one of the cooler mongo
281
+ # features.
282
+ def to_exception_report
283
+ params = {}
284
+ [:id, :application, :machine, :timestamp, :type, :exception, :digest, :backtrace].each do |field|
285
+ params[field] = send(field)
286
+ end
287
+
288
+ if data
289
+ params[:data] = {}.tap do |h|
290
+ data.map { |hash| h[hash.keys.first] = hash[hash.keys.first] }
291
+ end
292
+ end
293
+
294
+ ErrorStalker::ExceptionReport.new(params)
295
+ end
296
+
297
+ # Create a new mongoid exception report from +exception_report+.
298
+ def self.create_from_exception_report(exception_report)
299
+ object = new do |o|
300
+ [:application, :machine, :timestamp, :type, :exception, :digest].each do |field|
301
+ o.send("#{field}=", exception_report.send(field))
302
+ end
303
+
304
+ # Store data as a list of key-value pairs, so the index on 'data' catches them all
305
+ if exception_report.data && exception_report.data.kind_of?(Hash)
306
+ o.data = [].tap do |array|
307
+ exception_report.data.map {|key, value| array << {key => value}}
308
+ end
309
+ end
310
+
311
+ if exception_report.backtrace
312
+ o.backtrace = exception_report.backtrace
313
+ end
314
+ end
315
+ object.save
316
+ object
317
+ end
318
+ end