rorvswild 0.4.1 → 0.5.0

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: 91ff73c055e4c1842915157c1efa958eb24cfad1
4
- data.tar.gz: 6e6d5b3070ba4156499b9710c94260a857df5ac0
3
+ metadata.gz: 4149e3a909cd273aa8aa37ad8e59b77755382ad1
4
+ data.tar.gz: a99acd4ecbae3fa5b6a2d800e54a612aee0e628d
5
5
  SHA512:
6
- metadata.gz: 7ea535f196c3f3964d9b8bd33654e2591637bdd2949ef599d6aed5359b3ade919254d1ff489c0f00108eb780b5b2fb8cd48ae84598fedfa0066de314ceb41acd
7
- data.tar.gz: cc15c40d7b585a8496e4ba2d12f9681f5a7951b61acee64fc180ffe254b4ec236cbf22335375a41a75c429ec6917d8fe99d95e1bc0310c184f16c40e6f4ee736
6
+ metadata.gz: 9fee4cf6ba25541d6f3cb2723bfa427005fdc953688ba6984d0af8910c4c3ff7cd0cac170d7455d7e9c4eecef4f5d16c10409b690a5dd999ca09212be3ba0ca9
7
+ data.tar.gz: 29cc6945e10e4600423d7e1162d85b0a67f28dc67bcd050562cbca06059c2a55a161d66d9e63aba2d80a32ecfff8464a17e7212d04769b690d0c2e1d3e59a703
data/README.md CHANGED
@@ -1,10 +1,23 @@
1
1
  # RorVsWild
2
2
 
3
- All-in-one monitoring for Ruby on Rails applications.
3
+ Ruby on Rails app monitoring: performances & quality insights for rails developers.
4
4
 
5
5
  ## Installation
6
6
 
7
- Signup to http://www.rorvswild.com.
7
+ First you need an API key. Signup here https://www.rorvswild.com to get one and a 14 day free trial.
8
+
9
+ 1. Add in your Gemfile `gem "rorvswild"`
10
+ 2. Run `bundle install`
11
+ 3. Run `rorvswild-setup API_KEY`
12
+ 4. Restart / deploy your app !
13
+
14
+ The `rorvswild-setup` create a `config/rorvswild.yml` file.
15
+ For those who prefer to manually use an initializer, they can do the following.
16
+
17
+ ```ruby
18
+ # config/initializers/rorvswild.rb
19
+ RorVsWild::Client.new(api_key: API_KEY)
20
+ ```
8
21
 
9
22
  ## Contributing
