rorvswild 1.9.2 → 1.10.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
  SHA256:
3
- metadata.gz: 97a87e145dfb9f5da27bda89cc828855af232f03a268d7caa7ca89be40c09bd0
4
- data.tar.gz: f883c96fdf00e036ca750984ef64ce87318ba8d1ea353ea0b39cc1bd5c3dc71e
3
+ metadata.gz: f56b7bc131f07c6010a47ec4e9bf635c21d0b560b472ade68740cca05b4550fd
4
+ data.tar.gz: 5d72ca9672beb7cfe60cd81febb84aa9c5556259de713443ec7eac46d376f86a
5
5
  SHA512:
6
- metadata.gz: 6728519bcc2ecb291180d6aa1b57947c1b81b845c35ceea401ffe769af3aad83899b17bdd43da5da0f393d42f0f82340fc6376d0fb8aac02c43f9f14c74e0b1d
7
- data.tar.gz: 612cd0e042075ebc1249ac05932c9657590d71a783a6e9532ec2d0d85337ed3aeb7869c01f7f1ae82532a75bd29c1a5c1c9d9ddba21b8b72af5dc1b59604076d
6
+ metadata.gz: f5a6174758d7ef8617cf0e210fe4ad235e22153314add1dbc57fe62ece456bf4111a7586f8c104d7742491fd29ae24d04710121931787e8393e372eeb4ddad64
7
+ data.tar.gz: 94e9525e22117bc39cbae29459e6b1ec75bb12e54d21d605098d14e4fe8b0bcf3deeaab377aa6124c412bf0742912917e94a2522fea96091cdda5c18f7db22ae
@@ -15,7 +15,7 @@ module RorVsWild
15
15
  end
16
16
 
17
17
  def self.default_ignored_exceptions
18
- if defined?(Rails)
18
+ if defined?(ActionDispatch::ExceptionWrapper)
19
19
  ActionDispatch::ExceptionWrapper.rescue_responses.keys
20
20
  else
21
21
  []
@@ -48,7 +48,7 @@ module RorVsWild
48
48
  next if config[:ignore_plugins] && config[:ignore_plugins].include?(name.to_s)
49
49
  if (plugin = RorVsWild::Plugin.const_get(name)).respond_to?(:setup)
50
50
  RorVsWild.logger.debug("Setup RorVsWild::Plugin::#{name}")
51
- plugin.setup
51
+ plugin.setup(self)
52
52
  end
53
53
  end
54
54
  end
@@ -58,7 +58,7 @@ module RorVsWild
58
58
  end
59
59
 
60
60
  def measure_block(name = nil, kind = "code".freeze, &block)
61
- current_data ? measure_section(name, kind: kind, &block) : measure_job(name, &block)
61
+ current_execution ? measure_section(name, kind: kind, &block) : measure_job(name, &block)
62
62
  end
63
63
 
64
64
  def measure_method(method)
@@ -85,7 +85,7 @@ module RorVsWild
85
85
  end
86
86
 
87
87
  def measure_section(name, kind: "code", &block)
88
- return block.call unless current_data
88
+ return block.call unless current_execution
89
89
  begin
90
90
  RorVsWild::Section.start do |section|
91
91
  section.commands << name
@@ -98,32 +98,30 @@ module RorVsWild
98
98
  end
99
99
 
100
100
  def measure_job(name, parameters: nil, &block)
101
- return measure_section(name, &block) if current_data # For recursive jobs
101
+ return measure_section(name, &block) if current_execution # For recursive jobs
102
102
  return block.call if ignored_job?(name)
103
- initialize_data[:name] = name
103
+ start_execution(Execution::Job.new(name, parameters))
104
104
  begin
105
105
  block.call
106
106
  rescue Exception => ex
107
- push_exception(ex, parameters: parameters, job: {name: name})
107
+ current_execution.add_exception(ex)
108
108
  raise
109
109
  ensure
110
- gc = Section.stop_gc_timing(current_data[:gc_section])
111
- current_data[:sections] << gc if gc.calls > 0
112
- current_data[:runtime] = RorVsWild.clock_milliseconds - current_data[:started_at]
113
- queue_job
110
+ stop_execution
114
111
  end
115
112
  end
116
113
 
117
- def start_request(queue_time_ms = 0)
118
- current_data || initialize_data(queue_time_ms)
114
+ def start_execution(execution)
115
+ Thread.current[:rorvswild_execution] ||= execution
119
116
  end
120
117
 
121
- def stop_request
122
- return unless data = current_data
123
- gc = Section.stop_gc_timing(data[:gc_section])
124
- data[:sections] << gc if gc.calls > 0 && gc.total_ms > 0
125
- data[:runtime] = RorVsWild.clock_milliseconds - current_data[:started_at]
126
- queue_request
118
+ def stop_execution
119
+ return unless execution = current_execution
120
+ execution.stop
121
+ case execution
122
+ when Execution::Job then queue_job
123
+ when Execution::Request then queue_request
124
+ end
127
125
  end
128
126
 
129
127
  def catch_error(context = nil, &block)
@@ -136,44 +134,21 @@ module RorVsWild
136
134
  end
137
135
 
138
136
  def record_error(exception, context = nil)
139
- queue_error(exception_to_hash(exception, context)) if !ignored_exception?(exception)
140
- end
141
-
142
- def push_exception(exception, options = nil)
143
- return if ignored_exception?(exception)
144
- return unless current_data
145
- current_data[:error] = exception_to_hash(exception)
146
- current_data[:error].merge!(options) if options
147
- current_data[:error]
137
+ if !ignored_exception?(exception) && current_execution&.error&.exception != exception
138
+ queue_error(Error.new(exception, context).as_json)
139
+ end
148
140
  end
