error_stalker 0.0.12 → 0.0.13

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/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