10
23
 
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path("#{File.dirname(__FILE__)}/../lib"))
4
+
5
+ require "rorvswild/installer"
6
+
7
+ if ARGV[0]
8
+ RorVsWild::Installer.create_rails_config(ARGV[0])
9
+ else
10
+ puts "You must provide an API key."
11
+ end
@@ -0,0 +1,310 @@
1
+ require "json/ext"
2
+ require "net/http"
3
+ require "logger"
4
+ require "uri"
5
+ require "set"
6
+
7
+ module RorVsWild
8
+ 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
20
+
21
+ attr_reader :threads, :app_root_regex
22
+
23
+ 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]
28
+ @api_url = config[:api_url]
29
+ @api_key = config[:api_key]
30
+ @app_id = config[:app_id]
31
+ @logger = config[:logger]
32
+ @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::Resque.setup
62
+ Plugin::Sidekiq.setup
63
+ Kernel.at_exit(&method(:at_exit))
64
+ ActiveJob::Base.around_perform(&method(:around_active_job)) if defined?(ActiveJob::Base)
65
+ Delayed::Worker.lifecycle.around(:invoke_job, &method(:around_delayed_job)) if defined?(Delayed::Worker)
66
+ end
67
+
68
+ def before_http_request(name, start, finish, id, payload)
69
+ request.merge!(controller: payload[:controller], action: payload[:action], path: payload[:path], queries: [], views: {})
70
+ end
71
+
72
+ def after_http_request(name, start, finish, id, payload)
73
+ request[:db_runtime] = (payload[:db_runtime] || 0).round
74
+ request[:view_runtime] = (payload[:view_runtime] || 0).round
75
+ request[:other_runtime] = compute_duration(start, finish) - request[:db_runtime] - request[:view_runtime]
76
+ request[:error][:parameters] = filter_sensitive_data(payload[:params]) if request[:error]
77
+ post_request
78
+ rescue => exception
79
+ log_error(exception)
80
+ end
81
+
82
+ IGNORED_QUERIES = %w[EXPLAIN SCHEMA].freeze
83
+
84
+ def after_sql_query(name, start, finish, id, payload)
85
+ return if !queries || IGNORED_QUERIES.include?(payload[:name])
86
+ file, line, method = extract_most_relevant_location(caller)
87
+ runtime, sql = compute_duration(start, finish), payload[:sql]
88
+ plan = runtime >= explain_sql_threshold ? explain(payload[:sql], payload[:binds]) : nil
89
+ push_query(file: file, line: line, method: method, sql: sql, plan: plan, runtime: runtime)
90
+ rescue => exception
91
+ log_error(exception)
92
+ end
93
+
94
+ def after_view_rendering(name, start, finish, id, payload)
95
+ if views
96
+ if view = views[file = relative_path(payload[:identifier])]
97
+ view[:runtime] += compute_duration(start, finish)
98
+ view[:times] += 1
99
+ else
100
+ views[file] = {file: file, runtime: compute_duration(start, finish), times: 1}
101
+ end
102
+ end
103
+ end
104
+
105
+ def after_exception(exception, controller)
106
+ if !ignored_exception?(exception)
107
+ file, line = exception.backtrace.first.split(":")
108
+ request[:error] = exception_to_hash(exception).merge(
109
+ session: controller.session.to_hash,
110
+ environment_variables: filter_sensitive_data(filter_environment_variables(controller.request.env))
111
+ )
112
+ end
113
+ raise exception
114
+ end
115
+
116
+ def around_active_job(job, block)
117
+ measure_block(job.class.name, &block)
118
+ end
119
+
120
+ def around_delayed_job(job, &block)
121
+ measure_block(job.name) { block.call(job) }
122
+ end
123
+
124
+ def measure_code(code)
125
+ measure_block(code) { eval(code) }
126
+ end
127
+
128
+ def measure_block(name, &block)
129
+ return block.call if job[:name] # Prevent from recursive jobs
130
+ job[:name] = name
131
+ job[:queries] = []
132
+ started_at = Time.now
133
+ cpu_time_offset = cpu_time
134
+ begin
135
+ block.call
136
+ rescue Exception => ex
137
+ job[:error] = exception_to_hash(ex) if !ignored_exception?(ex)
138
+ raise
139
+ ensure
140
+ job[:runtime] = (Time.now - started_at) * 1000
141
+ job[:cpu_runtime] = (cpu_time - cpu_time_offset) * 1000
142
+ post_job
143
+ end
144
+ end
145
+
146
+ def catch_error(extra_details = nil, &block)
147
+ begin
148
+ block.call
149
+ rescue Exception => ex
150
+ record_error(ex, extra_details) if !ignored_exception?(ex)
151
+ ex
152
+ end
153
+ end
154
+
155
+ def record_error(exception, extra_details = nil)
156
+ post_error(exception_to_hash(exception, extra_details))
157
+ end
158
+
159
+ def cpu_time
160
+ time = Process.times
161
+ time.utime + time.stime + time.cutime + time.cstime
162
+ end
163
+
164
+ #######################
165
+ ### Private methods ###
166
+ #######################
167
+
168
+ private
169
+
170
+ def queries
171
+ data[:queries]
172
+ end
173
+
174
+ def views
175
+ data[:views]
176
+ end
177
+
178
+ def job
179
+ data
180
+ end
181
+
182
+ def request
183
+ data
184
+ end
185
+
186
+ def data
187
+ @data[Thread.current.object_id] ||= {}
188
+ end
189
+
190
+ def cleanup_data
191
+ @data.delete(Thread.current.object_id)
192
+ end
193
+
194
+ MEANINGLESS_QUERIES = %w[BEGIN COMMIT].freeze
195
+
196
+ def push_query(query)
197
+ hash = queries.find { |hash| hash[:line] == query[:line] && hash[:file] == query[:file] }
198
+ queries << hash = {file: query[:file], line: query[:line], runtime: 0, times: 0} if !hash
199
+ hash[:runtime] += query[:runtime]
200
+ if !MEANINGLESS_QUERIES.include?(query[:sql])
201
+ hash[:times] += 1
202
+ hash[:sql] ||= query[:sql]
203
+ hash[:plan] ||= query[:plan] if query[:plan]
204
+ end
205
+ end
206
+
207
+ def slowest_views
208
+ views.values.sort { |h1, h2| h2[:runtime] <=> h1[:runtime] }[0, 25]
209
+ end
210
+
211
+ def slowest_queries
212
+ queries.sort { |h1, h2| h2[:runtime] <=> h1[:runtime] }[0, 25]
213
+ end
214
+
215
+ SELECT_REGEX = /\Aselect/i.freeze
216
+
217
+ def explain(sql, binds)
218
+ ActiveRecord::Base.connection.explain(sql, binds) if sql =~ SELECT_REGEX
219
+ end
220
+
221
+ def post_request
222
+ attributes = request.merge(queries: slowest_queries, views: slowest_views)
223
+ post_async("/requests".freeze, request: attributes)
224
+ ensure
225
+ cleanup_data
226
+ end
227
+
228
+ def post_job
229
+ attributes = job.merge(queries: slowest_queries)
230
+ post_async("/jobs".freeze, job: attributes)
231
+ rescue => exception
232
+ log_error(exception)
233
+ ensure
234
+ cleanup_data
235
+ end
236
+
237
+ def post_error(hash)
238
+ post_async("/errors".freeze, error: hash)
239
+ end
240
+
241
+ def compute_duration(start, finish)
242
+ ((finish - start) * 1000)
243
+ end
244
+
245
+ def exception_to_hash(exception, extra_details = nil)
246
+ file, line, method = extract_most_relevant_location(exception.backtrace)
247
+ {
248
+ method: method,
249
+ line: line.to_i,
250
+ file: relative_path(file),
251
+ message: exception.message,
252
+ backtrace: exception.backtrace,
253
+ exception: exception.class.to_s,
254
+ extra_details: extra_details,
255
+ }
256
+ end
257
+
258
+ HTTPS = "https".freeze
259
+ CERTIFICATE_AUTHORITIES_PATH = File.expand_path("../../../cacert.pem", __FILE__)
260
+
261
+ def post(path, data)
262
+ uri = URI(api_url + path)
263
+ http = Net::HTTP.new(uri.host, uri.port)
264
+
265
+ if uri.scheme == HTTPS
266
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
267
+ http.ca_file = CERTIFICATE_AUTHORITIES_PATH
268
+ http.use_ssl = true
269
+ end
270
+
271
+ post = Net::HTTP::Post.new(uri.path)
272
+ post.content_type = "application/json".freeze
273
+ post.basic_auth(app_id, api_key)
274
+ post.body = data.to_json
275
+ http.request(post)
276
+ end
277
+
278
+ def post_async(path, data)
279
+ Thread.new do
280
+ begin
281
+ threads.add(Thread.current)
282
+ post(path, data)
283
+ ensure
284
+ threads.delete(Thread.current)
285
+ end
286
+ end
287
+ end
288
+
289
+ def at_exit
290
+ threads.each(&:join)
291
+ end
292
+
293
+ def filter_sensitive_data(hash)
294
+ @parameter_filter ? @parameter_filter.filter(hash) : hash
295
+ end
296
+
297
+ def filter_environment_variables(hash)
298
+ hash.clone.keep_if { |key,value| key == key.upcase }
299
+ end
300
+
301
+ def ignored_exception?(exception)
302
+ ignored_exceptions.include?(exception.class.to_s)
303
+ end
304
+
305
+ def log_error(exception)
306
+ @logger.error("[RorVsWild] " + exception.inspect)
307
+ @logger.error("[RorVsWild] " + exception.backtrace.join("\n[RorVsWild] "))
308
+ end
309
+ end
310
+ end
@@ -0,0 +1,33 @@
1
+ module RorVsWild
2
+ class Installer
3
+ PATH = "config/rorvswild.yml"
4
+
5
+ def self.create_rails_config(api_key)
6
+ if File.directory?("config")
7
+ if !File.exists?(PATH)
8
+ File.write(PATH, template(api_key))
9
+ puts "File #{PATH} has been created. Restart / deploy your app to start collecting data."
10
+ else
11
+ puts "File #{PATH} already exists."
12
+ end
13
+ else
14
+ puts "There is no config directory to create #{PATH}."
15
+ end
16
+ end
17
+
18
+ def self.template(api_key)
19
+ <<YAML
20
+ # Keep the development block for testing on your local machine only.
21
+ development:
22
+ api_key: #{api_key}
23
+
24
+ production:
25
+ api_key: #{api_key}
26
+ # explain_sql_threshold: 500 # Execute EXPLAIN for queries above the specified time in ms.
27
+ # ignored_exceptions:
28
+ # - ActionController::RoutingError
29
+ # - UncommentToIgnoreAnyExceptionNameListedHere
30
+ YAML
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,42 @@
1
+ module RorVsWild
2
+ module Location
3
+ def self.cleanup_method_name(method)
4
+ method.sub!("block in ".freeze, "".freeze)
5
+ method.sub!("in `".freeze, "".freeze)
6
+ method.sub!("'".freeze, "".freeze)
7
+ method.index("_app_views_".freeze) == 0 ? nil : method
8
+ end
9
+
10
+ def self.split_file_location(location)
11
+ file, line, method = location.split(":")
12
+ method = cleanup_method_name(method) if method
13
+ [file, line, method]
14
+ end
15
+
16
+ def extract_most_relevant_location(stack)
17
+ location = stack.find { |str| str =~ app_root_regex && !(str =~ gem_home_regex) } if app_root_regex
18
+ location ||= stack.find { |str| !(str =~ gem_home_regex) } if gem_home_regex
19
+ RorVsWild::Location.split_file_location(relative_path(location || stack.first))
20
+ end
21
+
22
+ def app_root_regex
23
+ @app_root_regex ||= RorVsWild.default_client.app_root ? /\A#{RorVsWild.default_client.app_root}/ : nil
24
+ end
25
+
26
+ def gem_home_regex
27
+ @gem_home_regex ||= gem_home ? /\A#{gem_home}/.freeze : /\/gems\//.freeze
28
+ end
29
+
30
+ def gem_home
31
+ if ENV["GEM_HOME"] && !ENV["GEM_HOME"].empty?
32
+ ENV["GEM_HOME"]
33
+ elsif ENV["GEM_PATH"] && !(first_gem_path = ENV["GEM_PATH"].split(":").first)
34
+ first_gem_path if first_gem_path && !first_gem_path.empty?
35
+ end
36
+ end
37
+
38
+ def relative_path(path)
39
+ app_root_regex ? path.sub(app_root_regex, "".freeze) : path
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,13 @@
1
+ module RorVsWild
2
+ module Plugin
3
+ module Resque
4
+ def self.setup
5
+ ::Resque::Job.send(:extend, Resque) if defined?(::Resque::Job)
6
+ end
7
+
8
+ def around_perform_rorvswild(*args, &block)
9
+ RorVsWild.measure_block(to_s, &block)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ module RorVsWild
2
+ module Plugin
3
+ class Sidekiq
4
+ def self.setup
5
+ if defined?(::Sidekiq)
6
+ ::Sidekiq.configure_server do |config|
7
+ config.server_middleware { |chain| chain.add(SidekiqPlugin) }
8
+ end
9
+ end
10
+ end
11
+
12
+ def call(worker, item, queue, &block)
13
+ # Wrapped contains the real class name of the ActiveJob wrapper
14
+ RorVsWild.measure_block(item["wrapped".freeze] || item["class".freeze], &block)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ module RorVsWild
2
+ class RailsLoader
3
+ @started = false
4
+
5
+ def self.start_on_rails_initialization
6
+ return if !defined?(Rails)
7
+ Rails::Railtie.initializer "rorvswild.detect_config_file" do
8
+ RorVsWild::RailsLoader.start
9
+ end
10
+ end
11
+
12
+ def self.start
13
+ return if @started
14
+ if (path = Rails.root.join("config/rorvswild.yml")).exist?
15
+ if config = RorVsWild::RailsLoader.load_config_file(path)[Rails.env]
16
+ RorVsWild::Client.new(config.symbolize_keys)
17
+ @started = true
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.load_config_file(path)
23
+ YAML.load(ERB.new(path.read).result)
24
+ end
25
+ end
26
+ end
@@ -1,3 +1,3 @@
1
1
  module RorVsWild
