error_stalker 0.0.12 → 0.0.13

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -7,10 +7,11 @@ group :server do
7
7
  gem 'sinatra', '~>1.1.2'
8
8
  gem 'vegas', '~>0.1.8'
9
9
  gem 'thin', '~>1.2.7'
10
+ gem 'will_paginate','~>3.0'
10
11
  end
11
12
 
12
13
  group :mongoid_store do
13
- gem 'mongoid', '2.0.0.beta.20'
14
+ gem 'mongoid', '~>2.2.0'
14
15
  end
15
16
 
16
17
  group :test do
data/README.rdoc CHANGED
@@ -1,3 +1,106 @@
1
- = Exception Logger
1
+ = ErrorStalker
2
2
 
3
- As the name says, this library allows you to log exceptions to an arbitrary backend.
3
+ ErrorStalker is an extensible exception monitor for Ruby apps.
4
+
5
+ == Getting Started
6
+
7
+ After installing the gem, you should require it:
8
+
9
+ require 'error_stalker'
10
+
11
+ Afterwards, reporting exceptions is simple:
12
+
13
+ ErrorStalker::Client.report(application_name, exception, extra_data)
14
+
15
+ This will report an exception to ErrorStalker.
16
+
17
+ In Rails, you should be able to put this code in your
18
+ ApplicationController to report all exceptions:
19
+
20
+ rescue_from Exception do |exception|
21
+ ErrorStalker::Client.report(application_name, exception)
22
+ raise exception
23
+ end
24
+
25
+ == The ErrorStalker Server
26
+
27
+ ErrorStalker has decent defaults for running in development -- when
28
+ you call ErrorStalker::Client.report, it logs the exception to a
29
+ logfile. If you're inside a Rails app, it will log to
30
+ log/exceptions.log, otherwise it will put the logfile in
31
+ /tmp/exceptions.log.
32
+
33
+ This is probably not what you want in production, so ErrorStalker
34
+ supports different backends, and comes stock with an ErrorStalker
35
+ Sinatra server that can persist, aggregate, and show the exceptions
36
+ your app is raising. Switching backends can be done like this:
37
+
38
+ require 'error_stalker/backend/server'
39
+ ErrorStalker::Client.backend = ErrorStalker::Backend::Server.new(:host => "localhost", :port => 8080)
40
+
41
+ From that point on, all exceptions will be posted to the ErrorStalker server running on localhost:8080.
42
+
43
+ That server is a standard Sinatra app, but you still need to run
44
+ it. You can do that by pointing your favorite rack-enabled server at a
45
+ config.ru that looks like this:
46
+
47
+ require 'error_stalker/server'
48
+
49
+ ErrorStalker::Server.configuration = {
50
+ ... # server config goes here
51
+ }
52
+
53
+ run ErrorStalker::Server
54
+
55
+ == Data Stores
56
+
57
+ The server takes two main bits of configuration. The most important is
58
+ the data store. Currently, the server supports an in-memory data store
59
+ (which will go away when the server does) and a persistent mongodb store (using
60
+ mongoid). You can configure these with a hash before you start the server:
61
+
62
+ ErrorStalker::Server.configuration = {
63
+ 'store' => {
64
+ 'class' => 'ErrorStalker::Store::Mongoid',
65
+ 'parameters' => '/path/to/mongoid.yml'
66
+ }
67
+ }
68
+
69
+ Each data store should call out the parameters it supports in its
70
+ documentation.
71
+
72
+ == Plugins
73
+
74
+ The ErrorStalker server also supports a simple plugin
75
+ architecture. Currently, it supports a simple lighthouse reporter and
76
+ an automatic email sender. Documentation for these plugins is in the
77
+ Rdoc for the specific classes. They are configured the same way as the
78
+ data store:
79
+
80
+ ErrorStalker::Server.configuration = {
81
+ 'store' => {
82
+ 'class' => 'ErrorStalker::Store::Mongoid',
83
+ 'parameters' => '/path/to/mongoid.yml'
84
+ },
85
+ 'plugin' => [
86
+ {
87
+ 'class' => 'ErrorStalker::Plugin::EmailSender',
88
+ 'parameters' => {
89
+ 'to' => 'to_address@example.com',
90
+ 'from' => 'from_address@example.com'
91
+ }
92
+ },
93
+ ...
94
+ ]
95
+ }
96
+
97
+ == Extending ExceptionStalker
98
+
99
+ Feel free to write your own backends, stores, or plugins as you need
100
+ them. As long as you follow the documentation in the ::Base classes,
101
+ your code should fit right in. Let me know what you come up with!
102
+
103
+ == License
104
+
105
+ Regardless of what any other file may say about it, this code is
106
+ licensed under the MIT license.
@@ -7,8 +7,8 @@ require 'digest/md5'
7
7
  # class, but they should be able to be treated as instances of this
