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
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ doc/*
data/Gemfile ADDED
@@ -0,0 +1,30 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in error_stalker.gemspec
4
+ gemspec
5
+
6
+ group :server do
7
+ gem 'sinatra', '~>1.1.2'
8
+ gem 'vegas', '~>0.1.8'
9
+ gem 'thin', '~>1.2.7'
10
+ end
11
+
12
+ group :mongoid_store do
13
+ gem 'mongoid', '2.0.0.beta.20'
14
+ end
15
+
16
+ group :test do
17
+ gem 'rack-test', '~>0.5.7'
18
+ gem 'mocha', '~>0.9.10'
19
+ end
20
+
21
+ group :lighthouse_reporter do
22
+ gem 'lighthouse-api', '2.0'
23
+ gem 'addressable', '~>2.2.2'
24
+ end
25
+
26
+ group :email_sender do
27
+ gem 'mail', '~>2.2.15'
28
+ end
29
+
30
+ gem 'json', '1.4.6', :platforms => :ruby_18
data/Gemfile.lock ADDED
@@ -0,0 +1,76 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ error_stalker (0.0.12)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ activemodel (3.0.3)
10
+ activesupport (= 3.0.3)
11
+ builder (~> 2.1.2)
12
+ i18n (~> 0.4)
13
+ activeresource (3.0.3)
14
+ activemodel (= 3.0.3)
15
+ activesupport (= 3.0.3)
16
+ activesupport (3.0.3)
17
+ addressable (2.2.2)
18
+ bson (1.2.0)
19
+ builder (2.1.2)
20
+ daemons (1.1.0)
21
+ eventmachine (0.12.10)
22
+ i18n (0.5.0)
23
+ json (1.4.6)
24
+ lighthouse-api (2.0)
25
+ activeresource (>= 3.0.0)
26
+ activesupport (>= 3.0.0)
27
+ mail (2.2.15)
28
+ activesupport (>= 2.3.6)
29
+ i18n (>= 0.4.0)
30
+ mime-types (~> 1.16)
31
+ treetop (~> 1.4.8)
32
+ mime-types (1.16)
33
+ mocha (0.9.10)
34
+ rake
35
+ mongo (1.2.0)
36
+ bson (>= 1.2.0)
37
+ mongoid (2.0.0.beta.20)
38
+ activemodel (~> 3.0)
39
+ mongo (~> 1.1)
40
+ tzinfo (~> 0.3.22)
41
+ will_paginate (~> 3.0.pre)
42
+ polyglot (0.3.1)
43
+ rack (1.2.1)
44
+ rack-test (0.5.7)
45
+ rack (>= 1.0)
46
+ rake (0.8.7)
47
+ sinatra (1.1.2)
48
+ rack (~> 1.1)
49
+ tilt (~> 1.2)
50
+ thin (1.2.7)
51
+ daemons (>= 1.0.9)
52
+ eventmachine (>= 0.12.6)
53
+ rack (>= 1.0.0)
54
+ tilt (1.2.1)
55
+ treetop (1.4.9)
56
+ polyglot (>= 0.3.1)
57
+ tzinfo (0.3.24)
58
+ vegas (0.1.8)
59
+ rack (>= 1.0.0)
60
+ will_paginate (3.0.pre2)
61
+
62
+ PLATFORMS
63
+ ruby
64
+
65
+ DEPENDENCIES
66
+ addressable (~> 2.2.2)
67
+ error_stalker!
68
+ json (= 1.4.6)
69
+ lighthouse-api (= 2.0)
70
+ mail (~> 2.2.15)
71
+ mocha (~> 0.9.10)
72
+ mongoid (= 2.0.0.beta.20)
73
+ rack-test (~> 0.5.7)
74
+ sinatra (~> 1.1.2)
75
+ thin (~> 1.2.7)
76
+ vegas (~> 0.1.8)
data/README.rdoc ADDED
@@ -0,0 +1,3 @@
1
+ = Exception Logger
2
+
3
+ As the name says, this library allows you to log exceptions to an arbitrary backend.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require 'bundler'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ task :default => :test
7
+ task :build => :test
8
+
9
+ Rake::TestTask.new do |t|
10
+ t.libs << "test"
11
+ t.test_files = FileList['test/**/*_test.rb']
12
+ t.verbose = true
13
+ end
14
+
15
+ Rake::RDocTask.new do |rd|
16
+ rd.main = "README.rdoc"
17
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
18
+ rd.rdoc_dir = 'doc'
19
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('../lib/error_stalker/server', File.dirname(__FILE__))
4
+
5
+ ENV['RACK_ENV'] ||= 'development'
6
+
7
+ if ARGV.length != 1
8
+ puts "Usage: create_indexes </path/to/error_stalker/config.yml>"
9
+ exit(1)
10
+ end
11
+
12
+ ErrorStalker::Server.configuration = YAML.load(File.read(ARGV[0]))[ENV['RACK_ENV']]
13
+ store = ErrorStalker::Server.store
14
+ store.create_indexes if store.respond_to?(:create_indexes)
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('../lib/error_stalker/server', File.dirname(__FILE__))
4
+ require 'vegas'
5
+ ENV['RACK_ENV'] ||= 'development'
6
+
7
+ Vegas::Runner.new(ErrorStalker::Server, 'error_stalker_server', {
8
+ :foreground => true,
9
+ :before_run => lambda {|v|
10
+ config_file = ENV['ERROR_STALKER_CONFIG'] || v.args.first
11
+ if config_file
12
+ params = YAML.load(File.read(config_file))[ENV['RACK_ENV']]
13
+ ErrorStalker::Server.configuration = params
14
+ end
15
+ }
16
+ })
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "error_stalker/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "error_stalker"
7
+ s.version = ErrorStalker::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Justin Weiss"]
10
+ s.email = ["jweiss@avvo.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{Logs exceptions to a pluggable backend and/or a pluggable store}
13
+ s.description = %q{Logs exceptions to a pluggable backend. Also provides a server for centralized exception logging using a pluggable data store.}
14
+
15
+ s.rubyforge_project = "error_stalker"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+ end
@@ -0,0 +1,4 @@
1
+ module ErrorStalker # :nodoc:
2
+ end
3
+
4
+ require 'error_stalker/client'
@@ -0,0 +1,14 @@
1
+ # ErrorStalker backends are objects representing a place that
2
+ # ErrorStalker::Client sends exceptions to. A backend simply needs to
3
+ # inherit from ErrorStalker::Backend::Base and override the +report+
4
+ # method, after which they can be set using the
5
+ # ErrorStalker::Client.backend attribute.
6
+ #
7
+ # The default backend is an ErrorStalker::Backend::LogFile instance,
8
+ # which logs exception data to a file.
9
+ module ErrorStalker::Backend
10
+ autoload :Base, 'error_stalker/backend/base'
11
+ autoload :InMemory, 'error_stalker/backend/in_memory'
12
+ autoload :LogFile, 'error_stalker/backend/log_file'
13
+ autoload :Server, 'error_stalker/backend/server'
14
+ end
@@ -0,0 +1,14 @@
1
+ module ErrorStalker::Backend
2
+
3
+ # The base class for exception logger backends. All backends should
4
+ # inherit from this class and implement the +report+ method.
5
+ class Base
6
+
7
+ # Store an exception report into this backend. Subclasses should
8
+ # override this method. +exception_report+ is an
9
+ # ErrorStalker::ExceptionReport instance.
10
+ def report(exception_report)
11
+ raise NotImplementedError, "This method must be overridden in subclasses"
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,25 @@
1
+ # Provides an in-memory backend that stores all exception reports in
2
+ # an array. This is mostly useful for tests, and probably shouldn't be
3
+ # used in real code.
4
+ class ErrorStalker::Backend::InMemory < ErrorStalker::Backend::Base
5
+
6
+ # A list of exceptions stored in this backend.
7
+ attr_reader :exceptions
8
+
9
+ # Create a new instance of this backend, with an empty exception
10
+ # list.
11
+ def initialize
12
+ clear
13
+ end
14
+
15
+ # Stores exception_report in the exceptions list.
16
+ def report(exception_report)
17
+ @exceptions << exception_report
18
+ end
19
+
20
+ # Clears the exception list. Pretty useful in a test +setup+ method!
21
+ def clear
22
+ @exceptions = []
23
+ end
24
+
25
+ end
@@ -0,0 +1,33 @@
1
+ require 'yaml'
2
+
3
+ # A backend that logs all exception reports to a file. This is
4
+ # probably what you want to be running in development mode, and is
5
+ # also the default backend.
6
+ class ErrorStalker::Backend::LogFile < ErrorStalker::Backend::Base
7
+
8
+ # The name of the file containing the exception reports
9
+ attr_reader :filename
10
+
11
+ # Creates a new LogFile backend that will log exceptions to +filename+
12
+ def initialize(filename)
13
+ @filename = filename
14
+ end
15
+
16
+ # Writes the information contained in +exception_report+ to the log
17
+ # file specified when this backend was initialized.
18
+ def report(exception_report)
19
+ File.open(filename, 'a') do |file|
20
+ file.puts "Machine: #{exception_report.machine}"
21
+ file.puts "Application: #{exception_report.application}"
22
+ file.puts "Timestamp: #{exception_report.timestamp}"
23
+ file.puts "Type: #{exception_report.type}"
24
+ file.puts "Exception: #{exception_report.exception}"
25
+ file.puts "Data: #{YAML.dump(exception_report.data)}"
26
+ if exception_report.backtrace
27
+ file.puts "Stack trace:"
28
+ file.puts exception_report.backtrace.join("\n")
29
+ end
30
+ file.puts
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,41 @@
1
+ require 'json'
2
+ require 'net/http'
3
+
4
+ # Stores reported exceptions to a central ErrorStalker server (a Rack
5
+ # server pointing to an ErrorStalker::Server instance). The most
6
+ # complicated ErrorStalker backend, it is also the most powerful. This
7
+ # is probably what you want to be using in production.
8
+ class ErrorStalker::Backend::Server < ErrorStalker::Backend::Base
9
+
10
+ # The hostname of the ErrorStalker server
11
+ attr_accessor :host
12
+
13
+ # The ErrorStalker server's port
14
+ attr_accessor :port
15
+
16
+ # http or https
17
+ attr_accessor :protocol
18
+
19
+ # The path of the ErrorStalker server, if applicable
20
+ attr_accessor :path
21
+
22
+ # Creates a new Server backend instance that will report exceptions
23
+ # to a centralized ErrorStalker server.
24
+ def initialize(params = {})
25
+ @protocol = params[:protocol] || 'http://'
26
+ @host = params[:host] || 'localhost'
27
+ @port = params[:port] || '5678'
28
+ @path = params[:path] || ''
29
+ end
30
+
31
+ # Reports +exception_report+ to a central ErrorStalker server.
32
+ def report(exception_report)
33
+ req = Net::HTTP::Post.new("#{path}/report.json")
34
+ req["content-type"] = "application/json"
35
+ req.body = exception_report.to_json
36
+ http = Net::HTTP.new(host, port)
37
+ http.read_timeout = 10
38
+ res = http.start { |http| http.request(req) }
39
+ res
40
+ end
41
+ end
@@ -0,0 +1,62 @@
1
+ require 'error_stalker/exception_report'
2
+ require 'error_stalker/backend'
3
+ require 'tempfile'
4
+
5
+ # The error_stalker client enables you to log exception data to a
6
+ # backend. The class method ErrorStalker::Client.report is usually the
7
+ # method you want to use out of this class, although for those who
8
+ # like block syntax, this class also provides a +report_exceptions+
9
+ # method that reports all exceptions raised inside a block.
10
+ class ErrorStalker::Client
11
+
12
+ # Sets the backend the client will use to report exceptions to
13
+ # +new_backend+, an ErrorStalker::Backend instance. By default,
14
+ # exceptions are logged using ErrorStalker::Backend::LogFileBackend.
15
+ def self.backend=(new_backend)
16
+ @backend = new_backend
17
+ end
18
+
19
+ # Report an exception to the exception logging backend.
20
+ #
21
+ # * application_name: A tag representing the name of the app that
22
+ # this exception occurred in. This is used for advanced filtering
23
+ # on the server, if the server backend is used.
24
+ #
25
+ # * exception: The exception object that was thrown. This can also
26
+ # be a string, but you won't get information like the backtrace if
27
+ # you don't pass an actual exception subclass.
28
+ #
29
+ # * extra_data: A hash of additional data to log with the
30
+ # exception. Depending on which backend the server uses, this may
31
+ # or may not be indexable.
32
+ def self.report(application_name, exception, extra_data = {})
33
+ begin
34
+ @backend.report(ErrorStalker::ExceptionReport.new(:application => application_name, :exception => exception, :data => extra_data))
35
+ rescue Exception => e # keep going if this fails
36
+ end
37
+ end
38
+
39
+ # Calls +report+ on all exceptions raised in the provided block of
40
+ # code. +options+ can be:
41
+ #
42
+ # [:reraise] if true, reraise exceptions caught in this
43
+ # block. Defaults to true.
44
+ def self.report_exceptions(application_name, options = {})
45
+ options = {:reraise => true}.merge(options)
46
+ begin
47
+ yield
48
+ rescue => e
49
+ report(application_name, e)
50
+ if options[:reraise]
51
+ raise e
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ logfile = File.join(Dir.tmpdir, "exceptions.log")
58
+
59
+ # let's put this in a better place if we're using rails
60
+ logfile = Rails.root + 'log/exceptions.log' if defined?(Rails)
61
+
62
+ ErrorStalker::Client.backend = ErrorStalker::Backend::LogFile.new(logfile)
@@ -0,0 +1,29 @@
1
+ # Container for information about a group of exceptions. This is used
2
+ # so that we don't get overwhelmed by duplicate exceptions. Some
3
+ # Exceptional::Stores may subclass and override this class, but the attributes
4
+ # called out here should always be valid.
5
+ class ErrorStalker::ExceptionGroup
6
+ # The number of times this exception has been reported
7
+ attr_accessor :count
8
+
9
+ # The unique identifier of this group. All similar exceptions should
10
+ # generate the same digest.
11
+ attr_accessor :digest
12
+
13
+ # The list of machines that have seen this exception
14
+ attr_accessor :machines
15
+
16
+ # The first time this exception occurred
17
+ attr_accessor :first_timestamp
18
+
19
+ # The most recent time this exception occurred
20
+ attr_accessor :most_recent_timestamp
21
+
22
+ # The most recent ExceptionReport instance belonging to this group
23
+ attr_accessor :most_recent_report
24
+
25
+ def type
26
+ most_recent_report.type unless most_recent_report.nil?
27
+ end
28
+
29
+ end
@@ -0,0 +1,116 @@
1
+ require 'json'
2
+ require 'digest/md5'
3
+
4
+ # An ExceptionReport contains all of the information we have on an
5
+ # exception, which can then be transformed into whatever format is
6
+ # needed for further investigation. Some data stores may override this
7
+ # class, but they should be able to be treated as instances of this
8
+ # class regardless.
9
+ class ErrorStalker::ExceptionReport
10
+
11
+ # The name of the application that caused this exception.
12
+ attr_reader :application
13
+
14
+ # The name of the machine that raised this exception.
15
+ attr_reader :machine
16
+
17
+ # The time that this exception occurred
18
+ attr_reader :timestamp
19
+
20
+ # The class name of +exception+
21
+ attr_reader :type
22
+
23
+ # The exception object (or string message) this report represents
24
+ attr_reader :exception
25
+
26
+ # A hash of extra data logged along with this exception.
27
+ attr_reader :data
28
+
29
+ # The backtrace corresponding to this exception. Should be an array
30
+ # of strings, each referring to a single stack frame.
31
+ attr_reader :backtrace
32
+
33
+ # A unique identifier for this exception
34
+ attr_accessor :id
35
+
36
+ # Build a new ExceptionReport. <tt>params[:application]</tt> is a
37
+ # string identifying the application or component the exception was
38
+ # sent from, <tt>params[:exception]</tt> is the exception object you
39
+ # want to report (or a string error message), and
40
+ # <tt>params[:data]</tt> is any extra arbitrary data you want to log
41
+ # along with this report.
42
+ def initialize(params = {})
43
+ params = symbolize_keys(params)
44
+ @id = params[:id]
45
+ @application = params[:application]
46
+ @machine = params[:machine] || machine_name
47
+ @timestamp = params[:timestamp] || Time.now
48
+ @type = params[:type] || params[:exception].class.name
49
+
50
+ if params[:exception].is_a?(Exception)
51
+ @exception = params[:exception].to_s
52
+ else
53
+ @exception = params[:exception]
54
+ end
55
+
56
+ @data = params[:data]
57
+
58
+ if params[:backtrace]
59
+ @backtrace = params[:backtrace]
60
+ else
61
+ @backtrace = params[:exception].backtrace if params[:exception].is_a?(Exception)
62
+ end
63
+
64
+ @digest = params[:digest] if params[:digest]
65
+ end
66
+
67
+ # The number of characters in this exception's stacktrace that
68
+ # should be used to uniquify this exception. Exceptions raised from
69
+ # the same place should have the same stacktrace, up to
70
+ # +STACK_DIGEST_LENGTH+ characters.
71
+ STACK_DIGEST_LENGTH = 4096
72
+
73
+ # Generate a 'mostly-unique' hash code for this exception, that
74
+ # should be the same for similar exceptions and different for
75
+ # different exceptions. This is used to group similar exceptions
76
+ # together.
77
+ def digest
78
+ @digest ||= Digest::MD5.hexdigest((backtrace ? backtrace.to_s[0,STACK_DIGEST_LENGTH] : exception.to_s) + type.to_s)
79
+ end
80
+
81
+ # Serialize this object to json, so we can send it over the wire.
82
+ def to_json
83
+ {
84
+ :application => application,
85
+ :machine => machine,
86
+ :timestamp => timestamp,
87
+ :type => type,
88
+ :exception => exception,
89
+ :data => data,
90
+ :backtrace => backtrace
91
+ }.to_json
92
+ end
93
+
94
+ private
95
+
96
+ # Shamelessly stolen from rails. Converts the keys in +hash+ from
97
+ # strings to symbols.
98
+ def symbolize_keys(hash)
99
+ hash.inject({}) do |options, (key, value)|
100
+ options[(key.to_sym rescue key) || key] = value
101
+ options
102
+ end
103
+ end
104
+
105
+ # Determine the name of this machine. Should work on Windows, Linux,
106
+ # and Mac OS X, but only tested on Debian, Ubuntu, and Mac OS X.
107
+ def machine_name
108
+ machine_name = 'unknown'
109
+ if RUBY_PLATFORM =~ /win32/
110
+ machine_name = ENV['COMPUTERNAME']
111
+ elsif RUBY_PLATFORM =~ /linux/ || RUBY_PLATFORM =~ /darwin/
112
+ machine_name = `/bin/hostname`.chomp
113
+ end
114
+ machine_name
115
+ end
116
+ end