2
- VERSION = "0.4.1".freeze
2
+ VERSION = "0.5.0".freeze
3
3
  end
data/lib/rorvswild.rb CHANGED
@@ -1,411 +1,36 @@
1
1
  require "rorvswild/version"
2
- require "json/ext"
3
- require "net/http"
4
- require "logger"
5
- require "uri"
6
- require "set"
2
+ require "rorvswild/location"
3
+ require "rorvswild/plugin/resque"
4
+ require "rorvswild/plugin/sidekiq"
5
+ require "rorvswild/client"
7
6
 
8
7
  module RorVsWild
9
- def self.new(*args)
10
- warn "WARNING: RorVsWild.new is deprecated. Use RorVsWild::Client.new instead."
11
- Client.new(*args) # Compatibility with 0.0.1
12
- end
13
-
14
- def self.detect_config_file
15
- return if !defined?(Rails)
16
- Rails::Railtie.initializer "rorvswild.detect_config_file" do
17
- if !RorVsWild.default_client && (path = Rails.root.join("config/rorvswild.yml")).exist?
18
- if config = RorVsWild.load_config_file(path)[Rails.env]
19
- RorVsWild::Client.new(config.symbolize_keys)
20
- end
21
- end
22
- end
23
- end
24
-
25
- def self.load_config_file(path)
26
- YAML.load(ERB.new(path.read).result)
27
- end
28
-
29
- def self.register_default_client(client)
30
- @default_client = client
31
- end
32
-
33
- def self.default_client
34
- @default_client
35
- end
36
-
37
- def self.measure_job(code)
38
- default_client ? default_client.measure_job(code) : eval(code)
39
- end
40
-
41
8
  def self.measure_code(code)