8
8
  # class regardless.
9
9
  class ErrorStalker::ExceptionReport
10
-
11
- # The name of the application that caused this exception.
10
+
11
+ # The name of the application that caused this exception.
12
12
  attr_reader :application
13
13
 
14
14
  # The name of the machine that raised this exception.
@@ -23,9 +23,6 @@ class ErrorStalker::ExceptionReport
23
23
  # The exception object (or string message) this report represents
24
24
  attr_reader :exception
25
25
 
26
- # A hash of extra data logged along with this exception.
27
- attr_reader :data
28
-
29
26
  # The backtrace corresponding to this exception. Should be an array
30
27
  # of strings, each referring to a single stack frame.
31
28
  attr_reader :backtrace
@@ -46,15 +43,15 @@ class ErrorStalker::ExceptionReport
46
43
  @machine = params[:machine] || machine_name
47
44
  @timestamp = params[:timestamp] || Time.now
48
45
  @type = params[:type] || params[:exception].class.name
49
-
46
+
50
47
  if params[:exception].is_a?(Exception)
51
48
  @exception = params[:exception].to_s
52
49
  else
53
50
  @exception = params[:exception]
54
51
  end
55
-
52
+
56
53
  @data = params[:data]
57
-
54
+
58
55
  if params[:backtrace]
59
56
  @backtrace = params[:backtrace]
60
57
  else
@@ -78,6 +75,17 @@ class ErrorStalker::ExceptionReport
78
75
  @digest ||= Digest::MD5.hexdigest((backtrace ? backtrace.to_s[0,STACK_DIGEST_LENGTH] : exception.to_s) + type.to_s)
79
76
  end
80
77
 
78
+ # A hash of extra data logged along with this exception.
79
+ def data
80
+ @data_with_fixed_encoding ||= JSON.parse(raw_data.to_json.encode('utf-8', 'ascii-8bit', :invalid => :replace, :undef => :replace))
81
+ end
82
+
83
+ # The extra data associated with this object, without fixing any
84
+ # broken encodings.
85
+ def raw_data
86
+ @data
87
+ end
88
+
81
89
  # Serialize this object to json, so we can send it over the wire.
82
90
  def to_json
83
91
  {
@@ -86,7 +94,7 @@ class ErrorStalker::ExceptionReport
86
94
  :timestamp => timestamp,
87
95
  :type => type,
88
96
  :exception => exception,
89
- :data => data,
97
+ :data => raw_data,
90
98
  :backtrace => backtrace
91
99
  }.to_json
92
100
  end
@@ -122,6 +122,10 @@ code {
122
122
  font-family: Inconsolata, Consolas, 'Lucida Console', monospace;
123
123
  }
124
124
 
