hydraulic_brake 0.0.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,242 @@
1
+ module HydraulicBrake
2
+ # Used to set up and modify settings for the notifier.
3
+ class Configuration
4
+
5
+ OPTIONS = [
6
+ :api_key,
7
+ :backtrace_filters,
8
+ :development_environments,
9
+ :development_lookup,
10
+ :environment_name,
11
+ :framework,
12
+ :host,
13
+ :http_open_timeout,
14
+ :http_read_timeout,
15
+ :notifier_name,
16
+ :notifier_url,
17
+ :notifier_version,
18
+ :params_filters,
19
+ :port,
20
+ :project_root,
21
+ :protocol,
22
+ :proxy_host,
23
+ :proxy_pass,
24
+ :proxy_port,
25
+ :proxy_user,
26
+ :rake_environment_filters,
27
+ :rescue_rake_exceptions,
28
+ :secure,
29
+ :use_system_ssl_cert_chain,
30
+ ].freeze
31
+
32
+ # The API key for your project, found on the project edit form.
33
+ attr_accessor :api_key
34
+
35
+ # The host to connect to
36
+ attr_accessor :host
37
+
38
+ # The port on which your Airbrake server runs (defaults to 443 for secure
39
+ # connections, 80 for insecure connections).
40
+ attr_accessor :port
41
+
42
+ # +true+ for https connections, +false+ for http connections.
43
+ attr_accessor :secure
44
+
45
+ # +true+ to use whatever CAs OpenSSL has installed on your system. +false+
46
+ # to use the ca-bundle.crt file included in HydraulicBrake itself
47
+ # (reccomended and default)
48
+ attr_accessor :use_system_ssl_cert_chain
49
+
50
+ # The HTTP open timeout in seconds (defaults to 2).
51
+ attr_accessor :http_open_timeout
52
+
53
+ # The HTTP read timeout in seconds (defaults to 5).
54
+ attr_accessor :http_read_timeout
55
+
56
+ # The hostname of your proxy server (if using a proxy)
57
+ attr_accessor :proxy_host
58
+
59
+ # The port of your proxy server (if using a proxy)
60
+ attr_accessor :proxy_port
61
+
62
+ # The username to use when logging into your proxy server (if using a proxy)
63
+ attr_accessor :proxy_user
64
+
65
+ # The password to use when logging into your proxy server (if using a proxy)
66
+ attr_accessor :proxy_pass
67
+
68
+ # A list of parameters that should be filtered out of what is sent to Airbrake.
69
+ # By default, all "password" attributes will have their contents replaced.
70
+ attr_reader :params_filters
71
+
72
+ # A list of filters for cleaning and pruning the backtrace. See #filter_backtrace.
73
+ attr_reader :backtrace_filters
74
+
75
+ # A list of environment keys that will be ignored from what is sent to Airbrake server
76
+ # Empty by default and used only in rake handler
77
+ attr_reader :rake_environment_filters
78
+
79
+ # A list of environments in which notifications should not be sent.
80
+ attr_accessor :development_environments
81
+
82
+ # +true+ if you want to check for production errors matching development errors, +false+ otherwise.
83
+ attr_accessor :development_lookup
84
+
85
+ # The name of the environment the application is running in
86
+ attr_accessor :environment_name
87
+
88
+ # The path to the project in which the error occurred, such as the Rails.root
89
+ attr_accessor :project_root
90
+
91
+ # The name of the notifier library being used to send notifications (such
92
+ # as "HydraulicBrake Notifier")
93
+ attr_accessor :notifier_name
94
+
95
+ # The version of the notifier library being used to send notifications (such as "1.0.2")
96
+ attr_accessor :notifier_version
97
+
98
+ # The url of the notifier library being used to send notifications
99
+ attr_accessor :notifier_url
100
+
101
+ # The logger used by HydraulicBrake
102
+ attr_accessor :logger
103
+
104
+ # The framework HydraulicBrake is configured to use
105
+ attr_accessor :framework
106
+
107
+ # Should HydraulicBrake catch exceptions from Rake tasks?
108
+ # (boolean or nil; set to nil to catch exceptions when rake isn't running from a terminal; default is nil)
109
+ attr_accessor :rescue_rake_exceptions
110
+
111
+ # User attributes that are being captured
112
+ attr_accessor :user_attributes
113
+
114
+
115
+ DEFAULT_PARAMS_FILTERS = %w(password password_confirmation).freeze
116
+
117
+ DEFAULT_USER_ATTRIBUTES = %w(id name username email).freeze
118
+
119
+ DEFAULT_BACKTRACE_FILTERS = [
120
+ lambda { |line|
121
+ if defined?(HydraulicBrake.configuration.project_root) && HydraulicBrake.configuration.project_root.to_s != ''
122
+ line.sub(/#{HydraulicBrake.configuration.project_root}/, "[PROJECT_ROOT]")
123
+ else
124
+ line
125
+ end
126
+ },
127
+ lambda { |line| line.gsub(/^\.\//, "") },
128
+ lambda { |line|
129
+ if defined?(Gem)
130
+ Gem.path.inject(line) do |line, path|
131
+ line.gsub(/#{path}/, "[GEM_ROOT]")
132
+ end
133
+ end
134
+ },
135
+ lambda { |line| line if line !~ %r{lib/hydraulic_brake} }
136
+ ].freeze
137
+
138
+ alias_method :secure?, :secure
139
+ alias_method :use_system_ssl_cert_chain?, :use_system_ssl_cert_chain
140
+
141
+ def initialize
142
+ @secure = false
143
+ @use_system_ssl_cert_chain= false
144
+ @host = 'api.airbrake.io'
145
+ @http_open_timeout = 2
146
+ @http_read_timeout = 5
147
+ @params_filters = DEFAULT_PARAMS_FILTERS.dup
148
+ @backtrace_filters = DEFAULT_BACKTRACE_FILTERS.dup
149
+ @development_environments = %w(development test cucumber)
150
+ @development_lookup = true
151
+ @notifier_name = 'HydraulicBrake Notifier'
152
+ @notifier_version = VERSION
153
+ @notifier_url = 'https://github.com/stevecrozz/hydraulic_brake'
154
+ @framework = 'Standalone'
155
+ @rescue_rake_exceptions = nil
156
+ @user_attributes = DEFAULT_USER_ATTRIBUTES.dup
157
+ @rake_environment_filters = []
158
+ end
159
+
160
+ # Takes a block and adds it to the list of backtrace filters. When the filters
161
+ # run, the block will be handed each line of the backtrace and can modify
162
+ # it as necessary.
163
+ #
164
+ # @example
165
+ # config.filter_bracktrace do |line|
166
+ # line.gsub(/^#{Rails.root}/, "[Rails.root]")
167
+ # end
168
+ #
169
+ # @param [Proc] block The new backtrace filter.
170
+ # @yieldparam [String] line A line in the backtrace.
171
+ def filter_backtrace(&block)
172
+ self.backtrace_filters << block
173
+ end
174
+
175
+ # Allows config options to be read like a hash
176
+ #
177
+ # @param [Symbol] option Key for a given attribute
178
+ def [](option)
179
+ send(option)
180
+ end
181
+
182
+ # Returns a hash of all configurable options
183
+ def to_hash
184
+ OPTIONS.inject({}) do |hash, option|
185
+ hash[option.to_sym] = self.send(option)
186
+ hash
187
+ end
188
+ end
189
+
190
+ # Returns a hash of all configurable options merged with +hash+
191
+ #
192
+ # @param [Hash] hash A set of configuration options that will take precedence over the defaults
193
+ def merge(hash)
194
+ to_hash.merge(hash)
195
+ end
196
+
197
+ # Determines if the notifier will send notices.
198
+ # @return [Boolean] Returns +false+ if in a development environment, +true+ otherwise.
199
+ def public?
200
+ !development_environments.include?(environment_name)
201
+ end
202
+
203
+ def port
204
+ @port || default_port
205
+ end
206
+
207
+ # Determines whether protocol should be "http" or "https".
208
+ # @return [String] Returns +"http"+ if you've set secure to +false+ in
209
+ # configuration, and +"https"+ otherwise.
210
+ def protocol
211
+ if secure?
212
+ 'https'
213
+ else
214
+ 'http'
215
+ end
216
+ end
217
+
218
+ def ca_bundle_path
219
+ if use_system_ssl_cert_chain? && File.exist?(OpenSSL::X509::DEFAULT_CERT_FILE)
220
+ OpenSSL::X509::DEFAULT_CERT_FILE
221
+ else
222
+ local_cert_path # ca-bundle.crt built from source, see resources/README.md
223
+ end
224
+ end
225
+
226
+ def local_cert_path
227
+ File.expand_path(File.join("..", "..", "..", "resources", "ca-bundle.crt"), __FILE__)
228
+ end
229
+
230
+ private
231
+ # Determines what port should we use for sending notices.
232
+ # @return [Fixnum] Returns 443 if you've set secure to true in your
233
+ # configuration, and 80 otherwise.
234
+ def default_port
235
+ if secure?
236
+ 443
237
+ else
238
+ 80
239
+ end
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,321 @@
1
+ require 'builder'
2
+ require 'socket'
3
+
4
+ module HydraulicBrake
5
+ class Notice
6
+
7
+ class << self
8
+ def attr_reader_with_tracking(*names)
9
+ attr_readers.concat(names)
10
+ attr_reader_without_tracking(*names)
11
+ end
12
+
13
+ alias_method :attr_reader_without_tracking, :attr_reader
14
+ alias_method :attr_reader, :attr_reader_with_tracking
15
+
16
+
17
+ def attr_readers
18
+ @attr_readers ||= []
19
+ end
20
+ end
21
+
22
+ # The exception that caused this notice, if any
23
+ attr_reader :exception
24
+
25
+ # The API key for the project to which this notice should be sent
26
+ attr_reader :api_key
27
+
28
+ # The backtrace from the given exception or hash.
29
+ attr_reader :backtrace
30
+
31
+ # The name of the class of error (such as RuntimeError)
32
+ attr_reader :error_class
33
+
34
+ # The name of the server environment (such as "production")
35
+ attr_reader :environment_name
36
+
37
+ # CGI variables such as HTTP_METHOD
38
+ attr_reader :cgi_data
39
+
40
+ # The message from the exception, or a general description of the error
41
+ attr_reader :error_message
42
+
43
+ # See Configuration#backtrace_filters
44
+ attr_reader :backtrace_filters
45
+
46
+ # See Configuration#params_filters
47
+ attr_reader :params_filters
48
+
49
+ # A hash of parameters from the query string or post body.
50
+ attr_reader :parameters
51
+ alias_method :params, :parameters
52
+
53
+ # The component (if any) which was used in this request (usually the controller)
54
+ attr_reader :component
55
+ alias_method :controller, :component
56
+
57
+ # The action (if any) that was called in this request
58
+ attr_reader :action
59
+
60
+ # A hash of session data from the request
61
+ attr_reader :session_data
62
+
63
+ # The path to the project that caused the error (usually Rails.root)
64
+ attr_reader :project_root
65
+
66
+ # The URL at which the error occurred (if any)
67
+ attr_reader :url
68
+
69
+ # The name of the notifier library sending this notice, such as "HydraulicBrake Notifier"
70
+ attr_reader :notifier_name
71
+
72
+ # The version number of the notifier library sending this notice, such as "2.1.3"
73
+ attr_reader :notifier_version
74
+
75
+ # A URL for more information about the notifier library sending this notice
76
+ attr_reader :notifier_url
77
+
78
+ # The host name where this error occurred (if any)
79
+ attr_reader :hostname
80
+
81
+ # Details about the user who experienced the error
82
+ attr_reader :user
83
+
84
+ private
85
+
86
+ # Private writers for all the attributes
87
+ attr_writer :exception, :api_key, :backtrace, :error_class, :error_message,
88
+ :backtrace_filters, :parameters, :params_filters, :project_root, :url,
89
+ :notifier_name, :notifier_url, :notifier_version, :component, :action,
90
+ :cgi_data, :environment_name, :hostname, :user, :session_data
91
+
92
+ # Arguments given in the initializer
93
+ attr_accessor :args
94
+
95
+ public
96
+
97
+ def initialize(args)
98
+ self.args = args
99
+ self.exception = args[:exception]
100
+ self.api_key = args[:api_key]
101
+ self.project_root = args[:project_root]
102
+ self.url = args[:url]
103
+
104
+ self.notifier_name = args[:notifier_name]
105
+ self.notifier_version = args[:notifier_version]
106
+ self.notifier_url = args[:notifier_url]
107
+
108
+ self.backtrace_filters = args[:backtrace_filters] || []
109
+ self.params_filters = args[:params_filters] || []
110
+ self.parameters = args[:parameters] || {}
111
+ self.component = args[:component] || args[:controller] || nil
112
+ self.action = args[:action] || nil
113
+
114
+ self.environment_name = args[:environment_name]
115
+ self.cgi_data = args[:cgi_data] || {}
116
+ self.backtrace = Backtrace.parse(exception_attribute(:backtrace, caller), :filters => self.backtrace_filters)
117
+ self.error_class = exception_attribute(:error_class) {|exception| exception.class.name }
118
+ self.error_message = exception_attribute(:error_message, 'Notification') do |exception|
119
+ "#{exception.class.name}: #{args[:error_message] || exception.message}"
120
+ end
121
+ self.session_data = args[:session_data] || {}
122
+
123
+ self.hostname = local_hostname
124
+ self.user = args[:user] || {}
125
+
126
+ clean_params
127
+ end
128
+
129
+ # Converts the given notice to XML
130
+ def to_xml
131
+ builder = Builder::XmlMarkup.new
132
+ builder.instruct!
133
+ xml = builder.notice(:version => HydraulicBrake::API_VERSION) do |notice|
134
+ notice.tag!("api-key", api_key)
135
+ notice.notifier do |notifier|
136
+ notifier.name(notifier_name)
137
+ notifier.version(notifier_version)
138
+ notifier.url(notifier_url)
139
+ end
140
+ notice.error do |error|
141
+ error.tag!('class', error_class)
142
+ error.message(error_message)
143
+ error.backtrace do |backtrace|
144
+ self.backtrace.lines.each do |line|
145
+ backtrace.line(:number => line.number,
146
+ :file => line.file,
147
+ :method => line.method)
148
+ end
149
+ end
150
+ end
151
+
152
+ if request_present?
153
+ notice.request do |request|
154
+ request.url(url)
155
+ request.component(controller)
156
+ request.action(action)
157
+ unless parameters.nil? || parameters.empty?
158
+ request.params do |params|
159
+ xml_vars_for(params, parameters)
160
+ end
161
+ end
162
+ unless session_data.empty?
163
+ request.session do |session|
164
+ xml_vars_for(session, session_data)
165
+ end
166
+ end
167
+ unless cgi_data.nil? || cgi_data.empty?
168
+ request.tag!("cgi-data") do |cgi_datum|
169
+ xml_vars_for(cgi_datum, cgi_data)
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ notice.tag!("server-environment") do |env|
176
+ env.tag!("project-root", project_root)
177
+ env.tag!("environment-name", environment_name)
178
+ env.tag!("hostname", hostname)
179
+ end
180
+ unless user.empty?
181
+ notice.tag!("current-user") do |u|
182
+ u.tag!("id",user[:id])
183
+ u.tag!("name",user[:name])
184
+ u.tag!("email",user[:email])
185
+ u.tag!("username",user[:username])
186
+ end
187
+ end
188
+ end
189
+ xml.to_s
190
+ end
191
+
192
+ # Allows properties to be accessed using a hash-like syntax
193
+ #
194
+ # @example
195
+ # notice[:error_message]
196
+ # @param [String] method The given key for an attribute
197
+ # @return The attribute value, or self if given +:request+
198
+ def [](method)
199
+ case method
200
+ when :request
201
+ self
202
+ else
203
+ send(method)
204
+ end
205
+ end
206
+
207
+ private
208
+
209
+ def request_present?
210
+ url ||
211
+ controller ||
212
+ action ||
213
+ !parameters.empty? ||
214
+ !cgi_data.empty? ||
215
+ !session_data.empty?
216
+ end
217
+
218
+ # Gets a property named +attribute+ of an exception, either from an actual
219
+ # exception or a hash.
220
+ #
221
+ # If an exception is available, #from_exception will be used. Otherwise,
222
+ # a key named +attribute+ will be used from the #args.
223
+ #
224
+ # If no exception or hash key is available, +default+ will be used.
225
+ def exception_attribute(attribute, default = nil, &block)
226
+ (exception && from_exception(attribute, &block)) || args[attribute] || default
227
+ end
228
+
229
+ # Gets a property named +attribute+ from an exception.
230
+ #
231
+ # If a block is given, it will be used when getting the property from an
232
+ # exception. The block should accept and exception and return the value for
233
+ # the property.
234
+ #
235
+ # If no block is given, a method with the same name as +attribute+ will be
236
+ # invoked for the value.
237
+ def from_exception(attribute)
238
+ if block_given?
239
+ yield(exception)
240
+ else
241
+ exception.send(attribute)
242
+ end
243
+ end
244
+
245
+ # Removes non-serializable data from the given attribute.
246
+ # See #clean_unserializable_data
247
+ def clean_unserializable_data_from(attribute)
248
+ self.send(:"#{attribute}=", clean_unserializable_data(send(attribute)))
249
+ end
250
+
251
+ # Removes non-serializable data. Allowed data types are strings, arrays,
252
+ # and hashes. All other types are converted to strings.
253
+ # TODO: move this onto Hash
254
+ def clean_unserializable_data(data, stack = [])
255
+ return "[possible infinite recursion halted]" if stack.any?{|item| item == data.object_id }
256
+
257
+ if data.respond_to?(:to_hash)
258
+ data.to_hash.inject({}) do |result, (key, value)|
259
+ result.merge(key => clean_unserializable_data(value, stack + [data.object_id]))
260
+ end
261
+ elsif data.respond_to?(:to_ary)
262
+ data.to_ary.collect do |value|
263
+ clean_unserializable_data(value, stack + [data.object_id])
264
+ end
265
+ else
266
+ data.to_s
267
+ end
268
+ end
269
+
270
+ # Replaces the contents of params that match params_filters.
271
+ # TODO: extract this to a different class
272
+ def clean_params
273
+ clean_unserializable_data_from(:parameters)
274
+ filter(parameters)
275
+ if cgi_data
276
+ clean_unserializable_data_from(:cgi_data)
277
+ filter(cgi_data)
278
+ end
279
+ end
280
+
281
+ def filter(hash)
282
+ if params_filters
283
+ hash.each do |key, value|
284
+ if filter_key?(key)
285
+ hash[key] = "[FILTERED]"
286
+ elsif value.respond_to?(:to_hash)
287
+ filter(hash[key])
288
+ end
289
+ end
290
+ end
291
+ end
292
+
293
+ def filter_key?(key)
294
+ params_filters.any? do |filter|
295
+ key.to_s.eql?(filter.to_s)
296
+ end
297
+ end
298
+
299
+ def xml_vars_for(builder, hash)
300
+ hash.each do |key, value|
301
+ if value.respond_to?(:to_hash)
302
+ builder.var(:key => key){|b| xml_vars_for(b, value.to_hash) }
303
+ else
304
+ builder.var(value.to_s, :key => key)
305
+ end
306
+ end
307
+ end
308
+
309
+ def local_hostname
310
+ Socket.gethostname
311
+ end
312
+
313
+ def to_s
314
+ content = []
315
+ self.class.attr_readers.each do |attr|
316
+ content << " #{attr}: #{send(attr)}"
317
+ end
318
+ content.join("\n")
319
+ end
320
+ end
321
+ end
@@ -0,0 +1,128 @@
1
+ module HydraulicBrake
2
+ # Sends out the notice to Airbrake
3
+ class Sender
4
+
5
+ NOTICES_URI = '/notifier_api/v2/notices/'.freeze
6
+ HTTP_ERRORS = [Timeout::Error,
7
+ Errno::EINVAL,
8
+ Errno::ECONNRESET,
9
+ EOFError,
10
+ Net::HTTPBadResponse,
11
+ Net::HTTPHeaderSyntaxError,
12
+ Net::ProtocolError,
13
+ Errno::ECONNREFUSED].freeze
14
+
15
+ def initialize(options = {})
16
+ [ :proxy_host,
17
+ :proxy_port,
18
+ :proxy_user,
19
+ :proxy_pass,
20
+ :protocol,
21
+ :host,
22
+ :port,
23
+ :secure,
24
+ :use_system_ssl_cert_chain,
25
+ :http_open_timeout,
26
+ :http_read_timeout
27
+ ].each do |option|
28
+ instance_variable_set("@#{option}", options[option])
29
+ end
30
+ end
31
+
32
+ # Sends the notice data off to Airbrake for processing.
33
+ #
34
+ # @param [Notice or String] notice The notice to be sent off
35
+ def send_to_airbrake(notice)
36
+ data = notice.respond_to?(:to_xml) ? notice.to_xml : notice
37
+ http = setup_http_connection
38
+
39
+ response = begin
40
+ http.post(url.path, data, HEADERS)
41
+ rescue *HTTP_ERRORS => e
42
+ log :level => :error,
43
+ :message => "Unable to contact the Airbrake server. HTTP Error=#{e}"
44
+ nil
45
+ end
46
+
47
+ case response
48
+ when Net::HTTPSuccess then
49
+ log :level => :info,
50
+ :message => "Success: #{response.class}",
51
+ :response => response
52
+ else
53
+ log :level => :error,
54
+ :message => "Failure: #{response.class}",
55
+ :response => response,
56
+ :notice => notice
57
+ end
58
+
59
+ if response && response.respond_to?(:body)
60
+ error_id = response.body.match(%r{<id[^>]*>(.*?)</id>})
61
+ error_id[1] if error_id
62
+ end
63
+ rescue => e
64
+ log :level => :error,
65
+ :message => "[HydraulicBrake::Sender#send_to_airbrake] Cannot send notification. Error: #{e.class}" +
66
+ " - #{e.message}\nBacktrace:\n#{e.backtrace.join("\n\t")}"
67
+
68
+ nil
69
+ end
70
+
71
+ attr_reader :proxy_host,
72
+ :proxy_port,
73
+ :proxy_user,
74
+ :proxy_pass,
75
+ :protocol,
76
+ :host,
77
+ :port,
78
+ :secure,
79
+ :use_system_ssl_cert_chain,
80
+ :http_open_timeout,
81
+ :http_read_timeout
82
+
83
+ alias_method :secure?, :secure
84
+ alias_method :use_system_ssl_cert_chain?, :use_system_ssl_cert_chain
85
+
86
+ private
87
+
88
+ def url
89
+ URI.parse("#{protocol}://#{host}:#{port}").merge(NOTICES_URI)
90
+ end
91
+
92
+ def log(opts = {})
93
+ opts[:logger].send opts[:level], LOG_PREFIX + opts[:message] if opts[:logger]
94
+ HydraulicBrake.report_environment_info
95
+ HydraulicBrake.report_response_body(opts[:response].body) if opts[:response] && opts[:response].respond_to?(:body)
96
+ HydraulicBrake.report_notice(opts[:notice]) if opts[:notice]
97
+ end
98
+
99
+ def logger
100
+ HydraulicBrake.logger
101
+ end
102
+
103
+ def setup_http_connection
104
+ http =
105
+ Net::HTTP::Proxy(proxy_host, proxy_port, proxy_user, proxy_pass).
106
+ new(url.host, url.port)
107
+
108
+ http.read_timeout = http_read_timeout
109
+ http.open_timeout = http_open_timeout
110
+
111
+ if secure?
112
+ http.use_ssl = true
113
+
114
+ http.ca_file = HydraulicBrake.configuration.ca_bundle_path
115
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
116
+ else
117
+ http.use_ssl = false
118
+ end
119
+
120
+ http
121
+ rescue => e
122
+ log :level => :error,
123
+ :message => "[HydraulicBrake::Sender#setup_http_connection] Failure initializing the HTTP connection.\n" +
124
+ "Error: #{e.class} - #{e.message}\nBacktrace:\n#{e.backtrace.join("\n\t")}"
125
+ raise e
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,5 @@
1
+ module HydraulicBrake
2
+
3
+ VERSION = File.read(File.join(File.dirname(__FILE__), "../../VERSION"))
4
+
5
+ end