149
141
 
150
142
  def merge_error_context(hash)
151
- self.error_context = error_context ? error_context.merge(hash) : hash
152
- end
153
-
154
- def error_context
155
- current_data[:error_context] if current_data
156
- end
157
-
158
- def error_context=(hash)
159
- current_data[:error_context] = hash if current_data
160
- end
161
-
162
- def send_server_timing=(boolean)
163
- current_data[:send_server_timing] = boolean if current_data
143
+ current_execution && current_execution.merge_error_context(hash)
164
144
  end
165
145
 
166
146
  def current_data
167
147
  Thread.current[:rorvswild_data]
168
148
  end
169
149
 
170
- def add_section(section)
171
- return unless current_data[:sections]
172
- if sibling = current_data[:sections].find { |s| s.sibling?(section) }
173
- sibling.merge(section)
174
- else
175
- current_data[:sections] << section
176
- end
150
+ def current_execution
151
+ Thread.current[:rorvswild_execution]
177
152
  end
178
153
 
179
154
  def ignored_request?(name)
@@ -186,7 +161,8 @@ module RorVsWild
186
161
 
187
162
  def ignored_exception?(exception)
188
163
  return false unless config[:ignore_exceptions]
189
- config[:ignore_exceptions].any? { |str_or_regex| str_or_regex === exception.class.to_s }
164
+ class_name = exception.class.to_s
165
+ config[:ignore_exceptions].any? { |str_or_regex| str_or_regex === class_name }
190
166
  end
191
167
 
192
168
  #######################
@@ -195,47 +171,24 @@ module RorVsWild
195
171
 
196
172
  private
197
173
 
198
- def initialize_data(queue_time_ms = 0)
199
- Thread.current[:rorvswild_data] = {
200
- started_at: RorVsWild.clock_milliseconds - queue_time_ms,
201
- gc_section: Section.start_gc_timing,
202
- environment: Host.to_h,
203
- section_stack: [],
204
- sections: [],
205
- }
206
- end
207
-
208
174
  def cleanup_data
209
- result = Thread.current[:rorvswild_data]
210
- Thread.current[:rorvswild_data] = nil
175
+ result = Thread.current[:rorvswild_execution]
176
+ Thread.current[:rorvswild_execution] = nil
211
177
  result
212
178
  end
213
179
 
214
180
  def queue_request
215
- (data = cleanup_data) && data[:name] && queue.push_request(data)
216
- data
181
+ if (execution = cleanup_data) && execution.name
182
+ queue.push_request(execution.as_json)
183
+ end
217
184
  end
218
185
 
219
186
  def queue_job
220
- queue.push_job(cleanup_data)
187
+ queue.push_job(cleanup_data.as_json)
221
188
  end
222
189
 
223
190
  def queue_error(hash)
224
191
  queue.push_error(hash)
225
192
  end
226
-
227
- def exception_to_hash(exception, context = nil)
228
- file, line = locator.find_most_relevant_file_and_line_from_exception(exception)
229
- context = context ? error_context.merge(context) : error_context if error_context
230
- {
231
- line: line.to_i,
232
- file: locator.relative_path(file),
233
- message: exception.message[0,1_000_000],
234
- backtrace: exception.backtrace || ["No backtrace"],
235
- exception: exception.class.to_s,
236
- context: context,
237
- environment: Host.to_h,
238
- }
239
- end
240
193
  end
241
194
  end
@@ -36,7 +36,7 @@ module RorVsWild
36
36
  uri = URI(api_url + path)
37
37
  post = Net::HTTP::Post.new(uri.path, @headers)
38
38
  post.basic_auth(nil, api_key)
39
- post.body = data.to_json
39
+ post.body = JSON.generate(data)
40
40
  transmit(post)
41
41
  end
42
42
 
@@ -4,6 +4,8 @@ require "open3"
4
4
 
5
5
  module RorVsWild
6
6
  module Deployment
7
+ @revision = @description = @author = @email = nil
8
+
7
9
  def self.load_config(config)
8
10
  read
9
11
  if hash = config[:deployment]
@@ -35,7 +37,7 @@ module RorVsWild
35
37
  end
36
38
 
37
39
  def self.rails
38
- Rails.version if defined?(Rails)
40
+ Rails.version if defined?(Rails) && Rails.respond_to?(:version)
39
41
  end
40
42
 
41
43
  def self.rorvswild
@@ -47,7 +49,7 @@ module RorVsWild
47
49
  end
48
50
 
49
51
  def self.read
50
- read_from_heroku || read_from_scalingo || read_from_git || read_from_capistrano
52
+ read_from_heroku || read_from_scalingo || read_from_kamal || read_from_git || read_from_capistrano
51
53
  end
52
54
 
53
55
  private
@@ -65,10 +67,7 @@ module RorVsWild
65
67
  def self.read_from_git
66
68
  return unless @revision = normalize_string(shell("git rev-parse HEAD"))
67
69
  return @revision unless log_stdout = shell("git log -1 --pretty=%an%n%ae%n%B")
68
- lines = log_stdout.lines
69
- @author = normalize_string(lines[0])
70
- @email = normalize_string(lines[1])
71
- @description = lines[2..-1] && normalize_string(lines[2..-1].join)
70
+ parse_git_log(log_stdout.lines)
72
71
  @revision