42
- default_client ? default_client.measure_code(code) : eval(code)
9
+ client ? client.measure_code(code) : eval(code)
43
10
  end
44
11
 
45
12
  def self.measure_block(name, &block)
46
- default_client ? default_client.measure_block(name , &block) : block.call
13
+ client ? client.measure_block(name , &block) : block.call
47
14
  end
48
15
 
49
16
  def self.catch_error(extra_details = nil, &block)
50
- default_client ? default_client.catch_error(extra_details, &block) : block.call
17
+ client ? client.catch_error(extra_details, &block) : block.call
51
18
  end
52
19
 
53
20
  def self.record_error(exception, extra_details = nil)
54
- default_client.record_error(exception, extra_details) if default_client
55
- end
56
-
57
- class Client
58
- def self.default_config
59
- {
60
- api_url: "https://www.rorvswild.com/api",
61
- explain_sql_threshold: 500,
62
- ignored_exceptions: [],
63
- }
64
- end
65
-
66
- attr_reader :api_url, :api_key, :app_id, :explain_sql_threshold, :app_root, :ignored_exceptions
67
-
68
- attr_reader :threads, :app_root_regex
69
-
70
- def initialize(config)
71
- config = self.class.default_config.merge(config)
72
- @explain_sql_threshold = config[:explain_sql_threshold]
73
- @ignored_exceptions = config[:ignored_exceptions]
74
- @app_root = config[:app_root]
75
- @api_url = config[:api_url]
76
- @api_key = config[:api_key]
77
- @app_id = config[:app_id]
78
- @logger = config[:logger]
79
- @threads = Set.new
80
- @data = {}
81
-
82
- if defined?(Rails)
83
- @logger ||= Rails.logger
84
- @app_root ||= Rails.root.to_s
85
- config = Rails.application.config
86
- @parameter_filter = ActionDispatch::Http::ParameterFilter.new(config.filter_parameters)
87
- @ignored_exceptions ||= %w[ActionController::RoutingError] + config.action_dispatch.rescue_responses.map { |(key,value)| key }
88
- end
89
-
90
- @logger ||= Logger.new(STDERR)
91
- @app_root_regex = app_root ? /\A#{app_root}/ : nil
92
-
93
- setup_callbacks
94
- RorVsWild.register_default_client(self)
95
- end
96
-
97
- def setup_callbacks
98
- client = self
99
- if defined?(ActiveSupport::Notifications)
100
- ActiveSupport::Notifications.subscribe("sql.active_record", &method(:after_sql_query))
101
- ActiveSupport::Notifications.subscribe("render_partial.action_view", &method(:after_view_rendering))
102
- ActiveSupport::Notifications.subscribe("render_template.action_view", &method(:after_view_rendering))
103
- ActiveSupport::Notifications.subscribe("process_action.action_controller", &method(:after_http_request))
104
- ActiveSupport::Notifications.subscribe("start_processing.action_controller", &method(:before_http_request))
105
- ActionController::Base.rescue_from(StandardError) { |exception| client.after_exception(exception, self) }
106
- end
107
-
108
- Kernel.at_exit(&method(:at_exit))
109
- Resque::Job.send(:extend, ResquePlugin) if defined?(Resque::Job)
110
- ActiveJob::Base.around_perform(&method(:around_active_job)) if defined?(ActiveJob::Base)
111
- Delayed::Worker.lifecycle.around(:invoke_job, &method(:around_delayed_job)) if defined?(Delayed::Worker)
112
- Sidekiq.configure_server { |config| config.server_middleware { |chain| chain.add(SidekiqPlugin) } } if defined?(Sidekiq)
113
- end
114
-
115
- def before_http_request(name, start, finish, id, payload)
116
- request.merge!(controller: payload[:controller], action: payload[:action], path: payload[:path], queries: [], views: {})
117
- end
118
-
119
- def after_http_request(name, start, finish, id, payload)
120
- request[:db_runtime] = (payload[:db_runtime] || 0).round
121
- request[:view_runtime] = (payload[:view_runtime] || 0).round
122
- request[:other_runtime] = compute_duration(start, finish) - request[:db_runtime] - request[:view_runtime]
123
- request[:error][:parameters] = filter_sensitive_data(payload[:params]) if request[:error]
124
- post_request
125
- rescue => exception
126
- log_error(exception)
127
- end
128
-
129
- IGNORED_QUERIES = %w[EXPLAIN SCHEMA].freeze
130
-
131
- def after_sql_query(name, start, finish, id, payload)
132
- return if !queries || IGNORED_QUERIES.include?(payload[:name])
133
- file, line, method = extract_most_relevant_location(caller)
134
- runtime, sql = compute_duration(start, finish), payload[:sql]
135
- plan = runtime >= explain_sql_threshold ? explain(payload[:sql], payload[:binds]) : nil
136
- push_query(file: file, line: line, method: method, sql: sql, plan: plan, runtime: runtime)
137
- rescue => exception
138
- log_error(exception)
139
- end
140
-
141
- def after_view_rendering(name, start, finish, id, payload)
142
- if views
143
- if view = views[file = relative_path(payload[:identifier])]
144
- view[:runtime] += compute_duration(start, finish)
145
- view[:times] += 1
146
- else
147
- views[file] = {file: file, runtime: compute_duration(start, finish), times: 1}
148
- end
149
- end
150
- end
151
-
152
- def after_exception(exception, controller)
153
- if !ignored_exception?(exception)
154
- file, line = exception.backtrace.first.split(":")
155
- request[:error] = exception_to_hash(exception).merge(
156
- session: controller.session.to_hash,
157
- environment_variables: filter_sensitive_data(filter_environment_variables(controller.request.env))
158
- )
159
- end
160
- raise exception
161
- end
162
-
163
- def around_active_job(job, block)
164
- measure_block(job.class.name, &block)
165
- end
166
-
167
- def around_delayed_job(job, &block)
168
- measure_block(job.name) { block.call(job) }
169
- end
170
-
171
- def measure_job(code)
172
- warn "WARNING: RorVsWild.measure_job is deprecated. Use RorVsWild.measure_code instead."
173
- measure_block(code) { eval(code) }
174
- end
175
-
176
- def measure_code(code)
177
- measure_block(code) { eval(code) }
178
- end
179
-
180
- def measure_block(name, &block)
181
- return block.call if job[:name] # Prevent from recursive jobs
182
- job[:name] = name
183
- job[:queries] = []
184
- started_at = Time.now
185
- cpu_time_offset = cpu_time
186
- begin
187
- block.call
188
- rescue Exception => ex
189
- job[:error] = exception_to_hash(ex) if !ignored_exception?(ex)
190
- raise
191
- ensure
192
- job[:runtime] = (Time.now - started_at) * 1000
193
- job[:cpu_runtime] = (cpu_time - cpu_time_offset) * 1000
194
- post_job
195
- end
196
- end
197
-
198
- def catch_error(extra_details = nil, &block)
199
- begin
200
- block.call
201
- rescue Exception => ex
202
- record_error(ex, extra_details) if !ignored_exception?(ex)
203
- ex
204
- end
205
- end
206
-
207
- def record_error(exception, extra_details = nil)
208
- post_error(exception_to_hash(exception, extra_details))
209
- end
210
-
211
- def cpu_time
212
- time = Process.times
213
- time.utime + time.stime + time.cutime + time.cstime
214
- end
215
-
216
- #######################
217
- ### Private methods ###
218
- #######################
219
-
220
- private
221
-
222
- def queries
223
- data[:queries]
224
- end
225
-
226
- def views
227
- data[:views]
228
- end
229
-
230
- def job
231
- data
232
- end
233
-
234
- def request
235
- data
236
- end
237
-
238
- def data
239
- @data[Thread.current.object_id] ||= {}
240
- end
241
-
242
- def cleanup_data
243
- @data.delete(Thread.current.object_id)
244
- end
245
-
246
- MEANINGLESS_QUERIES = %w[BEGIN COMMIT].freeze
247
-
248
- def push_query(query)
249
- hash = queries.find { |hash| hash[:line] == query[:line] && hash[:file] == query[:file] }
250
- queries << hash = {file: query[:file], line: query[:line], runtime: 0, times: 0} if !hash
251
- hash[:runtime] += query[:runtime]
252
- if !MEANINGLESS_QUERIES.include?(query[:sql])
253
- hash[:times] += 1
254
- hash[:sql] ||= query[:sql]
255
- hash[:plan] ||= query[:plan] if query[:plan]
256
- end
257
- end
258
-
259
- def slowest_views
260
- views.values.sort { |h1, h2| h2[:runtime] <=> h1[:runtime] }[0, 25]
261
- end
262
-
263
- def slowest_queries
264
- queries.sort { |h1, h2| h2[:runtime] <=> h1[:runtime] }[0, 25]
265
- end
266
-
267
- SELECT_REGEX = /\Aselect/i.freeze
268
-
269
- def explain(sql, binds)
270
- ActiveRecord::Base.connection.explain(sql, binds) if sql =~ SELECT_REGEX
271
- end
272
-
273
- def post_request
274
- attributes = request.merge(queries: slowest_queries, views: slowest_views)
275
- post_async("/requests".freeze, request: attributes)
276
- ensure
277
- cleanup_data
278
- end
279
-
280
- def post_job
281
- attributes = job.merge(queries: slowest_queries)
282
- post_async("/jobs".freeze, job: attributes)
283
- rescue => exception
284
- log_error(exception)
285
- ensure
286
- cleanup_data
287
- end
288
-
289
- def post_error(hash)
290
- post_async("/errors".freeze, error: hash)
291
- end
292
-
293
- def gem_home
294
- if ENV["GEM_HOME"] && !ENV["GEM_HOME"].empty?
295
- ENV["GEM_HOME"]
296
- elsif ENV["GEM_PATH"] && !(first_gem_path = ENV["GEM_PATH"].split(":").first)
297
- first_gem_path if first_gem_path && !first_gem_path.empty?
298
- end
299
- end
300
-
301
- def gem_home_regex
302
- @gem_home_regex ||= gem_home ? /\A#{gem_home}/.freeze : /\/gems\//.freeze
303
- end
304
-
305
- def extract_most_relevant_location(stack)
306
- location = stack.find { |str| str =~ app_root_regex && !(str =~ gem_home_regex) } if app_root_regex
307
- location ||= stack.find { |str| !(str =~ gem_home_regex) } if gem_home_regex
308
- split_file_location(relative_path(location || stack.first))
309
- end
310
-
311
- def split_file_location(location)
312
- file, line, method = location.split(":")
313
- method = cleanup_method_name(method) if method
314
- [file, line, method]
315
- end
316
-
317
- def cleanup_method_name(method)
318
- method.sub!("block in ".freeze, "".freeze)
319
- method.sub!("in `".freeze, "".freeze)
320
- method.sub!("'".freeze, "".freeze)
321
- method.index("_app_views_".freeze) == 0 ? nil : method
322
- end
323
-
324
- def compute_duration(start, finish)
325
- ((finish - start) * 1000)
326
- end
327
-
328
- def relative_path(path)
329
- app_root_regex ? path.sub(app_root_regex, "".freeze) : path
330
- end
331
-
332
- def exception_to_hash(exception, extra_details = nil)
333
- file, line, method = extract_most_relevant_location(exception.backtrace)
334
- {
335
- method: method,
336
- line: line.to_i,
337
- file: relative_path(file),
338
- message: exception.message,
339
- backtrace: exception.backtrace,
340
- exception: exception.class.to_s,
341
- extra_details: extra_details,
342
- }
343
- end
344
-
345
- HTTPS = "https".freeze
346
- CERTIFICATE_AUTHORITIES_PATH = File.expand_path("../../cacert.pem", __FILE__)
347
-
348
- def post(path, data)
349
- uri = URI(api_url + path)
350
- http = Net::HTTP.new(uri.host, uri.port)
351
-
352
- if uri.scheme == HTTPS
353
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
354
- http.ca_file = CERTIFICATE_AUTHORITIES_PATH
355
- http.use_ssl = true
356
- end
357
-
358
- post = Net::HTTP::Post.new(uri.path)
359
- post.content_type = "application/json".freeze
360
- post.basic_auth(app_id, api_key)
361
- post.body = data.to_json
362
- http.request(post)
363
- end
364
-
365
- def post_async(path, data)
366
- Thread.new do
367
- begin
368
- threads.add(Thread.current)
369
- post(path, data)
370
- ensure
371
- threads.delete(Thread.current)
372
- end
373
- end
374
- end
375
-
376
- def at_exit
377
- threads.each(&:join)
378
- end
379
-
380
- def filter_sensitive_data(hash)
381
- @parameter_filter ? @parameter_filter.filter(hash) : hash
382
- end
383
-
384
- def filter_environment_variables(hash)
385
- hash.clone.keep_if { |key,value| key == key.upcase }
386
- end
387
-
388
- def ignored_exception?(exception)
389
- ignored_exceptions.include?(exception.class.to_s)
390
- end
391
-
392
- def log_error(exception)
393
- @logger.error("[RorVsWild] " + exception.inspect)
394
- @logger.error("[RorVsWild] " + exception.backtrace.join("\n[RorVsWild] "))
395
- end
21
+ client.record_error(exception, extra_details) if client
396
22
  end
