never-forget 0.1.0

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.
@@ -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
+