rorvswild 0.6.1 → 1.0.0.pre.alpha

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fbf7691d74ad331f2e029ac10ae01a9539ee6214
4
- data.tar.gz: 0185bc142951be856aa7d10652bf7d265adac424
3
+ metadata.gz: 1736359534ad385b3f94023b6f58aedeb80758d9
4
+ data.tar.gz: 7b3977b85d8cc46cf332bccd8ada6c8ed482a1a4
5
5
  SHA512:
6
- metadata.gz: 506d3382ce683c60b22c18ac19479d150396051cce1f488e6229bcffd960c62e1b92739f29a11c78834542f83497757b1019ed59af59a92db4f19fd23dd133a8
7
- data.tar.gz: c97447ef50720e3a7d8519370c6d8769a08fc3c7706ce656fe46aa82d4f960c6e7416003885167658fb26c777a3376fe7143cce45b9bab2bb68a316378137bb8
6
+ metadata.gz: 0851e785429d0263cdf0f0b98792db188a9616fc16bb98847dd2e1ccb979e3627a5a591f6282468c62e18ca46ccba79aaa10441c60f2b258d51b174d28c25489
7
+ data.tar.gz: 02e3b948d90d5fb378c6707d6e2d563348a1549bec9465e7a5dd2d00488d918d229e9d5ef0ef11dc3cf4453ea8d686539b8ff6c1e6695ae5ef768ab1e8b021b3
data/Gemfile CHANGED
@@ -5,3 +5,11 @@ gemspec
5
5
 
6
6
  gem "mocha"
7
7
  gem "top_tests"
8
+
9
+ gem "mongo"
10
+ gem "redis"
11
+
12
+ gem "actionpack"
13
+ gem "activejob"
14
+
15
+ gem "delayed_job"
data/README.md CHANGED
@@ -16,9 +16,78 @@ For those who prefer to manually use an initializer, they can do the following.
16
16
 
17
17
  ```ruby
18
18
  # config/initializers/rorvswild.rb
19
- RorVsWild::Client.new(api_key: API_KEY)
19
+ RorVsWild.start(api_key: API_KEY)
20
20
  ```
21
21
 
22
+ ## Measure any code
23
+
24
+ You can measure any code like this (useful to monitor cronjobs):
25
+
26
+ ```ruby
27
+ RorVsWild.measure_code("User.all.do_something_great")
28
+ ```
29
+
30
+ Or like that:
31
+
32
+ ```ruby
33
+ RorVsWild.measure_block("A great job name") { User.all.do_something_great }
34
+ ```
35
+
36
+ Then it will appears in the jobs page.
37
+
38
+ Note that Calling `measure_code` or `measure_block` inside or a request or a job will add a section.
39
+ That is convenient to profile finely parts of your code.
40
+
41
+ ## Send errors manually
42
+
43
+ When you already have a begin / rescue block, this manner suits well:
44
+
45
+ ```ruby
46
+ begin
47
+ # Your code ...
48
+ rescue => exception
49
+ RorVsWild.record_error(exception)
50
+ end
51
+ ```
52
+
53
+ If you prefer to be concise, just run the code from a block:
54
+
55
+ ```ruby
56
+ RorVsWild.catch_error { 1 / 0 } # => #<ZeroDivisionError: divided by 0>
57
+ ```
58
+
59
+ Moreover, you can provide extra details when capturing errors:
60
+
61
+ ```ruby
62
+ RorVsWild.record_error(exception, {something: "important"})
63
+ ```
64
+
65
+ ```ruby
66
+ RorVsWild.catch_error(something: "important") { 1 / 0 }
67
+ ```
68
+
69
+ ## Ignore exceptions
70
+
71
+ By using the ignored_exceptions parameter you can prevent RorVsWild from recording specific exceptions.
72
+
73
+ ```yaml
74
+ # config/rorvswild.yml
75
+ production:
76
+ api_key: API_KEY
77
+ ignored_exceptions:
78
+ - ActionController::RoutingError
79
+ - ZeroDivisionError
80
+ ```
81
+
82
+ ```ruby
83
+ # config/initializers/rorvswild.rb
84
+ RorVsWild::Client.new(
85
+ api_key: "API_KEY",
86
+ ignored_exceptions: ["ActionController::RoutingError", "ZeroDivisionError"])
87
+ ```
88
+
89
+ By default ActionController::RoutingError is ignored in order to not be flooded with 404.
90
+
22
91
  ## Contributing