397
23
 
398
- module ResquePlugin
399
- def around_perform_rorvswild(*args, &block)
400
- RorVsWild.measure_block(to_s, &block)
401
- end
24
+ def self.register_client(client)
25
+ @client = client
402
26
  end
403
27
 
404
- class SidekiqPlugin
405
- def call(worker, item, queue, &block)
406
- RorVsWild.measure_block(item["wrapped".freeze] || item["class".freeze], &block)
407
- end
28
+ def self.client
29
+ @client
408
30
  end
409
31
  end
410
32
 
411
- RorVsWild.detect_config_file
33
+ if defined?(Rails)
34
+ require "rorvswild/rails_loader"
35
+ RorVsWild::RailsLoader.start_on_rails_initialization
36
+ end
@@ -35,13 +35,13 @@ class RorVsWildTest < Minitest::Test
35
35
  end
36
36
 
37
37
  def test_measure_code_when_no_client
38
- RorVsWild.register_default_client(nil)
38
+ RorVsWild.register_client(nil)
39
39
  RorVsWild::Client.any_instance.expects(:post_job).never
40
40
  assert_equal(2, RorVsWild.measure_code("1+1"))
41
41
  end
42
42
 
43
43
  def test_measure_block_when_no_client
44
- RorVsWild.register_default_client(nil)
44
+ RorVsWild.register_client(nil)
45
45
  RorVsWild::Client.any_instance.expects(:post_job).never