125
+ dl.group_details {
126
+ overflow: auto;
127
+ }
128
+
125
129
  dl.group_details dt {
126
130
  clear: left;
127
131
  float: left;
@@ -1,4 +1,4 @@
1
- <%= will_paginate @rows, :params => params %>
1
+ <%= will_paginate @rows %>
2
2
  <table id="exceptions" cellpadding="0">
3
3
  <tr>
4
4
  <th class="count">&nbsp;</th>
@@ -26,10 +26,10 @@
26
26
  <input id="type" type="text" name="type" value="<%= params[:type] %>" />
27
27
 
28
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] %>" />
29
+ <label for="data">Extra data</label>
30
+ <input id="data" type="text" name="data" value="<%= params[:data] %>" placeholder="REMOTE_ADDR:127.0.0.1 PATH:/test" />
31
31
  <% end %>
32
-
32
+
33
33
  <input type="submit" name="Search" value="Search" />
34
34
  </form>
35
35
  </div>
@@ -7,7 +7,7 @@
7
7
  <dt>First occurred</dt><dd><%= h(@group.first_timestamp) %></dd>
8
8
  <dt>Most recently occurred</dt><dd><%= h(@group.most_recent_timestamp) %></dd>
9
9
  <dt>On machines</dt><dd><%= h(@group.machines.join(', ')) %></dd>
10
- <dt>Exception class</dt><dd><%= h(@group.type) %></dd>
10
+ <dt>Exception class</dt><dd><%= h(@report.type) %></dd>
11
11
  </dl>
12
12
  <p><a href="/similar/<%= @report.digest%>.html">See all</a></p>
13
13
  <% plugins.each do |plugin| %>
@@ -28,5 +28,5 @@
28
28
 
29
29
  <% if @report.data && !@report.data.empty? %>
30
30
  <h3>Additional information</h3>
31
- <pre><code><%= h @report.data.to_yaml %></code></pre>
31
+ <pre><code><%= h JSON.pretty_generate(@report.data) %></code></pre>
32
32
  <% end %>
@@ -6,8 +6,6 @@ require 'error_stalker/store'
6
6
  require 'error_stalker/plugin'
7
7
  require 'erb'
8
8
  require 'will_paginate'
9
- require 'will_paginate/view_helpers/base'
10
- require 'error_stalker/sinatra_link_renderer'
11
9
  require 'error_stalker/version'
12
10
 
13
11
  module ErrorStalker
@@ -24,21 +22,21 @@ module ErrorStalker
24
22
 
25
23
  # The data store (ErrorStalker::Store instance) to use to store
26
24
  # exception data
27
- attr_accessor :store
28
-
25
+ attr_accessor :store
26
+
29
27
  # A list of plugins the server will use.
30
28
  attr_accessor :plugins
31
-
29
+
32
30
  set :root, File.dirname(__FILE__)
33
31
  set :public, Proc.new { File.join(root, "server/public") }
34
32
  set :views, Proc.new { File.join(root, "server/views") }
35
33
 
34
+ register WillPaginate::Sinatra
35
+
36
36
  helpers do
37
37
  include Rack::Utils
38
38
  alias_method :h, :escape_html
39
39
 
40
- include WillPaginate::ViewHelpers::Base
41
-
42
40
  # Generates a url from an array of strings representing the
43
41
  # parts of the path.
44
42
  def url(*path_parts)
@@ -62,7 +60,7 @@ module ErrorStalker
62
60
  class << self
63
61
  # A hash of configuration options, usually read from a
64
62
  # configuration file.
65
- attr_accessor :configuration
63
+ attr_accessor :configuration
66
64
  end
67
65
  self.configuration = {}
68
66
 
@@ -99,7 +97,7 @@ module ErrorStalker
99
97
  end
100
98
  self.store = self.class.store
101
99
  end
102
-
100
+
103
101
  get '/' do
104
102
  @records = store.recent.paginate(:page => params[:page], :per_page => PER_PAGE)
105
103
  erb :index
@@ -109,7 +107,7 @@ module ErrorStalker
109
107
  @results = store.search(params).paginate(:page => params[:page], :per_page => PER_PAGE) if params["Search"]
