never-forget 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ require 'never_forget'
@@ -0,0 +1,23 @@
1
+ module NeverForget
2
+ autoload :ExceptionHandler, 'never_forget/exception_handler'
3
+ autoload :Exception, 'never_forget/exception'
4
+
5
+ class << self
6
+ attr_writer :enabled
7
+ def enabled?() @enabled end
8
+ end
9
+ self.enabled = true
10
+
11
+ def self.log(error, env = {}, &block)
12
+ Exception.create(error, env, &block) if enabled?
13
+ rescue
14
+ warn "NeverForget: error saving exception (#{$!.class} #{$!})"
15
+ warn $!.backtrace.first
16
+ end
17
+ end
18
+
19
+ require 'never_forget/railtie' if defined? Rails::Railtie
20
+
21
+ if defined?(Sinatra) and Sinatra.respond_to? :register
22
+ require 'never_forget/sinatra'
23
+ end
@@ -0,0 +1,179 @@
1
+ require 'mingo'
2
+ require 'active_support/memoizable'
3
+ require 'active_support/core_ext/kernel/singleton_class'
4
+ require 'active_support/core_ext/enumerable'
5
+ require 'active_support/core_ext/hash'
6
+ require 'rack/request'
7
+ require 'rack/utils'
8
+
9
+ module NeverForget
10
+ class Exception < ::Mingo
11
+ def self.collection_name() "NeverForget" end
12
+ def self.collection
13
+ unless defined? @collection
14
+ db.create_collection collection_name, capped: true, size: 1.megabyte
15
+ end
16
+ super
17
+ end
18
+
19
+ extend ::ActiveSupport::Memoizable
20
+ include ::Mingo::Timestamps
21
+
22
+ def self.create(error, env)
23
+ if connected?
24
+ record = new.init(error, env)
25
+ yield record if block_given?
26
+ record.save
27
+ record
28
+ end
29
+ end
30
+
31
+ def self.recent
32
+ find.sort('$natural', :desc).limit(20)
33
+ end
34
+
35
+ KEEP = %w[rack.url_scheme action_dispatch.remote_ip]
36
+ EXCLUDE = %w[HTTP_COOKIE QUERY_STRING SERVER_ADDR]
37
+ KNOWN_MODULES = %w[
38
+ ActiveSupport::Dependencies::Blamable
39
+ JSON::Ext::Generator::GeneratorMethods::Object
40
+ ActiveSupport::Dependencies::Loadable
41
+ PP::ObjectMixin
42
+ Kernel
43
+ ]
44
+
45
+ attr_reader :exception, :env
46
+
47
+ def init(ex, env_hash)
48
+ @exception = unwrap_exception(ex)
49
+ @env = env_hash
50
+ self['name'] = exception.class.name
51
+ self['modules'] = tag_modules
52
+ self['backtrace'] = exception.backtrace.join("\n")
53
+ self['message'] = exception.message
54
+
55
+ if env['REQUEST_METHOD']
56
+ self['env'] = sanitized_env
57
+ self['params'] = extract_params
58
+ self['session'] = extract_session
59
+ self['cookies'] = extract_cookies
60
+ else
61
+ self['params'] = clean_unserializable_data(env)
62
+ end
63
+ self
64
+ end
65
+
66
+ def request
67
+ @request ||= Rack::Request.new(env)
68
+ end
69
+
70
+ def request?
71
+ self['env'].present?
72
+ end
73
+
74
+ def request_url
75
+ env = self['env']
76
+ scheme = env['rack::url_scheme']
77
+ host, port = env['HTTP_HOST'], env['SERVER_PORT'].to_i
78
+ host += ":#{port}" if 'http' == scheme && port != 80 or 'https' == scheme && port != 443
79
+
80
+ url = scheme + '://' + File.join(host, env['SCRIPT_NAME'], env['PATH_INFO'])
81
+ url << '?' << Rack::Utils::build_nested_query(self['params']) if get_request? and self['params'].present?
82
+ url
83
+ end
84
+
85
+ def request_method
86
+ self['env']['REQUEST_METHOD']
87
+ end
88
+
89
+ def get_request?
90
+ 'GET' == request_method
91
+ end
92
+
93
+ def xhr?
94
+ self['env']['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i
95
+ end
96
+
97
+ def remote_ip
98
+ self['env']['action_dispatch::remote_ip'] || self['env']['REMOTE_ADDR']
99
+ end
100
+
101
+ def tag_modules
102
+ Array(exception.singleton_class.included_modules).map(&:to_s) - KNOWN_MODULES
103
+ end
104
+ memoize :tag_modules
105
+
106
+ def unwrap_exception(exception)
107
+ if exception.respond_to?(:original_exception)
108
+ exception.original_exception
109
+ elsif exception.respond_to?(:continued_exception)
110
+ exception.continued_exception
111
+ else
112
+ exception
113
+ end
114
+ end
115
+
116
+ def extract_session
117
+ if session = env['rack.session']
118
+ session_hash = session.to_hash.stringify_keys.except('session_id', '_csrf_token')
119
+ clean_unserializable_data session_hash
120
+ end
121
+ end
122
+
123
+ def exclude_params
124
+ Array(env['action_dispatch.parameter_filter']).map(&:to_s)
125
+ end
126
+ memoize :exclude_params
127
+
128
+ def extract_params
129
+ if params = request.params and params.any?
130
+ filtered = params.each_with_object({}) { |(key, value), keep|
131
+ keep[key] = exclude_params.include?(key.to_s) ? '[FILTERED]' : value
132
+ }
133
+ clean_unserializable_data filtered.except('utf8')
134
+ end
135
+ end
136
+
137
+ def extract_cookies
138
+ if cookies = env['rack.request.cookie_hash']
139
+ if options = env['rack.session.options']
140
+ cookies = cookies.except(options[:key])
141
+ end
142
+ clean_unserializable_data cookies
143
+ end
144
+ end
145
+
146
+ def sanitized_env
147
+ clean_unserializable_data env.select { |key, _| keep_env? key }
148
+ end
149
+
150
+ def keep_env?(key)
151
+ ( key !~ /[a-z]/ or KEEP.include?(key) ) and not discard_env?(key)
152
+ end
153
+
154
+ def discard_env?(key)
155
+ EXCLUDE.include?(key) or
156
+ ( key == 'REMOTE_ADDR' and env['action_dispatch.remote_ip'] )
157
+ end
158
+
159
+ def sanitize_key(key)
160
+ key.to_s.gsub('.', '::').sub(/^\$/, 'DOLLAR::')
161
+ end
162
+
163
+ def clean_unserializable_data(data, stack = [])
164
+ return "[possible infinite recursion halted]" if stack.any?{|item| item == data.object_id }
165
+
166
+ if data.respond_to?(:to_hash)
167
+ data.to_hash.each_with_object({}) do |(key, value), result|
168
+ result[sanitize_key(key)] = clean_unserializable_data(value, stack + [data.object_id])
169
+ end
170
+ elsif data.respond_to?(:to_ary)
171
+ data.collect do |value|
172
+ clean_unserializable_data(value, stack + [data.object_id])
173
+ end
174
+ else
175
+ data.to_s
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,95 @@
1
+ require 'rbconfig'
2
+ require 'erubis'
3
+ require 'active_support/memoizable'
4
+
5
+ module NeverForget
6
+ class ExceptionHandler
7
+ TEMPLATE_FILE = File.expand_path('../list_exceptions.erb', __FILE__)
8
+
9
+ def initialize(app, options = {})
10
+ @app = app
11
+ @options = {:list_path => '/_exceptions'}.update(options)
12
+ end
13
+
14
+ def forward(env)
15
+ begin
16
+ @app.call(env)
17
+ rescue StandardError, ScriptError => error
18
+ NeverForget.log(error, env)
19
+ raise error
20
+ end
21
+ end
22
+
23
+ def call(env)
24
+ if env['PATH_INFO'] == File.join('/', @options[:list_path])
25
+ body = render_exceptions_view
26
+ [200, {'content-type' => 'text/html'}, [body]]
27
+ else
28
+ forward(env)
29
+ end
30
+ end
31
+
32
+ def render_exceptions_view
33
+ template = Erubis::Eruby.new(exceptions_template)
34
+ context = Erubis::Context.new(:recent => Exception.recent)
35
+ context.extend TemplateHelpers
36
+ template.evaluate(context)
37
+ end
38
+
39
+ def exceptions_template
40
+ File.read(TEMPLATE_FILE)
41
+ end
42
+ end
43
+
44
+ module TemplateHelpers
45
+ include RbConfig
46
+ extend ActiveSupport::Memoizable
47
+
48
+ def gem_path
49
+ paths = []
50
+ paths << Bundler.bundle_path << Bundler.user_bundle_path if defined? Bundler
51
+ paths << Gem.path if defined? Gem
52
+ paths.flatten.uniq
53
+ end
54
+
55
+ SYSDIRS = %w[ vendor site rubylib arch sitelib sitearch vendorlib vendorarch top ]
56
+
57
+ def system_path
58
+ SYSDIRS.map { |name| CONFIG["#{name}dir"] }.compact
59
+ end
60
+
61
+ def external_path
62
+ ['/usr/ruby1.9.2', '/home/heroku_rack', gem_path, system_path].flatten.uniq
63
+ end
64
+ memoize :external_path
65
+
66
+ def collapse_line?(line)
67
+ external_path.any? {|p| line.start_with? p }
68
+ end
69
+
70
+ def ignore_line?(line)
71
+ line.include? '/Library/Application Support/Pow/'
72
+ end
73
+
74
+ def strip_root(line)
75
+ if line =~ %r{/gems/([^/]+)-(\d[\w.]*)/}
76
+ gem_name, gem_version = $1, $2
77
+ path = line.split($&, 2).last
78
+ "#{gem_name} (#{gem_version}) #{path}"
79
+ elsif path = "#{root_path}/" and line.start_with? path
80
+ line.sub(path, '')
81
+ else
82
+ line
83
+ end
84
+ end
85
+
86
+ def root_path
87
+ if defined? Bundler then Bundler.root
88
+ elsif defined? Rails then Rails.root
89
+ elsif defined? Sinatra::Application then Sinatra::Application.root
90
+ else Dir.pwd
91
+ end
92
+ end
93
+ memoize :root_path
94
+ end
95
+ end
@@ -0,0 +1,111 @@
1
+ <!DOCTYPE html>
2
+ <meta charset="utf-8">
3
+ <meta name="viewport" content="initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">
4
+ <title>Exceptions</title>
5
+ <style>
6
+ body {
7
+ margin: 2em;
8
+ font: medium/1.6 Helvetica, sans-serif;
9
+ }
10
+ a:link, a:visited { color: darkblue; }
11
+ a:hover { color: crimson; }
12
+ .url, .ua { margin: .2em 0; }
13
+ .ua { color: gray; font-size: 85%; }
14
+ ol.trace {
15
+ margin: .5em 0;
16
+ padding: 0;
17
+ list-style: none;
18
+ font-size: 85%;
19
+ line-height: 1.2;
20
+ }
21
+ .trace .num { font-weight: bold; }
22
+ .trace .sys { display: none; }
23
+ .trace .expand a {
24
+ font-size: 80%; text-transform: uppercase;
25
+ }
26
+ .trace.expanded .sys { display: list-item; }
27
+ .trace.expanded .expand { display: none; }
28
+
29
+ h1 { margin: 0; padding: 0; }
30
+ article {
31
+ margin-top: 1.5em;
32
+ padding-top: 1em;
33
+ border-top: 2px solid #eee;
34
+ }
35
+ h2 {
36
+ margin: 0 0 .2em 0;
37
+ padding: 0 0 .1em 0;
38
+ display: inline-block;
39
+ }
40
+ time {
41
+ display: inline-block;
42
+ font: 80% "American Typewriter", monospace;
43
+ color: gray;
44
+ margin-left: 1em;
45
+ }
46
+ h2 span { font-weight: normal; }
47
+ h3 { margin: .5em 0 .2em 0; padding: .1em 0; font-size: 1.1em; }
48
+ pre { margin: .2em; }
49
+
50
+ @media only screen and (max-device-width:480px) {
51
+ body { margin: 7px; font-size: small; }
52
+ h1 { font-size: 22px; text-align: center; margin: 0; padding: 0; }
53
+ h2 { font-size: 18px; }
54
+ article { margin-top: .2em; padding-top: .2em; padding-bottom: .5em; }
55
+ }
56
+ </style>
57
+
58
+ <h1>Exceptions</h1>
59
+
60
+ <% if @recent.has_next? %>
61
+ <% for ex in @recent %>
62
+ <article>
63
+ <header><h2><%= ex['name'] %>: <span><%== ex['message'] %></span></h2>
64
+ <time><%== ex.created_at.strftime('%a, %b %e %T') %></time>
65
+ </header>
66
+
67
+ <% if ex.request? %>
68
+ <% url = ex.request_url %>
69
+ <p class="url">
70
+ <% if ex.xhr? %>Ajax<% end %> <%= ex.request_method %>
71
+ <a href="<%== url %>"><%== url.split('://').last %></a>
72
+ </p>
73
+ <p class="ua">
74
+ <span class="ip"><%= ex.remote_ip %></span> &mdash;
75
+ <%== ex['env']['HTTP_USER_AGENT'] %>
76
+ </p>
77
+ <% end %>
78
+
79
+ <% if ex['params'].present? %>
80
+ <h3>Params:</h3>
81
+ <pre class="params"><%== YAML.dump ex['params'] %></pre>
82
+ <% end %>
83
+
84
+ <% if ex['session'].present? %>
85
+ <h3>Session:</h3>
86
+ <pre class="session"><%== YAML.dump ex['session'] %></pre>
87
+ <% end %>
88
+
89
+ <h3>Backtrace:</h3>
90
+ <ol class="trace">
91
+ <% for line in ex['backtrace'].split("\n") %>
92
+ <% next if ignore_line? line %>
93
+ <li<%= collapse_line?(line) ? ' class="sys"' : '' %>><%= strip_root(line).sub(/:(\d+):/, ' <span class="num">\1</span> ') %></li>
94
+ <% end %>
95
+ <li class="expand"><a href="#expand">full trace</a></li>
96
+ </ol>
97
+ </article>
98
+ <% end %>
99
+
100
+ <script>
101
+ document.addEventListener('click', function(e) {
102
+ var el = e.target
103
+ if (el.nodeName == 'A' && el.getAttribute('href') == '#expand') {
104
+ e.preventDefault()
105
+ el.parentNode.parentNode.className += " expanded"
106
+ }
107
+ }, false)
108
+ </script>
109
+ <% else %>
110
+ <p><i>No exceptions captured yet.</i></p>
111
+ <% end %>
@@ -0,0 +1,26 @@
1
+ module NeverForget
2
+ class Railtie < ::Rails::Railtie
3
+ config.never_forget = ActiveSupport::OrderedOptions.new
4
+ config.never_forget.enabled = ::Rails.env.production? || ::Rails.env.staging?
5
+ config.never_forget.list_path = '/_exceptions'
6
+
7
+ initializer "never_forget" do |app|
8
+ if NeverForget.enabled = app.config.never_forget.enabled
9
+ app.config.middleware.insert_after 'ActionDispatch::ShowExceptions',
10
+ ExceptionHandler, :list_path => app.config.never_forget.list_path
11
+
12
+ ::ActiveSupport.on_load(:action_controller) { include NeverForget::ControllerRescue }
13
+ end
14
+ end
15
+ end
16
+
17
+ module ControllerRescue
18
+ def rescue_with_handler(exception)
19
+ if super
20
+ # the exception was handled, but we still want to save it
21
+ NeverForget.log(exception, request.env)
22
+ true
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,17 @@
1
+ module NeverForget
2
+ module Sinatra
3
+ module Helpers
4
+ def log_error(boom = $!, env = nil)
5
+ env = request.env if env.nil? and respond_to? :request
6
+ NeverForget.log(boom, env || {})
7
+ end
8
+ end
9
+
10
+ def self.registered(app)
11
+ app.use NeverForget::ExceptionHandler
12
+ app.helpers Helpers
13
+ end
14
+
15
+ ::Sinatra.register self
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: never-forget
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: ruby
7
+ authors:
8
+ - "Mislav Marohni\xC4\x87"
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-09-28 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: mingo
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0.2"
24
+ type: :runtime
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: activesupport
28
+ prerelease: false
29
+ requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: "0"
35
+ type: :runtime
36
+ version_requirements: *id002
37
+ - !ruby/object:Gem::Dependency
38
+ name: rack
39
+ prerelease: false
40
+ requirement: &id003 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ type: :runtime
47
+ version_requirements: *id003
48
+ - !ruby/object:Gem::Dependency
49
+ name: erubis
50
+ prerelease: false
51
+ requirement: &id004 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ type: :runtime
58
+ version_requirements: *id004
59
+ description: Never Forget is a layer of persistence for exceptions thrown in a Rack application at runtime.
60
+ email: mislav.marohnic@gmail.com
61
+ executables: []
62
+
63
+ extensions: []
64
+
65
+ extra_rdoc_files: []
66
+
67
+ files:
68
+ - lib/never-forget.rb
69
+ - lib/never_forget/exception.rb
70
+ - lib/never_forget/exception_handler.rb
71
+ - lib/never_forget/list_exceptions.erb
72
+ - lib/never_forget/railtie.rb
73
+ - lib/never_forget/sinatra.rb
74
+ - lib/never_forget.rb
75
+ homepage: https://github.com/mislav/never-forget
76
+ licenses: []
77
+
78
+ post_install_message:
79
+ rdoc_options: []
80
+
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: "0"
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: "0"
95
+ requirements: []
96
+
97
+ rubyforge_project:
98
+ rubygems_version: 1.8.8
99
+ signing_key:
100
+ specification_version: 3
101
+ summary: Saves exceptions to MongoDB
102
+ test_files: []
103
+