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