23
92
 
24
93
  1. Fork it ( https://github.com/[my-github-username]/rorvswild/fork )
@@ -1,35 +1,33 @@
1
1
  require "rorvswild/version"
2
2
  require "rorvswild/location"
3
- require "rorvswild/plugin/redis"
4
- require "rorvswild/plugin/mongo"
5
- require "rorvswild/plugin/resque"
6
- require "rorvswild/plugin/sidekiq"
7
- require "rorvswild/plugin/net_http"
3
+ require "rorvswild/section"
8
4
  require "rorvswild/client"
5
+ require "rorvswild/plugins"
6
+ require "rorvswild/agent"
9
7
 
10
8
  module RorVsWild
11
- def self.measure_code(code)
12
- client ? client.measure_code(code) : eval(code)
9
+ def self.start(config)
10
+ @agent = Agent.new(config)
13
11
  end
14
12
 
15
- def self.measure_block(name, &block)
16
- client ? client.measure_block(name , &block) : block.call
13
+ def self.agent
14
+ @agent
17
15
  end
18
16
 
19
- def self.catch_error(extra_details = nil, &block)
20
- client ? client.catch_error(extra_details, &block) : block.call
17
+ def self.measure_code(code)
18
+ agent ? agent.measure_code(code) : eval(code)
21
19
  end
22
20
 
23
- def self.record_error(exception, extra_details = nil)
24
- client.record_error(exception, extra_details) if client
21
+ def self.measure_block(name, &block)
22
+ agent ? agent.measure_block(name , &block) : block.call
25
23
  end
26
24
 
27
- def self.register_client(client)
28
- @client = client
25
+ def self.catch_error(extra_details = nil, &block)
26
+ agent ? agent.catch_error(extra_details, &block) : block.call
29
27
  end
30
28
 
31
- def self.client
32
- @client
29
+ def self.record_error(exception, extra_details = nil)
30
+ agent.record_error(exception, extra_details) if agent
33
31
  end
34
32
  end
35
33
 
@@ -0,0 +1,180 @@
1
+ require "logger"
2
+
3
+ module RorVsWild
4
+ class Agent
5
+ include RorVsWild::Location
6
+
7
+ def self.default_config
8
+ {
9
+ api_url: "https://www.rorvswild.com/api",
10
+ ignored_exceptions: [],
11
+ }
12
+ end
13
+
14
+ attr_reader :api_url, :api_key, :app_id, :app_root, :ignored_exceptions
15
+
16
+ attr_reader :app_root_regex, :client
17
+
18
+ def initialize(config)
19
+ config = self.class.default_config.merge(config)
20
+ @ignored_exceptions = config[:ignored_exceptions]
21
+ @app_root = config[:app_root]
22
+ @logger = config[:logger]
23
+ @data = {}
24
+ @client = Client.new(config)
25
+
26
+ if defined?(Rails)
27
+ @logger ||= Rails.logger
28
+ @app_root ||= Rails.root.to_s
29
+ config = Rails.application.config
30
+ @ignored_exceptions ||= %w[ActionController::RoutingError] + config.action_dispatch.rescue_responses.map { |(key,value)| key }
31
+ end
32
+
33
+ @logger ||= Logger.new(STDERR)
34
+ @app_root_regex = app_root ? /\A#{app_root}/ : nil
35
+
36
+ setup_plugins
37
+ end
38
+
39
+ def setup_plugins
40
+ Plugin::NetHttp.setup
41
+
42
+ Plugin::Redis.setup
43
+ Plugin::Mongo.setup
44
+
45
+ Plugin::Resque.setup
46
+ Plugin::Sidekiq.setup
47
+ Plugin::ActiveJob.setup
48
+ Plugin::DelayedJob.setup
49
+
50
+ Plugin::ActionView.setup
51
+ Plugin::ActiveRecord.setup
52
+ Plugin::ActionMailer.setup
53
+ Plugin::ActionController.setup
54
+ end
55
+
56
+ def measure_code(code)
57
+ measure_block(code) { eval(code) }
58
+ end
59
+
60
+ def measure_block(name, kind = "code", &block)
61
+ data[:name] ? measure_section(name, kind, &block) : measure_job(name, &block)
62
+ end
63
+
64
+ def measure_section(name, kind = "code", &block)
65
+ return block.call unless data[:name]
66
+ begin
67
+ RorVsWild::Section.start do |section|
68
+ section.command = name
69
+ section.kind = kind
70
+ end
71
+ block.call
72
+ ensure
73
+ RorVsWild::Section.stop
74
+ end
75
+ end
76
+
77
+ def measure_job(name, &block)
78
+ return block.call if data[:name] # Prevent from recursive jobs
79
+ initialize_data(name)
80
+ begin
81
+ block.call
82
+ rescue Exception => ex
83
+ data[:error] = exception_to_hash(ex) if !ignored_exception?(ex)
84
+ raise
85
+ ensure
86
+ data[:runtime] = (Time.now - data[:started_at]) * 1000
87
+ post_job
88
+ end
89
+ end
90
+
91
+ def start_request(payload)
92
+ return if data[:name]
93
+ initialize_data(payload[:name])
94
+ data[:path] = payload[:path]
95
+ end
96
+
97
+ def stop_request
98
+ return unless data[:name]
99
+ data[:runtime] = (Time.now.utc - data[:started_at]) * 1000
100
+ post_request
101
+ end
102
+
103
+ def catch_error(extra_details = nil, &block)
104
+ begin
105
+ block.call
106
+ rescue Exception => ex
107
+ record_error(ex, extra_details) if !ignored_exception?(ex)
108
+ ex
109
+ end
110
+ end
111
+
112
+ def record_error(exception, extra_details = nil)
113
+ post_error(exception_to_hash(exception, extra_details))
114
+ end
115
+
116
+ def push_exception(exception)
117
+ return if ignored_exception?(exception)
118
+ data[:error] = exception_to_hash(exception)
119
+ end
120
+
121
+ def data
122
+ @data[Thread.current.object_id] ||= {}
123
+ end
124
+
125
+ def add_section(section)
126
+ return unless data[:sections]
127
+ if sibling = data[:sections].find { |s| s.sibling?(section) }
128
+ sibling.merge(section)
129
+ else
130
+ data[:sections] << section
131
+ end
132
+ end
133
+
134
+ #######################
135
+ ### Private methods ###
136
+ #######################
137
+
138
+ private
139
+
140
+ def initialize_data(name)
141
+ data[:name] = name
142
+ data[:sections] = []
143
+ data[:section_stack] = []
144
+ data[:started_at] = Time.now.utc
145
+ end
146
+
147
+ def cleanup_data
148
+ @data.delete(Thread.current.object_id)
149
+ end
150
+
151
+ def post_request
152
+ client.post_async("/requests".freeze, request: cleanup_data)
153
+ end
154
+
155
+ def post_job
156
+ client.post_async("/jobs".freeze, job: cleanup_data)
157
+ end
158
+
159
+ def post_error(hash)
160
+ post_async("/errors".freeze, error: hash)
161
+ end
162
+
163
+ def exception_to_hash(exception, extra_details = nil)
164
+ file, line, method = extract_most_relevant_file_and_line(exception.backtrace_locations)
165
+ {
166
+ method: method,
167
+ line: line.to_i,
168
+ file: relative_path(file),
169
+ message: exception.message,
170
+ backtrace: exception.backtrace,
171
+ exception: exception.class.to_s,
172
+ extra_details: extra_details,
173
+ }
174
+ end
175
+
176
+ def ignored_exception?(exception)
177
+ ignored_exceptions.include?(exception.class.to_s)
178
+ end
179
+ end
180
+ end
@@ -1,281 +1,22 @@
1
- require "json/ext"
2
- require "net/http"
3
- require "logger"
4
- require "uri"
5
1
  require "set"
2
+ require "uri"
3
+ require "net/http"
4
+ require "json/ext"
6
5
 
7
6
  module RorVsWild
8
7
  class Client
9
- include RorVsWild::Location
10
-
11
- def self.default_config
12
- {
13
- api_url: "https://www.rorvswild.com/api",
14
- explain_sql_threshold: 500,
15
- ignored_exceptions: [],
16
- }
17
- end
18
-
19
- attr_reader :api_url, :api_key, :app_id, :explain_sql_threshold, :app_root, :ignored_exceptions
8
+ HTTPS = "https".freeze
9
+ CERTIFICATE_AUTHORITIES_PATH = File.expand_path("../../../cacert.pem", __FILE__)
20
10
 
21
- attr_reader :threads, :app_root_regex
11
+ attr_reader :api_url, :api_key, :threads
22
12
 
23
13
  def initialize(config)
24
- config = self.class.default_config.merge(config)
25
- @explain_sql_threshold = config[:explain_sql_threshold]
26
- @ignored_exceptions = config[:ignored_exceptions]
27
- @app_root = config[:app_root]
14
+ Kernel.at_exit(&method(:at_exit))
28
15
  @api_url = config[:api_url]
29
16
  @api_key = config[:api_key]
30
- @app_id = config[:app_id]
31
- @logger = config[:logger]
32
17
  @threads = Set.new
33
- @data = {}
34
-
35
- if defined?(Rails)
36
- @logger ||= Rails.logger
37
- @app_root ||= Rails.root.to_s
38
- config = Rails.application.config
39
- @parameter_filter = ActionDispatch::Http::ParameterFilter.new(config.filter_parameters)
40
- @ignored_exceptions ||= %w[ActionController::RoutingError] + config.action_dispatch.rescue_responses.map { |(key,value)| key }
41
- end
42
-
43
- @logger ||= Logger.new(STDERR)
44
- @app_root_regex = app_root ? /\A#{app_root}/ : nil
45
-
46
- setup_callbacks
47
- RorVsWild.register_client(self)
48
- end
49
-
50
- def setup_callbacks
51
- client = self
52
- if defined?(ActiveSupport::Notifications)
53
- ActiveSupport::Notifications.subscribe("sql.active_record", &method(:after_sql_query))
54
- ActiveSupport::Notifications.subscribe("render_partial.action_view", &method(:after_view_rendering))
55
- ActiveSupport::Notifications.subscribe("render_template.action_view", &method(:after_view_rendering))
56
- ActiveSupport::Notifications.subscribe("process_action.action_controller", &method(:after_http_request))
57
- ActiveSupport::Notifications.subscribe("start_processing.action_controller", &method(:before_http_request))
58
- ActionController::Base.rescue_from(StandardError) { |exception| client.after_exception(exception, self) }
59
- end
60
-
61
- Plugin::Redis.setup
62
- Plugin::Mongo.setup
63
- Plugin::Resque.setup
64
- Plugin::Sidekiq.setup
65
- Plugin::NetHttp.setup
66
- Kernel.at_exit(&method(:at_exit))
67
- ActiveJob::Base.around_perform(&method(:around_active_job)) if defined?(ActiveJob::Base)
68
- Delayed::Worker.lifecycle.around(:invoke_job, &method(:around_delayed_job)) if defined?(Delayed::Worker)
69
- end
70
-
71
- def before_http_request(name, start, finish, id, payload)
72
- request.merge!(controller: payload[:controller], action: payload[:action], path: payload[:path], queries: [], views: {})
73
- end
74
-
75
- def after_http_request(name, start, finish, id, payload)
76
- request[:db_runtime] = (payload[:db_runtime] || 0).round
77
- request[:view_runtime] = (payload[:view_runtime] || 0).round
78
- request[:other_runtime] = compute_duration(start, finish) - request[:db_runtime] - request[:view_runtime]
79
- request[:error][:parameters] = filter_sensitive_data(payload[:params]) if request[:error]
80
- post_request
81
- rescue => exception
82
- log_error(exception)
83
- end
84
-
85
- IGNORED_QUERIES = %w[EXPLAIN SCHEMA].freeze
86
-
87
- def after_sql_query(name, start, finish, id, payload)
88
- return if !queries || IGNORED_QUERIES.include?(payload[:name])
89
- file, line, method = extract_most_relevant_location(caller)
90
- runtime, sql = compute_duration(start, finish), payload[:sql]
91
- plan = runtime >= explain_sql_threshold ? explain(payload[:sql], payload[:binds]) : nil
92
- push_query(kind: "sql", file: file, line: line, method: method, command: sql, plan: plan, runtime: runtime)
93
- rescue => exception
94
- log_error(exception)
95
- end
96
-
97
- def after_view_rendering(name, start, finish, id, payload)
98
- if views
99
- if view = views[file = relative_path(payload[:identifier])]
100
- view[:runtime] += compute_duration(start, finish)
101
- view[:times] += 1
102
- else
103
- views[file] = {file: file, runtime: compute_duration(start, finish), times: 1}
104
- end
105
- end
106
- end
107
-
108
- def after_exception(exception, controller)
109
- if !ignored_exception?(exception)
110
- file, line = exception.backtrace.first.split(":")
111
- request[:error] = exception_to_hash(exception).merge(
112
- session: controller.session.to_hash,
113
- environment_variables: filter_sensitive_data(filter_environment_variables(controller.request.env))
114
- )
115
- end
116
- raise exception
117
- end
118
-
119
- def around_active_job(job, block)
120
- measure_block(job.class.name, &block)
121
- end
122
-
123
- def around_delayed_job(job, &block)
124
- measure_block(job.name) { block.call(job) }
125
- end
126
-
127
- def measure_code(code)
128
- measure_block(code) { eval(code) }
129
- end
130
-
131
- def measure_block(name, &block)
132
- return block.call if job[:name] # Prevent from recursive jobs
133
- job[:name] = name
134
- job[:queries] = []
135
- started_at = Time.now
136
- cpu_time_offset = cpu_time
137
- begin
138
- block.call
139
- rescue Exception => ex
140
- job[:error] = exception_to_hash(ex) if !ignored_exception?(ex)
141
- raise
142
- ensure
143
- job[:runtime] = (Time.now - started_at) * 1000
144
- job[:cpu_runtime] = (cpu_time - cpu_time_offset) * 1000
145
- post_job
146
- end
147
- end
148
-
149
- def measure_query(kind, command, &block)
150
- return block.call if @query_started_at # Prevent from recursive queries
151
- @query_started_at = Time.now.utc
152
- begin
153
- result = block.call
154
- runtime = (Time.now.utc - @query_started_at) * 1000
155
- file, line, method = extract_most_relevant_location(caller)
156
- push_query(kind: kind, command: command, file: file, line: line, method: method, runtime: runtime)
157
- result
158
- ensure
159
- @query_started_at = nil
160
- end
161
- end
162
-
163
- def catch_error(extra_details = nil, &block)
164
- begin
165
- block.call
166
- rescue Exception => ex
167
- record_error(ex, extra_details) if !ignored_exception?(ex)
168
- ex
169
- end
170
18
  end
171
19
 
172
- def record_error(exception, extra_details = nil)
173
- post_error(exception_to_hash(exception, extra_details))
174
- end
175
-
176
- def cpu_time
177
- time = Process.times
178
- time.utime + time.stime + time.cutime + time.cstime
179
- end
180
-
181
- #######################
182
- ### Private methods ###
183
- #######################
184
-
185
- private
186
-
187
- def queries
188
- data[:queries]
189
- end
190
-
191
- def views
192
- data[:views]
193
- end
194
-
195
- def job
196
- data
197
- end
198
-
199
- def request
200
- data
201
- end
202
-
203
- def data
204
- @data[Thread.current.object_id] ||= {}
205
- end
206
-
207
- def cleanup_data
208
- @data.delete(Thread.current.object_id)
209
- end
210
-
211
- MEANINGLESS_QUERIES = %w[BEGIN COMMIT].freeze
212
-
213
- def push_query(query)
214
- return if !queries
215
- hash = queries.find { |hash| hash[:line] == query[:line] && hash[:file] == query[:file] && hash[:kind] == query[:kind] }
216
- queries << hash = {kind: query[:kind], file: query[:file], line: query[:line], runtime: 0, times: 0} if !hash
217
- hash[:runtime] += query[:runtime]
218
- if !MEANINGLESS_QUERIES.include?(query[:command])
219
- hash[:times] += 1
220
- hash[:command] ||= query[:command]
221
- hash[:plan] ||= query[:plan] if query[:plan]
222
- end
223
- end
224
-
225
- def slowest_views
226
- views.values.sort { |h1, h2| h2[:runtime] <=> h1[:runtime] }[0, 25]
227
- end
228
-
229
- def slowest_queries
230
- queries.sort { |h1, h2| h2[:runtime] <=> h1[:runtime] }[0, 25]
231
- end
232
-
233
- SELECT_REGEX = /\Aselect/i.freeze
234
-
235
- def explain(sql, binds)
236
- ActiveRecord::Base.connection.explain(sql, binds) if sql =~ SELECT_REGEX
237
- end
238
-
239
- def post_request
240
- attributes = request.merge(queries: slowest_queries, views: slowest_views)
241
- post_async("/requests".freeze, request: attributes)
242
- ensure
243
- cleanup_data
244
- end
245
-
246
- def post_job
247
- attributes = job.merge(queries: slowest_queries)
248
- post_async("/jobs".freeze, job: attributes)
249
- rescue => exception
250
- log_error(exception)
251
- ensure
252
- cleanup_data
253
- end
254
-
255
- def post_error(hash)
256
- post_async("/errors".freeze, error: hash)
257
- end
258
-
259
- def compute_duration(start, finish)
260
- ((finish - start) * 1000)
261
- end
262
-
263
- def exception_to_hash(exception, extra_details = nil)
264
- file, line, method = extract_most_relevant_location(exception.backtrace)
265
- {
266
- method: method,
267
- line: line.to_i,
268
- file: relative_path(file),
269
- message: exception.message,
270
- backtrace: exception.backtrace,
271
- exception: exception.class.to_s,
272
- extra_details: extra_details,
273
- }
274
- end
275
-
276
- HTTPS = "https".freeze
277
- CERTIFICATE_AUTHORITIES_PATH = File.expand_path("../../../cacert.pem", __FILE__)
278
-
279
20
  def post(path, data)
280
21
  uri = URI(api_url + path)
281
22
  http = Net::HTTP.new(uri.host, uri.port)
@@ -286,9 +27,9 @@ module RorVsWild
286
27
  http.use_ssl = true
287
28
  end
288
29
 
289
- post = Net::HTTP::Post.new(uri.path)
30
+ post = Net::HTTP::Post.new(uri.path, "X-Gem-Version".freeze => RorVsWild::VERSION)
290
31
  post.content_type = "application/json".freeze
291
- post.basic_auth(app_id, api_key)
32
+ post.basic_auth(nil, api_key)
292
33
  post.body = data.to_json
293
34
  http.request(post)
294
35
  end
@@ -307,22 +48,5 @@ module RorVsWild
307
48
  def at_exit
308
49
  threads.each(&:join)
309
50
  end
310
-
311
- def filter_sensitive_data(hash)
312
- @parameter_filter ? @parameter_filter.filter(hash) : hash
313
- end
314
-
315
- def filter_environment_variables(hash)
316
- hash.clone.keep_if { |key,value| key == key.upcase }
317
- end
318
-
319
- def ignored_exception?(exception)
320
- ignored_exceptions.include?(exception.class.to_s)
321
- end
322
-
323
- def log_error(exception)
324
- @logger.error("[RorVsWild] " + exception.inspect)
325
- @logger.error("[RorVsWild] " + exception.backtrace.join("\n[RorVsWild] "))
326
- end
327
51
  end
328
52
  end