73
72
  end
74
73
 
@@ -76,13 +75,15 @@ module RorVsWild
76
75
  return unless File.readable?("REVISION")
77
76
  return unless @revision = File.read("REVISION")
78
77
  return unless stdout = shell("git --git-dir ../../repo log --format=%an%n%ae%n%B -n 1 #{@revision}")
79
- lines = stdout.lines
80
- @author = normalize_string(lines[0])
81
- @email = normalize_string(lines[1])
82
- @description = lines[2..-1] && normalize_string(lines[2..-1].join)
78
+ parse_git_log(stdout.lines)
83
79
  @revision
84
80
  end
85
81
 
82
+ def self.read_from_kamal
83
+ return unless ENV["KAMAL_VERSION"]
84
+ @revision = ENV["KAMAL_VERSION"]
85
+ end
86
+
86
87
  def self.normalize_string(string)
87
88
  if string
88
89
  string = string.strip
@@ -94,5 +95,11 @@ module RorVsWild
94
95
  stdout, _, process = Open3.capture3(command) rescue nil
95
96
  stdout if process && process.success?
96
97
  end
98
+
99
+ def self.parse_git_log(lines)
100
+ @author = normalize_string(lines[0])
101
+ @email = normalize_string(lines[1])
102
+ @description = lines[2..-1] && normalize_string(lines[2..-1].join)
103
+ end
97
104
  end
98
105
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RorVsWild
4
+ class Error
5
+ attr_reader :exception
6
+
7
+ attr_accessor :details
8
+
9
+ def initialize(exception, context = nil)
10
+ @exception = exception
11
+ @file, @line = locator.find_most_relevant_file_and_line_from_exception(exception)
12
+ @context = extract_context(context)
13
+ end
14
+
15
+ def locator
16
+ RorVsWild.agent.locator
17
+ end
18
+
19
+ def as_json(options = nil)
20
+ hash = {
21
+ line: @line.to_i,
22
+ file: locator.relative_path(@file),
23
+ message: exception.message[0,1_000_000],
24
+ backtrace: exception.backtrace || ["No backtrace"],
25
+ exception: exception.class.to_s,
26
+ context: @context,
27
+ environment: Host.to_h,
28
+ }
29
+ hash.merge!(details) if details
30
+ hash
31
+ end
32
+
33
+ def extract_context(given_context)
34
+ hash = defined?(ActiveSupport::ExecutionContext) ? ActiveSupport::ExecutionContext.to_h : {}
35
+ hash.merge!(RorVsWild.agent&.current_execution&.error_context || {})
36
+ hash.merge!(given_context) if given_context.is_a?(Hash)
37
+ hash
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RorVsWild
4
+ class Execution
5
+ attr_reader :parameters, :sections, :section_stack, :error_context, :runtime
6
+
7
+ attr_accessor :error, :name
8
+
9
+ def initialize(name, parameters)
10
+ @name = name
11
+ @parameters = parameters
12
+ @runtime = nil
13
+ @error = nil
14
+ @error_context = nil
15
+
16
+ @started_at = RorVsWild.clock_milliseconds
17
+ @gc_section = Section.start_gc_timing
18
+ @environment = Host.to_h
19
+ @section_stack = []
20
+ @sections = []
21
+ end
22
+
23
+ def add_section(section)
24
+ if sibling = @sections.find { |s| s.sibling?(section) }
25
+ sibling.merge(section)
26
+ else
27
+ @sections << section
28
+ end
29
+ end
30
+
31
+ def add_queue_time(queue_time_ms)
32
+ return unless queue_time_ms
33
+ @started_at -= queue_time_ms
34
+ section = Section.new
35
+ section.total_ms = queue_time_ms
36
+ section.gc_time_ms = 0
37
+ section.file = "queue"
38
+ section.line = 0
39
+ section.kind = "queue"
40
+ add_section(section)
41
+ end
42
+
43
+ def stop
44
+ Section.stop_gc_timing(@gc_section)
45
+ @sections << @gc_section if @gc_section.calls > 0 && @gc_section.total_ms > 0
46
+ @runtime = RorVsWild.clock_milliseconds - @started_at
47
+ end
48
+
49
+ def as_json(options = nil)
50
+ {
51
+ name: name,
52
+ runtime: @runtime,
53
+ error: @error && @error.as_json(options),
54
+ sections: @sections.map(&:as_json),
55
+ environment: Host.to_h,
56
+ }
57
+ end
58
+
59
+ def add_exception(exception)
60
+ @error = Error.new(exception) if !RorVsWild.agent.ignored_exception?(exception) && !@error
61
+ end
62
+
63
+ def merge_error_context(hash)
64
+ @error_context = @error_context ? @error_context.merge(hash) : hash
65
+ end
66
+
67
+ private
68
+
69
+ def start_gc_timing
70
+ section = Section.new
71
+ section.calls = GC.count
72
+ section.file, section.line = "ruby/gc.c", 0
73
+ section.add_command("GC.start")
74
+ section.kind = "gc"
75
+ section
76
+ end
77
+
78
+ if GC.respond_to?(:total_time)
79
+ def gc_total_ms
80
+ GC.total_time / 1_000_000.0 # nanosecond -> millisecond
81
+ end
82
+ else
83
+ def gc_total_ms
84
+ GC::Profiler.total_time * 1000 # second -> millisecond
85
+ end
86
+ end
87
+
88
+ class Job < Execution
89
+ def add_exception(exception)
90
+ super(exception)
91
+ @error && @error.details = {parameters: parameters, job: {name: name}}
92
+ @error
93
+ end
94
+ end
95
+
96
+ class Request < Execution
97
+ attr_reader :path
98
+
99
+ attr_accessor :controller
100
+
101
+ def initialize(path)
102
+ @path = path
103
+ super(nil, nil)
104
+ end
105
+
106
+ def add_exception(exception)
107
+ super(exception)
108
+ @error && @error.details = {
109
+ parameters: controller.request.filtered_parameters,
110
+ request: {
111
+ headers: headers,
112
+ name: "#{controller.class}##{controller.action_name}",
113
+ method: controller.request.method,
114
+ url: controller.request.url,
115
+ }
116
+ }
117
+ @error
118
+ end
119
+
120
+ def headers
121
+ controller.request.filtered_env.reduce({}) do |hash, (name, value)|
122
+ if name.start_with?("HTTP_") && name != "HTTP_COOKIE"
123
+ hash[name.delete_prefix("HTTP_").split("_").each(&:capitalize!).join("-")] = value
124
+ end
125
+ hash
126
+ end
127
+ end
128
+
129
+ def as_json(options = nil)
130
+ super(options).merge(path: @path)
131
+ end
132
+ end
133
+ end
134
+ end
@@ -11,7 +11,7 @@
11
11
  <div class="rorvswild-local-panel__header">