46
46
  assert_equal(2, RorVsWild.measure_block("1+1") { 1+1 })
47
47
  end
@@ -73,25 +73,25 @@ class RorVsWildTest < Minitest::Test
73
73
 
74
74
  def test_extract_most_relevant_location
75
75
  callstack = ["#{ENV["GEM_HOME"]}/lib/sql.rb:1:in `method1'", "/usr/lib/ruby/net/http.rb:2:in `method2'", "/rails/root/app/models/user.rb:3:in `method3'"]
76
- assert_equal(%w[/app/models/user.rb 3 method3], client.send(:extract_most_relevant_location, callstack))
76
+ assert_equal(%w[/app/models/user.rb 3 method3], client.extract_most_relevant_location(callstack))
77
77
 
78
- assert_equal(["#{ENV["GEM_HOME"]}/lib/sql.rb", "1", "method1"], client.send(:extract_most_relevant_location, ["#{ENV["GEM_HOME"]}/lib/sql.rb:1:in `method1'"]))
78
+ assert_equal(["#{ENV["GEM_HOME"]}/lib/sql.rb", "1", "method1"], client.extract_most_relevant_location(["#{ENV["GEM_HOME"]}/lib/sql.rb:1:in `method1'"]))
79
79
  end
80
80
 
81
81
  def test_extract_most_relevant_location_when_there_is_not_app_root
