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.
- data/.gitignore +4 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +76 -0
- data/README.rdoc +3 -0
- data/Rakefile +19 -0
- data/bin/create_indexes +14 -0
- data/bin/error_stalker_server +16 -0
- data/error_stalker.gemspec +21 -0
- data/lib/error_stalker.rb +4 -0
- data/lib/error_stalker/backend.rb +14 -0
- data/lib/error_stalker/backend/base.rb +14 -0
- data/lib/error_stalker/backend/in_memory.rb +25 -0
- data/lib/error_stalker/backend/log_file.rb +33 -0
- data/lib/error_stalker/backend/server.rb +41 -0
- data/lib/error_stalker/client.rb +62 -0
- data/lib/error_stalker/exception_group.rb +29 -0
- data/lib/error_stalker/exception_report.rb +116 -0
- data/lib/error_stalker/plugin.rb +42 -0
- data/lib/error_stalker/plugin/base.rb +24 -0
- data/lib/error_stalker/plugin/email_sender.rb +60 -0
- data/lib/error_stalker/plugin/lighthouse_reporter.rb +95 -0
- data/lib/error_stalker/plugin/views/exception_email.erb +18 -0
- data/lib/error_stalker/plugin/views/report.erb +18 -0
- data/lib/error_stalker/server.rb +152 -0
- data/lib/error_stalker/server/public/exception_logger.css +173 -0
- data/lib/error_stalker/server/public/grid.css +338 -0
- data/lib/error_stalker/server/public/images/background.png +0 -0
- data/lib/error_stalker/server/public/jquery-1.4.4.min.js +167 -0
- data/lib/error_stalker/server/views/_exception_message.erb +1 -0
- data/lib/error_stalker/server/views/_exception_table.erb +34 -0
- data/lib/error_stalker/server/views/index.erb +31 -0
- data/lib/error_stalker/server/views/layout.erb +18 -0
- data/lib/error_stalker/server/views/search.erb +41 -0
- data/lib/error_stalker/server/views/show.erb +32 -0
- data/lib/error_stalker/server/views/similar.erb +6 -0
- data/lib/error_stalker/sinatra_link_renderer.rb +25 -0
- data/lib/error_stalker/store.rb +11 -0
- data/lib/error_stalker/store/base.rb +75 -0
- data/lib/error_stalker/store/in_memory.rb +109 -0
- data/lib/error_stalker/store/mongoid.rb +318 -0
- data/lib/error_stalker/version.rb +4 -0
- data/test/test_helper.rb +8 -0
- data/test/unit/backend/base_test.rb +9 -0
- data/test/unit/backend/in_memory_test.rb +22 -0
- data/test/unit/backend/log_file_test.rb +25 -0
- data/test/unit/client_test.rb +67 -0
- data/test/unit/exception_report_test.rb +24 -0
- data/test/unit/plugins/email_sender_test.rb +12 -0
- data/test/unit/server_test.rb +141 -0
- data/test/unit/stores/in_memory_test.rb +58 -0
- 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"> </th>
|
5
|
+
<th class="last_occurred">Occurred</th>
|
6
|
+
<th class="exception">Exception</th>
|
7
|
+
<th class="links"> </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"> </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"> </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,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
|