12
12
  <div class="rorvswild-local-panel__width-limiter">
13
13
  <a href="https://www.rorvswild.com" class="rorvswild-local-panel__logo">
14
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 34.83 30.83">
14
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 34.83 30.83" fill="none" width="24px" height="24px">
15
15
  <polygon points="17.41 9.41 13.41 9.41 9.41 13.41 17.41 21.41 25.41 13.41 21.41 9.41 17.41 9.41"/>
16
16
  <polyline points="1.41 21.41 9.41 29.41 17.41 21.41 25.41 29.41 33.41 21.41"/>
17
17
  <polyline points="9.41 5.41 5.41 1.41 1.41 5.41"/>
@@ -3,6 +3,13 @@
3
3
  module RorVsWild
4
4
  module Local
5
5
  class Queue
6
+ def initialize(config = {})
7
+ @config = config
8
+ dir = File.directory?("tmp") ? "tmp" : Dir.tmpdir
9
+ @directoy = File.join(dir, "rorvswild", @config[:prefix].to_s)
10
+ FileUtils.mkpath(@directoy)
11
+ end
12
+
6
13
  def push_job(data)
7
14
  push_to(data, "jobs")
8
15
  push_error(data[:error]) if data[:error]
@@ -34,23 +41,28 @@ module RorVsWild
34
41
  def push_to(data, name)
35
42
  data[:queued_at] = Time.now
36
43
  data[:uuid] = SecureRandom.uuid
37
- array = load_data(name)
38
- array.unshift(data)
39
- array.pop if array.size > 100
40
- save_data(array, name)
41
- end
42
-
43
- def save_data(data, name)
44
- File.open(File.join(directoy, "#{name}.json"), "w") { |file| JSON.dump(data, file) }
44
+ File.open(File.join(@directoy, "#{name}.ndjson"), "a") { |file| file.write(JSON.dump(data) + "\n") }
45
45
  end
46
46
 
47
47
  def load_data(name)
48
- JSON.load_file(File.join(directoy, "#{name}.json"), symbolize_names: true) rescue []
48
+ return [] unless File.readable?(path = File.join(@directoy, "#{name}.ndjson"))
49
+ return [] unless lines = read_last_lines(path, 100)
50
+ lines.reverse.map { |line| JSON.parse(line, symbolize_names: true) }
49
51
  end
50
52
 
51
- def directoy
52
- dir = File.directory?("tmp") ? "tmp" : Dir.tmpdir
53
- FileUtils.mkpath(File.join(dir, "rorvswild"))[0]
53
+ def read_last_lines(path, desired_lines, max_read_size = 4096 * desired_lines)
54
+ read_lines, buffer = 0, []
55
+ File.open(path, "rb") do |file|
56
+ file.seek(0, IO::SEEK_END)
57
+ pos = file.pos
58
+ while pos > 0 && read_lines <= desired_lines
59
+ read_size = max_read_size < pos ? max_read_size : pos
60
+ file.seek(pos -= read_size, IO::SEEK_SET)
61
+ buffer << file.read(read_size)
62
+ read_lines += buffer.last.count("\n")
63
+ end
64
+ end
65
+ buffer.reverse.join.lines[1..desired_lines]
54
66
  end
55
67
  end
56
68
  end
@@ -4,7 +4,8 @@ require "rorvswild/local/queue"
4
4
  module RorVsWild
5
5
  module Local
6
6
  def self.start(config = {})
7
- RorVsWild.start(config.merge(queue: RorVsWild::Local::Queue.new))
7
+ queue = RorVsWild::Local::Queue.new(config[:queue] || {})
8
+ RorVsWild.start(config.merge(queue: queue))
8
9
  Rails.application.config.middleware.unshift(RorVsWild::Local::Middleware, nil)
9
10
  end
10
11
  end
@@ -12,7 +12,9 @@ module RorVsWild
12
12
  end
13
13
 
14
14
  def find_most_relevant_location(locations)
15
- locations.find { |l| relevant_path?(l.path) } || locations.find { |l| !l.path.start_with?(rorvswild_lib_path) } || locations.first
15
+ locations.find { |l| l.path && relevant_path?(l.path) } ||
16
+ locations.find { |l| l.path && !l.path.start_with?(rorvswild_lib_path) } ||
17
+ locations.first
16
18
  end