82
82
  client = initialize_client
83
83
  callstack = ["#{ENV["GEM_HOME"]}/lib/sql.rb:1:in `method1'", "/usr/lib/ruby/net/http.rb:2:in `method2'", "/rails/root/app/models/user.rb:3:in `method3'"]
84
- assert_equal(%w[/usr/lib/ruby/net/http.rb 2 method2], client.send(:extract_most_relevant_location, callstack))
84
+ assert_equal(%w[/usr/lib/ruby/net/http.rb 2 method2], client.extract_most_relevant_location(callstack))
85
85
  end
86
86
 
87
87
  def test_extract_most_relevant_location_when_there_is_no_method_name
88
- assert_equal(["/foo/bar.rb", "123", nil], client.send(:extract_most_relevant_location, ["/foo/bar.rb:123"]))
88
+ assert_equal(["/foo/bar.rb", "123", nil], client.extract_most_relevant_location(["/foo/bar.rb:123"]))
89
89
  end
90
90
 
91
91
  def test_extract_most_relevant_location_when_gem_home_is_in_heroku_app_root
92
92
  client = initialize_client(app_root: app_root = File.dirname(gem_home = ENV["GEM_HOME"]))
93
93
  callstack = ["#{gem_home}/lib/sql.rb:1:in `method1'", "/usr/lib/ruby/net/http.rb:2:in `method2'", "#{app_root}/app/models/user.rb:3:in `method3'"]
