hydraulic_brake 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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