17
19
 
18
20
  def find_most_relevant_file_and_line_from_exception(exception)
@@ -3,6 +3,7 @@ module RorVsWild
3
3
  attr_reader :cpu, :memory, :storage, :updated_at
4
4
 
5
5
  def initialize
6
+ @updated_at = nil
6
7
  @cpu = RorVsWild::Metrics::Cpu.new
7
8
  @memory = RorVsWild::Metrics::Memory.new
8
9
  @storage = RorVsWild::Metrics::Storage.new
@@ -1,15 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RorVsWild
2
4
  module Plugin
3
5
  class ActionController
4
- def self.setup
6
+ @installed = false
7
+
8
+ def self.setup(agent)
5
9
  return if @installed
6
10
  return unless defined?(::ActionController::Base)
7
11
  ::ActionController::Base.around_action(&method(:around_action))
8
- ::ActionController::Base.rescue_from(StandardError) { |ex| RorVsWild::Plugin::ActionController.after_exception(ex, self) }
9
-
10
12
  if defined?(::ActionController::API) && ::ActionController::API.respond_to?(:around_action)
11
13
  ::ActionController::API.around_action(&method(:around_action))
12
- ::ActionController::API.rescue_from(StandardError) { |ex| RorVsWild::Plugin::ActionController.after_exception(ex, self) }
13
14
  end
14
15
  @installed = true
15
16
  end
@@ -24,42 +25,19 @@ module RorVsWild
24
25
  section.file = RorVsWild.agent.locator.relative_path(section.file)
25
26
  section.commands << "#{controller.class}##{method_name}"
26
27
  end
27
- RorVsWild.agent.current_data[:name] = controller_action if RorVsWild.agent.current_data
28
+ if execution = RorVsWild.agent.current_execution
29
+ execution.name = controller_action
30
+ execution.controller = controller
31
+ end
28
32
  end
29
33
  block.call
34
+ rescue => exception
35
+ RorVsWild.agent.current_execution&.add_exception(exception)
36
+ raise
30
37
  ensure
31
38
  RorVsWild::Section.stop
32
39
  end
33
40
  end
34
-
35
- def self.after_exception(exception, controller)
36
- if hash = RorVsWild.agent.push_exception(exception)
37
- hash[:session] = controller.session.to_hash
38
- hash[:parameters] = controller.request.filtered_parameters
39
- hash[:request] = {
40
- headers: extract_http_headers(controller.request.filtered_env),
41
- name: "#{controller.class}##{controller.action_name}",
42
- method: controller.request.method,
43
- url: controller.request.url,
44
- }
45
- end
46
- raise exception
47
- end
48
-
49
- def self.extract_http_headers(headers)
50
- headers.reduce({}) do |hash, (name, value)|
51
- if name.index("HTTP_".freeze) == 0 && name != "HTTP_COOKIE".freeze
52
- hash[format_header_name(name)] = value
53
- end
54
- hash
55
- end
56
- end
57
-
58
- HEADER_REGEX = /^HTTP_/.freeze
59
-
60
- def self.format_header_name(name)
61
- name.sub(HEADER_REGEX, ''.freeze).split("_".freeze).map(&:capitalize).join("-".freeze)
62
- end
63
41
  end
64
42
  end
65
43
  end
@@ -1,9 +1,11 @@
1
1
  module RorVsWild
2
2
  module Plugin
3
3
  class ActionMailer
4
- def self.setup
4
+ @installed = false
5
+
6
+ def self.setup(agent)
5
7
  return if @installed
6
- return unless defined?(::ActiveSupport::Notifications.subscribe)
8
+ return unless defined?(ActiveSupport::Notifications.subscribe)
7
9
  ActiveSupport::Notifications.subscribe("deliver.action_mailer", new)
8
10
  @installed = true
9
11
  end
@@ -3,9 +3,11 @@
3
3
  module RorVsWild
4
4
  module Plugin
5
5
  class ActionView
6
- def self.setup
6
+ @installed = false
7
+
8
+ def self.setup(agent)
7
9
  return if @installed
8
- return unless defined?(::ActiveSupport::Notifications.subscribe)
10
+ return unless defined?(ActiveSupport::Notifications.subscribe)
9
11
  ActiveSupport::Notifications.subscribe("render_partial.action_view", plugin = new)
10
12
  ActiveSupport::Notifications.subscribe("render_template.action_view", plugin)
11
13
  ActiveSupport::Notifications.subscribe("render_collection.action_view", plugin)
@@ -1,7 +1,9 @@
1
1
  module RorVsWild
2
2
  module Plugin
3
3
  class ActiveJob
4
- def self.setup
4
+ @installed = false
5
+
6
+ def self.setup(agent)
5
7
  return if @installed
6
8
  return unless defined?(::ActiveJob::Base)
7
9
  ::ActiveJob::Base.around_perform(&method(:around_perform))
@@ -3,14 +3,16 @@
3
3
  module RorVsWild
4
4
  module Plugin
5
5
  class ActiveRecord
6
- def self.setup
6
+ @installed = false
7
+
8
+ def self.setup(agent)
7
9
  return if @installed
8
10
  setup_callback
9
11
  @installed = true
10
12
  end
11
13
 
12
14
  def self.setup_callback
13
- return unless defined?(::ActiveSupport::Notifications.subscribe)
15
+ return unless defined?(ActiveSupport::Notifications.subscribe)
14
16
  ActiveSupport::Notifications.subscribe("sql.active_record", new)