94
- assert_equal(["/app/models/user.rb", "3", "method3"], client.send(:extract_most_relevant_location, callstack))
94
+ assert_equal(["/app/models/user.rb", "3", "method3"], client.extract_most_relevant_location(callstack))
95
95
  end
96
96
 
97
97
  def test_extract_most_relevant_location_when_gem_path_is_set_instead_of_gem_home
@@ -99,7 +99,7 @@ class RorVsWildTest < Minitest::Test
99
99
  ENV["GEM_HOME"], ENV["GEM_PATH"] = "", "/gem/path"
100
100
 
101
101
  callstack = ["/gem/path/lib/sql.rb:1:in `method1'", "/usr/lib/ruby/net/http.rb:2:in `method2'", "/rails/root/app/models/user.rb:3:in `method3'"]
102
- assert_equal(%w[/app/models/user.rb 3 method3], client.send(:extract_most_relevant_location, callstack))
102
+ assert_equal(%w[/app/models/user.rb 3 method3], client.extract_most_relevant_location(callstack))
103
103
  ensure
104
104
  ENV["GEM_HOME"], ENV["GEM_PATH"] = original_gem_home, original_gem_path
105
105
  end
@@ -109,7 +109,7 @@ class RorVsWildTest < Minitest::Test
109
109
  ENV["GEM_HOME"], ENV["GEM_PATH"] = "", ""
110
110
 
111
111
  callstack = ["/gem/path/lib/sql.rb:1:in `method1'", "/usr/lib/ruby/net/http.rb:2:in `method2'", "/rails/root/app/models/user.rb:3:in `method3'"]
112
- assert_equal(%w[/app/models/user.rb 3 method3], client.send(:extract_most_relevant_location, callstack))
112
+ assert_equal(%w[/app/models/user.rb 3 method3], client.extract_most_relevant_location(callstack))
113
113
  ensure
114
114
  ENV["GEM_HOME"], ENV["GEM_PATH"] = original_gem_home, original_gem_path
115
115
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rorvswild
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexis Bernard
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-02-05 00:00:00.000000000 Z
11
+ date: 2017-02-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -27,7 +27,8 @@ dependencies:
27
27
  description: Performances & quality insights for rails developers.
28
28
  email:
29
29
  - alexis@bernard.io
30
- executables: []
30
+ executables:
31
+ - rorvswild-install
31
32
  extensions: []
32
33
  extra_rdoc_files: []
33
34
  files:
@@ -36,8 +37,15 @@ files:
36
37
  - LICENSE.txt
37
38
  - README.md
38
39
  - Rakefile
40
+ - bin/rorvswild-install
39
41
  - cacert.pem
40
42
  - lib/rorvswild.rb
43
+ - lib/rorvswild/client.rb
44
+ - lib/rorvswild/installer.rb
45
+ - lib/rorvswild/location.rb
46
+ - lib/rorvswild/plugin/resque.rb
47
+ - lib/rorvswild/plugin/sidekiq.rb
48
+ - lib/rorvswild/rails_loader.rb
41
49
  - lib/rorvswild/version.rb
42
50
  - rorvswild.gemspec
43
51
  - test/ror_vs_wild_test.rb