rorvswild 0.6.1 → 1.0.0.pre.alpha

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: 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