error_stalker 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
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
+ }