15
17
  end
16
18
 
@@ -38,7 +40,7 @@ module RorVsWild
38
40
  section.commands << normalize_sql_query(event.payload[:sql])
39
41
  section.kind = "sql"
40
42
  (parent = Section.current) && parent.children_ms += section.total_ms
41
- RorVsWild.agent.add_section(section)
43
+ execution = RorVsWild.agent.current_execution and execution.add_section(section)
42
44
  end
43
45
 
44
46
  SQL_STRING_REGEX = /'((?:''|\\'|[^'])*)'/
@@ -3,7 +3,9 @@
3
3
  module RorVsWild
4
4
  module Plugin
5
5
  module DelayedJob
6
- def self.setup
6
+ @installed = false
7
+
8
+ def self.setup(agent)
7
9
  return if @installed
8
10
  return unless defined?(Delayed::Plugin)
9
11
  Delayed::Worker.plugins << Class.new(Delayed::Plugin) do
@@ -1,7 +1,7 @@
1
1
  module RorVsWild
2
2
  module Plugin
3
3
  class Elasticsearch
4
- def self.setup
4
+ def self.setup(agent)
5
5
  return if !defined?(::Elasticsearch::Transport)
6
6
  return if ::Elasticsearch::Transport::Client.method_defined?(:perform_request_without_rorvswild)
7
7
 
@@ -1,7 +1,7 @@
1
1
  module RorVsWild
2
2
  module Plugin
3
3
  class Faktory
4
- def self.setup
4
+ def self.setup(agent)
5
5
  if defined?(::Faktory)
6
6
  ::Faktory.configure_worker do |config|
7
7
  config.worker_middleware { |chain| chain.add(Faktory) }
@@ -46,7 +46,9 @@ module RorVsWild
46
46
 
47
47
  include RequestQueueTime
48
48
 
49
- def self.setup
49
+ @installed = false
50
+
51
+ def self.setup(agent)
50
52
  return if @installed
51
53
  Rails.application.config.middleware.unshift(RorVsWild::Plugin::Middleware, nil) if defined?(Rails)
52
54
  @installed = true
@@ -57,10 +59,9 @@ module RorVsWild
57
59
  end
58
60
 
59
61
  def call(env)
60
- queue_time_ms = calculate_queue_time(env)
61
- RorVsWild.agent.start_request(queue_time_ms || 0)
62
- RorVsWild.agent.current_data[:path] = env["ORIGINAL_FULLPATH"]
63
- add_queue_time_section(queue_time_ms)
62
+ execution = RorVsWild::Execution::Request.new(env["ORIGINAL_FULLPATH"])
63
+ execution.add_queue_time(calculate_queue_time(env))
64
+ RorVsWild.agent.start_execution(execution)
64
65
  section = RorVsWild::Section.start
65
66
  section.file, section.line = rails_engine_location
66
67
  section.commands << "Rails::Engine#call"
@@ -68,24 +69,11 @@ module RorVsWild
68
69
  [code, headers, body]
69
70
  ensure
70
71
  RorVsWild::Section.stop
71
- inject_server_timing(RorVsWild.agent.stop_request, headers)
72
+ RorVsWild.agent.stop_execution
72
73
  end
73
74
 
74
75
  private
75
76
 
76
- def add_queue_time_section(queue_time_ms)
77
- return unless queue_time_ms
78
-
79
- section = Section.new
80
- section.stop
81
- section.total_ms = queue_time_ms
82
- section.gc_time_ms = 0
83
- section.file = "request-queue"
84
- section.line = 0
85
- section.kind = "queue"
86
- RorVsWild.agent.add_section(section)
87
- end
88
-
89
77
  def calculate_queue_time(env)
90
78
  queue_time_from_header = parse_queue_time_header(env)
91
79
 
@@ -95,49 +83,6 @@ module RorVsWild
95
83
  def rails_engine_location
96
84
  @rails_engine_location = ::Rails::Engine.instance_method(:call).source_location
97
85
  end
98
-
99
- def format_server_timing_header(sections)
100
- sections.map do |section|
101
- if section.kind == "view"
102
- "#{section.kind};dur=#{section.self_ms.round};desc=\"#{section.file}\""
103
- else
104
- "#{section.kind};dur=#{section.self_ms.round};desc=\"#{section.file}:#{section.line}\""
105
- end
106
- end.join(", ")
107
- end
108
-
109
- def format_server_timing_ascii(sections, total_width = 80)
110
- max_time = sections.map(&:self_ms).max
111
- chart_width = (total_width * 0.25).to_i
112
- rows = sections.map { |section|
113
- [
114
- section.kind == "view" ? section.file : "#{section.file}:#{section.line}",
115
- "█" * (section.self_ms * (chart_width-1) / max_time),
116
- "%.1fms" % section.self_ms,
117
- ]
118
- }
119
- time_width = rows.map { |cols| cols[2].size }.max + 1
120
- label_width = total_width - chart_width - time_width
121
- rows.each { |cols| cols[0] = truncate_backwards(cols[0], label_width) }
122
- template = "%-#{label_width}s%#{chart_width}s%#{time_width}s"
123
- rows.map { |cols| format(template, *cols) }.join("\n")
124
- end
125
-
126
- def truncate_backwards(string, width)
127
- string.size > width ? "…" + string[-(width - 1)..-1] : string
128
- end
129
-
130
- def inject_server_timing(data, headers)
131
- return if !data || !data[:send_server_timing] || !(sections = data[:sections])
132
- sections = sections.sort_by(&:self_ms).reverse[0,10]
133
- headers["Server-Timing"] = format_server_timing_header(sections)
134
- if data[:name] && RorVsWild.logger.level <= Logger::Severity::DEBUG
135
- RorVsWild.logger.debug(["┤ #{data[:name]} ├".center(80, "─"),
136
- format_server_timing_ascii(sections),
137
- "─" * 80, nil].join("\n")
138
- )
139
- end
140
- end
141
86
  end
142
87
  end
143
88
  end
@@ -1,7 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RorVsWild
2
4
  module Plugin
3
5
  class Mongo
4
- def self.setup
6
+ @installed = false
7
+
8
+ def self.setup(agent)
5
9
  return if @installed
6
10
  return if !defined?(::Mongo::Monitoring::Global)
7
11
  ::Mongo::Monitoring::Global.subscribe(::Mongo::Monitoring::COMMAND, Mongo.new)
@@ -10,13 +14,10 @@ module RorVsWild
10
14
 
11
15
  attr_reader :commands
12
16
 
13
- def initialize
14
- @commands = {}
15
- end
16
-
17
17
  def started(event)
18
- RorVsWild::Section.start
19
- commands[event.request_id] = event.command
18
+ section = RorVsWild::Section.start
19
+ section.kind = "mongo"
20
+ section.commands << normalize_query(event.command).to_json
20
21
  end
21
22
 
22
23
  def failed(event)
@@ -28,9 +29,40 @@ module RorVsWild
28
29
  end
29
30
 
30
31
  def after_query(event)
31
- RorVsWild::Section.stop do |section|
32
- section.kind = "mongo".freeze
33
- section.command = commands.delete(event.request_id).to_s
32
+ RorVsWild::Section.stop
33
+ end
34
+
35
+ QUERY_ROOT_KEYS = %w[
36
+ insert update delete findAndModify bulkWrite
37
+ find aggregate count distinct
38
+ create drop listCollections listDatabases dropDatabase
39
+ startSession commitTransaction abortTransaction
40
+ filter getMore collection sort
41
+ ]
42
+
43
+ def normalize_query(query)
44
+ query = query.slice(*QUERY_ROOT_KEYS)
45
+ query["filter"] = normalize_hash(query["filter"]) if query["filter"]
46
+ query["getMore"] = normalize_value(query["getMore"]) if query["getMore"]
47
+ query
48
+ end
49
+
50
+ def normalize_hash(hash)
51
+ (hash = hash.to_h).each { |key, val| hash[key] = normalize_value(val) }
52
+ end
53
+
54
+ def normalize_array(array)
55
+ array.map { |value| normalize_value(value) }
56
+ end
57
+
58
+ def normalize_value(value)
59
+ case value
60
+ when Hash then normalize_hash(value.to_h)
61
+ when Array then normalize_array(value)
62
+ when FalseClass then false
63
+ when TrueClass then true
64
+ when NilClass then nil
65
+ else "?"
34
66
  end
35
67
  end
36
68
  end
@@ -3,7 +3,7 @@ module RorVsWild
3
3
  class NetHttp
4
4
  HTTP = "http".freeze
5
5
 
6
- def self.setup
6
+ def self.setup(agent)
7
7
  return if !defined?(Net::HTTP)
8
8
  return if Net::HTTP.method_defined?(:request_without_rorvswild)
9
9
 
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RorVsWild
4
+ module Plugin
5
+ class RailsCache
6
+ @installed = false
7
+
8
+ def self.setup(agent)
9
+ return if @installed
10
+ return unless defined?(ActiveSupport::Notifications.subscribe)
11
+ plugin = new
12
+ for name in ["write", "read", "delete", "write_multi", "read_multi", "delete_multi", "increment", "decrement"]
13
+ ActiveSupport::Notifications.subscribe("cache_#{name}.active_support", plugin)
14
+ end
15
+ @installed = true
16
+ end
17
+
18
+ def start(name, id, payload)
19
+ RorVsWild::Section.start do |section|
20
+ section.commands << name.split(".")[0].delete_prefix("cache_")
21
+ section.kind = "cache"
22
+ end
23
+ end
24
+
25
+ def finish(name, id, payload)
26
+ RorVsWild::Section.stop
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RorVsWild
4
+ module Plugin
5
+ class RailsError
6
+ @installed = false
7
+
8
+ def self.setup(agent)
9
+ return if @installed
10
+ return if !defined?(Rails.error)
11
+ return if !defined?(ActiveSupport::ErrorReporter)
12
+ Rails.error.subscribe(new)
13
+ @installed = true
14
+ end
15
+
16
+ def report(error, handled: nil, severity: nil, context: nil, source: nil)
17
+ RorVsWild.record_error(error, context)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -3,7 +3,7 @@
3
3
  module RorVsWild
4
4
  module Plugin
5
5
  class Redis
6
- def self.setup
6
+ def self.setup(agent)
7
7
  return if !defined?(::Redis)
8
8
  if ::Redis::Client.method_defined?(:process)
9
9
  ::Redis::Client.prepend(V4)
@@ -1,7 +1,9 @@
1
1
  module RorVsWild
2
2
  module Plugin
3
3
  module Resque
4
- def self.setup
4
+ @installed = false
5
+
6
+ def self.setup(agent)
5
7
  return if @installed
6
8
  ::Resque::Job.send(:extend, Resque) if defined?(::Resque::Job)
7
9
  @installed = true
@@ -1,18 +1,33 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module RorVsWild
2
4
  module Plugin
3
5
  class Sidekiq
4
- def self.setup
6
+ INTERNAL_EXCEPTIONS = ["Sidekiq::JobRetry::Handled", "Sidekiq::JobRetry::Skip"]
7
+
8
+ def self.setup(agent)
5
9
  if defined?(::Sidekiq)
6
10
  ::Sidekiq.configure_server do |config|
7
11
  config.server_middleware { |chain| chain.add(Sidekiq) }
8
12
  end
13
+ # Prevent RailsError plugin from sending internal Sidekiq exceptions captured by ActiveSupport::ErrorReporter.
14
+ agent.config[:ignore_exceptions].concat(INTERNAL_EXCEPTIONS)
9
15
  end
10
16
  end
11
17
 
12
18
  def call(worker, item, queue, &block)
13
19
  # Wrapped contains the real class name of the ActiveJob wrapper
14
20
  name = item["wrapped".freeze] || item["class".freeze]
15
- RorVsWild.agent.measure_job(name, parameters: item["args".freeze], &block)
21
+ RorVsWild.agent.measure_job(name, parameters: item["args".freeze]) do
22
+ section = RorVsWild::Section.start
23
+ section.commands << "#{name}#perform"
24
+ if perform_method = worker.method(:perform)
25
+ section.file, section.line = worker.method(:perform).source_location
26
+ section.file = RorVsWild.agent.locator.relative_path(section.file)
27
+ end
28
+ block.call
29
+ RorVsWild::Section.stop
30
+ end
16
31
  end
17
32
  end
18
33
  end
@@ -15,6 +15,10 @@ module RorVsWild
15
15
  elsif Rails.env.development?
16
16
  require "rorvswild/local"
17
17
  RorVsWild::Local.start(config || {})
18
+ elsif Rails.env.test?
19
+ require "rorvswild/local"
20
+ path = "tests/#{DateTime.now.iso8601}"
21
+ RorVsWild::Local.start({widget: "hidden", queue: {prefix: path}}.merge(config || {}))
18
22
  end
19
23
  end
20
24
 
@@ -13,15 +13,15 @@ module RorVsWild
13
13
  end
14
14
 
15
15
  def self.stop(&block)
16
- return unless stack && section = stack.pop
16
+ return if !(sections = stack) || !(section = sections.pop)
17
17
  block.call(section) if block_given?
18
18
  section.stop
19
19
  current.children_ms += section.total_ms if current
20
- RorVsWild.agent.add_section(section)
20
+ execution = RorVsWild.agent.current_execution and execution.add_section(section)
21
21
  end
22
22
 
23
23
  def self.stack
24
- (data = RorVsWild.agent.current_data) && data[:section_stack]
24
+ execution = RorVsWild.agent.current_execution and execution.section_stack
25
25
  end
26
26
 
27
27
  def self.current
@@ -94,7 +94,7 @@ module RorVsWild
94
94
  end
95
95
 
96
96
  def as_json(options = nil)
97
- {calls: calls, total_runtime: total_ms, children_runtime: children_ms, async_runtime: async_ms, kind: kind, started_at: start_ms, file: file, line: line, command: command}
97
+ {calls: calls, total_runtime: total_ms, children_runtime: children_ms, async_runtime: async_ms, kind: kind, file: file, line: line, command: command}
98
98
  end
99
99
 
100
100
  def to_json(options = {})
@@ -1,3 +1,3 @@
1
1
  module RorVsWild
2
- VERSION = "1.9.2".freeze
2
+ VERSION = "1.10.0".freeze
3
3
  end
data/lib/rorvswild.rb CHANGED
@@ -7,6 +7,8 @@ require "rorvswild/section"
7
7
  require "rorvswild/client"
8
8
  require "rorvswild/plugins"
9
9
  require "rorvswild/queue"
10
+ require "rorvswild/error"
11
+ require "rorvswild/execution"
10
12
  require "rorvswild/agent"
11
13
 
12
14
  module RorVsWild
@@ -61,7 +63,7 @@ module RorVsWild
61
63
  end
62
64
 
63
65
  def self.send_server_timing=(boolean)
64
- agent.send_server_timing = boolean if agent
66
+ warn "[DEPRECATION] `RorVsWild.send_server_timing=` is deprecated and is no longer supported."
65
67
  end
66
68
 
67
69
  def self.initialize_logger(destination = nil)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rorvswild
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.2
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexis Bernard
@@ -27,6 +27,8 @@ files:
27
27
  - lib/rorvswild/agent.rb
28
28
  - lib/rorvswild/client.rb
29
29
  - lib/rorvswild/deployment.rb
30
+ - lib/rorvswild/error.rb
31
+ - lib/rorvswild/execution.rb
30
32
  - lib/rorvswild/host.rb
31
33
  - lib/rorvswild/installer.rb
32
34
  - lib/rorvswild/local.rb
@@ -56,6 +58,8 @@ files:
56
58
  - lib/rorvswild/plugin/middleware.rb
57
59
  - lib/rorvswild/plugin/mongo.rb
58
60
  - lib/rorvswild/plugin/net_http.rb
61
+ - lib/rorvswild/plugin/rails_cache.rb
62
+ - lib/rorvswild/plugin/rails_error.rb
59
63
  - lib/rorvswild/plugin/redis.rb
60
64
  - lib/rorvswild/plugin/resque.rb
61
65
  - lib/rorvswild/plugin/sidekiq.rb