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,42 @@
1
+ # The ErrorStalker server supports third-party plugins that add
2
+ # functionality to the server.
3
+ #
4
+ # Plugins should inherit from ErrorStalker::Plugin::Base. This base
5
+ # plugin provides functions that can be overridden in its
6
+ # children. Currently, a few hooks are provided for plugins to
7
+ # override:
8
+ #
9
+ # [ErrorStalker::Plugin::Base.new] Called when the server starts. This
10
+ # method can be overridden to do things
11
+ # like add extra routes to the server
12
+ # or initialize any data structures
13
+ # that the plugin needs to keep track
14
+ # of. Take a look at
15
+ # ErrorStalker::Plugin::LighthouseReporter
16
+ # for a good example of how this method
17
+ # can be hooked to provide additional
18
+ # functionality.
19
+ #
20
+ # [ErrorStalker::Plugin::Base#exception_links] Called when rendering
21
+ # exception details. This
22
+ # function should return an
23
+ # array of [link_text,
24
+ # link_href] pairs that
25
+ # will be used to link to
26
+ # additional routes that
27
+ # the plugin might add.
28
+ #
29
+ # [ErrorStalker::Plugin::Base#after_create] Called when a new exception
30
+ # is
31
+ # reported. ErrorStalker::Plugin::EmailSender
32
+ # has a good example of this
33
+ # being used.
34
+ #
35
+ # After creating a plugin, it can be added to the server in the server
36
+ # configuration file or manually added using the
37
+ # ErrorStalker::Server#plugins attribute.
38
+ module ErrorStalker::Plugin
39
+ autoload :Base, 'error_stalker/plugin/base'
40
+ autoload :LighthouseReporter, 'error_stalker/plugin/lighthouse_reporter'
41
+ autoload :EmailSender, 'error_stalker/plugin/email_sender'
42
+ end
@@ -0,0 +1,24 @@
1
+ # The base ErrorStalker::Plugin, that all plugins should inherit
2
+ # from. Provides default implementations of all the supported plugin
3
+ # methods, so you can (and should) call +super+ in your plugin subclasses.
4
+ class ErrorStalker::Plugin::Base
5
+
6
+ # Create a new instance of this plugin. +app+ is the sinatra
7
+ # ErrorStalker::Server instance, and +params+ is an arbitrary hash of
8
+ # plugin-specific parameters or options.
9
+ def initialize(app, params = {})
10
+ end
11
+
12
+ # An array of [name, href] pairs of links that will show up on the
13
+ # exception detail page. These are most commonly used to link to
14
+ # additional routes added by the plugin.
15
+ def exception_links(exception_report)
16
+ []
17
+ end
18
+
19
+ # Called after a new exception is reported. At the point that this
20
+ # is called, +exception_report+ will have an ID and has been
21
+ # associated with an exception group.
22
+ def after_create(app, exception_report)
23
+ end
24
+ end
@@ -0,0 +1,60 @@
1
+ require 'mail'
2
+
3
+ # The email sender plugin will send an email to an address the first
4
+ # time an exception in a group is reported. Future exceptions that go
5
+ # in the same group will not trigger emails.
6
+ class ErrorStalker::Plugin::EmailSender < ErrorStalker::Plugin::Base
7
+
8
+ # Parameters that are used in order to figure out how and where to
9
+ # send the report email. These parameters are passed directly to the
10
+ # {mail gem}[https://github.com/mikel/mail]. See that page for a
11
+ # reference. The subject and body are created by the plugin, but all
12
+ # other parameters (to, from, etc.) will have to be passed in.
13
+ attr_reader :mail_params
14
+
15
+ # Create a new instance of this plugin. +mail_params+ is a hash of
16
+ # parameters that are used to build the report email.
17
+ def initialize(app, mail_params = {})
18
+ super(app, mail_params)
19
+ @mail_params = mail_params
20
+ end
21
+
22
+ # Builds the mail object from +exception_report+ that we can later
23
+ # deliver. This is mostly here to make testing easier.
24
+ def build_email(exception_report, exception_url)
25
+ @report = exception_report
26
+ @url = exception_url
27
+
28
+ mail = Mail.new({
29
+ 'subject' => "Exception on #{exception_report.machine} - #{exception_report.exception.to_s[0, 64]}",
30
+ 'body' => ERB.new(File.read(File.expand_path('views/exception_email.erb', File.dirname(__FILE__)))).result(binding)
31
+ }.merge(mail_params))
32
+
33
+ if mail_params['delivery_method']
34
+ mail.delivery_method(mail_params['delivery_method'].to_sym, (mail_params['delivery_settings'] || {}))
35
+ end
36
+
37
+ mail
38
+ end
39
+
40
+ # Sets up the parameters we need to build a new exception report
41
+ # email, and sends the mail.
42
+ def send_email(app, exception_report)
43
+ request = app.request
44
+ host_with_port = request.host
45
+ host_with_port << ":#{request.port}" if request.port != 80
46
+ url = "#{request.scheme}://#{host_with_port}/exceptions/#{exception_report.id}.html"
47
+ mail = build_email(exception_report, url)
48
+ mail.deliver
49
+ end
50
+
51
+ # Hook to trigger an email when a new exception report with
52
+ # +report+'s digest comes in.
53
+ def after_create(app, report)
54
+ # Only send an email if it's the first exception of this type
55
+ # we've seen
56
+ send_email(app, report) if app.store.group(report.digest).count == 1
57
+ super(app, report)
58
+ end
59
+
60
+ end
@@ -0,0 +1,95 @@
1
+ require 'lighthouse'
2
+
3
+ # A simple plugin for reporting exceptions as new bugs in
4
+ # {Lighthouse}[http://lighthouseapp.com]. When this plugin is enabled,
5
+ # a new link will show up on the exception detail page that
6
+ # pre-populates a form for sending the exception report to Lighthouse.
7
+ class ErrorStalker::Plugin::LighthouseReporter < ErrorStalker::Plugin::Base
8
+
9
+ # The lighthouse project id that this plugin will report bugs to.
10
+ attr_reader :project_id
11
+
12
+ # Creates a new instance of this plugin. There are a few parameters
13
+ # that must be passed in in order for this plugin to work correctly:
14
+ #
15
+ # [params['account']] The Lighthouse account name these exceptions
16
+ # will be posted as
17
+ #
18
+ # [params['token']] The read/write token assigned by Lighthouse for
19
+ # API access
20
+ #
21
+ # [params['project_id']] The Lighthouse project these exceptions
22
+ # will be reported to
23
+ def initialize(app, params = {})
24
+ super(app, params)
25
+ app.class.send(:include, Actions)
26
+ app.lighthouse = self
27
+ Lighthouse.account = params['account']
28
+ Lighthouse.token = params['token']
29
+ @project_id = params['project_id']
30
+ end
31
+
32
+ # A list containing the link that will show up on the exception
33
+ # report's detail page. This link hooks into one of the new actions
34
+ # defined by this plugin.
35
+ def exception_links(exception_report)
36
+ super(exception_report) + [["Report to Lighthouse", "/lighthouse/report/#{exception_report.id}.html"]]
37
+ end
38
+
39
+ # Create a new Lighthouse ticket object pre-populated with information
40
+ # about +exception+.
41
+ def new_ticket(request, exception)
42
+ ticket = Lighthouse::Ticket.new(:project_id => project_id)
43
+ ticket.title = "Exception: #{exception.exception}"
44
+ host_with_port = request.host
45
+ host_with_port << ":#{request.port}" if request.port != 80
46
+ ticket_link = "#{request.scheme}://#{host_with_port}/exceptions/#{exception.id}.html"
47
+ ticket.body = ticket_link
48
+ ticket.tags = "exception"
49
+ ticket
50
+ end
51
+
52
+ # Post a new ticket to lighthouse with the params specified in
53
+ # +params+.
54
+ def post_ticket(params)
55
+ ticket = Lighthouse::Ticket.new(:project_id => project_id)
56
+ ticket.title = params[:title]
57
+ ticket.body = params[:body]
58
+ ticket.tags = params[:tags]
59
+ ticket.save
60
+ end
61
+
62
+ # Extra actions that will be added to the ErrorStalker::Server
63
+ # instance when this plugin is enabled. Provides actions for
64
+ # creating a new Lighthouse ticket with exception details and
65
+ # posting the ticket to Lighthouse. This module also adds a
66
+ # +lighthouse+ accessor to the server instance that can be used to
67
+ # build and send tickets to Lighthouse.
68
+ module Actions
69
+
70
+ # Adds the actions described above to the ErrorStalker::Server
71
+ # instance.
72
+ def self.included(base)
73
+ base.class_eval do
74
+
75
+ attr_accessor :lighthouse
76
+
77
+ get '/lighthouse/report/:id.html' do
78
+ @exception = store.find(params["id"])
79
+ @ticket = lighthouse.new_ticket(request, @exception)
80
+ erb File.read(File.expand_path('views/report.erb', File.dirname(__FILE__)))
81
+ end
82
+
83
+ post '/lighthouse/report/:id.html' do
84
+ if lighthouse.post_ticket(params)
85
+ redirect "/exceptions/#{params[:id]}.html"
86
+ else
87
+ @error = "There was an error submitting the ticket: <br />#{@ticket.errors.join("<br />")}"
88
+ erb File.read(File.expand_path('views/report.erb', File.dirname(__FILE__)))
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+
@@ -0,0 +1,18 @@
1
+ Exception: <%= @report.application %> (<%= @report.machine %>) at <%= @report.timestamp %>
2
+
3
+ <%= @url %>
4
+
5
+ Exception
6
+ =========
7
+
8
+ <%= @report.exception.to_s %>
9
+ <% if @report.backtrace %>
10
+ Stack Trace
11
+ ===========
12
+
13
+ <%= @report.backtrace.join("\n") %>
14
+ <% end %>
15
+ Extra Data
16
+ ==========
17
+
18
+ <%= @report.data.to_yaml %>
@@ -0,0 +1,18 @@
1
+ <%= h @error if @error %>
2
+
3
+ <h1>Import into Lighthouse</h1>
4
+
5
+ <div id="lighthouse_form" class="clearfix">
6
+ <form method="post" action="/lighthouse/report/<%= @exception.id %>.html">
7
+ <label for="title">Title</label>
8
+ <input id="title" type="text" name="title" value="<%= @ticket.title %>" />
9
+
10
+ <label for="tags">Tags</label>
11
+ <input id="tags" type="text" name="tags" value="<%= @ticket.tags %>" />
12
+
13
+ <label for="body">Body</label>
14
+ <textarea id="body" name="body"><%= @ticket.body %></textarea>
15
+
16
+ <input type="submit" name="Create" value="Create" />
17
+ </form>
18
+ </div>
@@ -0,0 +1,152 @@
1
+ require 'sinatra/base'
2
+
3
+ $: << File.expand_path('..', File.dirname(__FILE__))
4
+ require 'error_stalker'
5
+ require 'error_stalker/store'
6
+ require 'error_stalker/plugin'
7
+ require 'erb'
8
+ require 'will_paginate'
9
+ require 'will_paginate/view_helpers/base'
10
+ require 'error_stalker/sinatra_link_renderer'
11
+ require 'error_stalker/version'
12
+
13
+ module ErrorStalker
14
+ # The ErrorStalker server. Provides a UI for browsing, grouping, and
15
+ # searching exception reports, as well as a centralized store for
16
+ # keeping exception reports. As a Sinatra app, this can be run using
17
+ # a config.ru file or something like Vegas. A sample Vegas runner
18
+ # for the server is located in <tt>bin/error_stalker_server</tt>.
19
+ class Server < Sinatra::Base
20
+
21
+ # The number of exceptions or exception groups to show on each
22
+ # page.
23
+ PER_PAGE = 25
24
+
25
+ # The data store (ErrorStalker::Store instance) to use to store
26
+ # exception data
27
+ attr_accessor :store
28
+
29
+ # A list of plugins the server will use.
30
+ attr_accessor :plugins
31
+
32
+ set :root, File.dirname(__FILE__)
33
+ set :public, Proc.new { File.join(root, "server/public") }
34
+ set :views, Proc.new { File.join(root, "server/views") }
35
+
36
+ helpers do
37
+ include Rack::Utils
38
+ alias_method :h, :escape_html
39
+
40
+ include WillPaginate::ViewHelpers::Base
41
+
42
+ # Generates a url from an array of strings representing the
43
+ # parts of the path.
44
+ def url(*path_parts)
45
+ u = '/' + path_parts.join("/")
46
+ u += '.html' unless u =~ /\.\w{2,4}$/
47
+ u
48
+ end
49
+ alias_method :u, :url
50
+
51
+ # Cuts +str+ at +limit+ characters. If +str+ is too long, will
52
+ # apped '...' to the end of the returned string.
53
+ def cutoff(str, limit = 100)
54
+ if str.length > limit
55
+ str[0, limit] + '...'
56
+ else
57
+ str
58
+ end
59
+ end
60
+ end
61
+
62
+ class << self
63
+ # A hash of configuration options, usually read from a
64
+ # configuration file.
65
+ attr_accessor :configuration
66
+ end
67
+ self.configuration = {}
68
+
69
+ # The default ErrorStalker::Store subclass to use by default for
70
+ # this ErrorStalker::Server instance. This is defined as a class
71
+ # method as well as an instance method so that it can be set by a
72
+ # configuration file before rack creates the instance of this
73
+ # sinatra app.
74
+ def self.store
75
+ if configuration['store']
76
+ store_class = configuration['store']['class'].split('::').inject(Object) {|mod, string| mod.const_get(string)}
77
+ store_class.new(*Array(configuration['store']['parameters']))
78
+ else
79
+ ErrorStalker::Store::InMemory.new
80
+ end
81
+ end
82
+
83
+ # A hash of configuration options, usually read from a
84
+ # configuration file.
85
+ def configuration
86
+ self.class.configuration
87
+ end
88
+
89
+ # Creates a new instance of the server, based on the configuration
90
+ # contained in the +configuration+ attribute.
91
+ def initialize
92
+ super
93
+ self.plugins = []
94
+ if configuration['plugin']
95
+ configuration['plugin'].each do |config|
96
+ plugin_class = config['class'].split('::').inject(Object) {|mod, string| mod.const_get(string)}
97
+ self.plugins << plugin_class.new(self, config['parameters'])
98
+ end
99
+ end
100
+ self.store = self.class.store
101
+ end
102
+
103
+ get '/' do
104
+ @records = store.recent.paginate(:page => params[:page], :per_page => PER_PAGE)
105
+ erb :index
106
+ end
107
+
108
+ get '/search' do
109
+ @results = store.search(params).paginate(:page => params[:page], :per_page => PER_PAGE) if params["Search"]
110
+ erb :search
111
+ end
112
+
113
+ get '/similar/:digest.html' do
114
+ @group = store.reports_in_group(params[:digest]).paginate(:page => params[:page], :per_page => PER_PAGE)
115
+ if @group
116
+ erb :similar
117
+ else
118
+ 404
119
+ end
120
+ end
121
+
122
+ get '/exceptions/:id.html' do
123
+ @report = store.find(params[:id])
124
+ if @report
125
+ @group = store.group(@report.digest)
126
+ erb :show
127
+ else
128
+ 404
129
+ end
130
+ end
131
+
132
+ get '/stats.json' do
133
+ timestamp = Time.at(params[:timestamp].to_i) if params[:timestamp]
134
+ # default to 1 hour ago
135
+ timestamp ||= Time.now - (60*60)
136
+ stats = {}
137
+ stats[:timestamp] = timestamp.to_i
138
+ stats[:total_since] = store.total_since(timestamp)
139
+ stats[:total] = store.total
140
+ stats[:version] = ErrorStalker::VERSION
141
+ stats.to_json
142
+ end
143
+
144
+ post '/report.json' do
145
+ report = ErrorStalker::ExceptionReport.new(JSON.parse(request.body.read))
146
+ report.id = store.store(report)
147
+ plugins.each {|p| p.after_create(self, report)}
148
+ 200
149
+ end
150
+
151
+ end
152
+ end
@@ -0,0 +1,173 @@
1
+ body {
2
+ font-family: Helvetica, Calibri, sans-serif;
3
+ }
4
+
5
+ h1, h2, h3, h4 {
6
+ font-color: #222;
7
+ width: 960px;
8
+ overflow: hidden;
9
+ }
10
+
11
+ table {
12
+ border-collapse: separate;
13
+ border-spacing: 0;
14
+ margin: 0;
15
+ padding: 0;
16
+ border: 0;
17
+ outline: 0;
18
+ vertical-align: baseline;
19
+ }
20
+
21
+ h1 {
22
+ margin-top: 12px;
23
+ margin-bottom: 12px;
24
+ }
25
+
26
+ h3 {
27
+ margin-bottom: 4px;
28
+ }
29
+
30
+ a {
31
+ text-decoration: none;
32
+ color: #007a94;
33
+ }
34
+
35
+ a:visited {
36
+ color: #83389b;
37
+ }
38
+
39
+ table#exceptions td, table#exceptions th {
40
+ padding: 10px;
41
+ height: 80px;
42
+ word-wrap: break-word;
43
+ }
44
+
45
+ table#exceptions {
46
+ -moz-box-shadow: 0px 2px 5px #666;
47
+ -webkit-box-shadow: 0px 2px 5px #666;
48
+ box-shadow: 0px 2px 5px #666;
49
+ width: 960px;
50
+ }
51
+
52
+ table#exceptions, table#exceptions tr:first-child th:first-child {
53
+ -webkit-border-top-left-radius: 6px;
54
+ -moz-border-radius-topleft: 6px;
55
+ border-top-left-radius: 6px;
56
+ }
57
+
58
+ table#exceptions, table#exceptions tr:first-child th:last-child {
59
+ -webkit-border-top-right-radius: 6px;
60
+ -moz-border-radius-topright: 6px;
61
+ border-top-right-radius: 6px;
62
+ }
63
+
64
+ table#exceptions th {
65
+ color: #eee;
66
+ background-color: #333;
67
+ background: url('images/background.png');
68
+ border-bottom: 1px solid #dadada;
69
+ text-shadow:0 -1px 1px rgba(0,0,0,0.5);
70
+ }
71
+
72
+ table#exceptions tr.even {
73
+ background-color: #ccc;
74
+ }
75
+
76
+ table#exceptions tr.odd {
77
+ background-color: #eee;
78
+ }
79
+
80
+
81
+ table#exceptions .count {
82
+ width: 80px;
83
+ text-align: center;
84
+ }
85
+
86
+ table#exceptions .last_occurred {
87
+ width: 240px;
88
+ }
89
+
90
+ table#exceptions td.last_occurred {
91
+ font-size: 0.8em;
92
+ }
93
+
94
+ table#exceptions td.last_occurred .timestamp {
95
+ font-size: 1.2em;
96
+ }
97
+
98
+ table#exceptions .exception, table#exceptions .exception div {
99
+ width: 480px;
100
+ }
101
+
102
+ table#exceptions .links {
103
+ text-align: center;
104
+ width: 160px;
105
+ color: #777;
106
+ }
107
+
108
+ table#exceptions .details {
109
+ width: 240px;
110
+ }
111
+
112
+ pre {
113
+ overflow: auto;
114
+ padding: 8px;
115
+ background-color: #d6d6d6;
116
+ margin: 0px;
117
+ border: 1px solid #aaa;
118
+ }
119
+
120
+ code {
121
+ font-size: 14px;
122
+ font-family: Inconsolata, Consolas, 'Lucida Console', monospace;
123
+ }
124
+
125
+ dl.group_details dt {
126
+ clear: left;
127
+ float: left;
128
+ font-weight: bold;
129
+ }
130
+
131
+ dl.group_details dd {
132
+ margin-left: 220px;
133
+ }
134
+
135
+ form {
136
+ margin-left: 10px;
137
+ width: 380px;
138
+ }
139
+
140
+ form label {
141
+ width: 140px;
142
+ margin-right: 10px;
143
+ display: block;
144
+ float: left;
145
+ }
146
+
147
+ form input, #search_form select {
148
+ width: 220px;
149
+ margin-left: 10px;
150
+ float: left;
151
+ }
152
+
153
+ form textarea {
154
+ width: 220px;
155
+ height: 80px;
156
+ margin-left: 10px;
157
+ float: left;
158
+ }
159
+
160
+ form input[type=submit] {
161
+ clear: left;
162
+ width: auto;
163
+ margin: 8px 0px 0px 0px;
164
+ }
165
+
166
+ .pagination {
167
+ padding: 16px 0px;
168
+ }
169
+
170
+ .pagination em {
171
+ font-weight: bold;
172
+ font-style: normal;
173
+ }