rorvswild 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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