110
108
  erb :search
111
109
  end
112
-
110
+
113
111
  get '/similar/:digest.html' do
114
112
  @group = store.reports_in_group(params[:digest]).paginate(:page => params[:page], :per_page => PER_PAGE)
115
113
  if @group
@@ -118,7 +116,7 @@ module ErrorStalker
118
116
  404
119
117
  end
120
118
  end
121
-
119
+
122
120
  get '/exceptions/:id.html' do
123
121
  @report = store.find(params[:id])
124
122
  if @report
@@ -128,7 +126,7 @@ module ErrorStalker
128
126
  404
129
127
  end
130
128
  end
131
-
129
+
132
130
  get '/stats.json' do
133
131
  timestamp = Time.at(params[:timestamp].to_i) if params[:timestamp]
134
132
  # default to 1 hour ago
@@ -147,6 +145,6 @@ module ErrorStalker
147
145
  plugins.each {|p| p.after_create(self, report)}
148
146
  200
149
147
  end
150
-
148
+
151
149
  end
152
150
  end
@@ -1,13 +1,15 @@
1
1
  require 'mongoid'
2
2
  require 'error_stalker/store/base'
3
+ require 'error_stalker/will_paginate/mongoid'
4
+ require 'will_paginate/array'
3
5
 
4
6
  # Store exceptions using MongoDB. This store provides fast storage and
5
7
  # querying of exceptions, and long-term persistence. It also allows
6
8
  # querying based on arbitrary data stored in the +data+ hash of the
7
9
  # exception report, which allows for crazy things like searching
8
- # reports by URL or IP address.
10
+ # reports by URL or IP address.
9
11
  class ErrorStalker::Store::Mongoid < ErrorStalker::Store::Base
10
-
12
+
11
13
  # Configure mongoid from the mongoid config file found in
12
14
  # +config_file+. This mongoid config file should be similar to the
13
15
  # one on http://mongoid.org/docs/installation/, and must be indexed
@@ -69,7 +71,7 @@ class ErrorStalker::Store::Mongoid < ErrorStalker::Store::Base
69
71
  def reports_in_group(digest)
70
72
  ExceptionReport.where(:digest => digest).order_by(:timestamp.desc)
71
73
  end
72
-
74
+
73
75
  # returns the ExceptionGroup object corresponding to a particular
74
76
  # digest
75
77
  def group(digest)
@@ -80,11 +82,11 @@ class ErrorStalker::Store::Mongoid < ErrorStalker::Store::Base
80
82
  def supports_extended_searches?
81
83
  true
82
84
  end
83
-
85
+
84
86
  def total
85
87
  ExceptionReport.count()
86
88
  end
87
-
89
+
88
90
  def total_since(timestamp)
89
91
  ExceptionReport.where(:timestamp.gte => timestamp).count()
90
92
  end
@@ -98,23 +100,24 @@ class ErrorStalker::Store::Mongoid < ErrorStalker::Store::Base
98
100
 
99
101
  [:application, :machine].each do |param|
100
102
  if params[param] && !params[param].empty?
101
- scope.where(param => params[param])
103
+ puts "HERE: #{params.inspect}"
104
+ scope = scope.where(param => params[param])
102
105
  end
103
106
  end
104
107
 
105
108
  [:exception, :type].each do |param|
106
109
  if params[param] && !params[param].empty?
