squash_ruby 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
@@ -0,0 +1,232 @@
1
+ Squash Client Library: Ruby
2
+ ===========================
3
+
4
+ This client library reports exceptions to Squash, the Squarish exception
5
+ reporting and management system.
6
+
7
+ Documentation
8
+ -------------
9
+
10
+ Comprehensive documentation is written in YARD- and Markdown-formatted comments
11
+ throughout the source. To view this documentation as an HTML site, run
12
+ `rake doc`.
13
+
14
+ For an overview of the various components of Squash, see the website
15
+ documentation at https://github.com/SquareSquash/web.
16
+
17
+ Compatibility
18
+ -------------
19
+
20
+ This library is compatible with Ruby 1.8.6 and later, including Ruby Enterprise
21
+ Edition.
22
+
23
+ Requirements
24
+ ------------
25
+
26
+ The only dependency is the `json` gem (http://rubygems.org/gems/json). You can use
27
+ any JSON gem that conforms to the typical standard (`require 'json';
28
+ object.to_json`).
29
+
30
+ Usage
31
+ -----
32
+
33
+ Add the Squash client to your Gemfile with
34
+ `gem 'squash_ruby', :require => 'squash/ruby'`. Before you can use Squash, you
35
+ must configure it (see **Configuration** below). At a minimum, you must specify
36
+ the API host and which project you are recording exceptions for:
37
+
38
+ ```` ruby
39
+ Squash::Ruby.configure :api_key => 'YOUR_API_KEY',
40
+ :api_host => 'https://your.squash.host',
41
+ :environment => 'production'
42
+ ````
43
+
44
+ To use Squash to manage your exceptions, place a `begin::rescue` statement at
45
+ the highest level of your code. Inside the `rescue` block, make a call to
46
+ {Squash::Ruby.notify} with the exception. In general, you probably want to
47
+ rescue all subclasses of `Object`, so you catch every possible exception.
48
+ Example:
49
+
50
+ ```` ruby
51
+ begin
52
+ all_of_your_code
53
+ rescue Object => err
54
+ Squash::Ruby.notify err
55
+ raise
56
+ end
57
+ ````
58
+
59
+ In this example the exception is re-raised to take advantage of Ruby's typical
60
+ last-resort exception handling as well.
61
+
62
+ There are many additional features you can take advantage of; see **Additional
63
+ Features** below.
64
+
65
+ ### Additional Features
66
+
67
+ There are a number of other features you can take advantage of to help you debug
68
+ your exceptions:
69
+
70
+ #### User Data
71
+
72
+ Exceptions can be annotated with freeform user data. This data can take any
73
+ format and have any meaning, typically being relevant to the exception at hand.
74
+ This is in fact the system that `squash_rails` uses to annotate an exception
75
+ with information about the Rails request.
76
+
77
+ There are multiple ways to add user data to an exception. By default, user data
78
+ is culled from any instance variables set in the exception. This means that for
79
+ those exceptions that store additional information in instance variables, you
80
+ get user data "for free." An example is the `ActiveRecord::RecordInvalid`
81
+ exception, which stores the invalid record as an instance variable.
82
+
83
+ You can also add user data using the {Squash::Ruby.add_user_data} method:
84
+
85
+ ```` ruby
86
+ input = gets
87
+ Squash::Ruby.add_user_data(:input => input) do
88
+ process_input # may raise an exception
89
+ end
90
+ ````
91
+
92
+ And lastly, if you require `squash/ruby/exception_additions`, you can add user
93
+ data directly in the exception constructor:
94
+
95
+ ```` ruby
96
+ require 'squash/ruby/exception_additions'
97
+
98
+ def process_value(value)
99
+ raise ArgumentError.new("value must be a number", :value => value) unless value.kind_of?(Fixnum)
100
+ # [...]
101
+ end
102
+ ````
103
+
104
+ Requiring that file also lets you add user data to exceptions you catch and
105
+ re-raise:
106
+
107
+ ```` ruby
108
+ require 'squash/ruby/exception_additions'
109
+
110
+ begin
111
+ do_something_with_input(input)
112
+ rescue ArgumentError => err
113
+ err.user_data :input => input
114
+ raise # assumed that Squash::Ruby.notify is called somewhere further up in the stack
115
+ end
116
+ ````
117
+
118
+ If monkey-patching doesn't appeal to you, then don't load
119
+ `squash/ruby/exception_additions`; it's not required for the client to work.
120
+
121
+ #### Ignoring Exceptions
122
+
123
+ You can ignore certain exceptions within a block of code if those exceptions are
124
+ not worth sending to Squash. Use the {Squash::Ruby.ignore_exceptions} method:
125
+
126
+ ```` ruby
127
+ Squash::Ruby.ignore_exceptions(SocketError, Net::HTTPError) do
128
+ some_http_code_that_could_fail
129
+ end
130
+ ````
131
+
132
+ The exceptions _will_ be raised (not eaten) but will _not_ be reported to
133
+ Squash.
134
+
135
+ You can also globally ignore exceptions using the `ignored_exceptions`
136
+ configuration; see **Configuration** below.
137
+
138
+ Configuration
139
+ -------------
140
+
141
+ You can configure the client with the {Squash::Ruby.configure} method. Calling
142
+ this method multiple times will merge new values in with the existing
143
+ configuration. The method takes a hash, which accepts the following (symbol)
144
+ keys:
145
+
146
+ ### General
147
+
148
+ * `disabled`: If `true`, the Squash client will not report any errors.
149
+ * `api_key`: The API key of the project that exceptions will be associated with.
150
+ This configuration option is required. The value can be found by going to the
151
+ project's home page on Squash.
152
+ * `environment`: The environment that exceptions will be associated with.
153
+ * `project_root`: The path to your project's root directory. This path will be
154
+ stripped from backtrace lines. By default it's set to the working directory.
155
+
156
+ ### Revision Information
157
+
158
+ Squash can determine the current code revision using one of two methods. Specify
159
+ only one of the following configuration keys:
160
+
161
+ * `revision_file`: The path to a file storing the SHA1 of the current Git
162
+ revision. This is the revision of the code that is currently running.
163
+ * `repository_root`: The path to the working directory of the Git repository
164
+ that is currently running. Use this option if your deployed code is a working
165
+ Git repository.
166
+
167
+ By default, `repository_root` is assumed and is set to `Dir.getwd`.
168
+ `revision_file` overrides `repository_root`.
169
+
170
+ ### Error Transmission
171
+
172
+ * `api_host`: The host on which Squash is running. This field is required.
173
+ * `notify_path`: The path to post new exception notifications to. By default
174
+ it's set to `/api/1.0/notify`.
175
+ * `transmit_timeout`: The amount of time to wait before giving up on trasmitting
176
+ an error. By default this is treated as both an open and a read timeout.
177
+
178
+ ### Ignored Exceptions
179
+
180
+ * `ignored_exception_classes`: An array of exception class names that will not
181
+ be reported to Squash.
182
+ * `ignored_exception_messages`: A hash mapping an exception class name to an
183
+ array of regexes. Exceptions of that class whose messages match a regex in the
184
+ list will not be reported to Squash.
185
+ * `ignored_exception_procs`: An array of `Proc` objects that can be used to
186
+ filter exceptions. Takes as arguments 1) the exception and 2) the user data
187
+ hash. Should return `true` if the exception should be ignored (_not_ reported)
188
+ and false otherwise. The user data hash can include stuff useful to extended
189
+ client libraries (e.g., Squash Rails client); an example:
190
+
191
+ ```` ruby
192
+ Squash::Ruby.configure :ignored_exception_procs => lambda do |exception, user_data|
193
+ exception.kind_of?(ActiveRecord::RecordNotFound) && user_data[:headers]['X-Testing'].blank?
194
+ end
195
+ ````
196
+
197
+ ### Failsafe Reporting
198
+
199
+ * `failsafe_log`: The pathname of a log file where failsafe exceptions will be
200
+ recorded (see **Failsafe Reporting** below). By default, records to a file
201
+ named `squash.failsafe.log` in the current working directory.
202
+ * `disable_failsafe`: If `true`, the failsafe handler will be disabled.
203
+ Exceptions raised when Squash is processing another exception will be handled
204
+ normally by the Ruby interpreter.
205
+
206
+ Error Transmission
207
+ ------------------
208
+
209
+ Exceptions are transmitted to Squash using JSON-over-HTTPS. A default API
210
+ endpoint is pre-configured, though you can always set your own (see
211
+ **Configuration** above).
212
+
213
+ By default, `Net::HTTP` is used to transmit errors to the API server. If you
214
+ would prefer to use your own HTTP library, you can override the
215
+ {Squash::Ruby.http_transmit} method. This method is also used for deploy
216
+ notification.
217
+
218
+ Failsafe Reporting
219
+ ------------------
220
+
221
+ In the event that the Squash client itself raises an exception when processing
222
+ an exception, it will log that exception to the failsafe log. (See the
223
+ `failsafe_log` configuration option, described above.) Both the original
224
+ exception and the failsafe error will be logged. The original exception will
225
+ still be re-raised, but the failsafe error will be "eaten."
226
+
227
+ If for some reason the exceptions cannot be logged (e.g., a permissions error),
228
+ they will be logged to standard error.
229
+
230
+ It would behoove the engineers of a project using Squash to periodically check
231
+ the failsafe log, as it may contain exceptions that couldn't be reported due
232
+ to, e.g., bugs in generating user data.
@@ -0,0 +1,531 @@
1
+ # Copyright 2012 Square Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'yaml'
16
+ require 'socket'
17
+ require 'net/https'
18
+
19
+ require 'json'
20
+ begin
21
+ Gem::Specification.find_by_name('system_timer')
22
+ require 'system_timer'
23
+ rescue Gem::LoadError
24
+ end
25
+
26
+ # Container for methods relating to notifying Squash of exceptions.
27
+
28
+ module Squash
29
+ module Ruby
30
+ # Reserved instance variables that cannot be keys in a user-data hash.
31
+ EXCEPTION_RESERVED_IVARS = %W( mesg bt )
32
+ # Default values for different configuration variables.
33
+ CONFIGURATION_DEFAULTS = {
34
+ :notify_path => "/api/1.0/notify",
35
+ :deploy_path => "/api/1.0/deploy",
36
+ :open_timeout => 15,
37
+ :transmit_timeout => 15,
38
+ :ignored_exception_classes => [],
39
+ :ignored_exception_messages => {},
40
+ :ignored_exception_procs => [],
41
+ :failsafe_log => "squash.failsafe.log",
42
+ :repository_root => Dir.getwd,
43
+ :project_root => Dir.getwd
44
+ }
45
+ # Types that are serialized directly to JSON, rather than to a hash of
46
+ # object information. Subclasses are not considered members of this array.
47
+ JSON_NATIVE_TYPES = [String, NilClass, TrueClass, FalseClass, Integer,
48
+ Fixnum, Float]
49
+ # Array of user-data fields that should be moved out of the user data to
50
+ # become top-level attributes. A Rails client library would expand this
51
+ # constant to include Rails-specific fields, for example.
52
+ TOP_LEVEL_USER_DATA = []
53
+
54
+ # Notifies Squash of an exception.
55
+ #
56
+ # @param [Object] exception The exception. Must at least duck-type an
57
+ # `Exception` subclass.
58
+ # @param [Hash] user_data Any additional context-specific information about
59
+ # the exception.
60
+ # @return [true, false] Whether the exception was reported to Squash. (Some
61
+ # exceptions are ignored and not reported to Squash.)
62
+ # @raise [StandardError] If Squash has not yet been fully configured (see
63
+ # {.configure}).
64
+
65
+ def self.notify(exception, user_data={})
66
+ occurred = Time.now
67
+
68
+ return false if configuration(:disabled)
69
+ unless exception.respond_to?(:backtrace)
70
+ failsafe_log 'notify', "Tried to pass notify something other than an exception: #{exception.inspect}"
71
+ return false
72
+ end
73
+ unless exception.backtrace
74
+ failsafe_log 'notify', "Tried to pass notify an exception with no backtrace: #{exception}"
75
+ return false
76
+ end
77
+
78
+ raise "The :api_key configuration is required" unless configuration(:api_key)
79
+ raise "The :api_host configuration is required" unless configuration(:api_host)
80
+ raise "The :environment configuration is required" unless configuration(:environment)
81
+
82
+ begin
83
+ exception, parents = unroll(exception)
84
+ return false if ignored?(exception, user_data)
85
+ check_user_data user_data
86
+
87
+ hsh = exception_info_hash(exception, occurred, user_data, parents)
88
+ http_transmit configuration(:api_host) + configuration(:notify_path), {}, hsh.inject({}) { |h, (k, v)| h[k.to_s] = v; h }.to_json
89
+ return true
90
+ rescue Object => nested_error
91
+ raise if configuration(:disable_failsafe)
92
+ failsafe_handler exception, nested_error
93
+ :failsafe # a perfect example of http://thedailywtf.com/Articles/What_Is_Truth_0x3f_.aspx
94
+ end
95
+ end
96
+
97
+ # Raises an exception and immediately catches it and sends it to Squash. The
98
+ # exception is then eaten. This is meant to be used as a hackneyed form of
99
+ # event logging. You can pass in any user data you wish to record with the
100
+ # event.
101
+ #
102
+ # It should be emphasized that Squash is not a logging system, and there are
103
+ # far more appropriate products for this kind of thing, but this method is
104
+ # here nonetheless.
105
+ #
106
+ # @overload record(exception_class, message, user_data={})
107
+ # Specify both the exception class and the message.
108
+ # @param [Class] exception_class The exception class to raise.
109
+ # @param [String] message The exception message.
110
+ # @param [Hash] user_data Additional information to give to {.notify}.
111
+ # @overload record(message, user_data={})
112
+ # Specify only the message. The exception class will be `StandardError`.
113
+ # @param [String] message The exception message.
114
+ # @param [Hash] user_data Additional information to give to {.notify}.
115
+
116
+ def self.record(exception_class_or_message, message_or_options=nil, data=nil)
117
+ if message_or_options && data
118
+ exception_class = exception_class_or_message
119
+ message = message_or_options
120
+ elsif message_or_options.kind_of?(String)
121
+ message = message_or_options
122
+ exception_class = exception_class_or_message
123
+ elsif message_or_options.kind_of?(Hash)
124
+ data = message_or_options
125
+ message = exception_class_or_message
126
+ exception_class = StandardError
127
+ elsif message_or_options.nil?
128
+ message = exception_class_or_message
129
+ exception_class = StandardError
130
+ else
131
+ raise ArgumentError
132
+ end
133
+
134
+ begin
135
+ raise exception_class, message
136
+ rescue exception_class => error
137
+ notify error, data || {}
138
+ end
139
+ end
140
+
141
+ # Suppresses reporting of certain exceptions within a block of code. Any
142
+ # exceptions raised in the block will continue to be raised, however.
143
+ #
144
+ # Let's take a few examples. If `exception_classes` is `[RangeError]`, then
145
+ # obviously any raised `RangeError`s will not be reported. If
146
+ # `StandardError` is raised, it _will_ be reported, because it's a
147
+ # superclass of `RangeError`. If `FloatDomainError` is raised, it _will not_
148
+ # be reported because it is a _subclass_ of `RangeError`. Confusing? Sure,
149
+ # but I'm pretty sure this is the behavior most people would expect.
150
+ #
151
+ # @param [Array<Class>] exception_classes A list of exception classes to
152
+ # ignore. If not provided, ignores all exceptions raised in the block.
153
+ # @yield The code to ignore exceptions in.
154
+ # @return The result of the block.
155
+
156
+ def self.ignore_exceptions(exception_classes=nil)
157
+ raise ArgumentError, "Squash::Ruby.ignore_exceptions must be called with a block" unless block_given?
158
+ exception_classes = [exception_classes] if exception_classes.kind_of?(Class)
159
+
160
+ begin
161
+ yield
162
+ rescue Object => err
163
+ err.instance_variable_set(:@_squash_do_not_report, true) if exception_classes.nil? || exception_classes.map { |e| e.ancestors }.flatten.include?(err.class)
164
+ raise
165
+ end
166
+ end
167
+
168
+ # Adds user data to any exception raised within a block of code, and
169
+ # re-raises the exception.
170
+ #
171
+ # @param [Hash] user_data User data to add to an exception.
172
+ # @yield The code to run.
173
+ # @return The result of the block.
174
+ # @raise [ArgumentError] If `data` contains the keys `mesg` or `bt`.
175
+
176
+ def self.add_user_data(user_data)
177
+ raise ArgumentError, "Squash::Ruby.add_user_data must be called with a block" unless block_given?
178
+ check_user_data user_data
179
+
180
+ begin
181
+ yield
182
+ rescue Object => err
183
+ user_data.each { |ivar, val| err.send :instance_variable_set, :"@#{ivar}", val }
184
+ raise
185
+ end
186
+ end
187
+
188
+ # Sets configuration options for the client from a hash. See the README for
189
+ # a list of configuration options. Subsequent calls will merge in new
190
+ # configuration options.
191
+ #
192
+ # You must at a minimum specify the `:api_key` and `:environment` settings
193
+ # (see the README.md file).
194
+ #
195
+ # @param [Hash] options Configuration options.
196
+
197
+ def self.configure(options)
198
+ @configuration = (@configuration || CONFIGURATION_DEFAULTS.dup).merge(options.inject({}) { |hsh, (k, v)| hsh[(k.to_sym rescue k)] = v; hsh })
199
+ end
200
+
201
+ # @private
202
+ def self.check_user_data(data)
203
+ bad_ivars = EXCEPTION_RESERVED_IVARS.select { |name| data.keys.map { |k| k.to_s }.include? name }
204
+ raise ArgumentError, "The following cannot be used as user-data keys: #{bad_ivars.join(', ')}" unless bad_ivars.empty?
205
+ end
206
+
207
+ protected
208
+
209
+ # Posts an exception or deploy notification to the API endpoint. Only POST
210
+ # requests are supported. This method is used internally only. It is
211
+ # documented so that, in the event you wish to use an alternative HTTP
212
+ # library (other than `Net::HTTP`), you can override this method.
213
+ #
214
+ # This method will make a `POST` request to the given URL. The request will
215
+ # contain the given headers and body. It should not eat any exceptions
216
+ # relating to HTTP connectivity issues.
217
+ #
218
+ # Your implementation should also respect the value of the
219
+ # `transmit_timeout` configuration, which is accessible using
220
+ # `configuration(:transmit_timeout)`.
221
+ #
222
+ # @param [String] url The URL to post to. Could be an HTTP or HTTPS URL.
223
+ # @param [Hash<String, String>] headers The request headers.
224
+ # `Content-Type: application/json` is added by default.
225
+ # @param [String] body The request body.
226
+ # @return [true, false] Whether or not the response was successful.
227
+
228
+ def self.http_transmit(url, headers, body)
229
+ uri = URI.parse(url)
230
+ http = Net::HTTP.new(uri.host, uri.port)
231
+ http_options(uri).each { |k, v| http.send :"#{k}=", v }
232
+
233
+ block = lambda do
234
+ http.start do |http|
235
+ request = Net::HTTP::Post.new(uri.request_uri)
236
+ request.add_field 'Content-Type', 'application/json'
237
+ headers.each { |k, v| request.add_field k, v }
238
+ request.body = body
239
+ response = http.request request
240
+ if response.kind_of?(Net::HTTPSuccess)
241
+ return true
242
+ else
243
+ self.failsafe_log 'http_transmit', "Response from server: #{response.code}"
244
+ return false
245
+ end
246
+ end
247
+ end
248
+
249
+ if defined?(SystemTimer)
250
+ SystemTimer.timeout_after configuration(:open_timeout), &block
251
+ else
252
+ block.call
253
+ end
254
+ end
255
+
256
+ # Notifies Squash of a new deploy. Squash will then determine which bug
257
+ # fixes have been deployed and then mark those bugs as fix-deployed.
258
+ #
259
+ # @param [String] env The name of the environment that was deployed.
260
+ # @param [String] revision The repository revision that was deployed.
261
+ # @param [String] from_host The hostname of the computer that performed the
262
+ # deploy.
263
+ # @raise [StandardError] If an invalid response is received from the HTTP
264
+ # request.
265
+
266
+ def self.notify_deploy(env, revision, from_host)
267
+ return if configuration(:disabled)
268
+
269
+ success = http_transmit(
270
+ configuration(:api_host) + configuration(:deploy_path),
271
+ {},
272
+ {
273
+ 'project' => {'api_key' => configuration(:api_key)},
274
+ 'environment' => {'name' => env},
275
+ 'deploy' => {
276
+ 'deployed_at' => Time.now,
277
+ 'revision' => revision,
278
+ 'hostname' => from_host
279
+ }
280
+ }.to_json
281
+ )
282
+ $stderr.puts "[Squash] Bad response; see failsafe log" unless success
283
+ rescue Timeout::Error
284
+ $stderr.puts "[Squash] Timeout when trying to notify of the deploy"
285
+ end
286
+
287
+ # @abstract
288
+ #
289
+ # Override this method to filter sensitive information from any data that
290
+ # Squash intends to add to an occurrence. This method receives every object
291
+ # that Squash is about to serialize -- instance variables, user data,
292
+ # (for Rails) sessions, params, etc., just before serialization and
293
+ # transmission.
294
+ #
295
+ # This method gives you the opportunity to alter the object before
296
+ # serialization, for example to remove sensitive information. It's probably a
297
+ # good idea to clone the object, modify it, and then return the clone, so that
298
+ # the original object remains unmodified.
299
+ #
300
+ # The base implementation returns `value` unmodified.
301
+ #
302
+ # @param value A value that is about to be serialized for transmission.
303
+ # @return The object to serialize (filtered as necessary). May be a different
304
+ # object.
305
+ def self.value_filter(value) value end
306
+
307
+ private
308
+
309
+ def self.http_options(uri)
310
+ options = {:use_ssl => uri.scheme == 'https',
311
+ :open_timeout => configuration(:transmit_timeout),
312
+ :read_timeout => configuration(:transmit_timeout)}
313
+ options[:verify_mode] = OpenSSL::SSL::VERIFY_NONE if configuration(:skip_ssl_verification)
314
+ options
315
+ end
316
+
317
+ def self.configuration(key)
318
+ (@configuration || CONFIGURATION_DEFAULTS)[key] || CONFIGURATION_DEFAULTS[key]
319
+ end
320
+
321
+ def self.ignored?(exception, user_data)
322
+ return true if exception.instance_variable_get(:@_squash_do_not_report)
323
+
324
+ return true if Array(configuration(:ignored_exception_classes)).map do |class_name|
325
+ constantize class_name
326
+ end.compact.any? { |klass| exception.kind_of?(klass) }
327
+
328
+ return true if configuration(:ignored_exception_messages).any? do |class_name, ignored_messages|
329
+ ignored_messages = Array(ignored_messages).map { |str| str.kind_of?(String) ? Regexp.compile(str) : str }
330
+ (klass = constantize(class_name)) && exception.kind_of?(klass) && ignored_messages.any? { |msg| exception.to_s =~ msg }
331
+ end
332
+
333
+ return true if Array(configuration(:ignored_exception_procs)).any? do |proc|
334
+ proc.call(exception, user_data)
335
+ end
336
+
337
+ return false
338
+ end
339
+
340
+ def self.exception_info_hash(exception, occurred, user_data, parents)
341
+ top_level_user_data = Hash.new
342
+ user_data.delete_if do |key, value|
343
+ if TOP_LEVEL_USER_DATA.include?(key.to_s)
344
+ top_level_user_data[key.to_s] = valueify(value, true)
345
+ true
346
+ else
347
+ false
348
+ end
349
+ end
350
+
351
+ environment_data.merge(top_level_user_data).merge(
352
+ 'class_name' => exception.class.to_s,
353
+ 'message' => exception.to_s,
354
+ 'backtraces' => [["Active Thread/Fiber", true, prepare_backtrace(exception.backtrace)]],
355
+ 'occurred_at' => occurred,
356
+ 'revision' => current_revision,
357
+
358
+ 'environment' => configuration(:environment).to_s,
359
+ 'api_key' => configuration(:api_key).to_s,
360
+ 'client' => client_name,
361
+
362
+ 'ivars' => instance_variable_hash(exception),
363
+ 'user_data' => valueify(user_data, true),
364
+
365
+ 'parent_exceptions' => parents.nil? ? nil : parents.map do |parent|
366
+ {'class_name' => parent.class.to_s,
367
+ 'message' => parent.to_s,
368
+ 'backtraces' => [["Active Thread/Fiber", true, prepare_backtrace(parent.backtrace)]],
369
+ 'association' => 'original_exception',
370
+ 'ivars' => instance_variable_hash(parent)}
371
+ end
372
+ )
373
+ end
374
+
375
+ def self.prepare_backtrace(bt)
376
+ if defined?(JRuby)
377
+ bt.map do |element|
378
+ if element =~ /^((?:[a-z0-9_$]+\.)*(?:[a-z0-9_$]+))\.(\w+)\((\w+.java):(\d+)\)$/i
379
+ # special JRuby backtrace element of the form "org.jruby.RubyHash$27.visit(RubyHash.java:1646)"
380
+ ['_JAVA_', $3, $4.to_i, $2, $1]
381
+ else
382
+ if element.include?(' at ')
383
+ method, fileline = element.split(' at ')
384
+ method.lstrip!
385
+ file, line = fileline.split(':')
386
+ else
387
+ file, line, method = element.split(':')
388
+ if method =~ /^in `(.+)'$/
389
+ method = $1
390
+ end
391
+ method = nil if method && method.empty?
392
+ end
393
+ line = line.to_i
394
+ line = nil if line < 1
395
+ if method =~ /^in `(.+)'$/
396
+ method = $1
397
+ end
398
+ method = nil if method && method.empty?
399
+
400
+ # it could still be a java backtrace, even if it's not the special format
401
+ if file[-5, 5] == '.java'
402
+ ['_JAVA_', file.split('/').last, line, method, file.sub(/\.java$/, '').gsub('/', '.')]
403
+ else
404
+ # ok now we're sure it's a ruby backtrace
405
+ file.slice! 0, configuration(:project_root).length + 1 if file[0, configuration(:project_root).length + 1] == configuration(:project_root) + '/'
406
+ [file, line, method]
407
+ end
408
+ end
409
+ end
410
+ else
411
+ bt.map do |element|
412
+ file, line, method = element.split(':')
413
+ line = line.to_i
414
+ line = nil if line < 1
415
+
416
+ file.slice! 0, configuration(:project_root).length + 1 if file[0, configuration(:project_root).length + 1] == configuration(:project_root) + '/'
417
+
418
+ if method =~ /^in `(.+)'$/
419
+ method = $1
420
+ end
421
+ method = nil if method && method.empty?
422
+ [file, line, method]
423
+ end
424
+ end
425
+ end
426
+
427
+ def self.valueify(instance, elements_only=false)
428
+ if JSON_NATIVE_TYPES.any? { |klass| instance.class == klass }
429
+ instance
430
+ elsif instance.kind_of?(Hash) && elements_only
431
+ instance.inject({}) { |hsh, (k, v)| hsh[k.to_s] = valueify(v); hsh }
432
+ elsif instance.kind_of?(Array) && elements_only
433
+ instance.map { |i| valueify(i) }
434
+ else
435
+ filtered = value_filter(instance)
436
+ {
437
+ 'language' => 'ruby',
438
+ 'class_name' => filtered.class.to_s,
439
+ 'inspect' => filtered.inspect,
440
+ 'yaml' => (filtered.to_yaml rescue nil),
441
+ 'json' => (filtered.to_json rescue nil),
442
+ 'to_s' => (filtered.to_s rescue nil)
443
+ }
444
+ end
445
+ end
446
+
447
+ def self.instance_variable_hash(object)
448
+ object.instance_variables.inject({}) do |hsh, cur|
449
+ hsh[cur.to_s[1..-1]] = valueify(object.send(:instance_variable_get, cur.to_sym))
450
+ hsh
451
+ end
452
+ end
453
+
454
+ def self.constantize(class_name)
455
+ return class_name if class_name.kind_of?(Class)
456
+
457
+ parts = class_name.split('::').reject { |i| i.empty? }
458
+ constant = Object
459
+ while parts.any? do
460
+ begin
461
+ constant = constant.const_get(parts.shift)
462
+ rescue NameError
463
+ return nil
464
+ end
465
+ end
466
+ constant
467
+ end
468
+
469
+ # @private
470
+ def self.environment_data
471
+ {
472
+ 'pid' => Process.pid,
473
+ 'hostname' => Socket.gethostname,
474
+ 'env_vars' => ENV.inject({}) { |hsh, (k, v)| hsh[k.to_s] = valueify(v); hsh },
475
+ 'arguments' => ARGV.join(' ')
476
+ }
477
+ end
478
+
479
+ # @private
480
+ def self.failsafe_handler(original_error, nested_error)
481
+ log_entries = [
482
+ "#{Time.now.to_s} - Original error: (#{original_error.class.to_s}) #{original_error.to_s}",
483
+ (original_error.backtrace || []).map { |l| ' ' + l }.join("\n"),
484
+ "#{Time.now.to_s} - Error raised when reporting original error: (#{nested_error.class.to_s}) #{nested_error.to_s}",
485
+ nested_error.backtrace.map { |l| ' ' + l }.join("\n"),
486
+ "--- END SQUASH FAILSAFE ERROR ---"
487
+ ]
488
+ log_entries.each { |message| self.failsafe_log 'failsafe_handler', message }
489
+ end
490
+
491
+ # @private
492
+ def self.failsafe_log(tag, message)
493
+ File.open(configuration(:failsafe_log), 'a') do |f|
494
+ f.puts "#{Time.now.to_s}\t[#{tag}]\t#{message}"
495
+ end
496
+ rescue Object => err
497
+ $stderr.puts "Couldn't write to failsafe log (#{err.to_s}); writing to stderr instead."
498
+ $stderr.puts "#{Time.now.to_s}\t[#{tag}]\t#{message}"
499
+ end
500
+
501
+ # @private
502
+ def self.current_revision
503
+ revision = if configuration(:revision_file)
504
+ File.read(configuration(:revision_file)).chomp.strip
505
+ else
506
+ head_file = File.join(configuration(:repository_root), '.git', 'HEAD')
507
+ if File.exist?(head_file)
508
+ rev = File.read(head_file).chomp.strip
509
+ rev = File.read(File.join(configuration(:repository_root), '.git', $1)).chomp.strip if rev =~ /^ref: (.+?)$/
510
+ rev
511
+ else
512
+ raise "You must set the :revision_file configuration if the code is not running in a Git checkout"
513
+ end
514
+ end
515
+ raise "Unknown Git revision #{revision.inspect}" unless revision =~ /^[0-9a-f]{40}$/
516
+ revision
517
+ end
518
+
519
+ # @private
520
+ def self.unroll(exception)
521
+ if exception.respond_to?(:original_exception)
522
+ [exception.original_exception, [exception]]
523
+ else
524
+ [exception, nil]
525
+ end
526
+ end
527
+
528
+ # @private
529
+ def self.client_name() 'ruby' end
530
+ end
531
+ end
@@ -0,0 +1,55 @@
1
+ # Copyright 2012 Square Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # Reopens the `Exception` class to add a convenient way of appending user data
16
+ # to an exception at the time of the raise.
17
+ #
18
+ # @example
19
+ # raise ArgumentError.new("value must be a number", :value => value) unless value.kind_of?(Fixnum)
20
+
21
+ class Exception
22
+
23
+ # @overload new(message, user_data={})
24
+ # Creates a new exception instance, optionally with user data.
25
+ # @param [String] message The exception message.
26
+ # @param [Hash] user_data Additional data to report to Squash about the
27
+ # exception.
28
+ # @return [Exception] The initialized exception.
29
+ # @raise [ArgumentError] If `data` contains the keys `mesg` or `bt`.
30
+
31
+ def self.new(*args)
32
+ user_data = if args.last.is_a?(Hash)
33
+ args.pop
34
+ else
35
+ {}
36
+ end
37
+ Squash::Ruby.check_user_data user_data
38
+ super(*args).user_data(user_data)
39
+ end
40
+
41
+ # Annotates this exception with user data. Merges in any new data with
42
+ # existing user data.
43
+ #
44
+ # @param [Hash] data The user data to add.
45
+ # @return [Exception] The receiver.
46
+ # @raise [ArgumentError] If `data` contains the keys `mesg` or `bt`.
47
+
48
+ def user_data(data)
49
+ Squash::Ruby.check_user_data data
50
+ data.each do |ivar, value|
51
+ instance_variable_set :"@#{ivar}", value
52
+ end
53
+ self
54
+ end
55
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: squash_ruby
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Tim Morgan
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-12-07 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ prerelease: false
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ hash: 3
28
+ segments:
29
+ - 0
30
+ version: "0"
31
+ type: :runtime
32
+ name: json
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ prerelease: false
36
+ requirement: &id002 !ruby/object:Gem::Requirement
37
+ none: false
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ hash: 3
42
+ segments:
43
+ - 0
44
+ version: "0"
45
+ type: :development
46
+ name: rspec
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ prerelease: false
50
+ requirement: &id003 !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ hash: 3
56
+ segments:
57
+ - 0
58
+ version: "0"
59
+ type: :development
60
+ name: yard
61
+ version_requirements: *id003
62
+ - !ruby/object:Gem::Dependency
63
+ prerelease: false
64
+ requirement: &id004 !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ hash: 3
70
+ segments:
71
+ - 0
72
+ version: "0"
73
+ type: :development
74
+ name: redcarpet
75
+ version_requirements: *id004
76
+ - !ruby/object:Gem::Dependency
77
+ prerelease: false
78
+ requirement: &id005 !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ hash: 3
84
+ segments:
85
+ - 0
86
+ version: "0"
87
+ type: :development
88
+ name: jeweler
89
+ version_requirements: *id005
90
+ description: This client library records Ruby exceptions to Squash.
91
+ email: tim@squareup.com
92
+ executables: []
93
+
94
+ extensions: []
95
+
96
+ extra_rdoc_files:
97
+ - LICENSE.txt
98
+ - README.md
99
+ files:
100
+ - LICENSE.txt
101
+ - README.md
102
+ - lib/squash/ruby.rb
103
+ - lib/squash/ruby/exception_additions.rb
104
+ homepage: http://github.com/SquareSquash/ruby
105
+ licenses:
106
+ - Apache 2.0
107
+ post_install_message:
108
+ rdoc_options: []
109
+
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ hash: 3
118
+ segments:
119
+ - 0
120
+ version: "0"
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ hash: 3
127
+ segments:
128
+ - 0
129
+ version: "0"
130
+ requirements: []
131
+
132
+ rubyforge_project:
133
+ rubygems_version: 1.8.24
134
+ signing_key:
135
+ specification_version: 3
136
+ summary: Squash client for Ruby projects
137
+ test_files: []
138
+