107
- scope.where(param => /#{params[param]}/)
110
+ scope = scope.where(param => /#{params[param]}/)
108
111
  end
109
112
  end
110
113
 
111
114
  if params[:data] && !params[:data].empty?
112
115
  params[:data].split.each do |keyvalue|
113
116
  key, value = keyvalue.split(':')
114
- scope.where("data" => {"#{key}" => "#{value}"})
117
+ scope = scope.where("data" => {"#{key}" => "#{value}"})
115
118
  end
116
119
  end
117
-
120
+
118
121
  scope.order_by(:timestamp.desc)
119
122
  end
120
123
 
@@ -130,7 +133,7 @@ class ErrorStalker::Store::Mongoid < ErrorStalker::Store::Base
130
133
  # should eventually be more robust, like the Rails version, but for
131
134
  # now this should be fine.
132
135
  def migrate_data
133
- if SchemaMigrations.where(:version => 1).empty?
136
+ if SchemaMigrations.where(:version => "1").empty?
134
137
  ExceptionGroup.all.order_by(:timestamp).desc.each do |group|
135
138
  exceptions = ExceptionReport.where(:digest => group.digest).order_by(:timestamp)
136
139
  unless exceptions.empty?
@@ -142,7 +145,7 @@ class ErrorStalker::Store::Mongoid < ErrorStalker::Store::Base
142
145
  group.save
143
146
  end
144
147
  end
145
- SchemaMigrations.create(:version => 1)
148
+ SchemaMigrations.create(:version => "1")
146
149
  end
147
150
  end
148
151
 
@@ -173,12 +176,12 @@ class ErrorStalker::Store::Mongoid < ErrorStalker::Store::Base
173
176
  {:name => report.machine},
174
177
  {:name => report.machine},
175
178
  :upsert => true)
176
-
179
+
177
180
  Application.collection.update(
178
181
  {:name => report.application},
179
182
  {:name => report.application},
180
183
  :upsert => true)
181
-
184
+
182
185
  report.id
183
186
  end
184
187
  end
@@ -239,6 +242,8 @@ class ErrorStalker::Store::Mongoid::ExceptionGroup < ErrorStalker::ExceptionGrou
239
242
  # associated exception reports.
240
243
  def paginate(pagination_opts = {})
241
244
  recent = @criteria.paginate(pagination_opts)
245
+ total_entries = recent.total_entries
246
+
242
247
  exceptions = ErrorStalker::Store::Mongoid::ExceptionReport.where(:_id.in => recent.map(&:most_recent_report_id))
243
248
  # Fake association preloading
244
249
  id_map = {}.tap do |h|
@@ -246,12 +251,20 @@ class ErrorStalker::Store::Mongoid::ExceptionGroup < ErrorStalker::ExceptionGrou
246
251
  h[ex.id] = ex
247
252
  end
248
253
  end
249
-
254
+
255
+ # Because all the +recent+ stuff is still a scope at this point,
256
+ # we somehow lose the most_recent_report information that we try
257
+ # to assign below the next time we iterate over the
258
+ # scope. Forcing the scope to turn into an array solves that
259
+ # problem, although it means we have to 'fake-paginate' the
260
+ # array before we return it.
261
+ recent = recent.to_a
262
+
250
263
  recent.each do |r|
251
264
  r.most_recent_report = id_map[r.most_recent_report_id]
252
265
  end
253
-
254
- recent
266
+
267
+ recent.paginate(:per_page => pagination_opts[:per_page], :total_entries => total_entries)
255
268
  end
256
269
  end
257
270
  end
@@ -284,16 +297,16 @@ class ErrorStalker::Store::Mongoid::ExceptionReport
284
297
  [:id, :application, :machine, :timestamp, :type, :exception, :digest, :backtrace].each do |field|
285
298
  params[field] = send(field)
286
299
  end
287
-
300
+
288
301
  if data
289
302
  params[:data] = {}.tap do |h|
290
303
  data.map { |hash| h[hash.keys.first] = hash[hash.keys.first] }
291
304
  end
292
305
  end
293
-
306
+
294
307
  ErrorStalker::ExceptionReport.new(params)
295
308
  end
296
-
309
+
297
310
  # Create a new mongoid exception report from +exception_report+.
298
311
  def self.create_from_exception_report(exception_report)
299
312
  object = new do |o|
@@ -307,7 +320,7 @@ class ErrorStalker::Store::Mongoid::ExceptionReport
307
320
  exception_report.data.map {|key, value| array << {key => value}}
308
321
  end
309
322
  end
310
-
323
+
311
324
  if exception_report.backtrace
312
325
  o.backtrace = exception_report.backtrace
313
326
  end
@@ -1,4 +1,4 @@
1
1
  module ErrorStalker
2
2
  # ErrorStalker's current version.
3
- VERSION = "0.0.12"
3
+ VERSION = "0.0.13"
4
4
  end
@@ -0,0 +1,45 @@
1
+ # Borrowed from
2
+ # https://raw.github.com/dbackeus/will_paginate/master/lib/will_paginate/mongoid.rb
3
+ # until it makes it into will_paginate
4
+ require 'mongoid'
5
+ require 'will_paginate/collection'
6
+
7
+ module WillPaginate
8
+ module Mongoid
9
+ module CriteriaMethods
10
+ def paginate(options = {})
11
+ extend CollectionMethods
12
+ @current_page = WillPaginate::PageNumber(options[:page] || @current_page || 1)
13
+ @page_multiplier = current_page - 1
14
+ pp = (options[:per_page] || per_page || WillPaginate.per_page).to_i
15
+ limit(pp).skip(@page_multiplier * pp)
16
+ end
17
+
18
+ def per_page(value = :non_given)
19
+ value == :non_given ? options[:limit] : limit(value)
20
+ end
21
+
22
+ def page(page)
23
+ paginate(:page => page)
24
+ end
25
+ end
26
+
27
+ module CollectionMethods
28
+ attr_reader :current_page
29
+
30
+ def total_entries
31
+ @total_entries ||= count
32
+ end
33
+
34
+ def total_pages
35
+ (total_entries / per_page.to_f).ceil
36
+ end
37
+
38
+ def offset
39
+ @page_multiplier * per_page
40
+ end
41
+ end
42
+
43
+ ::Mongoid::Criteria.send(:include, CriteriaMethods)
44
+ end
45
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: error_stalker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.12
4
+ version: 0.0.13
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,8 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-08-10 00:00:00.000000000 -07:00
13
- default_executable:
12
+ date: 2011-12-23 00:00:00.000000000Z
14
13
  dependencies: []
15
14
  description: Logs exceptions to a pluggable backend. Also provides a server for centralized
16
15
  exception logging using a pluggable data store.
@@ -57,12 +56,12 @@ files:
57
56
  - lib/error_stalker/server/views/search.erb
58
57
  - lib/error_stalker/server/views/show.erb
59
58
  - lib/error_stalker/server/views/similar.erb
60
- - lib/error_stalker/sinatra_link_renderer.rb
61
59
  - lib/error_stalker/store.rb
62
60
  - lib/error_stalker/store/base.rb
63
61
  - lib/error_stalker/store/in_memory.rb
64
62
  - lib/error_stalker/store/mongoid.rb
65
63
  - lib/error_stalker/version.rb
64
+ - lib/error_stalker/will_paginate/mongoid.rb
66
65
  - test/test_helper.rb
67
66
  - test/unit/backend/base_test.rb
68
67
  - test/unit/backend/in_memory_test.rb
@@ -72,7 +71,6 @@ files:
72
71
  - test/unit/plugins/email_sender_test.rb
73
72
  - test/unit/server_test.rb
74
73
  - test/unit/stores/in_memory_test.rb
75
- has_rdoc: true
76
74
  homepage: ''
77
75
  licenses: []
78
76
  post_install_message:
@@ -93,7 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
91
  version: '0'
94
92
  requirements: []
95
93
  rubyforge_project: error_stalker
96
- rubygems_version: 1.6.2
94
+ rubygems_version: 1.8.11
97
95
  signing_key:
98
96
  specification_version: 3
99
97
  summary: Logs exceptions to a pluggable backend and/or a pluggable store
@@ -1,25 +0,0 @@
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